633 Commits

Author SHA1 Message Date
xbgmsharp
327324d7c3 Merge pull request #6 from xbgmsharp/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2024-04-07 10:20:42 +02:00
xbgmsharp
28ccc9e2da Revert test update 2024-04-07 10:20:11 +02:00
xbgmsharp
966846c93d Update test, fix actually check mermaid exit code 2024-04-07 10:09:15 +02:00
dependabot[bot]
d966d34a29 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-07 07:20:12 +00:00
xbgmsharp
8bd7594357 Add dependabot 2024-04-07 09:19:41 +02:00
xbgmsharp
4de47fd011 Update github-actions versions 2024-04-07 09:19:05 +02:00
xbgmsharp
cf0f178d13 Update README 2024-03-25 22:00:31 +01:00
xbgmsharp
b224374e65 Update frontend to 0.1.0-beta4 2024-03-25 21:57:09 +01:00
xbgmsharp
1c441af48c Update delete_account_fn and delete_vessel_fn 2024-03-25 21:56:16 +01:00
xbgmsharp
e5287d74f6 Fix cron_process_no_activity_fn 2024-03-11 09:28:40 +01:00
xbgmsharp
9cf334125a update public.cron_process_no_activity_fn, delete old data 2024-03-10 21:50:05 +01:00
xbgmsharp
1342dba298 Update public.cron_windy_fn, first notifition on first notification
Update delete_vessel_fn, return a jsonb with details
2024-03-09 22:24:09 +01:00
xbgmsharp
7e734c412b Bump up frontend v0.1.0-beta2 2024-03-03 23:09:44 +01:00
xbgmsharp
9ae10b0519 Fix migration, metadata_upsert_trigger_fn 2024-03-03 23:09:18 +01:00
xbgmsharp
ab1b4c7076 Update front-end tests, build web and web_dev 2024-03-03 23:08:46 +01:00
xbgmsharp
edb7bc7dd8 Update test, Bump up version 2024-03-03 12:55:24 +01:00
xbgmsharp
e124bdea44 Update README 2024-03-03 12:41:23 +01:00
xbgmsharp
b944c39507 Update tests, log runTime is now store as ISO duration format. 2024-03-03 12:40:36 +01:00
xbgmsharp
b4791f059c Update logbook_update_extra_json_fn: convert log extra json fields runtime in ISO format and log in NM
Update metadata_upsert_trigger_fn: strip special char from platform metadata
Drop deprecated fn
2024-03-03 12:21:38 +01:00
xbgmsharp
78ff78a6b1 Update OpenAPI 2024-03-02 11:24:26 +01:00
xbgmsharp
b6be05753c Update test, Bump up version 2024-03-02 11:22:58 +01:00
xbgmsharp
30e3797854 Update tests, add convertion output 2024-03-02 11:22:36 +01:00
xbgmsharp
e9dea3b124 Add migration 202403, Add new conversion metersToKnots, Update logbook_update_geojson_fn expose data in nautical units, Update process_lat_lon_fn process moorage to always set a country 2024-03-02 11:19:42 +01:00
xbgmsharp
1a08acda85 Update moorage process to always set country colunm 2024-03-02 11:18:40 +01:00
xbgmsharp
633e73b29d Update tests, add convertion unit test radiantToDegrees, valToPercent, metersToKnots 2024-03-02 11:17:40 +01:00
xbgmsharp
a383d0db53 Bump up timescaledb 2024-02-29 22:30:12 +01:00
xbgmsharp
0116ea4feb Bump up timescaledb 2024-02-29 22:24:57 +01:00
xbgmsharp
92bcf0ffa6 Update public.logbook_update_geojson_fn, add TWD and TWS and navigation.state into log GeoJSON 2024-02-29 22:17:37 +01:00
xbgmsharp
414736909b Update README 2024-02-23 23:49:17 +01:00
xbgmsharp
36173658a0 Update REAME 2024-02-23 21:30:34 +01:00
xbgmsharp
984a7c14da Add Grafana URL 2024-02-23 21:08:22 +01:00
xbgmsharp
48be759e4d Update README 2024-02-23 21:07:29 +01:00
xbgmsharp
e473baa5a0 Update docker, Add build arg 2024-02-22 22:53:50 +01:00
xbgmsharp
9ba79123ec Update README 2024-02-21 16:36:29 +01:00
xbgmsharp
82e7056b0c Update web container, build locally instead of cloud version 2024-02-21 16:28:12 +01:00
xbgmsharp
4829d0b848 Update test, trim users vessel name 2024-02-21 16:25:00 +01:00
xbgmsharp
9069b11f71 Update migrations files. Add license header
Update windy PWS, ensure the require path are present
Update send_notification_fn, fix pushover notificaiton if missing pushover key
Update vessel name, trim name
2024-02-21 15:24:20 +01:00
xbgmsharp
bf3eb3c806 Bump up frontend v0.1.0-beta 2024-02-19 21:44:18 +01:00
xbgmsharp
6e863ca355 Update migration 202402, add cron_prune_otp_fn 2024-02-19 09:32:18 +01:00
xbgmsharp
51128112f3 Update api_version 2024-02-18 18:51:46 +01:00
xbgmsharp
b150d9706f tag release v0.7.0 2024-02-18 18:27:33 +01:00
xbgmsharp
813460da7b Release v0.7.0 2024-02-18 18:04:18 +01:00
xbgmsharp
813b8088f3 Add migrations 2024-02-18 18:00:09 +01:00
xbgmsharp
f90911c523 Update pgcron, rename cron job non depending on the process_queue table 2024-02-18 17:54:05 +01:00
xbgmsharp
790bbb671c Update pgcron default username 2024-02-18 17:51:27 +01:00
xbgmsharp
5455d8246f Update versions, timescaledb, postgis, postgres 2024-02-18 12:59:21 +01:00
xbgmsharp
95d24c538d Change cron windy and alerts to use True wind speed 2024-02-08 10:28:19 +01:00
xbgmsharp
57799c9ee4 Update cron_process_alerts_fn, update alert message 2024-02-07 23:56:11 +01:00
xbgmsharp
437bfd0252 Update cron_process_alerts_fn and cron_process_windy_fn.
When enable for the first time, start sending data from the last 24h.
cron_process_windy_fn, fix main query and percentage thresold due testing and debug
2024-02-07 17:03:52 +01:00
xbgmsharp
dcceab2551 Update alert message 2024-02-06 22:03:36 +01:00
xbgmsharp
cdc2e4e55c Update alert message 2024-02-06 22:02:44 +01:00
xbgmsharp
76bbe29567 Add alert parsing in messages notifications 2024-02-06 21:30:04 +01:00
xbgmsharp
36b8eece52 Update cron process alerts, Add and merge default threshold with user settings 2024-02-06 21:15:52 +01:00
xbgmsharp
a5436479cf Add cron process alerts, send notification when users threshold are in reach with in user interval time frame 2024-02-06 19:41:46 +01:00
xbgmsharp
23ea3bd0d8 Update alert message 2024-02-06 18:11:49 +01:00
xbgmsharp
826566e097 Add conversion helpers, kelvinToCel, radiantToDegrees, valToPercent 2024-02-06 18:11:10 +01:00
xbgmsharp
f942076cc2 Update cron_process_windy_fn, fix main loop query 2024-02-06 13:51:32 +01:00
xbgmsharp
8dba0c21b6 Add alert notification template 2024-02-05 12:54:25 +01:00
xbgmsharp
a96160ef15 Add windy notifications templates 2024-02-05 12:53:42 +01:00
xbgmsharp
ccf91bb832 Add alerts cron job 2024-02-05 12:53:09 +01:00
xbgmsharp
b9993ed28f Add cron job for windy 2024-02-05 12:52:23 +01:00
xbgmsharp
0e5e619625 Add cron process windy, send observations to windy station 2024-02-05 12:51:43 +01:00
xbgmsharp
294a60d13a Update app settings, Expose windy api key 2024-02-05 12:51:07 +01:00
xbgmsharp
b6587b1287 Add windy_pws_py, send data to windy station
Reduce debug from keycloak and grafana integration
2024-02-05 12:50:10 +01:00
xbgmsharp
74512d0bf3 UPDATE REAME 2024-02-01 22:43:47 +01:00
xbgmsharp
322a479b4f Update grafana provisioning, if signalk vessel name is null then use vessel_id as name organisation name 2024-01-31 10:23:21 +01:00
xbgmsharp
c5cba6a59f Bump up frontend v0.0.9 2024-01-30 22:57:36 +01:00
xbgmsharp
22466430ac Release v0.6.1 2024-01-30 22:48:46 +01:00
xbgmsharp
643c16ad3f Update keycloak account status 2024-01-30 22:48:22 +01:00
xbgmsharp
59a3c41b4a Update grafana cron, Add keycloak provisioning to sync vessel_id as user attributs 2024-01-30 22:33:08 +01:00
xbgmsharp
371eb6c720 Update message template. Update grafana notification 2024-01-30 22:32:28 +01:00
xbgmsharp
c2ffe9777c Update get_app_settings_fn, add keycloak URI 2024-01-30 22:31:53 +01:00
xbgmsharp
c0261791f5 Add keycloak python user integration 2024-01-30 21:55:30 +01:00
xbgmsharp
e727954f83 Update api.vessel_details_fn, fix length 2024-01-29 21:45:26 +01:00
xbgmsharp
22b04334f8 Udpatr frontend, dump 0.0.9-beta3 2024-01-26 16:33:42 +01:00
xbgmsharp
a5ec4c0039 Update monitoring_view, remove status 2024-01-26 13:05:44 +01:00
xbgmsharp
6a63f7d02f Update cron comment, Add deprecated comment 2024-01-26 11:32:50 +01:00
xbgmsharp
2fec2e650c Update frontend to latest beta 2024-01-26 11:32:05 +01:00
xbgmsharp
ed8514bfb1 Update Grafana default dashboard. Update uuid for provisioning.
Fix metric order to get display the latest know vessel position vs the first one
2024-01-26 11:31:08 +01:00
xbgmsharp
f25e735674 Update overpass_py return jsonb versus a string when there is no value 2024-01-22 21:55:38 +01:00
xbgmsharp
c1b71cabd8 Fix test versions 2024-01-19 11:41:58 +01:00
xbgmsharp
495e25b838 Release 0.6.0 2024-01-19 11:35:29 +01:00
xbgmsharp
a547271496 Update cron_process_grafana_fn, fix SQL error 2024-01-19 09:23:03 +01:00
xbgmsharp
3a1d0baef8 Update grafana cron job, add notification 2024-01-19 00:13:17 +01:00
xbgmsharp
628de57b5f Update teamplate table, add grafana notification 2024-01-19 00:12:47 +01:00
xbgmsharp
9a5f27d21e Update api.timelapse_fn, fix typo using date. add api.timelapse2_fn to export geojson with notes 2024-01-18 23:54:47 +01:00
xbgmsharp
7892b615e0 Update frontend to latest beta 2024-01-18 22:29:41 +01:00
xbgmsharp
2c2f5d8605 Expose vessel status via API 2024-01-18 22:11:28 +01:00
xbgmsharp
47eda3dcaf Disable stationary detection in pre logbook, still some issue 2024-01-18 22:11:01 +01:00
xbgmsharp
0c0279767f Add comment for process_pre_logbook_fn 2024-01-17 23:03:57 +01:00
xbgmsharp
98e28aacea Updates tests, Update test with rls for anynomous role. Upgrade timescaledb version, 2024-01-17 20:27:23 +01:00
xbgmsharp
b04d336c0d Update permissions, add ROW LEVEL SECURITY for anonymous access.
Allow anonymous role for public access
Allow vessel role to new function for oauth
2024-01-17 20:25:51 +01:00
xbgmsharp
288c458c5a Update anonymous tests 2024-01-17 20:24:58 +01:00
xbgmsharp
d3dd46c834 Renane process_logbook_valid_fn to process_pre_logbook_fn 2024-01-17 20:20:49 +01:00
xbgmsharp
d0bc468ce7 Allow anonymous access for complete timelapse 2024-01-17 20:18:40 +01:00
xbgmsharp
3f51e89303 Update formating 2024-01-16 14:12:13 +01:00
xbgmsharp
000c5651e2 Add PostgREST Media Type Handlers support 2024-01-15 21:44:44 +01:00
xbgmsharp
4bec738826 Add new procedure, api.monitoring_history_fn 2024-01-13 23:23:53 +01:00
xbgmsharp
012812c898 Update eventlogs_view tests, as we skip pre-logbook check from user 2024-01-13 11:45:10 +01:00
xbgmsharp
1c04822cf8 Add cron_process_grafana_fn comment 2024-01-13 11:44:32 +01:00
xbgmsharp
50f018100b Update api.eventlogs_view, ignore pre_logbook check from user 2024-01-12 23:31:00 +01:00
xbgmsharp
13c461a038 Expose metadata platform to frontend 2024-01-12 23:28:16 +01:00
xbgmsharp
8763448523 fix sql versions , update versions 2024-01-12 22:53:21 +01:00
xbgmsharp
e02aaf3676 Update grafana dashboards path 2024-01-12 22:50:57 +01:00
xbgmsharp
ae61072ba4 Update default grafana config and refactor provisioning files 2024-01-12 22:47:28 +01:00
xbgmsharp
242a5554ea 0.6.0-beta 2024-01-12 22:34:00 +01:00
xbgmsharp
39888e1957 Update PostgSail and PostgREST version 2024-01-12 22:33:38 +01:00
xbgmsharp
666f69c42a Update API and SQL documentation 2024-01-12 22:31:19 +01:00
xbgmsharp
77f41251c5 Fix error from 480417917d 2024-01-12 22:22:49 +01:00
xbgmsharp
40a1e0fa39 Fix python error from 0a09d7bbfc 2024-01-12 22:19:40 +01:00
xbgmsharp
5cf2d10757 Fix syntax error from 489fb9562b 2024-01-12 22:13:54 +01:00
xbgmsharp
0dd6410589 Fix docker compose PGRST_SERVER_TIMING_ENABLED contains true 2024-01-12 22:07:16 +01:00
xbgmsharp
682c68a108 Add new postgsail settings app.keycloak_uri 2024-01-12 21:54:51 +01:00
xbgmsharp
5d1db984b8 Update postgrest container with new parameters
Update,fix grafana container, set properly the admin password
2024-01-12 21:53:01 +01:00
xbgmsharp
0a09d7bbfc Update keycloak_py, make uri and host,user,pass dynamic form config 2024-01-12 17:40:59 +01:00
xbgmsharp
14cc4f5ed2 Update cron_process_grafana_fn, add formating and comment for clarification. 2024-01-12 17:39:54 +01:00
xbgmsharp
ff23f5c2ad Update open api documentation 2024-01-10 22:47:01 +01:00
xbgmsharp
489fb9562b Trigger email otp validation only if the user is not coming for the oauth server 2024-01-10 22:39:16 +01:00
xbgmsharp
8c32345342 Feat: Allow signalk plugin webapp to register user and vessel to database via oauth 2024-01-10 22:38:04 +01:00
xbgmsharp
480417917d Feat: initial implementation for oauth support via Keycloak server or other 2024-01-10 22:37:11 +01:00
xbgmsharp
e557ed49a5 Add keycloak_py_fn. This function link the postgsail user with the oauth keycloak server 2024-01-09 22:04:22 +01:00
xbgmsharp
a3475dfe99 Update UUID v7 function 2024-01-09 22:03:45 +01:00
xbgmsharp
e670e11cd5 update ERd diagram 2024-01-07 22:18:44 +01:00
xbgmsharp
88de5003c2 feat: Add new metadata fields platform and configuration. This allow to do pre-defined configuration for well know device like Victron VenusOS decives. 2024-01-07 22:16:52 +01:00
xbgmsharp
c46a428fc2 Update tests, add more invalid metrics to be ignored, add sql metrics count check 2023-12-31 21:13:40 +01:00
xbgmsharp
db3bd6b06f Update metrics_trigger_fn, silently ignore invalid status. 2023-12-31 21:09:42 +01:00
xbgmsharp
4ea7a1b019 Update API documentation 2023-12-29 18:48:11 +01:00
xbgmsharp
ce106074dc Feat: Update tests for pre logbook check and grafana cron job support 2023-12-29 18:26:22 +01:00
xbgmsharp
e7d8229e83 Feat: Add grafana cron job 2023-12-29 18:23:07 +01:00
xbgmsharp
f14342bb07 Fix grafana cron job. update cron_process_grafana_fn 2023-12-29 18:16:59 +01:00
xbgmsharp
c4fbf7682d refactor documentation 2023-12-29 18:15:08 +01:00
xbgmsharp
f8c1f43f48 Fix typo 2023-12-29 12:16:43 +01:00
xbgmsharp
0d5089af2d Add new settings for Grafana HTTP API URL 2023-12-29 11:43:49 +01:00
xbgmsharp
da1952ed31 Feat: Add pre_logbook validation 2023-12-29 11:35:22 +01:00
xbgmsharp
a5d5585366 feat: Add pre logbook check. Split logbook process function 2023-12-29 11:34:19 +01:00
xbgmsharp
5f9a889a44 Feat: Add grafana provisioning on first contact from vessel
Feat: Add pre logbook validation
2023-12-29 11:32:57 +01:00
xbgmsharp
f9719bd174 Feat: Add cron for pre logbook check
Feat: Add cron for Grafana provisioning
2023-12-29 11:28:57 +01:00
xbgmsharp
8d1b8cb389 fix: Update template email message 2023-12-29 11:27:00 +01:00
xbgmsharp
acfd058d3b feat: Add uuid v7 helpers 2023-12-29 11:26:16 +01:00
xbgmsharp
eeae7c40c6 Feat: Add proper Grafana provisioning via HTTP API 2023-12-29 11:24:54 +01:00
xbgmsharp
2bbf27f3ad Update frontend 2023-12-29 11:24:18 +01:00
xbgmsharp
2ba81a935f Update reindex conr_job to use CONCURENTLY options, remove metrics reindex as managed by timescale. 2023-12-03 17:02:05 +01:00
xbgmsharp
0fbac67895 Remove Duplicate Indexes 2023-12-01 23:51:13 +01:00
xbgmsharp
228b234582 Disable remove duplicate index 2023-12-01 22:25:12 +01:00
xbgmsharp
75c8a9506a Update tests, fix check on stay name, add anonymous/public access tests, update badges format 2023-12-01 22:06:01 +01:00
xbgmsharp
2b48a66cd2 Update overpass_py_fn, Enforce area with a proper name tag 2023-12-01 22:01:11 +01:00
xbgmsharp
e642049e93 Update versions.
PostgreSQL-16.1
Postgis-3.4.1
Timescaledb-2.13
PostgSail-0.5.2
2023-12-01 21:59:11 +01:00
xbgmsharp
94e123c95e Update badges processing, update time properties to be at the logbook date 2023-12-01 13:15:12 +01:00
xbgmsharp
9787328990 Update badges, set badges time to log time versus processed time
Add dereprecated comment to unused functions.
2023-11-30 22:16:49 +01:00
xbgmsharp
de62d936d5 Update api.monitoring_view, add new properties wind and speed in to the geojson. 2023-11-30 21:50:21 +01:00
xbgmsharp
293a33da08 Update overpass_py_fn, improve geo location detection, first check against area, then find around with 400m 2023-11-30 21:47:56 +01:00
xbgmsharp
2b105db5c7 Update public.logbook_update_geojson_fn, fix geojson properties for the wind and add notes geojson properties for each Point.
Update process_logbook_queue_fn, improve invalid logbook detection mostly do to multiple GPS, remove logbook with more than 60% of metric are with 100m.
2023-11-30 16:10:50 +01:00
xbgmsharp
af003d5a62 Update api.export_moorages_gpx_fn, fix moorage id and url path.
Update api.delete_logbook_fn, set api.metrics to moored and substract -1 to moorage count
2023-11-30 15:53:50 +01:00
xbgmsharp
ecc9fd6d9f Update comment order 2023-11-30 12:50:46 +01:00
xbgmsharp
df5f667b41 Update README 2023-11-29 22:14:47 +01:00
xbgmsharp
1bfa04a057 Update pgcron jobs.
Fix clean_up process logs.
Add vaccum for auth schema.
Add reindex for api schema.
Run notificiation every minute instead of 2
2023-11-29 22:00:19 +01:00
xbgmsharp
a1e8827479 Release 0.5.1 2023-11-27 22:50:31 +01:00
xbgmsharp
d837dc57fb Update test version 2023-11-27 22:50:21 +01:00
xbgmsharp
90e8b24321 Update openapi documentation 2023-11-27 22:50:04 +01:00
xbgmsharp
a1ccfd5f7c Update frontend pre-release 0.8.0-beta2 2023-11-27 22:49:15 +01:00
xbgmsharp
6ecb345758 Fix Enforce public vessel name to be alphanumeric.
Fix typo for pushover notification
2023-11-27 22:44:26 +01:00
xbgmsharp
0709bc83c4 Fix api.monitoring_view, api.stays_view, api.stay_view and api.moorages_stays_view 2023-11-27 22:43:27 +01:00
xbgmsharp
8c777cd028 Update frontend pre-release 0.8.0 2023-11-24 12:06:04 +01:00
xbgmsharp
cfe3105f87 Update test to math latest release 2023-11-24 12:05:07 +01:00
xbgmsharp
5af24a1878 Release 0.5.0 2023-11-24 12:00:35 +01:00
xbgmsharp
0aae8d002b Update process_logbook enforce more than 2 metrics to be consider a log 2023-11-24 12:00:10 +01:00
xbgmsharp
22c69a2fd9 Update postgrest PGRST_DB_POOL to the connections limit of api_anonymous role 2023-11-23 17:42:34 +01:00
xbgmsharp
c5f1b85a16 Update openapi documentation 2023-11-23 15:14:58 +01:00
xbgmsharp
0157fe12e5 Update ERD schema 2023-11-23 15:14:45 +01:00
xbgmsharp
ead2b99e7f Update comments on api schema 2023-11-23 15:02:06 +01:00
xbgmsharp
711d5a0d40 Update auth.accounts, add public_id for future use. 2023-11-23 10:27:34 +01:00
xbgmsharp
7e52065ef8 Update metadata trigger, Ignore and overwrite the time sent be the vessel, fix #3 2023-11-23 10:24:16 +01:00
xbgmsharp
f65873db81 Update README 2023-11-21 20:44:36 +01:00
xbgmsharp
347299d76e Add missing description to python function 2023-11-21 20:44:09 +01:00
xbgmsharp
effeb29915 Update test to logs_view change 2023-11-20 21:22:25 +01:00
xbgmsharp
9329a6d04b Add triiger on vessel creation to set the public_vessel name 2023-11-20 21:16:17 +01:00
xbgmsharp
70be4fb295 Update logs_view, show only processed logbook 2023-11-20 21:15:37 +01:00
xbgmsharp
5960447297 Update/fix trigger, remove the actual data 2023-11-20 21:15:15 +01:00
xbgmsharp
f240222b98 Update test to match change from SERIAL to IDENTITY, fix d76964f3db 2023-11-19 22:18:07 +01:00
xbgmsharp
0218f2fa73 Follow best practive don't use timestamp(0) 2023-11-19 22:17:47 +01:00
xbgmsharp
79a96c7556 Update tests, debug mermerd Something went wrong! with github action 2023-11-19 21:50:40 +01:00
xbgmsharp
21f96483f5 Follow best practive don't use timestamp(0)
https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_timestamp.280.29_or_timestamptz.280.29
2023-11-19 21:49:19 +01:00
xbgmsharp
4c6d6290f0 Update schemalint to add more best practice support 2023-11-19 21:40:58 +01:00
xbgmsharp
dc02dc886d Update SERIAL to IDENTITY test 2023-11-19 21:38:57 +01:00
xbgmsharp
6355f98792 Update openapi documentation 2023-11-19 21:38:22 +01:00
xbgmsharp
7543c93dcf Update ERD schema 2023-11-19 21:38:07 +01:00
xbgmsharp
d76964f3db Update test to match change from SERIAL to IDENTITY 2023-11-19 21:36:15 +01:00
xbgmsharp
de651ea7ab Replace type SERIAL by IDENTITY as per best practice.
https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_serial
2023-11-19 21:34:20 +01:00
xbgmsharp
fddd3df05e Replace type TIMESTAMP WITHOUT TIME ZONE by TIMESTAMPZ with timezone forcing UTC as per best practice. 2023-11-19 21:10:28 +01:00
xbgmsharp
472131efbd refactor metrics_trigger_fn, set previous_metric as record instead of individual value 2023-11-19 18:03:54 +01:00
xbgmsharp
1f7bb433e2 Update gpx moorages export, make urk link dynamic 2023-11-18 23:03:42 +01:00
xbgmsharp
c4fa9f5512 Update reverse geo loc, avoid loop 2023-11-18 23:03:07 +01:00
xbgmsharp
b005f592e9 Update project description 2023-11-18 22:48:48 +01:00
xbgmsharp
6cc13313f1 Update tests to match the new anonymous access feature 2023-11-18 21:38:55 +01:00
xbgmsharp
fdb466abde Refactor anonymous access, update public.check_jwt, api.ispublic_fn to allow anoymous base on public vessel name 2023-11-18 21:37:20 +01:00
xbgmsharp
5f0adb67c8 Update openapi 2023-11-18 21:34:32 +01:00
xbgmsharp
2e170c5480 iUpdate ERD 2023-11-18 21:34:22 +01:00
xbgmsharp
5dd2875b91 Update README 2023-11-18 13:34:48 +01:00
xbgmsharp
ee131e0e70 Update metrics_trigger_fn, fix stay postprocessing, only when gone. 2023-11-15 00:16:25 +01:00
xbgmsharp
6426e14d54 Update openapi documentation 2023-11-14 23:56:54 +01:00
xbgmsharp
8fff17dee3 Update process_stay_queue_fn, process stays only when gone 2023-11-14 23:51:23 +01:00
xbgmsharp
eb8ba54230 Update tests to match refactore of logs,stays,moorages. Draft anonymous access 2023-11-14 23:49:43 +01:00
xbgmsharp
f9ed13761c Update ERD mermiad diagram 2023-11-14 23:49:30 +01:00
xbgmsharp
e1e7da779e Update tests to match refactor of Logs,Stays,Moorages
Update dynamic openapi documentation
Add dynamic mermaid schema documentation
2023-11-14 23:28:20 +01:00
xbgmsharp
c879c4bdab Refactor Logs,Moorages,Stays
Update Logs,Moorages,Stays view
Add public.process_lat_lon_fn
Add public.moorage_update_trigger_fn
Add public.moorage_delete_trigger_fn
Update debug on metrics_trigger_fn
Deprecated public.process_moorage_queue_fn
2023-11-14 23:24:30 +01:00
xbgmsharp
e5f2469358 Add overpass-turbo api to improve geo location detection 2023-11-14 23:20:58 +01:00
xbgmsharp
480409de12 Update ERD schema, add mermerd config 2023-11-12 15:21:45 +01:00
xbgmsharp
9d8a7294e0 Update ERD diagram 2023-11-12 14:55:16 +01:00
xbgmsharp
e3ae6b4243 Update ERD Mermaid diagram 2023-11-12 14:53:24 +01:00
xbgmsharp
268ce5b908 Update ERD README 2023-11-12 14:51:21 +01:00
xbgmsharp
ce55a58c87 Update diagram layout 2023-11-12 14:50:30 +01:00
xbgmsharp
14e2103e0f Update Mermaid 2023-11-12 13:09:25 +01:00
xbgmsharp
8025fc4d52 Update ERD doc 2023-11-12 13:06:09 +01:00
xbgmsharp
117bdd2e3f Update ERD diagram to be dynamic 2023-11-12 13:01:46 +01:00
xbgmsharp
b37c33bccb Update api.delete_logbook_fn, add missing query 2023-11-11 11:32:29 +01:00
xbgmsharp
2507545d3f Update anonymous access, add check to allow public access if header x-public-id is present with a base64 value mapping a path and an id. 2023-11-02 21:42:03 +01:00
xbgmsharp
b6ef06d382 Add unit test for public access 2023-11-02 21:39:37 +01:00
xbgmsharp
30de9b76af Update test to prepare for anonymous access 2023-11-02 21:38:25 +01:00
xbgmsharp
8f1558f436 Update OpenAPI doc 2023-11-02 21:37:15 +01:00
xbgmsharp
636fae7ce6 Update lint format 2023-11-02 21:36:53 +01:00
xbgmsharp
8197a26c49 Release 0.4.1 2023-10-31 10:18:02 +01:00
xbgmsharp
ba3b213423 Update SQL Uppercase type 2023-10-30 21:17:21 +01:00
xbgmsharp
0be57a4e70 Update role, add iew select for user_role 2023-10-30 21:16:44 +01:00
xbgmsharp
80163d3fe2 Update schemalint, disable tests, most require large review for future reference 2023-10-30 11:59:24 +01:00
xbgmsharp
c8795b15f3 Update test versions, latest PostgREST 2023-10-29 23:08:21 +01:00
xbgmsharp
e8c0ea5c94 revert logbook naming 2023-10-29 22:54:53 +01:00
xbgmsharp
38ad6084bb Updte openAPI 2023-10-29 22:54:12 +01:00
xbgmsharp
c726187b4d Update explore view 2023-10-29 22:38:45 +01:00
xbgmsharp
3eafa2e13f Update export functiond to order by date rether than id.
Update delete logbook to return a boolean
2023-10-29 21:31:15 +01:00
xbgmsharp
d13f096d4f Update process_logbook_queue_fn, remove the gpx handler 2023-10-29 18:44:43 +01:00
xbgmsharp
e2e37e1f01 Add explore view 2023-10-29 18:44:03 +01:00
xbgmsharp
3bbe309de3 Add delete logbook and dependency stays 2023-10-29 18:43:32 +01:00
xbgmsharp
2be7c787dd Remove duplicated 2023-10-28 22:28:14 +02:00
xbgmsharp
9aecda4752 Update reverse_geocode_py_fn, improve location detection, ignore tag road or highway. Recursive over lower zoom level. 2023-10-28 21:55:29 +02:00
xbgmsharp
9532075bc4 Update frontend to release 0.0.7 2023-10-25 22:18:56 +02:00
xbgmsharp
5996b4d483 Upgrade to the latest version and prepare for next release 2023-10-25 12:40:49 +02:00
xbgmsharp
fdd6fc18e1 Update logbook tests, no more track_gpx field 2023-10-25 12:08:11 +02:00
xbgmsharp
af3866fafe Add new test for export logbooks endpoint 2023-10-25 12:07:07 +02:00
xbgmsharp
53daaa9947 Update vessel_fn to handle empty vessel name from signalk in api.metadata 2023-10-25 09:28:10 +02:00
xbgmsharp
3fed9e0b6a Update public.logbook_update_geojson_fn, expose more data
Update public.logbook_metrics_dwithin_fn, expend large zone for invalid logbook, increase to 15metres.
Disable public.logbook_update_gpx_fn, back to using api.export_logbook_gpx_fn.
Add public.get_app_url_fn, allow limited access to the ap url settings.
2023-10-25 09:27:48 +02:00
xbgmsharp
f3168542fd Update api.timelapse_fn, use track_geom as geometry instead of track_geojson as geojson. x10 faster.
Update api.export_logbook_geojson_fn, output as JSONB as per best practice.
Update api.export_logbook_gpx_fn, dynamic from track_geojson on 'geometry'->>'type' = 'Point'.
Add api.export_logbooks_gpx_fn, export multiple logs in a GPX format
Add api.export_logbooks_kml_fn, export multiple logs in a KML format
2023-10-25 09:27:03 +02:00
xbgmsharp
d266485fef Update table api.logbook, remove tack_gpx colunm, update track_geojson to jsonb type 2023-10-25 09:25:51 +02:00
xbgmsharp
8738becd82 Update OpenAPI documentation 2023-10-25 09:24:27 +02:00
xbgmsharp
ad43ca6629 Update auth.accounts wiht comments 2023-10-23 21:39:53 +02:00
xbgmsharp
9368878963 Update .gitignore 2023-10-22 20:46:20 +02:00
xbgmsharp
496491a43a Update README 2023-10-22 20:46:08 +02:00
xbgmsharp
7494b39abc Update api views with standard name-casing 2023-10-22 19:50:13 +02:00
xbgmsharp
74426a75f8 Update maint tests, cleanup 2023-10-22 19:44:34 +02:00
xbgmsharp
9bac88a8cc Update main tests, add openapi.json update 2023-10-22 19:43:46 +02:00
xbgmsharp
c0af53155c Update tests to match new public_id colunm for auth.accounts 2023-10-22 19:43:06 +02:00
xbgmsharp
e0aa6a4d0e Udpate README 2023-10-22 19:39:39 +02:00
xbgmsharp
2425b674f7 Update auth.accounts, remove unused userid, add public_id integer for anonymous and grafana orgId references 2023-10-22 19:34:41 +02:00
xbgmsharp
b7a1462ec6 Update permissions 2023-10-22 19:28:03 +02:00
xbgmsharp
a31d857a6e Update open API documentation 2023-10-22 19:25:58 +02:00
xbgmsharp
dbeb64c0dc Add schemalint github actioni for best practice reference 2023-10-22 19:24:45 +02:00
xbgmsharp
229c219751 Update metrics_trigger_fn, Ignore metric entry if latitude,longitude are equal,l
Update api.logbook and api.stays tables, update type to text instead of varchar
2023-10-22 18:43:54 +02:00
xbgmsharp
3216ffe42c Update cron_process_no_activity_fn, check for vessel with no activity for more than 200 days then send notification
Add cron_process_deactivated_fn, check for inactivity for more than 1 year to send notification and delete data
2023-10-18 23:25:21 +02:00
xbgmsharp
e2e3e5814e Update api.vessel_fn expose data from the signalk rather than from user input
Update api.settings_fn expose accounts.public_id in settings
Update api.eventlogs_view, cleanup formating
Add api.ispublic_fn, check is a page is publicly accessible from user preferences
2023-10-18 23:21:22 +02:00
xbgmsharp
5f709eb71e Update mocha http tests, add export_logbook_kml_fn basic test 2023-10-18 11:45:39 +02:00
xbgmsharp
d5bf36a85c Update message template for notifications 2023-10-18 11:44:54 +02:00
xbgmsharp
90d48c0c52 Update cron job, fix job details cleanup 2023-10-16 21:46:09 +02:00
xbgmsharp
62707aa86c Update export_logbook_gpx_fn, update comment 2023-10-16 16:31:42 +02:00
xbgmsharp
ac187a1480 update kml export, fix linestrng xml export 2023-10-16 12:21:25 +02:00
xbgmsharp
7b0bf7494f Udpate get_user_settings_from_vesselid, optimize query use citext extensions.
Update public.check_jwt, fix typo..
2023-10-16 11:53:44 +02:00
xbgmsharp
c64219e249 Fix init export_gpx2 2023-10-16 00:55:33 +02:00
xbgmsharp
2127dd7fcb Update github actions test, increase delay to allow the db to be loaded from initdb 2023-10-16 00:48:42 +02:00
xbgmsharp
2a583b94dc Update api.export_logbook_kml_fn, allow export in SML content-type 2023-10-16 00:37:16 +02:00
xbgmsharp
147d9946c3 Update connections limit to 20 for anonymous 2023-10-16 00:29:16 +02:00
xbgmsharp
993cfaeaff Update cron jobs 2023-10-13 15:59:21 +02:00
xbgmsharp
3e70283221 Update public.logbook_update_geojson_fn formating 2023-10-13 15:58:40 +02:00
xbgmsharp
0697acb940 Update message template 2023-10-13 15:40:15 +02:00
xbgmsharp
8ca4d03649 Update permisions for user_role and grafana 2023-10-13 15:39:18 +02:00
xbgmsharp
7a465ff532 Update grafana home dashboard with geomap 2023-10-13 14:58:41 +02:00
xbgmsharp
96dce86678 Update api.export_logbook_kml_fn but still not working in REST 2023-10-11 17:33:55 +02:00
xbgmsharp
8dd827f70d Add new SQL tests for update_logbook_observations_fn 2023-10-11 17:31:43 +02:00
xbgmsharp
572f0cd19d Fix job_run_details_cleanup_fn 2023-10-11 17:20:08 +02:00
xbgmsharp
047f243758 Fix api.update_logbook_observations_fn. 2023-10-11 17:19:10 +02:00
xbgmsharp
5c494896c6 Increase default number of connection ofr grafana_auth 2023-10-11 17:18:41 +02:00
xbgmsharp
b7e717afbc Update notificaiton jobs, fix sql query for no_vessel,no_metadata,no_activitye 2023-10-10 13:51:51 +02:00
xbgmsharp
2f3912582a Update api.export_logbook_kml_fn fix export 2023-10-09 20:38:24 +02:00
xbgmsharp
f7b9a54a71 Update KML export with basic LineString support 2023-10-09 20:30:27 +02:00
xbgmsharp
4e554083b0 Add Row level security to vessel_view. Clean up code, unsing pg15 RLS can be apply to view 2023-10-09 16:27:19 +02:00
xbgmsharp
69b6490534 Update api.vessels_view LIMIT to last metrics.
Add Rowlevel security measures to view
2023-10-09 16:26:02 +02:00
xbgmsharp
8b336f6f9b Add explicit schema when public 2023-10-09 16:22:30 +02:00
xbgmsharp
ef5868d412 Update public.logbook_update_gpx_fn to be display in order
Update public.logbook_update_geojson_fn formating
2023-10-09 16:13:56 +02:00
xbgmsharp
ce532bbb4d Fix typo KLM KLM file extensions 2023-10-09 16:13:09 +02:00
xbgmsharp
66999ca9bb Update api.timelapse_fn, order logs by id to be disaply in order
Add draft support for KML export
2023-10-09 16:10:25 +02:00
xbgmsharp
65d0a6fe4b Update frontend to latest dev 2023-10-06 00:22:32 +02:00
xbgmsharp
f7724db62a Update home dashboard 2023-10-06 00:05:37 +02:00
xbgmsharp
01c20651a4 update grafana 2023-10-05 23:32:24 +02:00
xbgmsharp
57d38ba893 Update reverse geocode, fix error on invalid geocode 2023-10-05 00:42:07 +02:00
xbgmsharp
b817a837d0 Release 0.3.0 2023-10-04 16:59:49 +02:00
xbgmsharp
e1fccabba5 Revert notification, send reminders every Sunday 2023-10-04 16:59:20 +02:00
xbgmsharp
b386e307f9 Update tests output, release 0.3.0 and latest version of PostgREST 2023-10-04 16:54:44 +02:00
xbgmsharp
53b25e1656 Add public.delete_vessel_fn, delete all data received from a vessel 2023-10-04 16:40:45 +02:00
xbgmsharp
9c7301deac Update login fn to return 401 Unauthorized vs 403 Forbidden 2023-10-04 16:39:40 +02:00
xbgmsharp
0f08667d3f Update Notifications/Reminders for no vessel & no metadata & no activity to once month 'At 08:01 on day-of-month 6 and on Sunday.' 2023-10-03 22:30:38 +02:00
xbgmsharp
baea4031b8 Update tests to match language check 2023-10-02 21:41:02 +02:00
xbgmsharp
3dcae9199f Update reverse code, enforce english language result 2023-10-02 21:40:44 +02:00
xbgmsharp
e8259d231e Update tests, pg_language change 2023-10-01 22:55:13 +02:00
xbgmsharp
dd81d49895 Update ERD api and public schema change 2023-10-01 22:51:56 +02:00
xbgmsharp
b861e4151c Update open API 2023-10-01 22:21:01 +02:00
xbgmsharp
42cfa34de8 Update tests, update timescale version 2023-10-01 22:13:54 +02:00
xbgmsharp
fa48d23b1a Add new fn for new cron schedule jobs no_vessel,no_metadata,no_activity. Update logging, fix typo 2023-10-01 22:12:15 +02:00
xbgmsharp
a28ea4631b Add new weekly cron notification for no_vessel,no_metadata,no_activity 2023-10-01 22:11:10 +02:00
xbgmsharp
1793dba64f Add new email templates, for no vessel created, no vessel connected, no recent vessel data. 2023-10-01 22:09:50 +02:00
xbgmsharp
b8c70f43b9 Add new helper fn, isdouble 2023-10-01 13:47:45 +02:00
xbgmsharp
be5c3e9a6f Update api.metrics, remove CONSTRAINT on lat and lon to ingore silently invalid value 2023-10-01 13:46:59 +02:00
xbgmsharp
427d30681e Update vessels views and fn, add plugin version, offline status and duration 2023-09-29 22:42:43 +02:00
xbgmsharp
3130394ab0 Update api.export_moorages_geojson_fn, add stay code 2023-09-29 22:40:52 +02:00
xbgmsharp
4e1e890ef7 Fix reset password, ambigouis colunm 2023-09-24 15:39:28 +02:00
xbgmsharp
f46787ca72 Update API documentation 2023-09-22 12:23:21 +02:00
xbgmsharp
6bb3fd7243 Update tests to match github actions results 2023-09-22 12:12:03 +02:00
xbgmsharp
27ab0d590f Update tests to match github actions results 2023-09-22 12:05:34 +02:00
xbgmsharp
e295380bcf Update stay_at table, fix typo in description 2023-09-22 12:05:04 +02:00
xbgmsharp
f9cebf1bda Update tests results 2023-09-22 11:08:17 +02:00
xbgmsharp
51bfc3ca9a Update github actions, Revert previous, extra second in durattion! 2023-09-22 10:52:18 +02:00
xbgmsharp
7d3667726b Update tests restult to match github actions, internval have a 1S extra!?!, 2023-09-21 23:32:39 +02:00
xbgmsharp
5ec987e6bc Update test to math github actions rego reverse result 2023-09-21 23:26:06 +02:00
xbgmsharp
cbef039a26 Update tests results, new interval output style iso, new reverse_geo_py output with jsonb 2023-09-21 23:18:51 +02:00
xbgmsharp
23780e2c01 Update logbook,stays,moorage process functions to match reverse_geocode_py_fn jsonb ouput 2023-09-21 23:17:25 +02:00
xbgmsharp
a1306f06e2 Update reverse_geocode_py_fn, output jsonb to add country_code filed 2023-09-21 23:16:42 +02:00
xbgmsharp
ed90fdd01d Add debug in reverse_geocode_py, github action return different result 2023-09-20 16:56:44 +02:00
xbgmsharp
23bce1ad26 Update test result 2023-09-20 16:56:09 +02:00
xbgmsharp
093992443b Udpate tests results 2023-09-20 00:22:10 +02:00
xbgmsharp
99dea0dbc8 Add default database date and interval style, set interval style to iso_8601 format 2023-09-19 23:29:36 +02:00
xbgmsharp
7edd2be1fd Update api_fn, Add api.stats_stays_fn, Update api.stats_logs_fn, Add logs_by_day_fn 2023-09-19 23:29:15 +02:00
xbgmsharp
e8a899f36c Update metrics_trigger_fn, Add validation check for speedOverGround.
Ignore if speedOverGround is over 40.
2023-09-19 23:29:00 +02:00
xbgmsharp
35940917e0 Update api.moorages_view and api.moorage_view, add stay code and stay description in web view 2023-09-14 09:52:19 +02:00
xbgmsharp
ecb6e666d2 Update api.moorages_view 2023-09-13 21:58:51 +02:00
xbgmsharp
7b11de9d0d Add support for logbook observations jsonb 2023-09-13 21:57:38 +02:00
xbgmsharp
788b6f160b Update Grafana role with monitoring viewse 2023-09-13 21:56:26 +02:00
xbgmsharp
cad4d38595 Update README 2023-08-26 13:56:47 +02:00
xbgmsharp
1e177dd770 Update 2e2 frontend tests 2023-08-25 11:23:09 +02:00
xbgmsharp
96e91784c5 More github actions e2e testing 2023-08-25 11:14:10 +02:00
xbgmsharp
d5330ca482 Update github actions front-end e2e tests 2023-08-25 11:04:05 +02:00
xbgmsharp
4e304bfc53 Update README 2023-08-25 11:03:48 +02:00
xbgmsharp
b237d91990 Update github actions frontend tests 2023-08-23 13:00:36 +02:00
xbgmsharp
6ae7591f3d Update tests reduce check to postgres version to remove architecture from check 2023-08-23 12:48:49 +02:00
xbgmsharp
3a4aadc81a Update postgrest version parsing 2023-08-23 12:46:35 +02:00
xbgmsharp
c8c10dd51c Release 0.2.3 2023-08-23 12:30:55 +02:00
xbgmsharp
1bfea2ade5 Tests, add missing package.json 2023-08-23 12:29:11 +02:00
xbgmsharp
5105322eed Add unit tests mocha and sql 2023-08-23 12:23:51 +02:00
xbgmsharp
2d3c531960 Update api.logbook comment extra column 2023-08-22 17:00:43 +02:00
xbgmsharp
f37156f15c update github actions 2023-08-22 16:59:50 +02:00
xbgmsharp
11e0493964 Update README, openapi link 2023-08-22 16:59:40 +02:00
xbgmsharp
eee9ea6065 Update logbook extra json obj, add observations (seaState,cloudCoverage,Visibility) default values 2023-08-22 16:41:44 +02:00
xbgmsharp
b2f1e5e0e9 Update versions, add postgrest version 2023-08-22 16:41:01 +02:00
xbgmsharp
2a77216ef6 Update API schema comment for OpenAPI documentation. 2023-08-21 17:13:32 +02:00
xbgmsharp
eee862900d Update openapi documentation. 2023-08-21 15:24:43 +02:00
xbgmsharp
dc54d0c5e3 Update README 2023-08-21 15:11:07 +02:00
xbgmsharp
2684c83ce8 Update README 2023-08-21 15:09:20 +02:00
xbgmsharp
412a6e8a58 Add openapi documentation 2023-08-20 11:19:53 +02:00
xbgmsharp
f0198aeb3e Update github actions 2023-08-18 01:49:08 +02:00
xbgmsharp
2a8f74a62f Update github actions 2023-08-18 01:42:24 +02:00
xbgmsharp
0eda59d68a Update github actions 2023-08-18 01:36:53 +02:00
xbgmsharp
652b72c274 Update github actions 2023-08-18 01:26:53 +02:00
xbgmsharp
3b798c99c4 Update github actions e2e tests 2023-08-18 00:56:58 +02:00
xbgmsharp
4b6eeefdba Update web_dev and web_tests containers for e2e testings 2023-08-18 00:56:01 +02:00
xbgmsharp
306623a55a Add new API function, api.stats_logs_fn, export logs statistics 2023-08-18 00:54:58 +02:00
xbgmsharp
5230a83833 Update logbook table, add comment on column 2023-08-18 00:54:11 +02:00
xbgmsharp
96f80b9584 Update export logbook gpx function, move the function inside the logbook cron process workflow rather than directly on an API call. 2023-08-15 23:18:07 +02:00
xbgmsharp
c63bf63308 Update api.timelapse_fn, return only logbook with geojson data. 2023-08-15 10:16:23 +02:00
xbgmsharp
0f78d56b37 Ensure moorages view and function have a valid geographic point 2023-08-14 22:50:35 +02:00
xbgmsharp
ae57191cfb Release v0.2.2 2023-08-14 18:29:08 +02:00
xbgmsharp
9bdc777010 Update scheduler role row level security (RLS) 2023-08-14 17:55:36 +02:00
xbgmsharp
49bad13fe7 Update api.recoveir, Enforce/Enable email_notifications 2023-08-14 17:54:20 +02:00
xbgmsharp
d465d91a94 Update unit test README 2023-08-14 17:40:04 +02:00
xbgmsharp
2edff87269 Update foreign key comment. Update eventlogs_view output orderi. 2023-08-14 17:37:34 +02:00
xbgmsharp
e6ce0582d3 Update Why 2023-08-14 17:36:38 +02:00
xbgmsharp
31849a86b1 Update auth schema, remove unused table index 2023-08-14 16:59:18 +02:00
xbgmsharp
de33977c83 dd maintenance cron to rebuild indexs. 2023-08-01 00:10:06 +02:00
xbgmsharp
d0ace87fd7 Update reset password link 2023-08-01 00:05:25 +02:00
xbgmsharp
a7c6254f5f Add new fn to support extra logbook metric. fix english. update debug 2023-07-26 10:43:09 +02:00
xbgmsharp
4ab69d40ef Update api views, expost new etra logbook metric 2023-07-26 10:42:11 +02:00
xbgmsharp
2c62c7b92c Update logbook table schema, add colunm extra for additional json metrics. Fix english 2023-07-26 10:41:28 +02:00
xbgmsharp
75cf68dc78 Update frontend submodule to version 0.0.6 2023-07-21 17:10:32 +02:00
xbgmsharp
febc7f3a60 Fix MATERIALIZED VIEW comment 2023-07-21 17:07:12 +02:00
xbgmsharp
788f609f3b Add drop view statement. Add missing view comment 2023-07-21 17:04:20 +02:00
xbgmsharp
3aa26685eb Fix some more English typo 2023-07-21 17:01:05 +02:00
xbgmsharp
8ce04ec282 Update frontend submodule 2023-07-19 17:47:27 +02:00
xbgmsharp
21cc07f6c0 Update PostgSail architecture design 2023-07-19 17:47:07 +02:00
xbgmsharp
dea8452229 Update frontend submodule 2023-07-18 18:33:54 +02:00
xbgmsharp
6b0eb72b82 Update public.check_jwt (db-pre-request function) to add user.id session variables 2023-07-18 18:30:24 +02:00
xbgmsharp
94960ad391 Update api tables, add api.metrics primary key. Update metrics_trigger_fn to enforce a vessel_id. 2023-07-18 18:29:31 +02:00
xbgmsharp
577da72451 Update workflow name run test 2023-07-18 18:26:35 +02:00
xbgmsharp
ea2f3ec6d1 Add eventlogs view, list process_queue table by ref_id. Add has_vessel_metadata_fn to expose vessel metadata 2023-07-18 15:19:30 +02:00
xbgmsharp
2eb645123b Update and add monitoring views 2023-07-18 15:18:29 +02:00
xbgmsharp
197e080035 Update permisions, Add monitoring and eventlogs views. Add scheduler RLS permissions. 2023-07-18 15:16:42 +02:00
xbgmsharp
b6b082dd8c Update github workflows 2023-07-12 17:33:14 +02:00
xbgmsharp
fd97b4c616 Update github workflows 2023-07-12 17:24:35 +02:00
xbgmsharp
582cd4460e Update github workflows 2023-07-12 17:21:58 +02:00
xbgmsharp
c056737e2f Grafana remove warning or not unique dashboard uid 2023-07-12 17:08:33 +02:00
xbgmsharp
0ffe646050 Update README 2023-07-12 17:04:18 +02:00
xbgmsharp
dfdc54062d Update github workflow 2023-07-12 17:02:02 +02:00
xbgmsharp
4a0f4c77ca Update pgadmin config hostname 2023-07-12 00:29:24 +02:00
xbgmsharp
8038a95b60 Add README for tests 2023-07-12 00:28:35 +02:00
xbgmsharp
328cbc2741 Update grafana support 2023-07-12 00:24:48 +02:00
xbgmsharp
482510121c Update README 2023-07-12 00:15:55 +02:00
xbgmsharp
f0929fd633 Update and improve github workflows, add db and grafan tests 2023-07-12 00:15:22 +02:00
xbgmsharp
9483560a18 Add new github workflows 2023-07-11 22:36:18 +02:00
xbgmsharp
0f7284b8c8 dd dokcerfile for test image 2023-07-11 22:35:39 +02:00
xbgmsharp
3a2ef95e25 Remove comments 2023-07-11 01:23:20 +02:00
xbgmsharp
9bc463c45e Update frontend submodule 2023-07-11 01:21:58 +02:00
xbgmsharp
5bcb51f803 Update docker-compose.dev links for tests 2023-07-11 01:01:41 +02:00
xbgmsharp
c145d1c1df Update idocker-compose.dev environment for tests 2023-07-11 00:59:53 +02:00
xbgmsharp
87d9380882 Update github action test 2023-07-10 19:06:52 +02:00
xbgmsharp
40256a1c0e Update github action test 2023-07-10 19:00:21 +02:00
xbgmsharp
2ffbbbe885 Update github actions 2023-07-10 18:56:32 +02:00
xbgmsharp
ef89437660 Update github action test 2023-07-10 18:52:58 +02:00
xbgmsharp
f01a4b9605 Update env variable smaple base on docker-compose using hostname 2023-07-10 17:24:42 +02:00
xbgmsharp
8f4a8c14ee Update github actions test 2023-07-10 17:21:48 +02:00
xbgmsharp
c978df1edb Debug github action test 2023-07-10 17:18:38 +02:00
xbgmsharp
3525b88bc2 Update env example defualt. Udpate github action test 2023-07-10 17:11:13 +02:00
xbgmsharp
47249b90fe Update github actions test, source env 2023-07-10 17:07:32 +02:00
xbgmsharp
e06db937e5 Add github actions test 2023-07-10 17:03:03 +02:00
xbgmsharp
cca75d252a Add devcontainer support 2023-07-10 17:02:35 +02:00
xbgmsharp
5144050875 Udpate docker-compose file. refactor services and network 2023-07-10 17:01:32 +02:00
xbgmsharp
931544663e Update README 2023-07-09 23:59:56 +02:00
xbgmsharp
1c62aaa853 Update codesandbox tasks 2023-07-09 20:15:12 +02:00
xbgmsharp
f1903ba3eb Update frontend 2023-07-09 20:14:47 +02:00
xbgmsharp
ab5becb31d More docker dev env 2023-07-05 18:42:13 +02:00
xbgmsharp
44b034873e More docker dev environment 2023-07-05 18:38:30 +02:00
xbgmsharp
e398eb2a99 Update docker-compase path to OSx valid 2023-07-05 18:35:05 +02:00
xbgmsharp
57ff0b97ea Update mounted bind to be allow on OSX 2023-07-05 18:32:12 +02:00
xbgmsharp
a633731ae7 Add submodule init as task 2023-07-05 17:42:35 +02:00
xbgmsharp
b34162f11b Update pgadmin services with fixed IP and services dependencies
Move .codesandbox/servers.json -> pgadmin_servers.json
2023-07-05 17:41:35 +02:00
xbgmsharp
a5d1495864 Add Docker Dev Environments 2023-07-05 16:20:12 +02:00
xbgmsharp
896576d0f8 Merge pull request #2 from xbgmsharp/draft/sweet-snow
coddesandbox
2023-07-05 12:36:16 +02:00
Display name
e3309d9784 Update docker-compose.yml 2023-07-05 08:59:34 +00:00
Display name
861fbf5502 Add codesandbox. Create servers.json and tasks.json 2023-07-05 08:58:48 +00:00
Display name
3f84a731b2 Add submodules frontend 2023-07-05 08:58:13 +00:00
xbgmsharp
c66797fa4f Add database creation, missing from spliting api schema into multiple files 2023-07-05 09:41:16 +02:00
xbgmsharp
d56d5d54a8 Split api schema in multiples files 2023-07-03 14:02:31 +02:00
xbgmsharp
f86a1b4382 Update README , add cloud development sandbox 2023-07-03 10:50:31 +02:00
xbgmsharp
51b6e8fa7c release 0.2.1 2023-07-03 10:14:29 +02:00
xbgmsharp
89af44efcc Add why postgsail 2023-07-03 10:14:12 +02:00
xbgmsharp
a64ef5850d Update web frontend container 2023-07-03 10:13:48 +02:00
xbgmsharp
da100ddd18 Update ERD README 2023-07-03 09:38:42 +02:00
xbgmsharp
92ce0503dd Update delete_account_fn 2023-06-30 13:37:56 +02:00
xbgmsharp
61d40fd7b6 Update grafana dashboard to use vessel_id in replacement of client_id 2023-06-28 23:49:32 +02:00
xbgmsharp
c318f2d338 Cleanup check_jwt function from client_id checks 2023-06-28 15:51:35 +02:00
xbgmsharp
c7c14fa5a1 Release v0.2.0 2023-06-26 17:31:10 +02:00
xbgmsharp
4fc68ae805 Cleanup metrics trigger from http api call 2023-06-26 17:30:44 +02:00
xbgmsharp
3eb67abedb Update logging for api.metrics trigger.
Force vessel_id to import from SQL cli run as username role
2023-06-26 13:31:44 +02:00
xbgmsharp
894dbf0667 Limit cron processing per bath of 100 2023-06-26 12:26:33 +02:00
xbgmsharp
f526b99853 Cleanup logging for badges processing 2023-06-26 12:25:47 +02:00
xbgmsharp
a670038f28 Update ERD with vessel_id 2023-06-25 21:49:13 +02:00
xbgmsharp
2599f40f7b Add ref_id to process_queue table to allow timeline per user_id and/or vessel_id 2023-06-25 15:12:04 +02:00
xbgmsharp
4d833999e8 Update vessel dependency to vessel.id instead of client_id.
Large commit, to fix a long pending issue for vessel wihtout a proper client_id from signalk.
2023-06-25 09:53:25 +02:00
xbgmsharp
b4dc93ba0e Update comment 2023-06-25 09:51:07 +02:00
xbgmsharp
764a6d6457 Add postgis geography marine areas 2023-06-25 00:40:58 +02:00
xbgmsharp
2e9ede6da2 Fix badge notification 2023-06-24 13:10:48 +02:00
xbgmsharp
cc67a3b37d Release v0.1.0 2023-06-23 11:03:16 +02:00
xbgmsharp
64ecbfc698 Fix typo 2023-06-22 23:28:45 +02:00
xbgmsharp
b19eeed59a Update badges, renew badge 2023-06-22 23:28:05 +02:00
xbgmsharp
8f5cd4237d dd new API endpoint, api.vessel_details_fn(), extend additionals vessels properties 2023-06-22 23:26:44 +02:00
xbgmsharp
7b3a1451bb Update templates messages
Add iso3166 country list
Link MMSI MID Codes with iso3166 country list
2023-06-21 15:49:10 +02:00
xbgmsharp
a2cdd8ddfe Add badges support 2023-06-20 15:24:47 +02:00
xbgmsharp
7a04026e67 Marked old function as deprecated 2023-06-20 09:05:27 +02:00
xbgmsharp
fab496ea3d Add web frontend container and update telegram container env 2023-06-07 12:22:20 +02:00
xbgmsharp
4f31831c94 Update prepare jwt auth with user_id 2023-06-07 12:20:40 +02:00
xbgmsharp
300e4bee48 Update debug output 2023-06-07 12:19:15 +02:00
xbgmsharp
99e258c974 Update Send notification telegram SQL requets 2023-05-25 16:37:01 +02:00
xbgmsharp
970c85c11e Update reverse geoip python function
Update parsing geosjon python function
2023-05-25 16:35:39 +02:00
xbgmsharp
bbf4426f55 Update OTP, add support for telegram 2023-05-25 16:34:35 +02:00
xbgmsharp
a8620f4b4c Update api_anonymous function persmision to support telegram 2023-05-25 16:28:59 +02:00
xbgmsharp
15accaa4cb Update api.metadata version fields to type TEXT
Update debug output formating
Update SQL view statements, Make SQL error proof with REPLACE statement
2023-05-25 16:26:19 +02:00
xbgmsharp
8d382b48ac Add telegram bot 2023-05-22 11:34:17 +02:00
xbgmsharp
2983f149ad Update .env sample for Telegram-bot 2023-05-17 16:37:31 +02:00
xbgmsharp
a1ca97b549 Release 0.0.11 2023-05-11 13:32:32 +02:00
xbgmsharp
119c1778e6 Update README 2023-05-08 10:20:41 +02:00
xbgmsharp
11489ce4aa Update grafana dashboards and configuration 2023-05-03 17:04:08 +02:00
xbgmsharp
42b070baa8 Update permsisions for grafana_role and grafana_auth_proxy role 2023-05-02 18:29:27 +02:00
xbgmsharp
a1df7b218c Update versions to include used extensions 2023-05-02 18:27:04 +02:00
xbgmsharp
160d6aa569 Update comment on fonction send_notifications 2023-04-23 23:13:30 +02:00
xbgmsharp
a2903e08ac Add role comment for user scheduler 2023-04-23 23:12:35 +02:00
xbgmsharp
5a74914eac Export helpers function to a separate file 2023-04-23 11:06:49 +02:00
xbgmsharp
55dc6275ee Add permision to morrage_view for user_role 2023-04-23 11:06:24 +02:00
xbgmsharp
f2c68c82d8 Fix error if data type is None 2023-04-23 11:04:59 +02:00
xbgmsharp
578ca925db Export helpers/generic functions to a new file 2023-04-23 11:00:40 +02:00
xbgmsharp
ae14017cfc Update cron fn, fix tipo 2023-04-23 10:57:28 +02:00
xbgmsharp
1b42e3849f Update stay(s),moorage(s) view with more details 2023-04-12 21:13:03 +02:00
xbgmsharp
2ffcbc5586 Remove unused api essel funtions 2023-04-03 22:51:57 +02:00
xbgmsharp
235506f2bc Update fucntions, remove typo on Unknow 2023-04-03 22:50:47 +02:00
xbgmsharp
5a2ba54b2a Update export GPX API endpoints, moorages and log.
Logs gpx should be move to the cron process to remove api.metrics dependency
2023-04-03 22:49:16 +02:00
xbgmsharp
122c44c338 Update new API endpoint api.export_moorages_gpx_fn 2023-04-02 17:49:30 +02:00
xbgmsharp
2e451fa93c Update API schema with new endpoint, Add moorages map export (geojson,gpx) and update log export gpx 2023-04-01 19:29:12 +02:00
xbgmsharp
d26d008b47 Update job_run_details_cleanup_fn to remove logs older than 90 days 2023-04-01 19:28:38 +02:00
xbgmsharp
6a6239f344 Update description of the public schema 2023-04-01 19:28:15 +02:00
xbgmsharp
2f6a0a6133 Update public.logbook_update_geojson_fn to export id and time 2023-04-01 19:27:16 +02:00
xbgmsharp
bda652b87e Update python function to filter geojson, add parameter to filter on LineString or Point
Still pending work using pg type and transform json
2023-04-01 19:25:43 +02:00
xbgmsharp
2f6bb6d5d9 Add new public function public.jsonb_diff_val 2023-04-01 19:25:14 +02:00
xbgmsharp
2cd9b0dd6c Add API endpoint api.timelapse_fn 2023-03-28 19:15:41 +02:00
xbgmsharp
13e4f453d5 Add function job_run_details_cleanup_fn, delete old job details log 2023-03-28 19:14:53 +02:00
xbgmsharp
bc7d51c71e Add geojson_py_fn in python 2023-03-28 19:13:42 +02:00
xbgmsharp
95d3c5bded Add new public function jsonb_recursive_merge and input validation: isdate, istimestamptz 2023-03-28 19:12:35 +02:00
xbgmsharp
f0c6f92920 pg_cron, add job_run_details_cleanup 2023-03-28 19:11:02 +02:00
xbgmsharp
852d2ff583 Release v0.0.10 2023-03-03 16:09:05 +01:00
xbgmsharp
7cf7905694 Update pushover link to work in prod env 2023-03-03 08:35:08 +01:00
xbgmsharp
0f8107a672 Update api.pushover_subscribe_link_fn and fix api.generate_otp_fn 2023-02-26 23:23:07 +01:00
xbgmsharp
77dec463d1 Add urlescape_py_fn to url encode using python 2023-02-26 22:57:44 +01:00
xbgmsharp
8ff1d0a8ed Allow user_role to access new api view total_info_view, stats_logs_view, stats_moorages_view 2023-02-26 21:09:13 +01:00
xbgmsharp
859788d98d Update api.export_logbook_gpx api.export_logbook_geojson
Update api.moorage_view
Add Create api.total_info_view
Add Comment on missing api view
Add security_invoker on stats view
2023-02-25 23:11:32 +01:00
xbgmsharp
62642ffbd6 Enforce OTP verification on login 2023-02-24 15:59:08 +01:00
xbgmsharp
c3760c8689 Allow UPSERT of otp code in generate_otp_fn 2023-02-24 15:58:36 +01:00
xbgmsharp
763c9ae802 Update versions fn and view
Add new fn public.has_vessel_fn()
Deprecated unused and bad api.vessels2_view,api.vessel_p_view
2023-02-24 15:57:32 +01:00
xbgmsharp
37abb3ae1f Minimum valid distance is less than 0.010.
Exclude new function from vessel registration.
2023-02-24 15:55:55 +01:00
xbgmsharp
a6da3cab0a Fix vessel_fn to use the latest location rather than the first know location 2023-02-15 16:24:11 +01:00
xbgmsharp
22f756b3a9 Update permissions to views 2023-02-14 19:04:38 +01:00
xbgmsharp
cb3e9d8e57 Update moorages_view and moorage_view with security invoker 2023-02-14 19:04:13 +01:00
xbgmsharp
1997fe5a81 Update logbook_update_geojson_fn, expose less properties in geojson 2023-02-14 12:22:58 +01:00
xbgmsharp
5a1451ff69 Improve process_logbook_queue_fn. Detect and remove stationary movement.
Add logbook_metrics_dwithin_fn function.
2023-02-13 23:56:39 +01:00
xbgmsharp
a18abec1f1 Update views owner permission using security_invoker and security_barrier 2023-02-09 16:47:02 +01:00
xbgmsharp
322c3ed4fb Update messages templates for email,pushover, telegram 2023-02-09 16:46:23 +01:00
xbgmsharp
d648d119cc Update API expose views with the latest pg15 feature security_invoker 2023-02-09 16:31:06 +01:00
xbgmsharp
9109474e8a Fix permission issue when vessel is not connected in public.check_jwt() 2023-02-07 14:49:32 +01:00
xbgmsharp
ca92a15eba boat-listing, make last_contact retrun null rather than empty string 2023-02-07 11:19:30 +01:00
xbgmsharp
d745048a9c Update reverse_geocode_py to fallback base on more field
Don't exit with error so we don't stop the cron process
2023-02-07 11:18:25 +01:00
xbgmsharp
6a0c15d23c process_logbook_queue_fn add more debug and disable unused function 2023-02-07 11:17:24 +01:00
xbgmsharp
fc01374441 Release v0.0.9 2023-02-06 21:54:26 +01:00
xbgmsharp
0ec3f7fe02 Sending null to no value entry when no metadata available 2023-02-06 21:38:02 +01:00
xbgmsharp
2bae8bd861 Fix and Update parameters check for auth functions 2023-02-06 21:37:19 +01:00
xbgmsharp
38d185d058 Add api.recover comment function 2023-02-05 01:18:24 +01:00
xbgmsharp
4342e29c69 Update vessel check, remove mmsi dependency to vessel_id 2023-02-05 01:16:26 +01:00
xbgmsharp
13d8ad9b3d Allow api_anonymous to execute api.recover and api.reset functions 2023-02-04 23:45:31 +01:00
xbgmsharp
caec91b7f2 Add api.recover and api.reset function to allow password reset 2023-02-04 23:44:47 +01:00
xbgmsharp
665a9d30e6 Fix/update vessel_fn sub query because of function owner 2023-02-04 23:42:38 +01:00
xbgmsharp
eb3a14bee4 Add reset query-string support to send_email_py 2023-02-04 23:41:10 +01:00
xbgmsharp
ba935d7520 Update new logbook entry template message
Add new email reset template message
2023-02-04 23:37:40 +01:00
xbgmsharp
11d136214c Disable health check for api (postgrest) and app (grafana) containers
Missing wget or curl to run the test locally
2023-02-02 00:45:14 +01:00
xbgmsharp
ddbeff7d7e Add docker healthy check for PostgreSQL 2023-02-01 23:59:22 +01:00
xbgmsharp
569700e1b3 Update README 2023-01-31 21:28:50 +01:00
xbgmsharp
93f8476d26 Update main process function is retruning result.
Ensure the query is successful for process_logbook_queue_fn and process_stay_queue_fn
2023-01-31 21:17:49 +01:00
xbgmsharp
4eef5595bc Fix tipo 2023-01-31 21:16:09 +01:00
xbgmsharp
cf9c67bb64 Update README 2023-01-29 22:29:47 +01:00
xbgmsharp
1968f86448 Add debug in send_email_py 2023-01-29 22:20:33 +01:00
xbgmsharp
552faa0a16 Add docker healthy check for Grafana 2023-01-28 21:47:05 +01:00
xbgmsharp
c0b6f17488 Add Grafana ENV Settings 2023-01-28 21:34:21 +01:00
xbgmsharp
1ab6501aad Update README 2023-01-28 21:33:30 +01:00
xbgmsharp
07280f1f67 pg_cron async job
Update Notification cron job
Fix cron_vacuum cron job
Disable all cron job for debug
2023-01-28 21:32:11 +01:00
xbgmsharp
d419a582b9 Remove debug helper for OTP 2023-01-28 21:28:02 +01:00
xbgmsharp
69c8ec17f9 Add connected_at on auth.accounts table 2023-01-28 21:25:12 +01:00
xbgmsharp
89d50b7a6a Update postgreSQL conf for PG15 2023-01-28 21:24:33 +01:00
xbgmsharp
976fc52e9a Update telgram obj queries to supprt group chat id 2022-12-28 22:06:10 +01:00
xbgmsharp
cb0b89c8f3 vessel function and view, remove the millisecond from the timestamp result 2022-12-28 22:04:50 +01:00
xbgmsharp
bcbcfa040d Update telegram object queries 2022-12-28 22:04:04 +01:00
xbgmsharp
b7857e0be6 Update email_otp message, remove recipient firstname 2022-12-28 22:03:40 +01:00
xbgmsharp
089876b62a Update api.metrics status to enum type and allow vessel to switch from sailing to motoring 2022-12-25 17:54:48 +01:00
xbgmsharp
fc9fb8769a Update monitorin offline and online to use send_notification_fn 2022-12-17 23:33:19 +01:00
xbgmsharp
3432d358d3 Add reverse_geoip_py_fn python3 function 2022-12-17 23:32:23 +01:00
xbgmsharp
340bda704e Update publick.email_templates table 2022-12-17 23:17:15 +01:00
xbgmsharp
54156ae7c9 Update api.metrics to support 2 dimension hypertable. Update api.metadata tablesto with updated_at handle by moddatetime extension 2022-12-17 23:15:22 +01:00
xbgmsharp
4c4f0bbd37 Update permision for role grafana_auth and grafana 2022-12-17 23:13:18 +01:00
xbgmsharp
b58fce186a Rename grafan container to app 2022-12-12 23:52:24 +01:00
xbgmsharp
c6c78ecffc Update pgsql/timescale grafana source 2022-12-12 23:51:57 +01:00
xbgmsharp
db0e493900 Add grafana config,dashboards,provisioning 2022-12-12 22:28:01 +01:00
xbgmsharp
dea5b8ddf7 Add grafana 2022-12-12 22:12:22 +01:00
xbgmsharp
e9e63fad50 Add ERD description 2022-12-12 22:11:52 +01:00
xbgmsharp
8b45a171e8 Update vessel_role permission for new api.metrics trigger 2022-12-12 16:21:42 +01:00
xbgmsharp
a0216dad6a Refactor metrics_trigger_fn on api.metrics trigger to avoid multiple stay or logbook active 2022-12-09 12:35:18 +01:00
xbgmsharp
ca5bffd88f Add tables MMSI MID Code Filtered by Flag of Registration 2022-12-09 12:34:23 +01:00
xbgmsharp
1dbf71064e Update reverse_geocode_py_fn to return always data if name is null then fallback to address field road,neighbourhood,suburb 2022-12-09 12:33:00 +01:00
xbgmsharp
6888953cbb Add OTP notification, Add OTP DELETE, Add api.pushover_subscribe_link_fn, Add auth.telegram_session_exists_fn 2022-12-06 21:41:14 +01:00
xbgmsharp
105d6b9113 Add AIS Ship Types tables 2022-12-06 21:40:09 +01:00
xbgmsharp
0c2e4b1d83 cron_vaccum_fn does not work 2022-12-05 23:31:45 +01:00
xbgmsharp
f8b1fb472a Remove mmsi dependency, enforce add check valid longitude,latitude, ignore silently NULL longitude,latitude 2022-12-05 23:29:44 +01:00
xbgmsharp
613ac5e29a Remove mmsi dependency and ensure longitude,latitude exist prior post processing 2022-12-05 23:23:51 +01:00
xbgmsharp
5ce5b606e9 Remove mmsi dependency, update to use vessel_id instead 2022-12-05 23:19:58 +01:00
xbgmsharp
0f59a31cdc Fix SQL query IMMUTABLE STRICT 2022-12-05 23:17:45 +01:00
xbgmsharp
58407a84e9 Update auth.vessel.mmsi type to NUMERIC and add conistraint and remove dependency 2022-12-05 23:15:44 +01:00
xbgmsharp
9ae9553254 Allow to set password from env for role grafana_auth 2022-12-05 14:56:53 +01:00
xbgmsharp
494cc9a571 Add new function urlencode_py_fn()
Update debug for send_pushover and send_telegram
2022-12-02 14:36:01 +01:00
xbgmsharp
dbd29ca58a Add new variable PGSAIL_PUSHOVER_APP_URL 2022-12-02 11:50:33 +01:00
xbgmsharp
00cdd7ca18 Refactor web user notification
Add new cron job cron_new_notification
Add public.cron_process_new_notification_fn function
Add public.process_notification_queue_fn function
Update messages template table, align cron name with template notification name
2022-12-02 11:22:28 +01:00
xbgmsharp
34fe0898b2 Split public schame in file by type tables,functions and functions in python 2022-11-30 21:15:47 +01:00
xbgmsharp
3522d3b9d7 Update email type to CITEXT, https://www.postgresql.org/docs/current/citext.html 2022-11-30 21:12:49 +01:00
xbgmsharp
d4f79e7f71 A large commit with new features (pushover, telegram, otp) and fixes
Update reverse_geocode_py_fn validate input
Update email_templates add new message type for pushover, telegram and otp
Update send_email_py_fn mkae email From field humain friendly
Add send_pushover_py_fn python send pushover message
Add send_telegram_py_fn python send telegram message
Add process_account_otp_validation_queue_fn process handle for email validation
Add send_notification_fn refactor notification system to support email,pushover,telegram
Update public.process_queue table
Add new_account_otp_validation_entry_fn trigger
Update postgrest pre db check_jwt to support row security level
2022-11-29 23:50:59 +01:00
xbgmsharp
4df4fa993a U 2022-11-29 23:25:51 +01:00
xbgmsharp
94f79080aa Add new app paremeters for pushover and telegram
PGSAIL_PUSHOVER_APP_TOKEN -> app.pushover_app_token
PGSAIL_TELEGRAM_BOT_TOKEN -> app.telegram_bot_token
2022-11-29 23:24:39 +01:00
xbgmsharp
1a5c0f10c3 Fixed typo in vesselid for auth.vessels table 2022-11-29 23:20:01 +01:00
xbgmsharp
e6309875fb Limit connection to database to 100 2022-11-29 23:19:31 +01:00
xbgmsharp
2e269b9424 Update RSL to 'user.email' settings
Remove dependency to jwt for auth tables
2022-11-29 22:51:07 +01:00
xbgmsharp
40e25b1f8c Add Email OTP validation API endpoint 2022-11-29 22:49:50 +01:00
xbgmsharp
9eec9ad355 Add description to accounts and vessels triggers with moddatetime 2022-11-29 10:39:26 +01:00
xbgmsharp
90d2c3b3a0 Update OTP Code to a random 6 digit 2022-11-29 10:38:45 +01:00
xbgmsharp
d25f31ce0b Add a every 6 minute job cron_process_new_account_otp_validation_queue_fn, delay from cron_new_account 2022-11-28 22:30:55 +01:00
xbgmsharp
c8e722283c Fixed ERD images link 2022-11-28 22:18:55 +01:00
xbgmsharp
2095e9b561 Update images links 2022-11-28 22:13:53 +01:00
xbgmsharp
73addfa928 publish initial release of Entity-Relationship Diagram (ERD) 2022-11-28 22:11:13 +01:00
xbgmsharp
345f190f4e Add CRON for new account pending otp validation notification 2022-11-28 21:51:27 +01:00
xbgmsharp
0682f06ae9 Update update_user_preferences_fn to allow email or telegram user auth (to be improve) 2022-11-28 21:49:57 +01:00
xbgmsharp
8bc0fdaf17 Fixed auth.vessels table creation 2022-11-28 21:48:30 +01:00
xbgmsharp
ab1afeee42 Enable telegram bot auth 2022-11-28 21:47:28 +01:00
xbgmsharp
b6d60dd0d5 Update grafana_auth role description 2022-11-28 18:17:07 +01:00
xbgmsharp
295d0a0a5e Fix SQL vquery 2022-11-28 15:26:43 +01:00
xbgmsharp
a68a0ee3e3 Refactor auth tables (accounts,vessels)
Add unique userid column for jwt auth
Add unique vesselid column for jwt auth
Add new extensions citext,moddatetime
Update email column to citext type for fast queries
Add updated_at column to trak changed managed by moddatetime extension
Update index tables (accounts,vessels)
2022-11-25 23:14:30 +01:00
xbgmsharp
ea7301e1ed Add primary support for new stays,moorages,stats,timelapse views 2022-11-25 22:37:08 +01:00
xbgmsharp
98f5d75429 Add grafana_auth apache proxy auth role 2022-11-25 22:33:54 +01:00
xbgmsharp
adc6799c93 Add comment on role 2022-11-25 22:24:40 +01:00
xbgmsharp
a865e91ce7 Allow anonymous role to excecute telegram and pushover registration function 2022-11-25 22:22:29 +01:00
xbgmsharp
a64425b13f Add new CONSTRAINT on auth.vessels and auth.accounts tables
Add new index on auth.accounts table
2022-11-20 23:27:34 +01:00
xbgmsharp
0586d30381 Improve Row Level Security
Add Row Level Security for table auth.accounts
2022-11-20 23:25:06 +01:00
xbgmsharp
db1d7c63e2 Add function api.update_user_preferences_fn to allow user update their preferences via api 2022-11-20 23:22:29 +01:00
xbgmsharp
4acb4de539 Improve debug output 2022-11-20 23:20:19 +01:00
xbgmsharp
07043ddf08 Add new metadata index
Improve metrics debug output
2022-11-20 23:19:12 +01:00
xbgmsharp
bd05591205 Add a every 15 minute job cron_process_prune_otp_fn 2022-11-20 23:18:15 +01:00
xbgmsharp
95ff1d8ff2 Add missing foreign keys between api.metrics and api.metadata 2022-10-25 16:25:45 +02:00
xbgmsharp
e92515ba66 Fixed email notification,
Update conversion to Nautical Mile
2022-10-24 16:56:05 +02:00
xbgmsharp
8b8087e56d Release v0.0.8 2022-10-19 13:28:31 +02:00
xbgmsharp
7b7aae7dfe Limit scope of cron job using vessel.client_id
Ensure all queries to api.metrics are limited to a specific vessel using client_id
2022-10-19 13:25:49 +02:00
xbgmsharp
be27618dac Update scheduler role permissions 2022-10-19 13:19:03 +02:00
xbgmsharp
7fb24d8cae Add new parameter PGSAIL_APP_UR 2022-10-18 21:39:58 +02:00
xbgmsharp
07c7628973 Limit scope of process functions 2022-09-24 23:53:47 +02:00
xbgmsharp
e42e52eaf0 Update permisions for new API endpoint 2022-09-24 23:52:42 +02:00
xbgmsharp
97e739ffe9 Refactor user_settings 2022-09-24 23:32:17 +02:00
xbgmsharp
3fb2534263 Update README, fix tipo in links 2022-09-24 23:16:24 +02:00
xbgmsharp
9e8009a764 Release v0.0.7 2022-09-21 11:06:23 +02:00
xbgmsharp
dca77c3293 Update permisions for new API endpoint 2022-09-21 09:58:38 +02:00
xbgmsharp
8af527f574 Allow user_role to execute some fuctions without defined vessel 2022-09-21 09:57:53 +02:00
xbgmsharp
0f399293eb Add API endpoint for versions and vessels 2022-09-21 09:52:36 +02:00
xbgmsharp
57dfaf2158 Add new API endpoint to export logbook in GPX or GeoJSON format 2022-09-21 09:51:43 +02:00
xbgmsharp
3a2e091744 Update README 2022-09-21 08:58:09 +02:00
xbgmsharp
7c5bd21e80 Release v0.0.6 2022-09-01 19:33:00 +02:00
xbgmsharp
33af7bec1b Update permissions files 2022-09-01 19:31:25 +02:00
xbgmsharp
023ad56926 Add more api endpoint with dependencies 2022-09-01 19:27:11 +02:00
xbgmsharp
91cf679876 Update comment 2022-09-01 19:25:50 +02:00
xbgmsharp
1b81900036 Update permissions for log details view 2022-09-01 19:25:27 +02:00
xbgmsharp
91d4127405 Release v0.0.5 2022-08-28 23:10:39 +02:00
xbgmsharp
6315dca4b9 Update grafana ROLE permissions
Update vessel_role ROLE permissions
Update user_role ROLE permissions
Add Row Level securoty on auth.vessels
2022-08-28 22:58:29 +02:00
xbgmsharp
c35f353329 Update app url in messages table
Add logbook_update_geojson_fn
Update process_logbook_queue_fn with geojson field
Update check_jwt() include 'vessel.mmsi' and 'vessel.name' in user_role and vessel_role current session
2022-08-28 22:58:07 +02:00
xbgmsharp
54942a7558 Add api.logs_view and api.log_view 2022-08-28 22:23:28 +02:00
xbgmsharp
9a86c9f4f5 Fix tipo 2022-08-28 22:21:11 +02:00
93 changed files with 26882 additions and 2104 deletions

114
.codesandbox/tasks.json Normal file
View File

@@ -0,0 +1,114 @@
{
// These tasks will run in order when initializing your CodeSandbox project.
"setupTasks": [
{
"name": "git udpate",
"command": "cd ~/workspace/ && git pull"
},
{
"name": "git udpate submodule",
"command": "cd ~/workspace/ && git submodule update --recursive --remote"
}
],
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
"tasks": {
"docker-compose up db": {
"name": "docker-compose up db",
"command": "docker-compose up db",
"runAtStart": true
},
"docker network inspect network": {
"name": "docker network inspect postgsail_default",
"command": "docker network ls && docker network inspect postgsail_default",
"runAtStart": false
},
"docker-compose up api": {
"name": "docker-compose up api",
"command": "docker-compose up api",
"runAtStart": false,
"preview": {
"port": 3000,
"prLink": "direct"
}
},
"docker volume rm volume": {
"name": "docker volume rm volume",
"command": "docker volume ls && docker volume rm postgsail_data",
"runAtStart": false
},
"docker-compose rm db": {
"name": "docker-compose rm db",
"command": "docker-compose rm db",
"runAtStart": false
},
"docker-compose rm api": {
"name": "docker-compose rm api",
"command": "docker-compose rm api",
"runAtStart": false
},
"docker-compose clean": {
"name": "docker-compose clean",
"command": "docker-compose stop && docker-compose rm && docker volume ls && docker volume rm postgsail_data",
"runAtStart": false
},
"docker-compose pgadmin": {
"name": "docker-compose up pgadmin",
"command": "docker-compose up pgadmin",
"runAtStart": false,
"preview": {
"port": 5050,
"prLink": "direct"
}
},
"docker-compose web": {
"name": "docker-compose up web",
"command": "docker-compose up web",
"runAtStart": false,
"preview": {
"port": 8080,
"prLink": "direct"
}
},
"docker-compose ps": {
"name": "docker-compose ps -a",
"command": "docker-compose ps -a",
"runAtStart": false
},
"docker ps": {
"name": "docker ps -a",
"command": "docker ps -a",
"runAtStart": false
},
"docker-compose stop": {
"name": "docker-compose stop",
"command": "docker-compose stop",
"runAtStart": false
},
"npm i": {
"name": "npm i",
"command": "cd frontend/ && npm i",
"runAtStart": false
},
"git submodule add https://github.com/xbgmsharp/vuestic-postgsail frontend": {
"name": "git submodule add https://github.com/xbgmsharp/vuestic-postgsail frontend",
"command": "git submodule add https://github.com/xbgmsharp/vuestic-postgsail frontend",
"runAtStart": false
},
"git submodule update --init --recursive": {
"name": "git submodule update --init --recursive",
"command": "git submodule update --init --recursive",
"runAtStart": false
},
"git submodule update --recursive --remote": {
"name": "git submodule update --recursive --remote",
"command": "git submodule update --recursive --remote",
"runAtStart": false
},
"git pull": {
"name": "git pull",
"command": "git pull",
"runAtStart": false
}
}
}

76
.devcontainer.json Normal file
View File

@@ -0,0 +1,76 @@
{
"name": "PostgSail",
//"image": "mcr.microsoft.com/devcontainers/base:alpine",
"dockerComposeFile": ["docker-compose.dev.yml", "docker-compose.yml"],
"service": "dev",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// Use this environment variable if you need to bind mount your local source code into a new container.
"remoteEnv": {
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}",
"POSTGRES_PASSWORD": "${localEnv:POSTGRES_PASSWORD}",
"POSTGRES_USER": "${localEnv:POSTGRES_USER}",
"POSTGRES_DB": "${localEnv:POSTGRES_DB}",
"PGSAIL_AUTHENTICATOR_PASSWORD": "${localEnv:PGSAIL_AUTHENTICATOR_PASSWORD}"
},
"containerEnv": {
//"GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}",
//"GITHUB_USER": "${localEnv:GITHUB_USER}"
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
"forwardPorts": ["db:5432", "api:3000", "pgadmin:5050", "web:8080"],
// Use 'portsAttributes' to set default properties for specific forwarded ports.
// More info: https://containers.dev/implementors/json_reference/#port-attributes
"portsAttributes": {
"3000": {
"label": "api",
"onAutoForward": "notify"
},
"5050": {
"label": "pgadmin",
"onAutoForward": "notify"
},
"5342": {
"label": "database",
"onAutoForward": "notify"
},
"8080": {
"label": "web",
"onAutoForward": "notify"
}
},
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "docker --version",
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
"settings": {
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/bin/bash"
}
},
"terminal.integrated.defaultProfile.linux": "bash",
"editor.formatOnSave": true
},
"extensions": [
"streetsidesoftware.code-spell-checker",
"esbenp.prettier-vscode",
"ckolkman.vscode-postgres",
"ms-azuretools.vscode-docker"
]
}
}
}

View File

@@ -1,15 +1,22 @@
# POSTGRESQL ENV Settings
POSTGRES_USER=username
POSTGRES_PASSWORD=password
POSTGRES_DB=postgres
# PostgSail ENV Settings
PGSAIL_AUTHENTICATOR_PASSWORD=password
PGSAIL_GRAFANA_PASSWORD=password
PGSAIL_GRAFANA_AUTH_PASSWORD=password
PGSAIL_EMAIL_FROM=root@localhost
PGSAIL_EMAIL_SERVER=localhost
#PGSAIL_EMAIL_USER= Comment if not use
#PGSAIL_EMAIL_PASS= Comment if not use
#PGSAIL_PUSHOVER_TOKEN= Comment if not use
#PGSAIL_PUSHOVER_APP= Comment if not use
#PGSAIL_PUSHOVER_APP_TOKEN= Comment if not use
#PGSAIL_PUSHOVER_APP_URL= Comment if not use
#PGSAIL_TELEGRAM_BOT_TOKEN= Comment if not use
PGSAIL_APP_URL=http://localhost:8080
PGSAIL_API_URL=http://localhost:3000
# POSTGREST ENV Settings
PGRST_DB_URI=postgres://authenticator:${PGSAIL_AUTHENTICATOR_PASSWORD}@127.0.0.1:5432/signalk
PGRST_DB_URI=postgres://authenticator:${PGSAIL_AUTHENTICATOR_PASSWORD}@db:5432/signalk
PGRST_JWT_SECRET=_at_least_32__char__long__random
# Grafana ENV Settings
GF_SECURITY_ADMIN_PASSWORD=password

14
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
version: 2
updates:
# Enable version updates for Docker
- package-ecosystem: "docker"
# Look for a `Dockerfile` in the `root` directory
directory: "/"
# Check for updates once a week
schedule:
interval: "weekly"
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly

55
.github/workflows/db-lint.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Linting rules on database schema.
on:
pull_request:
paths:
- 'initdb/**'
branches:
- 'main'
push:
branches:
- 'main'
paths:
- 'initdb/**'
tags:
- "*"
workflow_dispatch:
jobs:
schemalint:
name: schemalint
runs-on: ubuntu-latest
steps:
- name: Check out the source
uses: actions/checkout@v4
- name: Set env
run: cp .env.example .env
- name: Pull Docker images
run: docker-compose pull db api
- name: Run PostgSail Database & schemalint
# Environment variables
env:
# The hostname used to communicate with the PostgreSQL service container
PGHOST: localhost
PGPORT: 5432
PGDATABASE: signalk
PGUSER: username
PGPASSWORD: password
run: |
set -eu
source .env
docker-compose stop || true
docker-compose rm || true
docker-compose up -d db && sleep 30 && docker-compose up -d api && sleep 5
docker-compose ps -a
echo ${PGSAIL_API_URL}
curl ${PGSAIL_API_URL}
npm i -D schemalint
npx schemalint
- name: Show the logs
if: always()
run: |
docker-compose logs

73
.github/workflows/db-test.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
name: Test services db, api
on:
pull_request:
paths:
- 'initdb/**'
- 'tests/**'
branches:
- 'main'
push:
branches:
- 'main'
paths:
- 'initdb/**'
- 'tests/**'
tags:
- "*"
workflow_dispatch:
jobs:
smoketest:
name: tests
runs-on: ubuntu-latest
steps:
- name: Check out the source
uses: actions/checkout@v4
- name: Set env
run: cp .env.example .env
- name: Pull Docker images
run: docker-compose pull db api
- name: Build Docker images
run: docker-compose -f docker-compose.dev.yml -f docker-compose.yml build tests
- name: Install psql
run: sudo apt install postgresql-client
- name: Run PostgSail Database & API tests
# Environment variables
env:
# The hostname used to communicate with the PostgreSQL service container
PGHOST: localhost
PGPORT: 5432
PGDATABASE: signalk
PGUSER: username
PGPASSWORD: password
run: |
set -eu
source .env
docker-compose stop || true
docker-compose rm || true
docker-compose up -d db && sleep 30 && docker-compose up -d api && sleep 5
docker-compose ps -a
echo ${PGSAIL_API_URL}
curl ${PGSAIL_API_URL}
psql -c "select 1"
echo "Test PostgreSQL version"
psql -c "SELECT version();"
echo "Test PostgSail version"
psql -c "SELECT value FROM app_settings WHERE name = 'app.version';"
echo "Test PostgSail Unit Test"
docker compose -f docker-compose.dev.yml -f docker-compose.yml up tests --abort-on-container-exit --exit-code-from tests
if [ $? != 0 ];
then
echo "Error running db-tests"
exit 1
fi
- name: Show the logs
if: always()
run: |
docker-compose logs

70
.github/workflows/frontend-test.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: Test services db, api, web
on:
pull_request:
paths:
- 'frontend/**'
branches:
- 'main'
push:
branches:
- 'main'
paths:
- 'frontend/**'
tags:
- "*"
workflow_dispatch:
jobs:
ci-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: 'true'
- name: Set env
run: cp .env.example .env
- name: Pull Docker images
run: docker compose -f docker-compose.dev.yml -f docker-compose.yml pull db api web_tests
- name: Build Docker images
run: |
set -eu
source .env
docker compose -f docker-compose.dev.yml -f docker-compose.yml build web_dev
docker compose -f docker-compose.dev.yml -f docker-compose.yml build web
- name: Run PostgSail Web tests
# Environment variables
env:
# The hostname used to communicate with the PostgreSQL service container
PGHOST: localhost
PGPORT: 5432
PGDATABASE: signalk
PGUSER: username
PGPASSWORD: password
run: |
set -eu
source .env
docker-compose stop || true
docker-compose rm || true
docker-compose up -d db && sleep 30 && docker-compose up -d api && sleep 5
docker-compose ps -a
echo "Test PostgSail Web Unit Test"
docker compose -f docker-compose.dev.yml -f docker-compose.yml up -d web_dev && sleep 100
docker compose -f docker-compose.dev.yml -f docker-compose.yml logs web_dev
docker compose ps -a
curl http://localhost:8080/
docker compose -f docker-compose.dev.yml -f docker-compose.yml up web_tests --abort-on-container-exit --exit-code-from web_tests
if [ $? != 0 ];
then
echo "Error running frontend-tests"
exit 1
fi
- name: Show the logs
if: always()
run: |
docker-compose logs

54
.github/workflows/grafana-test.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Test services db, grafana
on:
pull_request:
paths:
- 'grafana/**'
branches:
- 'main'
push:
branches:
- 'main'
paths:
- 'grafana/**'
tags:
- "*"
workflow_dispatch:
jobs:
ci-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set env
run: cp .env.example .env
- name: Pull Docker images
run: docker-compose pull db app
- name: Run PostgSail Grafana test
# Environment variables
env:
# The hostname used to communicate with the PostgreSQL service container
PGHOST: localhost
PGPORT: 5432
PGDATABASE: signalk
PGUSER: username
PGPASSWORD: password
run: |
set -eu
source .env
docker-compose stop || true
docker-compose rm || true
docker-compose up -d db && sleep 30
docker-compose ps -a
echo "Test PostgSail Grafana Unit Test"
docker-compose up -d app && sleep 5
docker-compose ps -a
curl http://localhost:3001/
- name: Show the logs
if: always()
run: |
docker-compose logs

8
.gitignore vendored
View File

@@ -1,2 +1,10 @@
.DS_Store
.env
initdb/*.csv
initdb/*.no
initdb/*.jwk
tests/node_modules/
tests/output/
assets/*
.pnpm-store/
db-data/

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "frontend"]
path = frontend
url = https://github.com/xbgmsharp/vuestic-postgsail

22
.schemalintrc.js Normal file
View File

@@ -0,0 +1,22 @@
module.exports = {
connection: {
host: process.env.PGHOST,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
database: process.env.PGDATABASE,
charset: "utf8",
},
rules: {
"name-casing": ["error", "snake"],
"prefer-jsonb-to-json": ["error"],
"prefer-text-to-varchar": ["error"],
"prefer-timestamptz-to-timestamp": ["error"],
"prefer-identity-to-serial": ["error"],
//"name-inflection": ["error", "singular"],
},
schemas: [{ name: "public" }, { name: "api" },{ name: "auth" }],
ignores: [],
};

BIN
PostgSail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

170
README.md
View File

@@ -1,52 +1,152 @@
# PostgSail
Effortless cloud based solution for storing and sharing your SignalK data. Allow to effortlessly log your sails and monitor your boat.
### Context
It is all about SQL, object-relational, time-series, spatial database with a bit python.
Effortless cloud based solution for storing and sharing your SignalK data. Allow you to effortlessly log your sails and monitor your boat with historical data.
[![release](https://img.shields.io/github/release/xbgmsharp/postgsail?include_prereleases=&sort=semver&color=blue)](https://github.com/xbgmsharp/postgsail/releases/latest)
[![License](https://img.shields.io/github/license/xbgmsharp/postgsail)](#license)
[![issues - postgsail](https://img.shields.io/github/issues/xbgmsharp/postgsail)](https://github.com/xbgmsharp/postgsail/issues)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
[![Test services db, api](https://github.com/xbgmsharp/postgsail/actions/workflows/db-test.yml/badge.svg)](https://github.com/xbgmsharp/postgsail/actions/workflows/db-test.yml)
[![Test services db, api, web](https://github.com/xbgmsharp/postgsail/actions/workflows/frontend-test.yml/badge.svg)](https://github.com/xbgmsharp/postgsail/actions/workflows/frontend-test.yml)
[![Test services db, grafana](https://github.com/xbgmsharp/postgsail/actions/workflows/grafana-test.yml/badge.svg)](https://github.com/xbgmsharp/postgsail/actions/workflows/grafana-test.yml)
signalk-postgsail:
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/signalk-postgsail.svg)](https://github.com/xbgmsharp/signalk-postgsail/releases/latest)
postgsail-frontend:
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/vuestic-postgsail.svg)](https://github.com/xbgmsharp/vuestic-postgsail/releases/latest)
postgsail-telegram-bot:
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/postgsail-telegram-bot.svg)](https://github.com/xbgmsharp/postgsail-telegram-bot/releases/latest)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8124/badge)](https://www.bestpractices.dev/projects/8124)
## Features
### Features
- Automatically log your voyages without manually starting or stopping a trip.
- Automatically capture the details of your voyages (boat speed, heading, wind speed, etc).
- Timelapse video your trips!
- Timelapse video your trips, with or without time control.
- Add custom notes to your logs.
- Export to CSV or GPX and download your logs.
- Export to CSV, GPX, GeoJSON, KML and download your logs.
- Aggregate your trip statistics: Longest voyage, time spent at anchorages, home ports etc.
- See your moorages on a global map, with incoming and outgoing voyages from each trip.
- Monitor your boat (position, depth, wind, temperature, battery charge status, etc.) remotely.
- History: view trends.
- Alert monitoring: get notification on low voltage or low fuel remotely.
- Notification via email or PushOver.
- Notification via email or PushOver, Telegram.
- Offline mode.
- Low Bandwidth mode.
- Awesome statistics and graphs.
- Create and manage your own dashboards.
- Windy PWS (Personal Weather Station).
- Engine Hours Logger.
- Polar performance.
- Anything missing? just ask!
### Cloud
The cloud advantage.
## Context
It is all about SQL, object-relational, time-series, spatial databases with a bit of python.
PostgSail is an open-source alternative to traditional vessel data management.
It is based on a well known open-source technology stack, Signalk, PostgreSQL, TimescaleDB, PostGIS, PostgREST. It does perfectly integrate with standard monitoring tool stack like Grafana.
To understand the why and how, you might want to read [Why.md](https://github.com/xbgmsharp/postgsail/tree/main/Why.md)
## Architecture
A simple scalable architecture:
For more clarity and visibility the complete [Architecture overview](https://github.com/xbgmsharp/postgsail/blob/main/docs/README.md).
For more clarity and visibility the complete [Entity-Relationship Diagram (ERD)](https://github.com/xbgmsharp/postgsail/blob/main/docs/ERD/README.md) is export as Mermaid, PNG and SVG file.
## Cloud
If you prefer not to install or administer your instance of PostgSail, hosted versions of PostgSail are available in the cloud of your choice.
### The cloud advantage.
Hosted and fullymanaged options for PostgSail, designed for all your deployment and business needs. Register and try for free at https://iot.openplotter.cloud/.
## Using PostgSail
A full-featured development environment.
#### With CodeSandbox
- Develop on [![CodeSandbox Ready-to-Code](https://img.shields.io/badge/CodeSandbox-Ready--to--Code-blue?logo=codesandbox)](https://codesandbox.io/p/github/xbgmsharp/postgsail/main)
- or via [direct link](https://codesandbox.io/p/github/xbgmsharp/postgsail/main)
#### With DevPod
- [![Open in DevPod!](https://devpod.sh/assets/open-in-devpod.svg)](https://devpod.sh/open#https://github.com/xbgmsharp/postgsail/&workspace=postgsail&provider=docker&ide=openvscode)
- or via [direct link](https://devpod.sh/open#https://github.com/xbgmsharp/postgsail&workspace=postgsail&provider=docker&ide=openvscode)
#### With Docker Dev Environments
- [Open in Docker dev-envs!](https://open.docker.com/dashboard/dev-envs?url=https://github.com/xbgmsharp/postgsail/)
### pre-deploy configuration
To get these running, copy `.env.example` and rename to `.env` then set the value accordinly.
To get these running, copy `.env.example` and rename to `.env` then set the value accordingly.
```bash
# cp .env.example .env
```
Notice, that `PGRST_JWT_SECRET` must be at least 32 characters long.
`$ head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 ; echo ''`
`$ head /dev/urandom | tr -dc A-Za-z0-9 | head -c 42 ; echo ''`
```bash
# nano .env
```
### Deploy
By default there is no network set and the postgresql data are store in a docker volume.
You can update the default settings by editing `docker-compose.yml` to your need.
Then simply excecute:
```
$ docker-compose up
By default there is no network set and all data are store in a docker volume.
You can update the default settings by editing `docker-compose.yml` and `docker-compose.dev.yml` to your need.
First let's initialize the database.
#### Step 1. Initialize database
First let's import the SQL schema, execute:
```bash
$ docker compose up db
```
### PostgSail Configuration
#### Step 2. Start backend (db, api)
Then launch the full stack (db, api) backend, execute:
```bash
$ docker compose up db api
```
The API should be accessible via port HTTP/3000.
The database should be accessible via port TCP/5432.
You can connect to the database via a web gui like [pgadmin](https://www.pgadmin.org/) or you can use a client [dbeaver](https://dbeaver.io/).
#### Step 3. Start frontend (web)
Then launch the web frontend, execute:
```bash
$ docker compose up web
```
The frontend should be accessible via port HTTP/8080.
### SQL Configuration
Check and update your postgsail settings via SQL in the table `app_settings`:
```
select * from app_settings;
```sql
SELECT * FROM app_settings;
```
```
```sql
UPDATE app_settings
SET
value = 'new_value'
@@ -54,50 +154,60 @@ UPDATE app_settings
```
### Ingest data
Next, to ingest data from signalk, you need to install [signalk-postgsail](https://github.com/xbgmsharp/signalk-postgsail) plugin on your signalk server instance.
Also, if you like, you can import saillogger data using the postgsail helpers, [postgsail-helpers](https://github.com/xbgmsharp/postgsail-helpers).
You might want to import your influxdb1 data as weel, [outflux](https://github.com/timescale/outflux).
Any taker on influxdb2 to PostgSail? It is definitly possible.
You might want to import your influxdb1 data as well, [outflux](https://github.com/timescale/outflux).
For InfluxDB 2.x and 3.x. You will need to enable the 1.x APIs to use them. Consult the InfluxDB documentation for more details.
Last, if you like, you can import the sample data from Signalk NMEA Plaka by running the tests.
If everything goes well all tests pass sucessfully and you should recieve a few notifications by email or PushOver.
If everything goes well all tests pass successfully and you should receive a few notifications by email or PushOver or Telegram.
[End-to-End (E2E) Testing.](https://github.com/xbgmsharp/postgsail/blob/main/tests/)
```
$ docker-compose up tests
```
### API Documentation
The OpenAPI description output depends on the permissions of the role that is contained in the JWT role claim.
Other applications can also use the [PostgSAIL API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/xbgmsharp/postgsail/main/openapi.json).
API anonymous:
```
$ curl http://localhost:3000/
```
API user_role:
```
$ curl http://localhost:3000/ -H 'Authorization: Bearer my_token_from_login_or_signup_fn'
```
API vessel_role:
```
$ curl http://localhost:3000/ -H 'Authorization: Bearer my_token_from_register_vessel_fn'
```
#### API main workflow
Check the [unit test sample](https://github.com/xbgmsharp/PostgSail/blob/main/tests/index.js).
Check the [End-to-End (E2E) test sample](https://github.com/xbgmsharp/postgsail/blob/main/tests/).
### Docker dependencies
`docker-compose` is used to start environment dependencies. Dependencies consist of 2 containers:
`docker-compose` is used to start environment dependencies. Dependencies consist of 3 containers:
- `timescaledb-postgis` alias `db`, PostgreSQL with TimescaleDB extension along with the PostGIS extension.
- `postgrest` alias `api`, Standalone web server that turns your PostgreSQL database directly into a RESTful API.
- `grafana` alias `app`, visualize and monitor your data
### Optional docker images
- [Grafana](https://hub.docker.com/r/grafana/grafana), visualize and monitor your data
- [pgAdmin](https://hub.docker.com/r/dpage/pgadmin4), web UI to monitor and manage multiple PostgreSQL
- [Swagger](https://hub.docker.com/r/swaggerapi/swagger-ui), web UI to visualize documentation from PostgREST
@@ -106,7 +216,9 @@ docker-compose -f docker-compose-optional.yml up
```
### Software reference
Out of the box iot platform using docker with the following software:
An out of the box IoT platform using Docker (could be extend to K3 or K8) with the following software:
- [Signal K server, a Free and Open Source universal marine data exchange format](https://signalk.org)
- [PostgreSQL, open source object-relational database system](https://postgresql.org)
- [TimescaleDB, Time-series data extends PostgreSQL](https://www.timescale.com)
@@ -115,7 +227,7 @@ Out of the box iot platform using docker with the following software:
### Support
To get support, please create new [issue](https://github.com/xbgmsharp/PostgSail/issues).
To get support, please create new [issue](https://github.com/xbgmsharp/postgsail/issues).
There is more likely security flows and bugs.
@@ -126,4 +238,4 @@ Feel free to contribute.
### License
This script is free software, Apache License Version 2.0.
This is a free software, Apache License Version 2.0.

15
Why.md Normal file
View File

@@ -0,0 +1,15 @@
#### Why not InfluxDB vs TimescaleDB
I had an InfluxDBv1 on my RPI that kill the sdcard/usbkey. I had an InfluxDBv2, but there is no more ARM support and had to learn flux. Also could not find a good way to store data when offline. How do you export your data from a InfluxDBv2? Still looking for a solution.
With TimescaleDB, we already know SQL and there is a lot of tools and libraries that work with Postgres.
However, InfluxDB does simplify things like schema and provide an http endpoint.
With TimescaleDB, you are using a standard SQL table schema to store data from Signalk.
#### Why not MQTT vs HTTP
Having MQTT, makes your application micro service approach. however you multiple the components and dependency. HTTP seem a more reliable solution specially for offline support as MQTT library have a buffer limitation.
Using PostgREST is an alternative to manual CRUD programming. Custom API servers suffer problems. Writing business logic often duplicates, ignores or hobbles database structure. Object-relational mapping is a leaky abstraction leading to slow imperative code. The PostgREST philosophy establishes a single declarative source of truth: the data itself.
#### PostgreSQL got it all!
No additional dependencies other than PostgreSQL, thanks to the extensions ecosystem.
With PostgSail is based on PostGis and TimescaleDB and a few other pg extensions, https://github.com/xbgmsharp/timescaledb-postgis, fore more details.

135
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,135 @@
version: "3.9"
services:
dev:
container_name: dev
image: mcr.microsoft.com/devcontainers/base:ubuntu
volumes:
- ../:/workspaces:cached
- /var/run/docker.sock:/var/run/docker.sock
#network_mode: service:db
links:
- "api:postgrest"
- "db:database"
#- "web_dev:web_dev"
command: sleep infinity
pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
restart: unless-stopped
volumes:
- data:/var/lib/pgadmin
- ./pgadmin_servers.json:/servers.json:ro
links:
- "db:database"
ports:
- 5050:5050
environment:
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL}
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD}
- PGADMIN_LISTEN_ADDRESS=0.0.0.0
- PGADMIN_LISTEN_PORT=5050
- PGADMIN_SERVER_JSON_FILE=/servers.json
- PGADMIN_DISABLE_POSTFIX=true
depends_on:
- db
logging:
options:
max-size: 10m
swagger:
image: swaggerapi/swagger-ui
container_name: swagger
restart: unless-stopped
links:
- "api:postgrest"
ports:
- "8181:8080"
expose:
- "8080"
environment:
- API_URL=http://api:3000/
depends_on:
- db
- api
logging:
options:
max-size: 10m
tests:
image: xbgmsharp/postgsail-tests
build:
context: ./tests
dockerfile: Dockerfile
container_name: tests
volumes:
- ./tests:/mnt
working_dir: /mnt
command: 'bash tests.sh'
links:
- "api:postgrest"
- "db:database"
env_file: .env
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- PGPASSWORD=${POSTGRES_PASSWORD}
- PGSAIL_API_URI=http://api:3000
- PGSAIL_DB_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/signalk
depends_on:
- db
- api
logging:
options:
max-size: 10m
web_dev:
image: xbgmsharp/postgsail-vuestic:dev
build:
context: https://github.com/xbgmsharp/vuestic-postgsail.git#live
dockerfile: Dockerfile_dev
container_name: web_dev
hostname: web_dev
restart: unless-stopped
volumes:
- ./frontend:/app
links:
- "api:postgrest"
ports:
- 8080:8080
env_file: .env
environment:
- VITE_PGSAIL_URL=http://api:3000
- VITE_APP_INCLUDE_DEMOS=false
- VITE_APP_BUILD_VERSION=true
- VITE_APP_TITLE=${VITE_APP_TITLE}
depends_on:
- db
- api
logging:
options:
max-size: 10m
web_tests:
image: cypress/included
container_name: web_tests
restart: unless-stopped
volumes:
- ./frontend/e2e:/e2e
links:
- "api:postgrest"
- "web_dev:frontend"
env_file: .env
environment:
- CYPRESS_BASE_URL=http://web_dev:8080
depends_on:
- db
- api
- web_dev
logging:
options:
max-size: 10m
volumes:
data: {}

View File

@@ -1,41 +1,148 @@
version: '3.9'
version: "3.9"
services:
db:
image: xbgmsharp/timescaledb-postgis
container_name: db
hostname: db
restart: unless-stopped
env_file: .env
environment:
- POSTGRES_DB=postgres
- TIMESCALEDB_TELEMETRY=off
- PGDATA=/var/lib/postgresql/data/pgdata
- TZ=UTC
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
- PGSAIL_AUTHENTICATOR_PASSWORD=${PGSAIL_AUTHENTICATOR_PASSWORD}
ports:
- "5432:5432"
volumes:
- data:/var/lib/postgresql/data
- $PWD/initdb:/docker-entrypoint-initdb.d
- ./db-data:/var/lib/postgresql/data
- ./initdb:/docker-entrypoint-initdb.d
logging:
options:
max-size: 10m
healthcheck:
test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_USER} -d signalk'"]
interval: 60s
timeout: 10s
retries: 5
start_period: 100s
api:
image: postgrest/postgrest
container_name: api
hostname: api
restart: unless-stopped
links:
- "db:database"
ports:
- "3000:3000"
- "3003:3003"
env_file: .env
environment:
PGRST_DB_SCHEMA: api
PGRST_DB_ANON_ROLE: api_anonymous
PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
PGRST_DB_PRE_REQUEST: public.check_jwt
network_mode: "host"
PGRST_DB_POOL: 20
PGRST_DB_URI: ${PGRST_DB_URI}
PGRST_JWT_SECRET: ${PGRST_JWT_SECRET}
PGRST_SERVER_TIMING_ENABLED: 1
PGRST_DB_MAX_ROWS: 500
PGRST_JWT_CACHE_MAX_LIFETIME: 3600
depends_on:
- db
logging:
options:
max-size: 10m
#healthcheck:
# test: ["CMD-SHELL", "sh -c 'curl --fail http://localhost:3003/live || exit 1'"]
# interval: 60s
# timeout: 10s
# retries: 5
# start_period: 100s
app:
image: grafana/grafana:latest
container_name: app
restart: unless-stopped
links:
- "db:database"
volumes:
- data:/var/lib/grafana
- data:/var/log/grafana
- ./grafana:/etc/grafana
ports:
- "3001:3000"
env_file: .env
environment:
- GF_INSTALL_PLUGINS=pr0ps-trackmap-panel,fatcloud-windrose-panel
- GF_SECURITY_ADMIN_PASSWORD=${PGSAIL_GRAFANA_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SMTP_ENABLED=false
depends_on:
- db
logging:
options:
max-size: 10m
#healthcheck:
# test: ["CMD-SHELL", "sh -c 'curl --fail http://localhost:3000/healthz || exit 1'"]
# interval: 60s
# timeout: 10s
# retries: 5
# start_period: 100s
telegram:
image: xbgmsharp/postgsail-telegram-bot
container_name: telegram
restart: unless-stopped
links:
- "api:postgrest"
ports:
- "3005:8080"
environment:
- BOT_TOKEN=${PGSAIL_TELEGRAM_BOT_TOKEN}
- PGSAIL_URL=${PGSAIL_API_URL}
depends_on:
- db
- api
logging:
options:
max-size: 10m
web:
image: vuestic-postgsail
build:
context: https://github.com/xbgmsharp/vuestic-postgsail.git#live
dockerfile: Dockerfile
args:
- VITE_PGSAIL_URL=${PGSAIL_API_URL}
- VITE_APP_INCLUDE_DEMOS=false
- VITE_APP_BUILD_VERSION=true
- VITE_APP_TITLE=${VITE_APP_TITLE}
- VITE_GRAFANA_URL=${VITE_GRAFANA_URL}
hostname: web
container_name: web
restart: unless-stopped
links:
- "api:postgrest"
ports:
- 8080:8080
env_file: .env
environment:
- VITE_PGSAIL_URL=${PGSAIL_API_URL}
- VITE_APP_INCLUDE_DEMOS=false
- VITE_APP_BUILD_VERSION=true
- VITE_APP_TITLE=${VITE_APP_TITLE}
- VITE_GRAFANA_URL=${VITE_GRAFANA_URL}
depends_on:
- db
- api
logging:
options:
max-size: 10m
volumes:
data: {}

75
docs/ERD/README.md Normal file
View File

@@ -0,0 +1,75 @@
# PostgSail ERD
The Entity-Relationship Diagram (ERD) provides a graphical representation of database tables, columns, and inter-relationships. ERD can give sufficient information for the database administrator to follow when developing and maintaining the database.
## A global overview
Auto generated Mermaid diagram using [mermerd](https://github.com/KarnerTh/mermerd) and [MermaidJs](https://github.com/mermaid-js/mermaid).
[PostgSail SQL Schema](https://github.com/xbgmsharp/postgsail/tree/main/docs/ERD/postgsail.md "PostgSail SQL Schema")
## Further
There is 3 main schemas:
- API Schema:
- tables
- metrics
- logbook
- ...
- functions
- ...
- Auth Schema:
- tables
- accounts
- vessels
- ...
- functions
- ...
- Public Schema:
- tables
- app_settings
- tpl_messages
- ...
- functions
- ...
## Overview
- Insert data into table metadata from API using PostgREST
- Insert data into table metrics from API using PostgREST
- TimescaleDB Hypertable to store signalk metrics
- pgsql functions to generate logbook, stays, moorages
- CRON functions to process logbook, stays, moorages
- python functions for geo reverse and send notification via email and/or pushover
- Views statistics, timelapse, monitoring, logs
- Always store time in UTC
## Ingest flowchart
```mermaid
graph LR
A[SignalK] -- HTTP POST --> B{PostgREST}
B -- SQL --> C{PostgreSQL}
C --> D((metadata trigger))
C --> E((metrics trigger))
D --> F{tbl.metadata}
E --> G{tbl.metrics}
E --> H{tbl.logs}
E --> I{tbl.stays}
```
## pg_cron flowchart
```mermaid
flowchart TD
A[pg_cron] --> B((cron_new_notification))
A --> C((cron_pre_logbook))
A --> D((cron_new_logbook))
A --> E((cron_new_stay))
A --> F((cron_monitor_offline))
A --> G((cron_monitor_online))
C --> K{Validate logbook details}
D --> L{Update logbook details}
E --> M{Update stay details}
L --> N{Update Moorages details}
M --> N{Update Moorages details}
B --> O{Update account,vessel,otp}
F --> P{Update metadata}
G --> P
```

View File

@@ -0,0 +1,35 @@
# Connection properties
connectionString: ${PGSAIL_DB_URI}
# Define what schemas should be used
#useAllSchemas: true
# or
schema:
- "public"
- "api"
- "auth"
# Define what tables should be used
useAllTables: true
# or
#selectedTables:
# - city
# - customer
# Additional flags
showAllConstraints: true
encloseWithMermaidBackticks: true
outputFileName: "postgsail.md"
debug: true
omitConstraintLabels: true
omitAttributeKeys: true
showDescriptions:
- enumValues
- columnComments
- notNull
showSchemaPrefix: true
schemaPrefixSeparator: "_"
# Names must match the pattern <schema><schema_prefix><table>
#relationshipLabels:
# - "public_table public_another-table : label"

261
docs/ERD/postgsail.md Normal file
View File

@@ -0,0 +1,261 @@
```mermaid
erDiagram
api_logbook {
text _from
double_precision _from_lat
double_precision _from_lng
integer _from_moorage_id "Link api.moorages with api.logbook via FOREIGN KEY and REFERENCES"
timestamp_with_time_zone _from_time "{NOT_NULL}"
text _to
double_precision _to_lat
double_precision _to_lng
integer _to_moorage_id "Link api.moorages with api.logbook via FOREIGN KEY and REFERENCES"
timestamp_with_time_zone _to_time
boolean active
double_precision avg_speed
numeric distance "in NM"
interval duration "Best to use standard ISO 8601"
jsonb extra "computed signalk metrics of interest, runTime, currentLevel, etc"
integer id "{NOT_NULL}"
double_precision max_speed
double_precision max_wind_speed
text name
text notes
geography track_geog "postgis geography type default SRID 4326 Unit: degres"
jsonb track_geojson "store generated geojson with track metrics data using with LineString and Point features, we can not depend api.metrics table"
geometry track_geom "postgis geometry type EPSG:4326 Unit: degres"
text vessel_id "{NOT_NULL}"
}
api_metadata {
boolean active "trigger monitor online/offline"
boolean active
double_precision beam
text client_id
text configuration
timestamp_with_time_zone created_at "{NOT_NULL}"
double_precision height
integer id "{NOT_NULL}"
double_precision length
numeric mmsi
text name
text platform
text plugin_version "{NOT_NULL}"
numeric ship_type
text signalk_version "{NOT_NULL}"
timestamp_with_time_zone time "{NOT_NULL}"
timestamp_with_time_zone updated_at "{NOT_NULL}"
text vessel_id "Link auth.vessels with api.metadata via FOREIGN KEY and REFERENCES {NOT_NULL}"
text vessel_id "{NOT_NULL}"
}
api_metrics {
double_precision anglespeedapparent
text client_id
double_precision courseovergroundtrue
double_precision latitude "With CONSTRAINT but allow NULL value to be ignored silently by trigger"
double_precision longitude "With CONSTRAINT but allow NULL value to be ignored silently by trigger"
jsonb metrics
double_precision speedoverground
text status
timestamp_with_time_zone time "{NOT_NULL}"
text vessel_id "{NOT_NULL}"
double_precision windspeedapparent
}
api_moorages {
text country
geography geog "postgis geography type default SRID 4326 Unit: degres"
boolean home_flag
integer id "{NOT_NULL}"
double_precision latitude
double_precision longitude
text name
jsonb nominatim
text notes
jsonb overpass
integer reference_count
integer stay_code "Link api.stays_at with api.moorages via FOREIGN KEY and REFERENCES"
interval stay_duration "Best to use standard ISO 8601"
text vessel_id "{NOT_NULL}"
}
api_stays {
boolean active
timestamp_with_time_zone arrived "{NOT_NULL}"
timestamp_with_time_zone departed
interval duration "Best to use standard ISO 8601"
geography geog "postgis geography type default SRID 4326 Unit: degres"
integer id "{NOT_NULL}"
double_precision latitude
double_precision longitude
integer moorage_id "Link api.moorages with api.stays via FOREIGN KEY and REFERENCES"
text name
text notes
integer stay_code "Link api.stays_at with api.stays via FOREIGN KEY and REFERENCES"
text vessel_id "{NOT_NULL}"
}
api_stays_at {
text description "{NOT_NULL}"
integer stay_code "{NOT_NULL}"
}
auth_accounts {
timestamp_with_time_zone connected_at "{NOT_NULL}"
timestamp_with_time_zone created_at "{NOT_NULL}"
citext email "{NOT_NULL}"
text first "User first name with CONSTRAINT CHECK {NOT_NULL}"
integer id "{NOT_NULL}"
text last "User last name with CONSTRAINT CHECK {NOT_NULL}"
text pass "{NOT_NULL}"
jsonb preferences
name role "{NOT_NULL}"
timestamp_with_time_zone updated_at "{NOT_NULL}"
text user_id "{NOT_NULL}"
}
auth_otp {
text otp_pass "{NOT_NULL}"
timestamp_with_time_zone otp_timestamp
smallint otp_tries "{NOT_NULL}"
citext user_email "{NOT_NULL}"
}
auth_users {
timestamp_with_time_zone connected_at "{NOT_NULL}"
timestamp_with_time_zone created_at "{NOT_NULL}"
name email "{NOT_NULL}"
text first "{NOT_NULL}"
name id "{NOT_NULL}"
text last "{NOT_NULL}"
jsonb preferences
name role "{NOT_NULL}"
timestamp_with_time_zone updated_at "{NOT_NULL}"
text user_id "{NOT_NULL}"
}
auth_vessels {
timestamp_with_time_zone created_at "{NOT_NULL}"
numeric mmsi "MMSI can be optional but if present must be a valid one and unique but must be in numeric range between 100000000 and 800000000"
text name "{NOT_NULL}"
citext owner_email "{NOT_NULL}"
name role "{NOT_NULL}"
timestamp_with_time_zone updated_at "{NOT_NULL}"
text vessel_id "{NOT_NULL}"
}
public_aistypes {
text description
numeric id
}
public_app_settings {
text name "application settings name key {NOT_NULL}"
text value "application settings value {NOT_NULL}"
}
public_badges {
text description
text name
}
public_email_templates {
text email_content
text email_subject
text name
text pushover_message
text pushover_title
}
public_geocoders {
text name
text reverse_url
text url
}
public_iso3166 {
text alpha_2
text alpha_3
text country
integer id
}
public_mid {
text country
integer country_id
numeric id
}
public_ne_10m_geography_marine_polys {
text changed
text featurecla
geometry geom
integer gid "{NOT_NULL}"
text label
double_precision max_label
double_precision min_label
text name
text name_ar
text name_bn
text name_de
text name_el
text name_en
text name_es
text name_fa
text name_fr
text name_he
text name_hi
text name_hu
text name_id
text name_it
text name_ja
text name_ko
text name_nl
text name_pl
text name_pt
text name_ru
text name_sv
text name_tr
text name_uk
text name_ur
text name_vi
text name_zh
text name_zht
text namealt
bigint ne_id
text note
smallint scalerank
text wikidataid
}
public_process_queue {
text channel "{NOT_NULL}"
integer id "{NOT_NULL}"
text payload "{NOT_NULL}"
timestamp_with_time_zone processed
text ref_id "either user_id or vessel_id {NOT_NULL}"
timestamp_with_time_zone stored "{NOT_NULL}"
}
public_spatial_ref_sys {
character_varying auth_name
integer auth_srid
character_varying proj4text
integer srid "{NOT_NULL}"
character_varying srtext
}
api_logbook }o--|| api_metadata : ""
api_logbook }o--|| api_moorages : ""
api_logbook }o--|| api_moorages : ""
api_metadata }o--|| auth_vessels : ""
api_metrics }o--|| api_metadata : ""
api_moorages }o--|| api_metadata : ""
api_stays }o--|| api_metadata : ""
api_moorages }o--|| api_stays_at : ""
api_stays }o--|| api_moorages : ""
api_stays }o--|| api_stays_at : ""
auth_otp |o--|| auth_accounts : ""
auth_vessels |o--|| auth_accounts : ""
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

BIN
docs/ERD/signalk - api.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

BIN
docs/ERD/signalk - auth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

4
docs/README.md Normal file
View File

@@ -0,0 +1,4 @@
Simple and scalable architecture.
![Architecture overview](https://raw.githubusercontent.com/xbgmsharp/postgsail/main/PostgSail.png "Architecture overview")

1
frontend Submodule

Submodule frontend added at 31579e219d

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,472 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Logs,Moorages,Stays",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 1,
"links": [
{
"asDropdown": false,
"icon": "external link",
"includeVars": true,
"keepTime": false,
"tags": [],
"targetBlank": true,
"title": "New link",
"tooltip": "",
"type": "dashboards",
"url": ""
}
],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"filterable": false,
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "id"
},
"properties": [
{
"id": "custom.width",
"value": 41
}
]
},
{
"matcher": {
"id": "byName",
"options": "distance"
},
"properties": [
{
"id": "custom.width",
"value": 104
}
]
}
]
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "9.4.3",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT * from api.logs_view",
"refId": "A",
"select": [
[
{
"params": [
"_from_lat"
],
"type": "column"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"table": "logs",
"timeColumn": "_from_time",
"timeColumnType": "timestamp",
"where": []
}
],
"title": "Logbook ${__user.email} / ${__user.login}",
"type": "table"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"filterable": false,
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "id"
},
"properties": [
{
"id": "custom.width",
"value": 41
}
]
},
{
"matcher": {
"id": "byName",
"options": "distance"
},
"properties": [
{
"id": "custom.width",
"value": 104
}
]
}
]
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 10
},
"id": 5,
"options": {
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "9.4.3",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT * from api.stays_view",
"refId": "A",
"select": [
[
{
"params": [
"_from_lat"
],
"type": "column"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"table": "logs",
"timeColumn": "_from_time",
"timeColumnType": "timestamp",
"where": []
}
],
"title": "Stays",
"type": "table"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"filterable": false,
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "id"
},
"properties": [
{
"id": "custom.width",
"value": 41
}
]
},
{
"matcher": {
"id": "byName",
"options": "distance"
},
"properties": [
{
"id": "custom.width",
"value": 104
}
]
}
]
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 20
},
"id": 6,
"options": {
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "9.4.3",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nselect * from api.moorages_view",
"refId": "A",
"select": [
[
{
"params": [
"_from_lat"
],
"type": "column"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"table": "logs",
"timeColumn": "_from_time",
"timeColumnType": "timestamp",
"where": []
}
],
"title": "Moorages",
"type": "table"
}
],
"refresh": "",
"revision": 1,
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"definition": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
"description": "Vessel Name",
"hide": 0,
"includeAll": false,
"label": "Boat",
"multi": false,
"name": "boat",
"options": [],
"query": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-15d",
"to": "now"
},
"timepicker": {},
"timezone": "utc",
"title": "Logbook",
"uid": "pgsail_tpl_logbook",
"version": 1,
"weekStart": ""
}

View File

@@ -0,0 +1,738 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Monitoring view",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 3,
"links": [
{
"asDropdown": false,
"icon": "external link",
"includeVars": true,
"keepTime": false,
"tags": [],
"targetBlank": true,
"title": "New link",
"tooltip": "",
"type": "dashboards",
"url": ""
}
],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "volt"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 12,
"x": 0,
"y": 0
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "10.1.0",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "time_series",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'electrical.batteries.AUX2.voltage' AS numeric) AS AUX2Voltage\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
"refId": "A",
"select": [
[
{
"params": [
"_from_lat"
],
"type": "column"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"table": "trip_in_progress",
"timeColumn": "_from_time",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"title": "AUX2 Voltage",
"type": "stat"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "celsius"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 12,
"x": 12,
"y": 0
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "10.1.0",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "time_series",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS OutsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
"refId": "A",
"select": [
[
{
"params": [
"_from_lat"
],
"type": "column"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"table": "trip_in_progress",
"timeColumn": "_from_time",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"title": "Temperature",
"type": "stat"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"aux2"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"graph": true,
"legend": false,
"tooltip": false
}
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 4
},
"id": 4,
"options": {
"graph": {},
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "7.5.4",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "time_series",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'electrical.batteries.AUX2.voltage' AS numeric) AS AUX2,\n\tcast(metrics-> 'electrical.batteries.House.voltage' AS numeric) AS House,\n\tcast(metrics-> 'environment.rpi.pijuice.gpioVoltage' AS numeric) AS gpioVoltage,\n\tcast(metrics-> 'electrical.batteries.Seatalk.voltage' AS numeric) AS SeatalkVoltage,\n\tcast(metrics-> 'electrical.batteries.Starter.voltage' AS numeric) AS StarterVoltage,\n\tcast(metrics-> 'environment.rpi.pijuice.batteryVoltage' AS numeric) AS RPIBatteryVoltage,\n\tcast(metrics-> 'electrical.batteries.victronDevice.voltage' AS numeric) AS victronDeviceVoltage\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n\tAND vessel_id = '${boat}'\nORDER BY 1",
"refId": "A",
"select": [
[
{
"params": [
"_from_lat"
],
"type": "column"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"table": "trip_in_progress",
"timeColumn": "_from_time",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"title": "Voltage",
"type": "timeseries"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 13
},
"id": 2,
"options": {
"graph": {},
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "7.5.4",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.water.temperature' AS numeric) - 273.15 AS waterTemperature,\n\tcast(metrics-> 'environment.inside.temperature' AS numeric) - 273.15 AS insideTemperature,\n\tcast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS outsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
"refId": "A",
"select": [
[
{
"params": [
"_from_lat"
],
"type": "column"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"table": "trip_in_progress",
"timeColumn": "_from_time",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"title": "Temperatures",
"type": "timeseries"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 22
},
"id": 5,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.3.1",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nselect * from api.monitoring_humidity;\n",
"refId": "A",
"select": [
[
{
"params": [
"_from_lat"
],
"type": "column"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"table": "trip_in_progress",
"timeColumn": "_from_time",
"timeColumnType": "timestamp",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"title": "environment.%.humidity",
"type": "timeseries"
}
],
"refresh": "5m",
"revision": 1,
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"definition": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
"description": "Vessel name",
"hide": 0,
"includeAll": false,
"label": "Boat",
"multi": false,
"name": "boat",
"options": [],
"query": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-30d",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "utc",
"title": "Monitor",
"uid": "pgsail_tpl_monitor",
"version": 1,
"weekStart": ""
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,635 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Solar energy",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 5,
"links": [
{
"asDropdown": false,
"icon": "external link",
"includeVars": true,
"keepTime": false,
"tags": [],
"targetBlank": true,
"title": "New link",
"tooltip": "",
"type": "dashboards",
"url": ""
}
],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"decimals": 1,
"mappings": [
{
"options": {
"0": {
"text": "Aus"
}
},
"type": "value"
},
{
"options": {
"match": "null",
"result": {
"text": "Aus"
}
},
"type": "special"
}
],
"max": 400,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "semi-dark-red",
"value": null
},
{
"color": "#EAB839",
"value": 32
},
{
"color": "dark-green",
"value": 50
},
{
"color": "semi-dark-green",
"value": 100
},
{
"color": "light-green",
"value": 200
}
]
},
"unit": "watt"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 0,
"y": 0
},
"id": 46,
"interval": "",
"options": {
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true,
"text": {
"valueSize": 48
}
},
"pluginVersion": "10.1.0",
"targets": [
{
"alias": "Watt",
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
}
],
"measurement": "solarcharger/Yield/Power",
"orderByTime": "ASC",
"policy": "default",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.solar.Main.panelPower' as NUMERIC) as panelPower FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}' LIMIT 1;\n",
"refId": "Watt",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "last"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"tags": []
}
],
"transparent": true,
"type": "gauge"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "watt"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 6,
"y": 0
},
"id": 48,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.solar.Main.panelPower' as NUMERIC) as panelPower FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "panelPower",
"type": "timeseries"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Volts",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "volt"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "current"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "blue",
"mode": "fixed"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "panelvoltage"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "yellow",
"mode": "fixed"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "current"
},
"properties": [
{
"id": "unit",
"value": "amp"
},
{
"id": "custom.axisLabel",
"value": "Amps"
}
]
}
]
},
"gridPos": {
"h": 7,
"w": 18,
"x": 0,
"y": 8
},
"id": 47,
"options": {
"legend": {
"calcs": [
"mean",
"max",
"min"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "10.1.0",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "electrical.batteries.256.voltage",
"orderByTime": "ASC",
"policy": "default",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.solar.Main.panelVoltage' as NUMERIC) as panelVoltage FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"tags": []
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"hide": false,
"measurement": "electrical.batteries.256.current",
"orderByTime": "ASC",
"policy": "default",
"query": "SELECT mean(\"value\") FROM \"electrical.batteries.256.current\" WHERE $timeFilter GROUP BY time($__interval) fill(null)",
"rawQuery": true,
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.batteries.House.current' as NUMERIC) as current FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
"refId": "B",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
},
"tags": []
}
],
"title": "panelVoltage",
"type": "timeseries"
}
],
"refresh": "5m",
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"definition": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
"description": "Vessel Name",
"hide": 0,
"includeAll": false,
"label": "Boat",
"multi": false,
"name": "boat",
"options": [],
"query": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"definition": "SET vessel.id = '${__user.login}';\nSELECT rtrim(key, 'panelVoltage') AS __text ,key AS __value FROM api.monitoring_view2 where key ILIKE 'electrical.solar%panelVoltage';",
"description": "Solar Panel",
"hide": 0,
"includeAll": false,
"label": "solarPanel",
"multi": false,
"name": "solar_panel",
"options": [],
"query": "SET vessel.id = '${__user.login}';\nSELECT rtrim(key, 'panelVoltage') AS __text ,key AS __value FROM api.monitoring_view2 where key ILIKE 'electrical.solar%panelVoltage';",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-24h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "utc",
"title": "Solar System",
"uid": "pgsail_tpl_solar",
"version": 1,
"weekStart": ""
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,297 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 5,
"links": [
{
"asDropdown": false,
"icon": "external link",
"includeVars": true,
"keepTime": false,
"tags": [],
"targetBlank": true,
"title": "New link",
"tooltip": "",
"type": "dashboards",
"url": ""
}
],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "postgres",
"uid": "OIttR1sVk"
},
"gridPos": {
"h": 13,
"w": 10,
"x": 0,
"y": 0
},
"id": 3,
"links": [],
"options": {
"folderId": 0,
"includeVars": false,
"keepTime": false,
"maxItems": 30,
"query": "",
"showHeadings": true,
"showRecentlyViewed": true,
"showSearch": false,
"showStarred": true,
"tags": []
},
"pluginVersion": "10.1.4",
"tags": [],
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "OIttR1sVk"
},
"refId": "A"
}
],
"title": "PostgSail Dashboards",
"type": "dashlist"
},
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 13,
"w": 12,
"x": 10,
"y": 0
},
"id": 5,
"maxDataPoints": 500,
"options": {
"basemap": {
"config": {},
"name": "Layer 0",
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"showLegend": true,
"style": {
"color": {
"fixed": "dark-green"
},
"opacity": 0.4,
"rotation": {
"fixed": 0,
"max": 360,
"min": -360,
"mode": "mod"
},
"size": {
"fixed": 5,
"max": 15,
"min": 2
},
"symbol": {
"fixed": "img/icons/marker/circle.svg",
"mode": "fixed"
},
"textConfig": {
"fontSize": 12,
"offsetX": 0,
"offsetY": 0,
"textAlign": "center",
"textBaseline": "middle"
}
}
},
"filterData": {
"id": "byRefId",
"options": "A"
},
"location": {
"latitude": "value",
"longitude": "value",
"mode": "auto"
},
"name": "Boat",
"tooltip": true,
"type": "markers"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"allLayers": true,
"id": "fit",
"lat": 0,
"lon": 0,
"zoom": 5
}
},
"pluginVersion": "10.1.4",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT latitude, longitude FROM api.metrics WHERE vessel_id = '${boat}' ORDER BY time DESC LIMIT 1;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Location",
"type": "geomap"
}
],
"refresh": "",
"revision": 1,
"schemaVersion": 38,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"datasource": {
"type": "postgres",
"uid": "PCC52D03280B7034C"
},
"definition": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
"description": "Vessel Name",
"hide": 0,
"includeAll": false,
"label": "Boat",
"multi": false,
"name": "boat",
"options": [],
"query": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-90d",
"to": "now"
},
"timepicker": {
"hidden": true,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"type": "timepicker"
},
"timezone": "browser",
"title": "Home",
"uid": "pgsail_tpl_home",
"version": 1,
"weekStart": ""
}

24
grafana/grafana.ini Normal file
View File

@@ -0,0 +1,24 @@
[users]
allow_sign_up = false
auto_assign_org = true
auto_assign_org_role = Editor
[dashboards]
default_home_dashboard_path = /etc/grafana/dashboards/tpl/home.json
min_refresh_interval = 1m
[alerting]
enabled = false
[unified_alerting]
enabled = false
[analytics]
feedback_links_enabled = false
reporting_enabled = false
[news]
news_feed_enabled = false
[help]
enabled = false

View File

@@ -0,0 +1,25 @@
apiVersion: 1
providers:
# <string> an unique provider name. Required
- name: 'PostgSail'
# <int> Org id. Default to 1
orgId: 1
# <string> name of the dashboard folder.
#folder: 'PostgSail'
# <string> folder UID. will be automatically generated if not specified
#folderUid: ''
# <string> provider type. Default to 'file'
type: file
# <bool> disable dashboard deletion
disableDeletion: false
# <int> how often Grafana will scan for changed dashboards
updateIntervalSeconds: 60
# <bool> allow updating provisioned dashboards from the UI
allowUiUpdates: true
options:
# <string, required> path to dashboard files on disk. Required when using the 'file' type
path: /etc/grafana/dashboards/tpl/
# <bool> use folder names from filesystem to create folders in Grafana
foldersFromFilesStructure: true

View File

@@ -0,0 +1,18 @@
apiVersion: 1
datasources:
- name: PostgreSQL
isDefault: true
type: postgres
url: '${PGSAIL_GRAFANA_URI}'
database: signalk
user: grafana
secureJsonData:
password: '${PGSAIL_GRAFANA_PASSWORD}'
jsonData:
sslmode: 'disable' # disable/require/verify-ca/verify-full
maxOpenConns: 10 # Grafana v5.4+
maxIdleConns: 2 # Grafana v5.4+
connMaxLifetime: 14400 # Grafana v5.4+
postgresVersion: 1500 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10
timescaledb: true

View File

@@ -7,20 +7,30 @@ echo $PGDATA
echo "${PGDATA}/postgresql.conf"
cat << 'EOF' >> ${PGDATA}/postgresql.conf
# PostgSail pg15
# Add settings for extensions here
shared_preload_libraries = 'timescaledb,pg_stat_statements,pg_cron'
# TimescaleDB - time series database
# Disable timescaleDB telemetry
timescaledb.telemetry_level=off
# pg_cron - Run periodic jobs in PostgreSQL
# pg_cron database
#cron.database_name = 'signalk'
# pg_cron connect via a unix domain socket
cron.host = '/var/run/postgresql/'
# Increase the number of available background workers from the default of 8
#max_worker_processes = 8
# monitoring https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-IO-TIMING
track_io_timing = on
stats_temp_directory = '/tmp'
track_functions = all
# Remove in pg-15, does not exist anymore
#stats_temp_directory = '/tmp'
# Postgrest
# PostgREST - turns your PostgreSQL database directly into a RESTful API
# send logs where the collector can access them
#log_destination = 'stderr'
log_destination = 'stderr'
# collect stderr output to log files
#logging_collector = on
# save logs in pg_log/ under the pg data directory
@@ -29,5 +39,19 @@ stats_temp_directory = '/tmp'
#log_filename = 'postgresql-%Y-%m-%d.log'
# log every kind of SQL statement
#log_statement = 'all'
# Do not enable log_statement as its log format will not be parsed by pgBadger.
# pgBadger - a fast PostgreSQL log analysis report
# log all the queries that are taking more than 1 second:
#log_min_duration_statement = 1000
#log_checkpoints = on
#log_connections = on
#log_disconnections = on
#log_lock_waits = on
#log_temp_files = 0
#log_autovacuum_min_duration = 0
#log_error_verbosity = default
# Francois
log_min_messages = NOTICE
EOF

83
initdb/01signalk.sql Executable file
View File

@@ -0,0 +1,83 @@
---------------------------------------------------------------------------
-- PostgSail => Postgres + TimescaleDB + PostGIS + PostgREST
--
-- Inspired from:
-- https://groups.google.com/g/signalk/c/W2H15ODCic4
--
-- Description:
-- Insert data into table api.metadata from API using PostgREST
-- Insert data into table api.metrics from API using PostgREST
-- TimescaleDB Hypertable to store signalk metrics on table api.metrics
-- pgsql functions to generate logbook, stays, moorages from table api.metrics
-- CRON functions to process logbook, stays, moorages
-- python functions for geo reverse and send notification via email, pushover, telegram
-- Views statistics, timelapse, monitoring, logs
-- Always store time in UTC
---------------------------------------------------------------------------
-- vessels signalk -(POST)-> metadata -> metadata_upsert_trigger -(BEFORE INSERT)-> metadata_upsert_trigger_fn (INSERT or UPDATE)
-- vessels signalk -(POST)-> metrics -> metrics_trigger -(BEFORE INSERT)-> metrics_trigger_fn (INSERT or UPDATE new log,stay)
---------------------------------------------------------------------------
-- Drop database
-- % docker exec -i timescaledb-postgis psql -Uusername -W postgres -c "drop database signalk;"
-- Import Schema
-- % cat signalk.sql | docker exec -i timescaledb-postgis psql -Uusername postgres
-- Export hypertable
-- % docker exec -i timescaledb-postgis psql -Uusername -W signalk -c "\COPY (SELECT * FROM api.metrics ORDER BY time ASC) TO '/var/lib/postgresql/data/metrics.csv' DELIMITER ',' CSV"
-- Export hypertable to gzip
-- # docker exec -i timescaledb-postgis psql -Uusername -W signalk -c "\COPY (SELECT * FROM api.metrics ORDER BY time ASC) TO PROGRAM 'gzip > /var/lib/postgresql/data/metrics.csv.gz' CSV HEADER;"
DO $$
BEGIN
RAISE WARNING '
_________.__ .__ ____ __.
/ _____/|__| ____ ____ _____ | | | |/ _|
\_____ \ | |/ ___\ / \\__ \ | | | <
/ \| / /_/ > | \/ __ \| |_| | \
/_______ /|__\___ /|___| (____ /____/____|__ \
\/ /_____/ \/ \/ \/
%', now();
END $$;
select version();
-- Database
CREATE DATABASE signalk;
-- Limit connection to 100
ALTER DATABASE signalk WITH CONNECTION LIMIT = 100;
-- Set timezone to UTC
ALTER DATABASE signalk SET TIMEZONE='UTC';
-- Set datestyle output
ALTER DATABASE signalk SET datestyle TO "ISO, DMY";
-- Set intervalstyle output
ALTER DATABASE signalk SET intervalstyle TO 'iso_8601';
-- connect to the DB
\c signalk
-- Schema
CREATE SCHEMA IF NOT EXISTS api;
COMMENT ON SCHEMA api IS
$$PostgSail API
A RESTful API that serves PostgSail data using postgrest.$$;
-- Revoke default privileges to all public functions
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
-- Extensions
CREATE EXTENSION IF NOT EXISTS timescaledb; -- provides time series functions for PostgreSQL
-- CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit; -- provides time series functions for PostgreSQL
CREATE EXTENSION IF NOT EXISTS postgis; -- adds support for geographic objects to the PostgreSQL object-relational database
CREATE EXTENSION IF NOT EXISTS plpgsql; -- PL/pgSQL procedural language
CREATE EXTENSION IF NOT EXISTS plpython3u; -- implements PL/Python based on the Python 3 language variant.
CREATE EXTENSION IF NOT EXISTS jsonb_plpython3u CASCADE; -- tranform jsonb to python json type.
CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- provides a means for tracking planning and execution statistics of all SQL statements executed
CREATE EXTENSION IF NOT EXISTS "moddatetime"; -- provides functions for tracking last modification time
-- Trust plpython3u language by default
UPDATE pg_language SET lanpltrusted = true WHERE lanname = 'plpython3u';

View File

@@ -0,0 +1,694 @@
-- connect to the DB
\c signalk
---------------------------------------------------------------------------
-- Tables
--
---------------------------------------------------------------------------
-- Metadata from signalk
CREATE TABLE IF NOT EXISTS api.metadata(
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NULL,
mmsi NUMERIC NULL,
client_id TEXT NULL,
-- vessel_id link auth.vessels with api.metadata
vessel_id TEXT NOT NULL UNIQUE,
length DOUBLE PRECISION NULL,
beam DOUBLE PRECISION NULL,
height DOUBLE PRECISION NULL,
ship_type NUMERIC NULL,
plugin_version TEXT NOT NULL,
signalk_version TEXT NOT NULL,
time TIMESTAMPTZ NOT NULL, -- should be rename to last_update !?
platform TEXT NULL,
configuration TEXT NULL,
active BOOLEAN DEFAULT True, -- trigger monitor online/offline
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Description
COMMENT ON TABLE
api.metadata
IS 'Stores metadata received from vessel, aka signalk plugin';
COMMENT ON COLUMN api.metadata.active IS 'trigger monitor online/offline';
COMMENT ON COLUMN api.metadata.vessel_id IS 'vessel_id link auth.vessels with api.metadata';
-- Duplicate Indexes
--CREATE INDEX metadata_vessel_id_idx ON api.metadata (vessel_id);
---------------------------------------------------------------------------
-- Metrics from signalk
-- Create vessel status enum
CREATE TYPE status_type AS ENUM ('sailing', 'motoring', 'moored', 'anchored');
-- Table api.metrics
CREATE TABLE IF NOT EXISTS api.metrics (
time TIMESTAMPTZ NOT NULL,
client_id TEXT NULL,
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
latitude DOUBLE PRECISION NULL,
longitude DOUBLE PRECISION NULL,
speedOverGround DOUBLE PRECISION NULL,
courseOverGroundTrue DOUBLE PRECISION NULL,
windSpeedApparent DOUBLE PRECISION NULL,
angleSpeedApparent DOUBLE PRECISION NULL,
status TEXT NULL,
metrics JSONB NULL,
--CONSTRAINT valid_client_id CHECK (length(client_id) > 10),
--CONSTRAINT valid_latitude CHECK (latitude >= -90 and latitude <= 90),
--CONSTRAINT valid_longitude CHECK (longitude >= -180 and longitude <= 180),
PRIMARY KEY (time, vessel_id)
);
-- Description
COMMENT ON TABLE
api.metrics
IS 'Stores metrics from vessel';
COMMENT ON COLUMN api.metrics.latitude IS 'With CONSTRAINT but allow NULL value to be ignored silently by trigger';
COMMENT ON COLUMN api.metrics.longitude IS 'With CONSTRAINT but allow NULL value to be ignored silently by trigger';
-- Index
CREATE INDEX ON api.metrics (vessel_id, time DESC);
CREATE INDEX ON api.metrics (status, time DESC);
-- json index??
CREATE INDEX ON api.metrics using GIN (metrics);
-- timescaledb hypertable
SELECT create_hypertable('api.metrics', 'time', chunk_time_interval => INTERVAL '7 day');
-- timescaledb hypertable with space partitions
-- ERROR: new row for relation "_hyper_1_2_chunk" violates check constraint "constraint_4"
-- ((_timescaledb_internal.get_partition_hash(vessel_id) < 1073741823))
--SELECT create_hypertable('api.metrics', 'time', 'vessel_id',
-- number_partitions => 2,
-- chunk_time_interval => INTERVAL '7 day',
-- if_not_exists => true);
---------------------------------------------------------------------------
-- Logbook
-- todo add consumption fuel?
-- todo add engine hour?
-- todo add geom object http://epsg.io/4326 EPSG:4326 Unit: degres
-- todo add geog object http://epsg.io/3857 EPSG:3857 Unit: meters
-- https://postgis.net/workshops/postgis-intro/geography.html#using-geography
-- https://medium.com/coord/postgis-performance-showdown-geometry-vs-geography-ec99967da4f0
-- virtual logbook by boat by client_id impossible?
-- https://www.postgresql.org/docs/current/ddl-partitioning.html
-- Issue:
-- https://www.reddit.com/r/PostgreSQL/comments/di5mbr/postgresql_12_foreign_keys_and_partitioned_tables/f3tsoop/
-- Check unused index
CREATE TABLE IF NOT EXISTS api.logbook(
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
active BOOLEAN DEFAULT false,
name TEXT,
_from_moorage_id INT NULL,
_from TEXT,
_from_lat DOUBLE PRECISION NULL,
_from_lng DOUBLE PRECISION NULL,
_to_moorage_id INT NULL,
_to TEXT,
_to_lat DOUBLE PRECISION NULL,
_to_lng DOUBLE PRECISION NULL,
--track_geom Geometry(LINESTRING)
track_geom geometry(LINESTRING,4326) NULL,
track_geog geography(LINESTRING) NULL,
track_geojson JSONB NULL,
_from_time TIMESTAMPTZ NOT NULL,
_to_time TIMESTAMPTZ NULL,
distance NUMERIC, -- meters?
duration INTERVAL, -- duration in days and hours?
avg_speed DOUBLE PRECISION NULL,
max_speed DOUBLE PRECISION NULL,
max_wind_speed DOUBLE PRECISION NULL,
notes TEXT NULL, -- remarks
extra JSONB NULL -- computed signalk metrics of interest
);
-- Description
COMMENT ON TABLE
api.logbook
IS 'Stores generated logbook';
COMMENT ON COLUMN api.logbook.distance IS 'in NM';
COMMENT ON COLUMN api.logbook.extra IS 'computed signalk metrics of interest, runTime, currentLevel, etc';
COMMENT ON COLUMN api.logbook.duration IS 'Best to use standard ISO 8601';
-- Index todo!
CREATE INDEX logbook_vessel_id_idx ON api.logbook (vessel_id);
CREATE INDEX logbook_from_time_idx ON api.logbook (_from_time);
CREATE INDEX logbook_to_time_idx ON api.logbook (_to_time);
CREATE INDEX logbook_from_moorage_id_idx ON api.logbook (_from_moorage_id);
CREATE INDEX logbook_to_moorage_id_idx ON api.logbook (_to_moorage_id);
CREATE INDEX ON api.logbook USING GIST ( track_geom );
COMMENT ON COLUMN api.logbook.track_geom IS 'postgis geometry type EPSG:4326 Unit: degres';
CREATE INDEX ON api.logbook USING GIST ( track_geog );
COMMENT ON COLUMN api.logbook.track_geog IS 'postgis geography type default SRID 4326 Unit: degres';
-- Otherwise -- ERROR: Only lon/lat coordinate systems are supported in geography.
COMMENT ON COLUMN api.logbook.track_geojson IS 'store generated geojson with track metrics data using with LineString and Point features, we can not depend api.metrics table';
---------------------------------------------------------------------------
-- Stays
-- virtual logbook by boat?
CREATE TABLE IF NOT EXISTS api.stays(
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
active BOOLEAN DEFAULT false,
moorage_id INT NULL,
name TEXT,
latitude DOUBLE PRECISION NULL,
longitude DOUBLE PRECISION NULL,
geog GEOGRAPHY(POINT) NULL,
arrived TIMESTAMPTZ NOT NULL,
departed TIMESTAMPTZ,
duration INTERVAL, -- duration in days and hours?
stay_code INT DEFAULT 1, -- REFERENCES api.stays_at(stay_code),
notes TEXT NULL
);
-- Description
COMMENT ON TABLE
api.stays
IS 'Stores generated stays';
COMMENT ON COLUMN api.stays.duration IS 'Best to use standard ISO 8601';
-- Index
CREATE INDEX stays_vessel_id_idx ON api.stays (vessel_id);
CREATE INDEX stays_moorage_id_idx ON api.stays (moorage_id);
CREATE INDEX ON api.stays USING GIST ( geog );
COMMENT ON COLUMN api.stays.geog IS 'postgis geography type default SRID 4326 Unit: degres';
-- With other SRID ERROR: Only lon/lat coordinate systems are supported in geography.
---------------------------------------------------------------------------
-- Moorages
-- virtual logbook by boat?
CREATE TABLE IF NOT EXISTS api.moorages(
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
name TEXT,
country TEXT,
stay_code INT DEFAULT 1, -- needed? REFERENCES api.stays_at(stay_code)
stay_duration INTERVAL NULL,
reference_count INT DEFAULT 1,
latitude DOUBLE PRECISION NULL,
longitude DOUBLE PRECISION NULL,
geog GEOGRAPHY(POINT) NULL,
home_flag BOOLEAN DEFAULT false,
notes TEXT NULL,
overpass JSONB NULL,
nominatim JSONB NULL
);
-- Description
COMMENT ON TABLE
api.moorages
IS 'Stores generated moorages';
-- Index
CREATE INDEX moorages_vessel_id_idx ON api.moorages (vessel_id);
CREATE INDEX ON api.moorages USING GIST ( geog );
COMMENT ON COLUMN api.moorages.geog IS 'postgis geography type default SRID 4326 Unit: degres';
-- With other SRID ERROR: Only lon/lat coordinate systems are supported in geography.
COMMENT ON COLUMN api.moorages.stay_duration IS 'Best to use standard ISO 8601';
---------------------------------------------------------------------------
-- Stay Type
CREATE TABLE IF NOT EXISTS api.stays_at(
stay_code INTEGER UNIQUE NOT NULL,
description TEXT NOT NULL
);
-- Description
COMMENT ON TABLE api.stays_at IS 'Stay Type';
-- Insert default possible values
INSERT INTO api.stays_at(stay_code, description) VALUES
(1, 'Unknown'),
(2, 'Anchor'),
(3, 'Mooring Buoy'),
(4, 'Dock');
---------------------------------------------------------------------------
-- Trigger Functions Metadata table
--
-- UPSERT - Insert vs Update for Metadata
DROP FUNCTION IF EXISTS metadata_upsert_trigger_fn;
CREATE FUNCTION metadata_upsert_trigger_fn() RETURNS trigger AS $metadata_upsert$
DECLARE
metadata_id integer;
metadata_active boolean;
BEGIN
-- Set client_id to new value to allow RLS
--PERFORM set_config('vessel.client_id', NEW.client_id, false);
-- UPSERT - Insert vs Update for Metadata
--RAISE NOTICE 'metadata_upsert_trigger_fn';
--PERFORM set_config('vessel.id', NEW.vessel_id, true);
--RAISE WARNING 'metadata_upsert_trigger_fn [%] [%]', current_setting('vessel.id', true), NEW;
SELECT m.id,m.active INTO metadata_id, metadata_active
FROM api.metadata m
WHERE m.vessel_id IS NOT NULL AND m.vessel_id = current_setting('vessel.id', true);
--RAISE NOTICE 'metadata_id is [%]', metadata_id;
IF metadata_id IS NOT NULL THEN
-- send notification if boat is back online
IF metadata_active is False THEN
-- Add monitor online entry to process queue for later notification
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('monitoring_online', metadata_id, now(), current_setting('vessel.id', true));
END IF;
-- Update vessel metadata
UPDATE api.metadata
SET
name = NEW.name,
mmsi = NEW.mmsi,
client_id = NEW.client_id,
length = NEW.length,
beam = NEW.beam,
height = NEW.height,
ship_type = NEW.ship_type,
plugin_version = NEW.plugin_version,
signalk_version = NEW.signalk_version,
platform = NEW.platform,
configuration = NEW.configuration,
-- time = NEW.time, ignore the time sent by the vessel as it is out of sync sometimes.
time = NOW(), -- overwrite the time sent by the vessel
active = true
WHERE id = metadata_id;
RETURN NULL; -- Ignore insert
ELSE
IF NEW.vessel_id IS NULL THEN
-- set vessel_id from jwt if not present in INSERT query
NEW.vessel_id := current_setting('vessel.id');
END IF;
-- Ignore and overwrite the time sent by the vessel
NEW.time := NOW();
-- Insert new vessel metadata
RETURN NEW; -- Insert new vessel metadata
END IF;
END;
$metadata_upsert$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.metadata_upsert_trigger_fn
IS 'process metadata from vessel, upsert';
CREATE TRIGGER metadata_moddatetime
BEFORE UPDATE ON api.metadata
FOR EACH ROW
EXECUTE PROCEDURE moddatetime (updated_at);
-- Description
COMMENT ON TRIGGER metadata_moddatetime
ON api.metadata
IS 'Automatic update of updated_at on table modification';
-- FUNCTION Metadata notification for new vessel after insert
DROP FUNCTION IF EXISTS metadata_notification_trigger_fn;
CREATE FUNCTION metadata_notification_trigger_fn() RETURNS trigger AS $metadata_notification$
DECLARE
BEGIN
RAISE NOTICE 'metadata_notification_trigger_fn [%]', NEW;
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('monitoring_online', NEW.id, now(), NEW.vessel_id);
RETURN NULL;
END;
$metadata_notification$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.metadata_notification_trigger_fn
IS 'process metadata notification from vessel, monitoring_online';
-- FUNCTION Metadata grafana provisioning for new vessel after insert
DROP FUNCTION IF EXISTS metadata_grafana_trigger_fn;
CREATE FUNCTION metadata_grafana_trigger_fn() RETURNS trigger AS $metadata_grafana$
DECLARE
BEGIN
RAISE NOTICE 'metadata_grafana_trigger_fn [%]', NEW;
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('grafana', NEW.id, now(), NEW.vessel_id);
RETURN NULL;
END;
$metadata_grafana$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.metadata_grafana_trigger_fn
IS 'process metadata grafana provisioning from vessel';
---------------------------------------------------------------------------
-- Trigger metadata table
--
-- Metadata trigger BEFORE INSERT
CREATE TRIGGER metadata_upsert_trigger BEFORE INSERT ON api.metadata
FOR EACH ROW EXECUTE FUNCTION metadata_upsert_trigger_fn();
-- Description
COMMENT ON TRIGGER
metadata_upsert_trigger ON api.metadata
IS 'BEFORE INSERT ON api.metadata run function metadata_upsert_trigger_fn';
-- Metadata trigger AFTER INSERT
CREATE TRIGGER metadata_notification_trigger AFTER INSERT ON api.metadata
FOR EACH ROW EXECUTE FUNCTION metadata_notification_trigger_fn();
-- Description
COMMENT ON TRIGGER
metadata_notification_trigger ON api.metadata
IS 'AFTER INSERT ON api.metadata run function metadata_notification_trigger_fn for later notification on new vessel';
-- Metadata trigger AFTER INSERT
CREATE TRIGGER metadata_grafana_trigger AFTER INSERT ON api.metadata
FOR EACH ROW EXECUTE FUNCTION metadata_grafana_trigger_fn();
-- Description
COMMENT ON TRIGGER
metadata_grafana_trigger ON api.metadata
IS 'AFTER INSERT ON api.metadata run function metadata_grafana_trigger_fn for later grafana provisioning on new vessel';
---------------------------------------------------------------------------
-- Trigger Functions metrics table
--
-- Create a logbook or stay entry base on the vessel state, eg: navigation.state
-- https://github.com/meri-imperiumi/signalk-autostate
DROP FUNCTION IF EXISTS metrics_trigger_fn;
CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
DECLARE
previous_metric record;
stay_code INTEGER;
logbook_id INTEGER;
stay_id INTEGER;
valid_status BOOLEAN := False;
_vessel_id TEXT;
distance BOOLEAN := False;
BEGIN
--RAISE NOTICE 'metrics_trigger_fn';
--RAISE WARNING 'metrics_trigger_fn [%] [%]', current_setting('vessel.id', true), NEW;
-- Ensure vessel.id to new value to allow RLS
IF NEW.vessel_id IS NULL THEN
-- set vessel_id from jwt if not present in INSERT query
NEW.vessel_id := current_setting('vessel.id');
END IF;
-- Boat metadata are check using api.metrics REFERENCES to api.metadata
-- Fetch the latest entry to compare status against the new status to be insert
SELECT * INTO previous_metric
FROM api.metrics m
WHERE m.vessel_id IS NOT NULL
AND m.vessel_id = current_setting('vessel.id', true)
ORDER BY m.time DESC LIMIT 1;
--RAISE NOTICE 'Metrics Status, New:[%] Previous:[%]', NEW.status, previous_metric.status;
IF previous_metric.time = NEW.time THEN
-- Ignore entry if same time
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], duplicate time [%] = [%]', NEW.vessel_id, previous_metric.time, NEW.time;
RETURN NULL;
END IF;
IF previous_metric.time > NEW.time THEN
-- Ignore entry if new time is later than previous time
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], new time is older than previous_metric.time [%] > [%]', NEW.vessel_id, previous_metric.time, NEW.time;
RETURN NULL;
END IF;
-- Check if latitude or longitude are type double
--IF public.isdouble(NEW.latitude::TEXT) IS False OR public.isdouble(NEW.longitude::TEXT) IS False THEN
-- -- Ignore entry if null latitude,longitude
-- RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], not a double type for latitude or longitude [%] [%]', NEW.vessel_id, NEW.latitude, NEW.longitude;
-- RETURN NULL;
--END IF;
-- Check if latitude or longitude are null
IF NEW.latitude IS NULL OR NEW.longitude IS NULL THEN
-- Ignore entry if null latitude,longitude
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], null latitude or longitude [%] [%]', NEW.vessel_id, NEW.latitude, NEW.longitude;
RETURN NULL;
END IF;
-- Check if valid latitude
IF NEW.latitude >= 90 OR NEW.latitude <= -90 THEN
-- Ignore entry if invalid latitude,longitude
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], invalid latitude >= 90 OR <= -90 [%] [%]', NEW.vessel_id, NEW.latitude, NEW.longitude;
RETURN NULL;
END IF;
-- Check if valid longitude
IF NEW.longitude >= 180 OR NEW.longitude <= -180 THEN
-- Ignore entry if invalid latitude,longitude
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], invalid longitude >= 180 OR <= -180 [%] [%]', NEW.vessel_id, NEW.latitude, NEW.longitude;
RETURN NULL;
END IF;
-- Check if valid longitude and latitude not close to -0.0000001 from Victron Cerbo
IF NEW.latitude = NEW.longitude THEN
-- Ignore entry if latitude,longitude are equal
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], latitude and longitude are equal [%] [%]', NEW.vessel_id, NEW.latitude, NEW.longitude;
RETURN NULL;
END IF;
-- Check distance with previous point is > 10km
--SELECT ST_Distance(
-- ST_MakePoint(NEW.latitude,NEW.longitude)::geography,
-- ST_MakePoint(previous_metric.latitude,previous_metric.longitude)::geography) > 10000 INTO distance;
--IF distance IS True THEN
-- RAISE WARNING 'Metrics Ignoring metric, distance between previous metric and new metric is too large, vessel_id [%] distance[%]', NEW.vessel_id, distance;
-- RETURN NULL;
--END IF;
-- Check if status is null but speed is over 3knots set status to sailing
IF NEW.status IS NULL AND NEW.speedoverground >= 3 THEN
RAISE WARNING 'Metrics Unknown NEW.status, vessel_id [%], null status, set to sailing because of speedoverground is +3 from [%]', NEW.vessel_id, NEW.status;
NEW.status := 'sailing';
-- Check if status is null then set status to default moored
ELSIF NEW.status IS NULL THEN
RAISE WARNING 'Metrics Unknown NEW.status, vessel_id [%], null status, set to default moored from [%]', NEW.vessel_id, NEW.status;
NEW.status := 'moored';
END IF;
IF previous_metric.status IS NULL THEN
IF NEW.status = 'anchored' THEN
RAISE WARNING 'Metrics Unknown previous_metric.status from vessel_id [%], [%] set to default current status [%]', NEW.vessel_id, previous_metric.status, NEW.status;
previous_metric.status := NEW.status;
ELSE
RAISE WARNING 'Metrics Unknown previous_metric.status from vessel_id [%], [%] set to default status moored vs [%]', NEW.vessel_id, previous_metric.status, NEW.status;
previous_metric.status := 'moored';
END IF;
-- Add new stay as no previous entry exist
INSERT INTO api.stays
(vessel_id, active, arrived, latitude, longitude, stay_code)
VALUES (current_setting('vessel.id', true), true, NEW.time, NEW.latitude, NEW.longitude, 1)
RETURNING id INTO stay_id;
-- Add stay entry to process queue for further processing
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('new_stay', stay_id, now(), current_setting('vessel.id', true));
RAISE WARNING 'Metrics Insert first stay as no previous metrics exist, stay_id stay_id [%] [%] [%]', stay_id, NEW.status, NEW.time;
END IF;
-- Check if status is valid enum
SELECT NEW.status::name = any(enum_range(null::status_type)::name[]) INTO valid_status;
IF valid_status IS False THEN
-- Ignore entry if status is invalid
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], invalid status [%]', NEW.vessel_id, NEW.status;
RETURN NULL;
END IF;
-- Check if speedOverGround is valid value
IF NEW.speedoverground >= 40 THEN
-- Ignore entry as speedOverGround is invalid
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], speedOverGround is invalid, over 40 < [%]', NEW.vessel_id, NEW.speedoverground;
RETURN NULL;
END IF;
-- Check the state and if any previous/current entry
-- If change of state and new status is sailing or motoring
IF previous_metric.status::TEXT <> NEW.status::TEXT AND
( (NEW.status::TEXT = 'sailing' AND previous_metric.status::TEXT <> 'motoring')
OR (NEW.status::TEXT = 'motoring' AND previous_metric.status::TEXT <> 'sailing') ) THEN
RAISE WARNING 'Metrics Update status, try new logbook, New:[%] Previous:[%]', NEW.status, previous_metric.status;
-- Start new log
logbook_id := public.trip_in_progress_fn(current_setting('vessel.id', true)::TEXT);
IF logbook_id IS NULL THEN
INSERT INTO api.logbook
(vessel_id, active, _from_time, _from_lat, _from_lng)
VALUES (current_setting('vessel.id', true), true, NEW.time, NEW.latitude, NEW.longitude)
RETURNING id INTO logbook_id;
RAISE WARNING 'Metrics Insert new logbook, logbook_id [%] [%] [%]', logbook_id, NEW.status, NEW.time;
ELSE
UPDATE api.logbook
SET
active = false,
_to_time = NEW.time,
_to_lat = NEW.latitude,
_to_lng = NEW.longitude
WHERE id = logbook_id;
RAISE WARNING 'Metrics Existing logbook logbook_id [%] [%] [%]', logbook_id, NEW.status, NEW.time;
END IF;
-- End current stay
stay_id := public.stay_in_progress_fn(current_setting('vessel.id', true)::TEXT);
IF stay_id IS NOT NULL THEN
UPDATE api.stays
SET
active = false,
departed = NEW.time
WHERE id = stay_id;
-- Add stay entry to process queue for further processing
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('new_stay', stay_id, NOW(), current_setting('vessel.id', true));
RAISE WARNING 'Metrics Updating Stay end current stay_id [%] [%] [%]', stay_id, NEW.status, NEW.time;
ELSE
RAISE WARNING 'Metrics Invalid stay_id [%] [%]', stay_id, NEW.time;
END IF;
-- If change of state and new status is moored or anchored
ELSIF previous_metric.status::TEXT <> NEW.status::TEXT AND
( (NEW.status::TEXT = 'moored' AND previous_metric.status::TEXT <> 'anchored')
OR (NEW.status::TEXT = 'anchored' AND previous_metric.status::TEXT <> 'moored') ) THEN
-- Start new stays
RAISE WARNING 'Metrics Update status, try new stay, New:[%] Previous:[%]', NEW.status, previous_metric.status;
stay_id := public.stay_in_progress_fn(current_setting('vessel.id', true)::TEXT);
IF stay_id IS NULL THEN
RAISE WARNING 'Metrics Inserting new stay [%]', NEW.status;
-- If metric status is anchored set stay_code accordingly
stay_code = 1;
IF NEW.status = 'anchored' THEN
stay_code = 2;
END IF;
-- Add new stay
INSERT INTO api.stays
(vessel_id, active, arrived, latitude, longitude, stay_code)
VALUES (current_setting('vessel.id', true), true, NEW.time, NEW.latitude, NEW.longitude, stay_code)
RETURNING id INTO stay_id;
RAISE WARNING 'Metrics Insert new stay, stay_id [%] [%] [%]', stay_id, NEW.status, NEW.time;
ELSE
RAISE WARNING 'Metrics Invalid stay_id [%] [%]', stay_id, NEW.time;
UPDATE api.stays
SET
active = false,
departed = NEW.time,
notes = 'Invalid stay?'
WHERE id = stay_id;
END IF;
-- End current log/trip
-- Fetch logbook_id by vessel_id
logbook_id := public.trip_in_progress_fn(current_setting('vessel.id', true)::TEXT);
IF logbook_id IS NOT NULL THEN
-- todo check on time start vs end
RAISE WARNING 'Metrics Updating logbook status [%] [%] [%]', logbook_id, NEW.status, NEW.time;
UPDATE api.logbook
SET
active = false,
_to_time = NEW.time,
_to_lat = NEW.latitude,
_to_lng = NEW.longitude
WHERE id = logbook_id;
-- Add logbook entry to process queue for later processing
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('pre_logbook', logbook_id, NOW(), current_setting('vessel.id', true));
ELSE
RAISE WARNING 'Metrics Invalid logbook_id [%] [%] [%]', logbook_id, NEW.status, NEW.time;
END IF;
END IF;
RETURN NEW; -- Finally insert the actual new metric
END;
$metrics$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.metrics_trigger_fn
IS 'process metrics from vessel, generate pre_logbook and new_stay.';
--
-- Triggers logbook update on metrics insert
CREATE TRIGGER metrics_trigger BEFORE INSERT ON api.metrics
FOR EACH ROW EXECUTE FUNCTION metrics_trigger_fn();
-- Description
COMMENT ON TRIGGER
metrics_trigger ON api.metrics
IS 'BEFORE INSERT ON api.metrics run function metrics_trigger_fn';
-- Function update of name and stay_code on logbook and stays reference
DROP FUNCTION IF EXISTS moorage_update_trigger_fn;
CREATE FUNCTION moorage_update_trigger_fn() RETURNS trigger AS $moorage_update$
DECLARE
BEGIN
RAISE NOTICE 'moorages_update_trigger_fn [%]', NEW;
IF ( OLD.name != NEW.name) THEN
UPDATE api.logbook SET _from = NEW.name WHERE _from_moorage_id = NEW.id;
UPDATE api.logbook SET _to = NEW.name WHERE _to_moorage_id = NEW.id;
END IF;
IF ( OLD.stay_code != NEW.stay_code) THEN
UPDATE api.stays SET stay_code = NEW.stay_code WHERE moorage_id = NEW.id;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$moorage_update$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.moorage_update_trigger_fn
IS 'Automatic update of name and stay_code on logbook and stays reference';
-- Triggers moorage update after update
CREATE TRIGGER moorage_update_trigger AFTER UPDATE ON api.moorages
FOR EACH ROW EXECUTE FUNCTION moorage_update_trigger_fn();
-- Description
COMMENT ON TRIGGER moorage_update_trigger
ON api.moorages
IS 'Automatic update of name and stay_code on logbook and stays reference';
-- Function delete logbook and stays reference when delete a moorage
DROP FUNCTION IF EXISTS moorage_delete_trigger_fn;
CREATE FUNCTION moorage_delete_trigger_fn() RETURNS trigger AS $moorage_delete$
DECLARE
BEGIN
RAISE NOTICE 'moorages_delete_trigger_fn [%]', OLD;
DELETE FROM api.stays WHERE moorage_id = OLD.id;
DELETE FROM api.logbook WHERE _from_moorage_id = OLD.id;
DELETE FROM api.logbook WHERE _to_moorage_id = OLD.id;
RETURN OLD; -- result is ignored since this is an AFTER trigger
END;
$moorage_delete$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.moorage_delete_trigger_fn
IS 'Automatic delete logbook and stays reference when delete a moorage';
-- Triggers moorage delete
CREATE TRIGGER moorage_delete_trigger BEFORE DELETE ON api.moorages
FOR EACH ROW EXECUTE FUNCTION moorage_delete_trigger_fn();
-- Description
COMMENT ON TRIGGER moorage_delete_trigger
ON api.moorages
IS 'Automatic delete logbook and stays reference when delete a moorage';
-- Function process_new on completed logbook
DROP FUNCTION IF EXISTS logbook_completed_trigger_fn;
CREATE FUNCTION logbook_completed_trigger_fn() RETURNS trigger AS $logbook_completed$
DECLARE
BEGIN
RAISE NOTICE 'logbook_completed_trigger_fn [%]', OLD;
RAISE NOTICE 'logbook_completed_trigger_fn [%] [%]', OLD._to_time, NEW._to_time;
-- Add logbook entry to process queue for later processing
--IF ( OLD._to_time <> NEW._to_time ) THEN
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('new_logbook', NEW.id, NOW(), current_setting('vessel.id', true));
--END IF;
RETURN OLD; -- result is ignored since this is an AFTER trigger
END;
$logbook_completed$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.logbook_completed_trigger_fn
IS 'Automatic process_queue for completed logbook._to_time';
-- Triggers logbook completed
--CREATE TRIGGER logbook_completed_trigger AFTER UPDATE ON api.logbook
-- FOR EACH ROW
-- WHEN (OLD._to_time IS DISTINCT FROM NEW._to_time)
-- EXECUTE FUNCTION logbook_completed_trigger_fn();
-- Description
--COMMENT ON TRIGGER logbook_completed_trigger
-- ON api.logbook
-- IS 'Automatic process_queue for completed logbook';
-- Function process_new on completed Stay
DROP FUNCTION IF EXISTS stay_completed_trigger_fn;
CREATE FUNCTION stay_completed_trigger_fn() RETURNS trigger AS $stay_completed$
DECLARE
BEGIN
RAISE NOTICE 'stay_completed_trigger_fn [%]', OLD;
RAISE NOTICE 'stay_completed_trigger_fn [%] [%]', OLD.departed, NEW.departed;
-- Add stay entry to process queue for later processing
--IF ( OLD.departed <> NEW.departed ) THEN
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('new_stay', NEW.id, NOW(), current_setting('vessel.id', true));
--END IF;
RETURN OLD; -- result is ignored since this is an AFTER trigger
END;
$stay_completed$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.stay_completed_trigger_fn
IS 'Automatic process_queue for completed stay.departed';
-- Triggers stay completed
--CREATE TRIGGER stay_completed_trigger AFTER UPDATE ON api.stays
-- FOR EACH ROW
-- WHEN (OLD.departed IS DISTINCT FROM NEW.departed)
-- EXECUTE FUNCTION stay_completed_trigger_fn();
-- Description
--COMMENT ON TRIGGER stay_completed_trigger
-- ON api.stays
-- IS 'Automatic process_queue for completed stay';

View File

@@ -0,0 +1,938 @@
-- connect to the DB
\c signalk
---------------------------------------------------------------------------
-- API helper functions
--
---------------------------------------------------------------------------
-- PostgREST Media Type Handlers
CREATE DOMAIN "text/xml" AS xml;
CREATE DOMAIN "application/geo+json" AS jsonb;
CREATE DOMAIN "application/gpx+xml" AS xml;
CREATE DOMAIN "application/vnd.google-earth.kml+xml" AS xml;
---------------------------------------------------------------------------
-- Functions API schema
-- Timelapse - replay logs
DROP FUNCTION IF EXISTS api.timelapse_fn;
CREATE OR REPLACE FUNCTION api.timelapse_fn(
IN start_log INTEGER DEFAULT NULL,
IN end_log INTEGER DEFAULT NULL,
IN start_date TEXT DEFAULT NULL,
IN end_date TEXT DEFAULT NULL,
OUT geojson JSONB) RETURNS JSONB AS $timelapse$
DECLARE
_geojson jsonb;
BEGIN
-- Using sub query to force id order by
-- Merge GIS track_geom into a GeoJSON MultiLineString
IF start_log IS NOT NULL AND public.isnumeric(start_log::text) AND public.isnumeric(end_log::text) THEN
WITH logbook as (
SELECT track_geom
FROM api.logbook
WHERE id >= start_log
AND id <= end_log
AND track_geom IS NOT NULL
ORDER BY _from_time ASC
)
SELECT ST_AsGeoJSON(geo.*) INTO _geojson FROM (
SELECT ST_Collect(
ARRAY(
SELECT track_geom FROM logbook))
) as geo;
--raise WARNING 'by log id _geojson %' , _geojson;
ELSIF start_date IS NOT NULL AND public.isdate(start_date::text) AND public.isdate(end_date::text) THEN
WITH logbook as (
SELECT track_geom
FROM api.logbook
WHERE _from_time >= start_date::TIMESTAMPTZ
AND _to_time <= end_date::TIMESTAMPTZ + interval '23 hours 59 minutes'
AND track_geom IS NOT NULL
ORDER BY _from_time ASC
)
SELECT ST_AsGeoJSON(geo.*) INTO _geojson FROM (
SELECT ST_Collect(
ARRAY(
SELECT track_geom FROM logbook))
) as geo;
--raise WARNING 'by date _geojson %' , _geojson;
ELSE
WITH logbook as (
SELECT track_geom
FROM api.logbook
WHERE track_geom IS NOT NULL
ORDER BY _from_time ASC
)
SELECT ST_AsGeoJSON(geo.*) INTO _geojson FROM (
SELECT ST_Collect(
ARRAY(
SELECT track_geom FROM logbook))
) as geo;
--raise WARNING 'all result _geojson %' , _geojson;
END IF;
-- Return a GeoJSON MultiLineString
-- result _geojson [null, null]
--raise WARNING 'result _geojson %' , _geojson;
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', ARRAY[_geojson] ) INTO geojson;
END;
$timelapse$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.timelapse_fn
IS 'Export all selected logs geometry `track_geom` to a geojson as MultiLineString with empty properties';
DROP FUNCTION IF EXISTS api.timelapse2_fn;
CREATE OR REPLACE FUNCTION api.timelapse2_fn(
IN start_log INTEGER DEFAULT NULL,
IN end_log INTEGER DEFAULT NULL,
IN start_date TEXT DEFAULT NULL,
IN end_date TEXT DEFAULT NULL,
OUT geojson JSONB) RETURNS JSONB AS $timelapse2$
DECLARE
_geojson jsonb;
BEGIN
-- Using sub query to force id order by
-- Merge GIS track_geom into a GeoJSON Points
IF start_log IS NOT NULL AND public.isnumeric(start_log::text) AND public.isnumeric(end_log::text) THEN
SELECT jsonb_agg(
jsonb_build_object('type', 'Feature',
'properties', jsonb_build_object( 'notes', f->'properties'->>'notes'),
'geometry', jsonb_build_object( 'coordinates', f->'geometry'->'coordinates', 'type', 'Point'))
) INTO _geojson
FROM (
SELECT jsonb_array_elements(track_geojson->'features') AS f
FROM api.logbook
WHERE id >= start_log
AND id <= end_log
AND track_geojson IS NOT NULL
ORDER BY _from_time ASC
) AS sub
WHERE (f->'geometry'->>'type') = 'Point';
ELSIF start_date IS NOT NULL AND public.isdate(start_date::text) AND public.isdate(end_date::text) THEN
SELECT jsonb_agg(
jsonb_build_object('type', 'Feature',
'properties', jsonb_build_object( 'notes', f->'properties'->>'notes'),
'geometry', jsonb_build_object( 'coordinates', f->'geometry'->'coordinates', 'type', 'Point'))
) INTO _geojson
FROM (
SELECT jsonb_array_elements(track_geojson->'features') AS f
FROM api.logbook
WHERE _from_time >= start_date::TIMESTAMPTZ
AND _to_time <= end_date::TIMESTAMPTZ + interval '23 hours 59 minutes'
AND track_geojson IS NOT NULL
ORDER BY _from_time ASC
) AS sub
WHERE (f->'geometry'->>'type') = 'Point';
ELSE
SELECT jsonb_agg(
jsonb_build_object('type', 'Feature',
'properties', jsonb_build_object( 'notes', f->'properties'->>'notes'),
'geometry', jsonb_build_object( 'coordinates', f->'geometry'->'coordinates', 'type', 'Point'))
) INTO _geojson
FROM (
SELECT jsonb_array_elements(track_geojson->'features') AS f
FROM api.logbook
WHERE track_geojson IS NOT NULL
ORDER BY _from_time ASC
) AS sub
WHERE (f->'geometry'->>'type') = 'Point';
END IF;
-- Return a GeoJSON MultiLineString
-- result _geojson [null, null]
raise WARNING 'result _geojson %' , _geojson;
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', _geojson ) INTO geojson;
END;
$timelapse2$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.timelapse2_fn
IS 'Export all selected logs geometry `track_geom` to a geojson as points with notes properties';
-- export_logbook_geojson_fn
DROP FUNCTION IF EXISTS api.export_logbook_geojson_fn;
CREATE FUNCTION api.export_logbook_geojson_fn(IN _id integer, OUT geojson JSONB) RETURNS JSONB AS $export_logbook_geojson$
-- validate with geojson.io
DECLARE
logbook_rec record;
BEGIN
-- If _id is is not NULL and > 0
IF _id IS NULL OR _id < 1 THEN
RAISE WARNING '-> export_logbook_geojson_fn invalid input %', _id;
RETURN;
END IF;
-- Gather log details
SELECT * INTO logbook_rec
FROM api.logbook WHERE id = _id;
-- Ensure the query is successful
IF logbook_rec.vessel_id IS NULL THEN
RAISE WARNING '-> export_logbook_geojson_fn invalid logbook %', _id;
RETURN;
END IF;
geojson := logbook_rec.track_geojson;
END;
$export_logbook_geojson$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.export_logbook_geojson_fn
IS 'Export a log entry to geojson with features LineString and Point';
-- Generate GPX XML file output
-- https://opencpn.org/OpenCPN/info/gpxvalidation.html
--
DROP FUNCTION IF EXISTS api.export_logbook_gpx_fn;
CREATE OR REPLACE FUNCTION api.export_logbook_gpx_fn(IN _id INTEGER) RETURNS "text/xml"
AS $export_logbook_gpx$
DECLARE
app_settings jsonb;
BEGIN
-- If _id is is not NULL and > 0
IF _id IS NULL OR _id < 1 THEN
RAISE WARNING '-> export_logbook_gpx_fn invalid input %', _id;
RETURN '';
END IF;
-- Gather url from app settings
app_settings := get_app_url_fn();
--RAISE DEBUG '-> logbook_update_gpx_fn app_settings %', app_settings;
-- Generate GPX XML, extract Point features from geojson.
RETURN xmlelement(name gpx,
xmlattributes( '1.1' as version,
'PostgSAIL' as creator,
'http://www.topografix.com/GPX/1/1' as xmlns,
'http://www.opencpn.org' as "xmlns:opencpn",
app_settings->>'app.url' as "xmlns:postgsail",
'http://www.w3.org/2001/XMLSchema-instance' as "xmlns:xsi",
'http://www.garmin.com/xmlschemas/GpxExtensions/v3' as "xmlns:gpxx",
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www8.garmin.com/xmlschemas/GpxExtensionsv3.xsd' as "xsi:schemaLocation"),
xmlelement(name metadata,
xmlelement(name link, xmlattributes(app_settings->>'app.url' as href),
xmlelement(name text, 'PostgSail'))),
xmlelement(name trk,
xmlelement(name name, l.name),
xmlelement(name desc, l.notes),
xmlelement(name link, xmlattributes(concat(app_settings->>'app.url', '/log/', l.id) as href),
xmlelement(name text, l.name)),
xmlelement(name extensions, xmlelement(name "postgsail:log_id", l.id),
xmlelement(name "postgsail:link", concat(app_settings->>'app.url', '/log/', l.id)),
xmlelement(name "opencpn:guid", uuid_generate_v4()),
xmlelement(name "opencpn:viz", '1'),
xmlelement(name "opencpn:start", l._from_time),
xmlelement(name "opencpn:end", l._to_time)
),
xmlelement(name trkseg, xmlagg(
xmlelement(name trkpt,
xmlattributes(features->'geometry'->'coordinates'->1 as lat, features->'geometry'->'coordinates'->0 as lon),
xmlelement(name time, features->'properties'->>'time')
)))))::pg_catalog.xml
FROM api.logbook l, jsonb_array_elements(track_geojson->'features') AS features
WHERE features->'geometry'->>'type' = 'Point'
AND l.id = _id
GROUP BY l.name,l.notes,l.id;
END;
$export_logbook_gpx$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.export_logbook_gpx_fn
IS 'Export a log entry to GPX XML format';
-- Generate KML XML file output
-- https://developers.google.com/kml/documentation/kml_tut
-- TODO https://developers.google.com/kml/documentation/time#timespans
DROP FUNCTION IF EXISTS api.export_logbook_kml_fn;
CREATE OR REPLACE FUNCTION api.export_logbook_kml_fn(IN _id INTEGER) RETURNS "text/xml"
AS $export_logbook_kml$
DECLARE
logbook_rec record;
BEGIN
-- If _id is is not NULL and > 0
IF _id IS NULL OR _id < 1 THEN
RAISE WARNING '-> export_logbook_kml_fn invalid input %', _id;
return '';
END IF;
-- Gather log details
SELECT * INTO logbook_rec
FROM api.logbook WHERE id = _id;
-- Ensure the query is successful
IF logbook_rec.vessel_id IS NULL THEN
RAISE WARNING '-> export_logbook_kml_fn invalid logbook %', _id;
return '';
END IF;
-- Extract POINT from LINESTRING to generate KML XML
RETURN xmlelement(name kml,
xmlattributes( '1.0' as version,
'PostgSAIL' as creator,
'http://www.w3.org/2005/Atom' as "xmlns:atom",
'http://www.opengis.net/kml/2.2' as "xmlns",
'http://www.google.com/kml/ext/2.2' as "xmlns:gx",
'http://www.opengis.net/kml/2.2' as "xmlns:kml"),
xmlelement(name "Document",
xmlelement(name name, logbook_rec.name),
xmlelement(name "Placemark",
xmlelement(name name, logbook_rec.notes),
ST_AsKML(logbook_rec.track_geom)::pg_catalog.xml)
))::pg_catalog.xml
FROM api.logbook WHERE id = _id;
END;
$export_logbook_kml$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.export_logbook_kml_fn
IS 'Export a log entry to KML XML format';
DROP FUNCTION IF EXISTS api.export_logbooks_gpx_fn;
CREATE OR REPLACE FUNCTION api.export_logbooks_gpx_fn(
IN start_log INTEGER DEFAULT NULL,
IN end_log INTEGER DEFAULT NULL) RETURNS "application/gpx+xml"
AS $export_logbooks_gpx$
declare
merged_jsonb jsonb;
app_settings jsonb;
BEGIN
-- Merge GIS track_geom of geometry type Point into a jsonb array format
IF start_log IS NOT NULL AND public.isnumeric(start_log::text) AND public.isnumeric(end_log::text) THEN
SELECT jsonb_agg(
jsonb_build_object('coordinates', f->'geometry'->'coordinates', 'time', f->'properties'->>'time')
) INTO merged_jsonb
FROM (
SELECT jsonb_array_elements(track_geojson->'features') AS f
FROM api.logbook
WHERE id >= start_log
AND id <= end_log
AND track_geojson IS NOT NULL
ORDER BY _from_time ASC
) AS sub
WHERE (f->'geometry'->>'type') = 'Point';
ELSE
SELECT jsonb_agg(
jsonb_build_object('coordinates', f->'geometry'->'coordinates', 'time', f->'properties'->>'time')
) INTO merged_jsonb
FROM (
SELECT jsonb_array_elements(track_geojson->'features') AS f
FROM api.logbook
WHERE track_geojson IS NOT NULL
ORDER BY _from_time ASC
) AS sub
WHERE (f->'geometry'->>'type') = 'Point';
END IF;
--RAISE WARNING '-> export_logbooks_gpx_fn _jsonb %' , _jsonb;
-- Gather url from app settings
app_settings := get_app_url_fn();
--RAISE WARNING '-> export_logbooks_gpx_fn app_settings %', app_settings;
-- Generate GPX XML, extract Point features from geojson.
RETURN xmlelement(name gpx,
xmlattributes( '1.1' as version,
'PostgSAIL' as creator,
'http://www.topografix.com/GPX/1/1' as xmlns,
'http://www.opencpn.org' as "xmlns:opencpn",
app_settings->>'app.url' as "xmlns:postgsail"),
xmlelement(name metadata,
xmlelement(name link, xmlattributes(app_settings->>'app.url' as href),
xmlelement(name text, 'PostgSail'))),
xmlelement(name trk,
xmlelement(name name, 'logbook name'),
xmlelement(name trkseg, xmlagg(
xmlelement(name trkpt,
xmlattributes(features->'coordinates'->1 as lat, features->'coordinates'->0 as lon),
xmlelement(name time, features->'properties'->>'time')
)))))::pg_catalog.xml
FROM jsonb_array_elements(merged_jsonb) AS features;
END;
$export_logbooks_gpx$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.export_logbooks_gpx_fn
IS 'Export a logs entries to GPX XML format';
DROP FUNCTION IF EXISTS api.export_logbooks_kml_fn;
CREATE OR REPLACE FUNCTION api.export_logbooks_kml_fn(
IN start_log INTEGER DEFAULT NULL,
IN end_log INTEGER DEFAULT NULL) RETURNS "text/xml"
AS $export_logbooks_kml$
DECLARE
_geom geometry;
app_settings jsonb;
BEGIN
-- Merge GIS track_geom into a GeoJSON MultiLineString
IF start_log IS NOT NULL AND public.isnumeric(start_log::text) AND public.isnumeric(end_log::text) THEN
WITH logbook as (
SELECT track_geom
FROM api.logbook
WHERE id >= start_log
AND id <= end_log
AND track_geom IS NOT NULL
ORDER BY _from_time ASC
)
SELECT ST_Collect(
ARRAY(
SELECT track_geom FROM logbook))
into _geom;
ELSE
WITH logbook as (
SELECT track_geom
FROM api.logbook
WHERE track_geom IS NOT NULL
ORDER BY _from_time ASC
)
SELECT ST_Collect(
ARRAY(
SELECT track_geom FROM logbook))
into _geom;
--raise WARNING 'all result _geojson %' , _geojson;
END IF;
-- Extract POINT from LINESTRING to generate KML XML
RETURN xmlelement(name kml,
xmlattributes( '1.0' as version,
'PostgSAIL' as creator,
'http://www.w3.org/2005/Atom' as "xmlns:atom",
'http://www.opengis.net/kml/2.2' as "xmlns",
'http://www.google.com/kml/ext/2.2' as "xmlns:gx",
'http://www.opengis.net/kml/2.2' as "xmlns:kml"),
xmlelement(name "Document",
xmlelement(name name, 'logbook name'),
xmlelement(name "Placemark",
ST_AsKML(_geom)::pg_catalog.xml
)
)
)::pg_catalog.xml;
END;
$export_logbooks_kml$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.export_logbooks_kml_fn
IS 'Export a logs entries to KML XML format';
-- Find all log from and to moorage geopoint within 100m
DROP FUNCTION IF EXISTS api.find_log_from_moorage_fn;
CREATE OR REPLACE FUNCTION api.find_log_from_moorage_fn(IN _id INTEGER, OUT geojson JSONB) RETURNS JSONB AS $find_log_from_moorage$
DECLARE
moorage_rec record;
_geojson jsonb;
BEGIN
-- If _id is is not NULL and > 0
IF _id IS NULL OR _id < 1 THEN
RAISE WARNING '-> find_log_from_moorage_fn invalid input %', _id;
RETURN;
END IF;
-- Gather moorage details
SELECT * INTO moorage_rec
FROM api.moorages m
WHERE m.id = _id;
-- Find all log from and to moorage geopoint within 100m
SELECT jsonb_agg(l.track_geojson->'features') INTO _geojson
FROM api.logbook l
WHERE ST_DWithin(
Geography(ST_MakePoint(l._from_lng, l._from_lat)),
moorage_rec.geog,
1000 -- in meters ?
);
-- Return a GeoJSON filter on LineString
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', public.geojson_py_fn(_geojson, 'Point'::TEXT) ) INTO geojson;
END;
$find_log_from_moorage$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.find_log_from_moorage_fn
IS 'Find all log from moorage geopoint within 100m';
DROP FUNCTION IF EXISTS api.find_log_to_moorage_fn;
CREATE OR REPLACE FUNCTION api.find_log_to_moorage_fn(IN _id INTEGER, OUT geojson JSONB) RETURNS JSONB AS $find_log_to_moorage$
DECLARE
moorage_rec record;
_geojson jsonb;
BEGIN
-- If _id is is not NULL and > 0
IF _id IS NULL OR _id < 1 THEN
RAISE WARNING '-> find_log_from_moorage_fn invalid input %', _id;
RETURN;
END IF;
-- Gather moorage details
SELECT * INTO moorage_rec
FROM api.moorages m
WHERE m.id = _id;
-- Find all log from and to moorage geopoint within 100m
SELECT jsonb_agg(l.track_geojson->'features') INTO _geojson
FROM api.logbook l
WHERE ST_DWithin(
Geography(ST_MakePoint(l._to_lng, l._to_lat)),
moorage_rec.geog,
1000 -- in meters ?
);
-- Return a GeoJSON filter on LineString
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', public.geojson_py_fn(_geojson, 'Point'::TEXT) ) INTO geojson;
END;
$find_log_to_moorage$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.find_log_to_moorage_fn
IS 'Find all log to moorage geopoint within 100m';
-- Find all stay within 100m of moorage geopoint
DROP FUNCTION IF EXISTS api.find_stay_from_moorage_fn;
CREATE OR REPLACE FUNCTION api.find_stay_from_moorage_fn(IN _id INTEGER) RETURNS void AS $find_stay_from_moorage$
DECLARE
moorage_rec record;
stay_rec record;
BEGIN
-- If _id is is not NULL and > 0
SELECT * INTO moorage_rec
FROM api.moorages m
WHERE m.id = _id;
-- find all log from and to moorage geopoint within 100m
--RETURN QUERY
SELECT s.id,s.arrived,s.departed,s.duration,sa.description
FROM api.stays s, api.stays_at sa
WHERE ST_DWithin(
s.geog,
moorage_rec.geog,
100 -- in meters ?
)
AND departed IS NOT NULL
AND s.name IS NOT NULL
AND s.stay_code = sa.stay_code
ORDER BY s.arrived DESC;
END;
$find_stay_from_moorage$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.find_stay_from_moorage_fn
IS 'Find all stay within 100m of moorage geopoint';
-- trip_in_progress_fn
DROP FUNCTION IF EXISTS public.trip_in_progress_fn;
CREATE FUNCTION public.trip_in_progress_fn(IN _vessel_id TEXT) RETURNS INT AS $trip_in_progress$
DECLARE
logbook_id INT := NULL;
BEGIN
SELECT id INTO logbook_id
FROM api.logbook l
WHERE l.vessel_id IS NOT NULL
AND l.vessel_id = _vessel_id
AND active IS true
LIMIT 1;
RETURN logbook_id;
END;
$trip_in_progress$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.trip_in_progress_fn
IS 'trip_in_progress';
-- stay_in_progress_fn
DROP FUNCTION IF EXISTS public.stay_in_progress_fn;
CREATE FUNCTION public.stay_in_progress_fn(IN _vessel_id TEXT) RETURNS INT AS $stay_in_progress$
DECLARE
stay_id INT := NULL;
BEGIN
SELECT id INTO stay_id
FROM api.stays s
WHERE s.vessel_id IS NOT NULL
AND s.vessel_id = _vessel_id
AND active IS true
LIMIT 1;
RETURN stay_id;
END;
$stay_in_progress$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.stay_in_progress_fn
IS 'stay_in_progress';
-- logs_by_month_fn
DROP FUNCTION IF EXISTS api.logs_by_month_fn;
CREATE FUNCTION api.logs_by_month_fn(OUT charts JSONB) RETURNS JSONB AS $logs_by_month$
DECLARE
data JSONB;
BEGIN
-- Query logs by month
SELECT json_object_agg(month,count) INTO data
FROM (
SELECT
to_char(date_trunc('month', _from_time), 'MM') as month,
count(*) as count
FROM api.logbook
GROUP BY month
ORDER BY month
) AS t;
-- Merge jsonb to get all 12 months
SELECT '{"01": 0, "02": 0, "03": 0, "04": 0, "05": 0, "06": 0, "07": 0, "08": 0, "09": 0, "10": 0, "11": 0,"12": 0}'::jsonb ||
data::jsonb INTO charts;
END;
$logs_by_month$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.logs_by_month_fn
IS 'logbook by month for web charts';
-- logs_by_day_fn
DROP FUNCTION IF EXISTS api.logs_by_day_fn;
CREATE FUNCTION api.logs_by_day_fn(OUT charts JSONB) RETURNS JSONB AS $logs_by_day$
DECLARE
data JSONB;
BEGIN
-- Query logs by day
SELECT json_object_agg(day,count) INTO data
FROM (
SELECT
to_char(date_trunc('day', _from_time), 'D') as day,
count(*) as count
FROM api.logbook
GROUP BY day
ORDER BY day
) AS t;
-- Merge jsonb to get all 7 days
SELECT '{"01": 0, "02": 0, "03": 0, "04": 0, "05": 0, "06": 0, "07": 0}'::jsonb ||
data::jsonb INTO charts;
END;
$logs_by_day$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.logs_by_day_fn
IS 'logbook by day for web charts';
-- moorage_geojson_fn
DROP FUNCTION IF EXISTS api.export_moorages_geojson_fn;
CREATE FUNCTION api.export_moorages_geojson_fn(OUT geojson JSONB) RETURNS JSONB AS $export_moorages_geojson$
DECLARE
BEGIN
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features',
( SELECT
json_agg(ST_AsGeoJSON(m.*)::JSON) as moorages_geojson
FROM
( SELECT
id,name,stay_code,
EXTRACT(DAY FROM justify_hours ( stay_duration )) AS Total_Stay,
geog
FROM api.moorages
WHERE geog IS NOT NULL
) AS m
)
) INTO geojson;
END;
$export_moorages_geojson$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.export_moorages_geojson_fn
IS 'Export moorages as geojson';
DROP FUNCTION IF EXISTS api.export_moorages_gpx_fn;
CREATE FUNCTION api.export_moorages_gpx_fn() RETURNS "text/xml" AS $export_moorages_gpx$
DECLARE
app_settings jsonb;
BEGIN
-- Gather url from app settings
app_settings := get_app_url_fn();
-- Generate XML
RETURN xmlelement(name gpx,
xmlattributes( '1.1' as version,
'PostgSAIL' as creator,
'http://www.topografix.com/GPX/1/1' as xmlns,
'http://www.opencpn.org' as "xmlns:opencpn",
app_settings->>'app.url' as "xmlns:postgsail",
'http://www.w3.org/2001/XMLSchema-instance' as "xmlns:xsi",
'http://www.garmin.com/xmlschemas/GpxExtensions/v3' as "xmlns:gpxx",
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www8.garmin.com/xmlschemas/GpxExtensionsv3.xsd' as "xsi:schemaLocation"),
xmlagg(
xmlelement(name wpt, xmlattributes(m.latitude as lat, m.longitude as lon),
xmlelement(name name, m.name),
xmlelement(name time, 'TODO first seen'),
xmlelement(name desc,
concat('Last Stayed On: ', 'TODO last seen',
E'\nTotal Stays: ', m.stay_duration,
E'\nTotal Arrivals and Departures: ', m.reference_count,
E'\nLink: ', concat(app_settings->>'app.url','/moorage/', m.id)),
xmlelement(name "opencpn:guid", uuid_generate_v4())),
xmlelement(name sym, 'anchor'),
xmlelement(name type, 'WPT'),
xmlelement(name link, xmlattributes(concat(app_settings->>'app.url','moorage/', m.id) as href),
xmlelement(name text, m.name)),
xmlelement(name extensions, xmlelement(name "postgsail:mooorage_id", m.id),
xmlelement(name "postgsail:link", concat(app_settings->>'app.url','/moorage/', m.id)),
xmlelement(name "opencpn:guid", uuid_generate_v4()),
xmlelement(name "opencpn:viz", '1'),
xmlelement(name "opencpn:scale_min_max", xmlattributes(true as UseScale, 30000 as ScaleMin, 0 as ScaleMax)
))))
)::pg_catalog.xml
FROM api.moorages m
WHERE geog IS NOT NULL;
END;
$export_moorages_gpx$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.export_moorages_gpx_fn
IS 'Export moorages as gpx';
----------------------------------------------------------------------------------------------
-- Statistics
DROP FUNCTION IF EXISTS api.stats_logs_fn;
CREATE OR REPLACE FUNCTION api.stats_logs_fn(
IN start_date TEXT DEFAULT NULL,
IN end_date TEXT DEFAULT NULL,
OUT stats JSONB) RETURNS JSONB AS $stats_logs$
DECLARE
_start_date TIMESTAMPTZ DEFAULT '1970-01-01';
_end_date TIMESTAMPTZ DEFAULT NOW();
BEGIN
IF start_date IS NOT NULL AND public.isdate(start_date::text) AND public.isdate(end_date::text) THEN
RAISE WARNING '--> stats_fn, filter result stats by date [%]', start_date;
_start_date := start_date::TIMESTAMPTZ;
_end_date := end_date::TIMESTAMPTZ;
END IF;
RAISE NOTICE '--> stats_fn, _start_date [%], _end_date [%]', _start_date, _end_date;
WITH
meta AS (
SELECT m.name FROM api.metadata m ),
logs_view AS (
SELECT *
FROM api.logbook l
WHERE _from_time >= _start_date::TIMESTAMPTZ
AND _to_time <= _end_date::TIMESTAMPTZ + interval '23 hours 59 minutes'
),
first_date AS (
SELECT _from_time as first_date from logs_view ORDER BY first_date ASC LIMIT 1
),
last_date AS (
SELECT _to_time as last_date from logs_view ORDER BY _to_time DESC LIMIT 1
),
max_speed_id AS (
SELECT id FROM logs_view WHERE max_speed = (SELECT max(max_speed) FROM logs_view) ),
max_wind_speed_id AS (
SELECT id FROM logs_view WHERE max_wind_speed = (SELECT max(max_wind_speed) FROM logs_view)),
max_distance_id AS (
SELECT id FROM logs_view WHERE distance = (SELECT max(distance) FROM logs_view)),
max_duration_id AS (
SELECT id FROM logs_view WHERE duration = (SELECT max(duration) FROM logs_view)),
logs_stats AS (
SELECT
count(*) AS count,
max(max_speed) AS max_speed,
max(max_wind_speed) AS max_wind_speed,
max(distance) AS max_distance,
sum(distance) AS sum_distance,
max(duration) AS max_duration,
sum(duration) AS sum_duration
FROM logs_view l )
--select * from logbook;
-- Return a JSON
SELECT jsonb_build_object(
'name', meta.name,
'first_date', first_date.first_date,
'last_date', last_date.last_date,
'max_speed_id', max_speed_id.id,
'max_wind_speed_id', max_wind_speed_id.id,
'max_duration_id', max_duration_id.id,
'max_distance_id', max_distance_id.id)::jsonb || to_jsonb(logs_stats.*)::jsonb INTO stats
FROM max_speed_id, max_wind_speed_id, max_distance_id, max_duration_id,
logs_stats, meta, logs_view, first_date, last_date;
-- TODO Add moorages
END;
$stats_logs$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.stats_logs_fn
IS 'Logs stats by date';
DROP FUNCTION IF EXISTS api.stats_stays_fn;
CREATE OR REPLACE FUNCTION api.stats_stays_fn(
IN start_date TEXT DEFAULT NULL,
IN end_date TEXT DEFAULT NULL,
OUT stats JSON) RETURNS JSON AS $stats_stays$
DECLARE
_start_date TIMESTAMPTZ DEFAULT '1970-01-01';
_end_date TIMESTAMPTZ DEFAULT NOW();
BEGIN
IF start_date IS NOT NULL AND public.isdate(start_date::text) AND public.isdate(end_date::text) THEN
RAISE NOTICE '--> stats_stays_fn, custom filter result stats by date [%]', start_date;
_start_date := start_date::TIMESTAMPTZ;
_end_date := end_date::TIMESTAMPTZ;
END IF;
RAISE NOTICE '--> stats_stays_fn, _start_date [%], _end_date [%]', _start_date, _end_date;
WITH
moorages_log AS (
SELECT s.id as stays_id, m.id as moorages_id, *
FROM api.stays s, api.moorages m
WHERE arrived >= _start_date::TIMESTAMPTZ
AND departed <= _end_date::TIMESTAMPTZ + interval '23 hours 59 minutes'
AND s.id = m.stay_id
),
home_ports AS (
select count(*) as home_ports from moorages_log m where home_flag is true
),
unique_moorage AS (
select count(*) as unique_moorage from moorages_log m
),
time_at_home_ports AS (
select sum(m.stay_duration) as time_at_home_ports from moorages_log m where home_flag is true
),
sum_stay_duration AS (
select sum(m.stay_duration) as sum_stay_duration from moorages_log m where home_flag is false
),
time_spent_away AS (
select m.stay_code,sum(m.stay_duration) as stay_duration from api.moorages m where home_flag is false group by m.stay_code order by m.stay_code
),
time_spent as (
select jsonb_agg(t.*) as time_spent_away from time_spent_away t
)
-- Return a JSON
SELECT jsonb_build_object(
'home_ports', home_ports.home_ports,
'unique_moorage', unique_moorage.unique_moorage,
'time_at_home_ports', time_at_home_ports.time_at_home_ports,
'sum_stay_duration', sum_stay_duration.sum_stay_duration,
'time_spent_away', time_spent.time_spent_away) INTO stats
FROM moorages_log, home_ports, unique_moorage,
time_at_home_ports, sum_stay_duration, time_spent;
-- TODO Add moorages
END;
$stats_stays$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.stats_stays_fn
IS 'Stays/Moorages stats by date';
DROP FUNCTION IF EXISTS api.delete_logbook_fn;
CREATE OR REPLACE FUNCTION api.delete_logbook_fn(IN _id integer) RETURNS BOOLEAN AS $delete_logbook$
DECLARE
logbook_rec record;
previous_stays_id numeric;
current_stays_departed text;
current_stays_id numeric;
current_stays_active boolean;
BEGIN
-- If _id is not NULL
IF _id IS NULL OR _id < 1 THEN
RAISE WARNING '-> delete_logbook_fn invalid input %', _id;
RETURN FALSE;
END IF;
SELECT * INTO logbook_rec
FROM api.logbook l
WHERE id = _id;
-- Update logbook
UPDATE api.logbook l
SET notes = 'mark for deletion'
WHERE l.vessel_id = current_setting('vessel.id', false)
AND id = logbook_rec.id;
-- Update metrics status to moored
UPDATE api.metrics
SET status = 'moored'
WHERE time >= logbook_rec._from_time::TIMESTAMPTZ
AND time <= logbook_rec._to_time::TIMESTAMPTZ
AND vessel_id = current_setting('vessel.id', false);
-- Get related stays
SELECT id,departed,active INTO current_stays_id,current_stays_departed,current_stays_active
FROM api.stays s
WHERE s.vessel_id = current_setting('vessel.id', false)
AND s.arrived = logbook_rec._to_time;
-- Update related stays
UPDATE api.stays s
SET notes = 'mark for deletion'
WHERE s.vessel_id = current_setting('vessel.id', false)
AND s.arrived = logbook_rec._to_time;
-- Find previous stays
SELECT id INTO previous_stays_id
FROM api.stays s
WHERE s.vessel_id = current_setting('vessel.id', false)
AND s.arrived < logbook_rec._to_time
ORDER BY s.arrived DESC LIMIT 1;
-- Update previous stays with the departed time from current stays
-- and set the active state from current stays
UPDATE api.stays
SET departed = current_stays_departed::TIMESTAMPTZ,
active = current_stays_active
WHERE vessel_id = current_setting('vessel.id', false)
AND id = previous_stays_id;
-- Clean up, remove invalid logbook and stay entry
DELETE FROM api.logbook WHERE id = logbook_rec.id;
RAISE WARNING '-> delete_logbook_fn delete logbook [%]', logbook_rec.id;
DELETE FROM api.stays WHERE id = current_stays_id;
RAISE WARNING '-> delete_logbook_fn delete stays [%]', current_stays_id;
-- Clean up, Subtract (-1) moorages ref count
UPDATE api.moorages
SET reference_count = reference_count - 1
WHERE vessel_id = current_setting('vessel.id', false)
AND id = previous_stays_id;
RETURN TRUE;
END;
$delete_logbook$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.delete_logbook_fn
IS 'Delete a logbook and dependency stay';
CREATE OR REPLACE FUNCTION api.monitoring_history_fn(IN time_interval TEXT DEFAULT '24', OUT history_metrics JSONB) RETURNS JSONB AS $monitoring_history$
DECLARE
bucket_interval interval := '5 minutes';
BEGIN
RAISE NOTICE '-> monitoring_history_fn';
SELECT CASE time_interval
WHEN '24' THEN '5 minutes'
WHEN '48' THEN '2 hours'
WHEN '72' THEN '4 hours'
WHEN '168' THEN '7 hours'
ELSE '5 minutes'
END bucket INTO bucket_interval;
RAISE NOTICE '-> monitoring_history_fn % %', time_interval, bucket_interval;
WITH history_table AS (
SELECT time_bucket(bucket_interval::INTERVAL, time) AS time_bucket,
avg((metrics->'environment.water.temperature')::numeric) AS waterTemperature,
avg((metrics->'environment.inside.temperature')::numeric) AS insideTemperature,
avg((metrics->'environment.outside.temperature')::numeric) AS outsideTemperature,
avg((metrics->'environment.wind.speedOverGround')::numeric) AS windSpeedOverGround,
avg((metrics->'environment.inside.relativeHumidity')::numeric) AS insideHumidity,
avg((metrics->'environment.outside.relativeHumidity')::numeric) AS outsideHumidity,
avg((metrics->'environment.outside.pressure')::numeric) AS outsidePressure,
avg((metrics->'environment.inside.pressure')::numeric) AS insidePressure,
avg((metrics->'electrical.batteries.House.capacity.stateOfCharge')::numeric) AS batteryCharge,
avg((metrics->'electrical.batteries.House.voltage')::numeric) AS batteryVoltage,
avg((metrics->'environment.depth.belowTransducer')::numeric) AS depth
FROM api.metrics
WHERE time > (NOW() AT TIME ZONE 'UTC' - INTERVAL '1 hours' * time_interval::NUMERIC)
GROUP BY time_bucket
ORDER BY time_bucket asc
)
SELECT jsonb_agg(history_table) INTO history_metrics FROM history_table;
END
$monitoring_history$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.monitoring_history_fn
IS 'Export metrics from a time period 24h, 48h, 72h, 7d';
CREATE OR REPLACE FUNCTION api.status_fn(out status jsonb) RETURNS JSONB AS $status_fn$
DECLARE
in_route BOOLEAN := False;
BEGIN
RAISE NOTICE '-> status_fn';
SELECT EXISTS ( SELECT id
FROM api.logbook l
WHERE active IS True
LIMIT 1
) INTO in_route;
IF in_route IS True THEN
-- In route from <logbook.from_name> arrived at <>
SELECT jsonb_build_object('status', sa.description, 'location', m.name, 'departed', l._from_time) INTO status
from api.logbook l, api.stays_at sa, api.moorages m
where s.stay_code = sa.stay_code AND l._from_moorage_id = m.id AND l.active IS True;
ELSE
-- At <Stat_at.Desc> in <Moorage.name> departed at <>
SELECT jsonb_build_object('status', sa.description, 'location', m.name, 'arrived', s.arrived) INTO status
from api.stays s, api.stays_at sa, api.moorages m
where s.stay_code = sa.stay_code AND s.moorage_id = m.id AND s.active IS True;
END IF;
END
$status_fn$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.status_fn
IS 'generate vessel status';

View File

@@ -0,0 +1,510 @@
-- connect to the DB
\c signalk
---------------------------------------------------------------------------
-- API helper views
--
---------------------------------------------------------------------------
---------------------------------------------------------------------------
-- Views
-- Views are invoked with the privileges of the view owner,
-- make the user_role the views owner.
-- to bypass this limit you need pg15+ with specific settings
-- security_invoker=true,security_barrier=true
---------------------------------------------------------------------------
CREATE VIEW public.first_metric AS
SELECT *
FROM api.metrics
ORDER BY time ASC LIMIT 1;
CREATE VIEW public.last_metric AS
SELECT *
FROM api.metrics
ORDER BY time DESC LIMIT 1;
DROP VIEW IF EXISTS public.trip_in_progress;
CREATE VIEW public.trip_in_progress AS
SELECT *
FROM api.logbook
WHERE active IS true;
DROP VIEW IF EXISTS public.stay_in_progress;
CREATE VIEW public.stay_in_progress AS
SELECT *
FROM api.stays
WHERE active IS true;
-- TODO: Use materialized views instead as it is not live data
-- Logs web view
DROP VIEW IF EXISTS api.logs_view;
CREATE OR REPLACE VIEW api.logs_view WITH (security_invoker=true,security_barrier=true) AS
SELECT id,
name as "name",
_from as "from",
_from_time as "started",
_to as "to",
_to_time as "ended",
distance as "distance",
duration as "duration",
_from_moorage_id,_to_moorage_id
FROM api.logbook l
WHERE name IS NOT NULL
AND _to_time IS NOT NULL
ORDER BY _from_time DESC;
-- Description
COMMENT ON VIEW
api.logs_view
IS 'Logs web view';
-- Initial try of MATERIALIZED VIEW - does not support RLS
CREATE MATERIALIZED VIEW api.logs_mat_view AS
SELECT id,
name as "name",
_from as "from",
_from_time as "started",
_to as "to",
_to_time as "ended",
distance as "distance",
duration as "duration",
_from_moorage_id,_to_moorage_id
FROM api.logbook l
WHERE name IS NOT NULL
AND _to_time IS NOT NULL
ORDER BY _from_time DESC;
-- Description
COMMENT ON MATERIALIZED VIEW
api.logs_mat_view
IS 'Logs MATERIALIZED web view';
DROP VIEW IF EXISTS api.log_view;
CREATE OR REPLACE VIEW api.log_view WITH (security_invoker=true,security_barrier=true) AS
SELECT id,
name as "name",
_from as "from",
_from_time as "started",
_to as "to",
_to_time as "ended",
distance as "distance",
duration as "duration",
notes as "notes",
track_geojson as geojson,
avg_speed as avg_speed,
max_speed as max_speed,
max_wind_speed as max_wind_speed,
extra as extra,
_from_moorage_id as from_moorage_id,
_to_moorage_id as to_moorage_id
FROM api.logbook l
WHERE _to_time IS NOT NULL
ORDER BY _from_time DESC;
-- Description
COMMENT ON VIEW
api.log_view
IS 'Log web view';
-- Stays web view
DROP VIEW IF EXISTS api.stays_view;
CREATE OR REPLACE VIEW api.stays_view WITH (security_invoker=true,security_barrier=true) AS
SELECT s.id,
s.name AS "name",
m.name AS "moorage",
m.id AS "moorage_id",
(s.departed-s.arrived) AS "duration",
sa.description AS "stayed_at",
sa.stay_code AS "stayed_at_id",
s.arrived AS "arrived",
_from.id as "arrived_log_id",
_from._to_moorage_id as "arrived_from_moorage_id",
_from._to as "arrived_from_moorage_name",
s.departed AS "departed",
_to.id AS "departed_log_id",
_to._from_moorage_id AS "departed_to_moorage_id",
_to._from AS "departed_to_moorage_name",
s.notes AS "notes"
FROM api.stays_at sa, api.moorages m, api.stays s
LEFT JOIN api.logbook AS _from ON _from._from_time = s.departed
LEFT JOIN api.logbook AS _to ON _to._to_time = s.arrived
WHERE s.departed IS NOT NULL
AND _from._to_moorage_id IS NOT NULL
AND s.name IS NOT NULL
AND s.stay_code = sa.stay_code
AND s.moorage_id = m.id
ORDER BY s.arrived DESC;
-- Description
COMMENT ON VIEW
api.stays_view
IS 'Stays web view';
DROP VIEW IF EXISTS api.stay_view;
CREATE OR REPLACE VIEW api.stay_view WITH (security_invoker=true,security_barrier=true) AS
SELECT s.id,
s.name AS "name",
m.name AS "moorage",
m.id AS "moorage_id",
(s.departed-s.arrived) AS "duration",
sa.description AS "stayed_at",
sa.stay_code AS "stayed_at_id",
s.arrived AS "arrived",
_from.id as "arrived_log_id",
_from._to_moorage_id as "arrived_from_moorage_id",
_from._to as "arrived_from_moorage_name",
s.departed AS "departed",
_to.id AS "departed_log_id",
_to._from_moorage_id AS "departed_to_moorage_id",
_to._from AS "departed_to_moorage_name",
s.notes AS "notes"
FROM api.stays_at sa, api.moorages m, api.stays s
LEFT JOIN api.logbook AS _from ON _from._from_time = s.departed
LEFT JOIN api.logbook AS _to ON _to._to_time = s.arrived
WHERE s.departed IS NOT NULL
AND _from._to_moorage_id IS NOT NULL
AND s.name IS NOT NULL
AND s.stay_code = sa.stay_code
AND s.moorage_id = m.id
ORDER BY s.arrived DESC;
-- Description
COMMENT ON VIEW
api.stay_view
IS 'Stay web view';
-- Moorages web view
-- TODO, this is wrong using distinct (m.name) should be using postgis geog feature
--DROP VIEW IF EXISTS api.moorages_view_old;
--CREATE VIEW api.moorages_view_old AS
-- SELECT
-- m.name AS Moorage,
-- sa.description AS "Default Stay",
-- sum((m.departed-m.arrived)) OVER (PARTITION by m.name) AS "Total Stay",
-- count(m.departed) OVER (PARTITION by m.name) AS "Arrivals & Departures"
-- FROM api.moorages m, api.stays_at sa
-- WHERE departed is not null
-- AND m.name is not null
-- AND m.stay_code = sa.stay_code
-- GROUP BY m.name,sa.description,m.departed,m.arrived
-- ORDER BY 4 DESC;
-- the good way?
DROP VIEW IF EXISTS api.moorages_view;
CREATE OR REPLACE VIEW api.moorages_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
SELECT m.id,
m.name AS Moorage,
sa.description AS Default_Stay,
sa.stay_code AS Default_Stay_Id,
EXTRACT(DAY FROM justify_hours ( m.stay_duration )) AS Total_Stay, -- in days
m.stay_duration AS Total_Duration,
m.reference_count AS Arrivals_Departures
-- m.geog
-- m.stay_duration,
-- justify_hours ( m.stay_duration )
FROM api.moorages m, api.stays_at sa
-- m.stay_duration is only process on a stay
WHERE m.stay_duration IS NOT NULL
AND m.geog IS NOT NULL
AND m.stay_code = sa.stay_code
GROUP BY m.id,m.name,sa.description,m.stay_duration,m.reference_count,m.geog,sa.stay_code
-- ORDER BY 4 DESC;
-- ORDER BY m.reference_count DESC;
ORDER BY m.stay_duration DESC;
-- Description
COMMENT ON VIEW
api.moorages_view
IS 'Moorages listing web view';
DROP VIEW IF EXISTS api.moorage_view;
CREATE OR REPLACE VIEW api.moorage_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
SELECT id,
m.name AS Name,
sa.description AS Default_Stay,
sa.stay_code AS Default_Stay_Id,
m.home_flag AS Home,
EXTRACT(DAY FROM justify_hours ( m.stay_duration )) AS Total_Stay,
m.stay_duration AS Total_Duration,
m.reference_count AS Arrivals_Departures,
m.notes
-- m.geog
FROM api.moorages m, api.stays_at sa
-- m.stay_duration is only process on a stay
WHERE m.stay_duration IS NOT NULL
AND geog IS NOT NULL
AND m.stay_code = sa.stay_code;
-- Description
COMMENT ON VIEW
api.moorage_view
IS 'Moorage details web view';
DROP VIEW IF EXISTS api.moorages_stays_view;
CREATE OR REPLACE VIEW api.moorages_stays_view WITH (security_invoker=true,security_barrier=true) AS
SELECT
_to.id AS _to_id,
_to._to_time,
_from.id AS _from_id,
_from._from_time,
s.stay_code,s.duration,m.id
FROM api.stays_at sa, api.moorages m, api.stays s
LEFT JOIN api.logbook AS _from ON _from._from_time = s.departed
LEFT JOIN api.logbook AS _to ON _to._to_time = s.arrived
WHERE s.departed IS NOT NULL
AND s.name IS NOT NULL
AND s.stay_code = sa.stay_code
AND s.moorage_id = m.id
ORDER BY _to._to_time DESC;
-- Description
COMMENT ON VIEW
api.moorages_stays_view
IS 'Moorages stay listing web view';
-- All moorage in 100 meters from the start of a logbook.
-- ST_DistanceSphere Returns minimum distance in meters between two lon/lat points.
--SELECT
-- m.name, ST_MakePoint(m._lng,m._lat),
-- l._from, ST_MakePoint(l._from_lng,l._from_lat),
-- ST_DistanceSphere(ST_MakePoint(m._lng,m._lat), ST_MakePoint(l._from_lng,l._from_lat))
-- FROM api.moorages m , api.logbook l
-- WHERE ST_DistanceSphere(ST_MakePoint(m._lng,m._lat), ST_MakePoint(l._from_lng,l._from_lat)) <= 100;
-- Stats web view
-- TODO....
-- first time entry from metrics
----> select * from api.metrics m ORDER BY m.time desc limit 1
-- last time entry from metrics
----> select * from api.metrics m ORDER BY m.time asc limit 1
-- max speed from logbook
-- max wind speed from logbook
----> select max(l.max_speed) as max_speed, max(l.max_wind_speed) as max_wind_speed from api.logbook l;
-- Total Distance from logbook
----> select sum(l.distance) as "Total Distance" from api.logbook l;
-- Total Time Underway from logbook
----> select sum(l.duration) as "Total Time Underway" from api.logbook l;
-- Longest Nonstop Sail from logbook, eg longest trip duration and distance
----> select max(l.duration),max(l.distance) from api.logbook l;
CREATE OR REPLACE VIEW api.stats_logs_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
WITH
meta AS (
SELECT m.name FROM api.metadata m ),
last_metric AS (
SELECT m.time FROM api.metrics m ORDER BY m.time DESC limit 1),
first_metric AS (
SELECT m.time FROM api.metrics m ORDER BY m.time ASC limit 1),
logbook AS (
SELECT
count(*) AS "number_of_log_entries",
max(l.max_speed) AS "max_speed",
max(l.max_wind_speed) AS "max_wind_speed",
sum(l.distance) AS "total_distance",
sum(l.duration) AS "total_time_underway",
concat( max(l.distance), ' NM, ', max(l.duration), ' hours') AS "longest_nonstop_sail"
FROM api.logbook l)
SELECT
m.name AS name,
fm.time AS first,
lm.time AS last,
l.*
FROM first_metric fm, last_metric lm, logbook l, meta m;
COMMENT ON VIEW
api.stats_logs_view
IS 'Statistics Logs web view';
-- Home Ports / Unique Moorages
----> select count(*) as "Home Ports" from api.moorages m where home_flag is true;
-- Unique Moorages
----> select count(*) as "Home Ports" from api.moorages m;
-- Time Spent at Home Port(s)
----> select sum(m.stay_duration) as "Time Spent at Home Port(s)" from api.moorages m where home_flag is true;
-- OR
----> select m.stay_duration as "Time Spent at Home Port(s)" from api.moorages m where home_flag is true;
-- Time Spent Away
----> select sum(m.stay_duration) as "Time Spent Away" from api.moorages m where home_flag is false;
-- Time Spent Away order by, group by stay_code (Dock, Anchor, Mooring Buoys, Unclassified)
----> select sa.description,sum(m.stay_duration) as "Time Spent Away" from api.moorages m, api.stays_at sa where home_flag is false AND m.stay_code = sa.stay_code group by m.stay_code,sa.description order by m.stay_code;
CREATE OR REPLACE VIEW api.stats_moorages_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
WITH
home_ports AS (
select count(*) as home_ports from api.moorages m where home_flag is true
),
unique_moorage AS (
select count(*) as unique_moorage from api.moorages m
),
time_at_home_ports AS (
select sum(m.stay_duration) as time_at_home_ports from api.moorages m where home_flag is true
),
time_spent_away AS (
select sum(m.stay_duration) as time_spent_away from api.moorages m where home_flag is false
)
SELECT
home_ports.home_ports as "home_ports",
unique_moorage.unique_moorage as "unique_moorages",
time_at_home_ports.time_at_home_ports "time_spent_at_home_port(s)",
time_spent_away.time_spent_away as "time_spent_away"
FROM home_ports, unique_moorage, time_at_home_ports, time_spent_away;
COMMENT ON VIEW
api.stats_moorages_view
IS 'Statistics Moorages web view';
CREATE OR REPLACE VIEW api.stats_moorages_away_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
SELECT sa.description,sum(m.stay_duration) as time_spent_away_by
FROM api.moorages m, api.stays_at sa
WHERE home_flag IS false
AND m.stay_code = sa.stay_code
GROUP BY m.stay_code,sa.description
ORDER BY m.stay_code;
COMMENT ON VIEW
api.stats_moorages_away_view
IS 'Statistics Moorages Time Spent Away web view';
--CREATE VIEW api.stats_view AS -- todo
-- WITH
-- logs AS (
-- SELECT * FROM api.stats_logs_view ),
-- moorages AS (
-- SELECT * FROM api.stats_moorages_view)
-- SELECT
-- l.*,
-- m.*
-- FROM logs l, moorages m;
--COMMENT ON VIEW
-- api.stats_moorages_away_view
-- IS 'Statistics Moorages Time Spent Away web view';
-- View main monitoring for web app
DROP VIEW IF EXISTS api.monitoring_view;
CREATE VIEW api.monitoring_view WITH (security_invoker=true,security_barrier=true) AS
SELECT
time AS "time",
(NOW() AT TIME ZONE 'UTC' - time) > INTERVAL '70 MINUTES' as offline,
metrics-> 'environment.water.temperature' AS waterTemperature,
metrics-> 'environment.inside.temperature' AS insideTemperature,
metrics-> 'environment.outside.temperature' AS outsideTemperature,
metrics-> 'environment.wind.speedOverGround' AS windSpeedOverGround,
metrics-> 'environment.wind.directionTrue' AS windDirectionTrue,
metrics-> 'environment.inside.relativeHumidity' AS insideHumidity,
metrics-> 'environment.outside.relativeHumidity' AS outsideHumidity,
metrics-> 'environment.outside.pressure' AS outsidePressure,
metrics-> 'environment.inside.pressure' AS insidePressure,
metrics-> 'electrical.batteries.House.capacity.stateOfCharge' AS batteryCharge,
metrics-> 'electrical.batteries.House.voltage' AS batteryVoltage,
metrics-> 'environment.depth.belowTransducer' AS depth,
jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(st_makepoint(longitude,latitude))::jsonb,
'properties', jsonb_build_object(
'name', current_setting('vessel.name', false),
'latitude', m.latitude,
'longitude', m.longitude,
'time', m.time,
'speedoverground', m.speedoverground,
'windspeedapparent', m.windspeedapparent
)::jsonb ) AS geojson,
current_setting('vessel.name', false) AS name
--( SELECT api.status_fn() ) AS status
FROM api.metrics m
ORDER BY time DESC LIMIT 1;
COMMENT ON VIEW
api.monitoring_view
IS 'Monitoring static web view';
DROP VIEW IF EXISTS api.monitoring_humidity;
CREATE VIEW api.monitoring_humidity WITH (security_invoker=true,security_barrier=true) AS
SELECT m.time, key, value
FROM api.metrics m,
jsonb_each_text(m.metrics)
WHERE key ILIKE 'environment.%.humidity' OR key ILIKE 'environment.%.relativeHumidity'
ORDER BY m.time DESC;
COMMENT ON VIEW
api.monitoring_humidity
IS 'Monitoring environment.%.humidity web view';
-- View System RPI monitoring for grafana
-- View Electric monitoring for grafana
-- View main monitoring for grafana
-- LAST Monitoring data from json!
DROP VIEW IF EXISTS api.monitoring_temperatures;
CREATE VIEW api.monitoring_temperatures WITH (security_invoker=true,security_barrier=true) AS
SELECT m.time, key, value
FROM api.metrics m,
jsonb_each_text(m.metrics)
WHERE key ILIKE 'environment.%.temperature'
ORDER BY m.time DESC;
COMMENT ON VIEW
api.monitoring_temperatures
IS 'Monitoring environment.%.temperature web view';
-- json key regexp
-- https://stackoverflow.com/questions/38204467/selecting-for-a-jsonb-array-contains-regex-match
-- Last voltage data from json!
DROP VIEW IF EXISTS api.monitoring_voltage;
CREATE VIEW api.monitoring_voltage WITH (security_invoker=true,security_barrier=true) AS
SELECT m.time, key, value
FROM api.metrics m,
jsonb_each_text(m.metrics)
WHERE key ILIKE 'electrical.%.voltage'
ORDER BY m.time DESC;
COMMENT ON VIEW
api.monitoring_voltage
IS 'Monitoring electrical.%.voltage web view';
-- Last whatever data from json!
DROP VIEW IF EXISTS api.monitoring_view2;
CREATE VIEW api.monitoring_view2 WITH (security_invoker=true,security_barrier=true) AS
SELECT
*
FROM
jsonb_each(
( SELECT metrics FROM api.metrics m ORDER BY time DESC LIMIT 1)
);
-- WHERE key ilike 'tanks.%.capacity%'
-- or key ilike 'electrical.solar.%.panelPower'
-- or key ilike 'electrical.batteries%stateOfCharge'
-- or key ilike 'tanks\.%currentLevel'
COMMENT ON VIEW
api.monitoring_view2
IS 'Monitoring Last whatever data from json web view';
-- Timeseries whatever data from json!
DROP VIEW IF EXISTS api.monitoring_view3;
CREATE VIEW api.monitoring_view3 WITH (security_invoker=true,security_barrier=true) AS
SELECT m.time, key, value
FROM api.metrics m,
jsonb_each_text(m.metrics)
ORDER BY m.time DESC;
-- WHERE key ILIKE 'electrical.batteries%voltage';
-- WHERE key ilike 'tanks.%.capacity%'
-- or key ilike 'electrical.solar.%.panelPower'
-- or key ilike 'electrical.batteries%stateOfCharge';
-- key ILIKE 'propulsion.%.runTime'
-- key ILIKE 'navigation.log'
COMMENT ON VIEW
api.monitoring_view3
IS 'Monitoring Timeseries whatever data from json web view';
-- Infotiles web app
DROP VIEW IF EXISTS api.total_info_view;
CREATE VIEW api.total_info_view WITH (security_invoker=true,security_barrier=true) AS
-- Infotiles web app, not used calculated client side
WITH
l as (SELECT count(*) as logs FROM api.logbook),
s as (SELECT count(*) as stays FROM api.stays),
m as (SELECT count(*) as moorages FROM api.moorages)
SELECT * FROM l,s,m;
COMMENT ON VIEW
api.total_info_view
IS 'total_info_view web view';
DROP VIEW IF EXISTS api.explore_view;
CREATE VIEW api.explore_view WITH (security_invoker=true,security_barrier=true) AS
-- Expose last metrics
WITH raw_metrics AS (
SELECT m.time, m.metrics
FROM api.metrics m
ORDER BY m.time desc limit 1
)
SELECT raw_metrics.time, key, value
FROM raw_metrics,
jsonb_each_text(raw_metrics.metrics)
ORDER BY key ASC;
COMMENT ON VIEW
api.explore_view
IS 'explore_view web view';

View File

@@ -1,896 +0,0 @@
---------------------------------------------------------------------------
-- PostSail => Postgres + TimescaleDB + PostGIS + PostgREST
--
-- Inspired from:
-- https://groups.google.com/g/signalk/c/W2H15ODCic4
--
-- Description:
-- Insert data into table metadata from API using PostgREST
-- Insert data into table metrics from API using PostgREST
-- TimescaleDB Hypertable to store signalk metrics
-- pgsql functions to generate logbook, stays, moorages
-- CRON functions to process logbook, stays, moorages
-- python functions for geo reverse and send notification via email and/or pushover
-- Views statistics, timelapse, monitoring, logs
-- Always store time in UTC
---------------------------------------------------------------------------
-- vessels signalk -(POST)-> metadata -> metadata_upsert -(trigger)-> metadata_upsert_fn (INSERT or UPDATE)
-- vessels signalk -(POST)-> metrics -> metrics -(trigger)-> metrics_fn new log,stay,moorage
---------------------------------------------------------------------------
-- Drop database
-- % docker exec -i timescaledb-postgis psql -Uusername -W postgres -c "drop database signalk;"
-- Import Schema
-- % cat signalk.sql | docker exec -i timescaledb-postgis psql -Uusername postgres
-- Export hypertable
-- % docker exec -i timescaledb-postgis psql -Uusername -W signalk -c "\COPY (SELECT * FROM api.metrics ORDER BY time ASC) TO '/var/lib/postgresql/data/metrics.csv' DELIMITER ',' CSV"
-- Export hypertable to gzip
-- # docker exec -i timescaledb-postgis psql -Uusername -W signalk -c "\COPY (SELECT * FROM api.metrics ORDER BY time ASC) TO PROGRAM 'gzip > /var/lib/postgresql/data/metrics.csv.gz' CSV HEADER;"
DO $$
BEGIN
RAISE WARNING '
_________.__ .__ ____ __.
/ _____/|__| ____ ____ _____ | | | |/ _|
\_____ \ | |/ ___\ / \\__ \ | | | <
/ \| / /_/ > | \/ __ \| |_| | \
/_______ /|__\___ /|___| (____ /____/____|__ \
\/ /_____/ \/ \/ \/
%', now();
END $$;
select version();
-- Database
CREATE DATABASE signalk;
-- connext to the DB
\c signalk
-- Schema
CREATE SCHEMA IF NOT EXISTS api;
COMMENT ON SCHEMA api IS 'api schema expose to postgrest';
-- Revoke default privileges to all public functions
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
-- Extensions
CREATE EXTENSION IF NOT EXISTS timescaledb; -- provides time series functions for PostgreSQL
-- CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit; -- provides time series functions for PostgreSQL
CREATE EXTENSION IF NOT EXISTS postgis; -- adds support for geographic objects to the PostgreSQL object-relational database
CREATE EXTENSION IF NOT EXISTS plpgsql; -- PL/pgSQL procedural language
CREATE EXTENSION IF NOT EXISTS plpython3u; -- implements PL/Python based on the Python 3 language variant.
CREATE EXTENSION IF NOT EXISTS jsonb_plpython3u CASCADE; -- tranform jsonb to python json type.
CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- provides a means for tracking planning and execution statistics of all SQL statements executed
-- Trust plpython3u language by default
UPDATE pg_language SET lanpltrusted = true WHERE lanname = 'plpython3u';
---------------------------------------------------------------------------
-- Tables
--
-- Metrics from signalk
CREATE TABLE IF NOT EXISTS api.metrics (
time TIMESTAMP WITHOUT TIME ZONE NOT NULL,
client_id VARCHAR(255) NOT NULL,
latitude DOUBLE PRECISION NULL,
longitude DOUBLE PRECISION NULL,
speedOverGround DOUBLE PRECISION NULL,
courseOverGroundTrue DOUBLE PRECISION NULL,
windSpeedApparent DOUBLE PRECISION NULL,
angleSpeedApparent DOUBLE PRECISION NULL,
status VARCHAR(100) NULL,
metrics jsonb NULL
);
-- Description
COMMENT ON TABLE
api.metrics
IS 'Stores metrics from vessel';
-- Index todo!
CREATE INDEX ON api.metrics (client_id, time DESC);
CREATE INDEX ON api.metrics (status, time DESC);
-- json index??
CREATE INDEX ON api.metrics using GIN (metrics);
-- timescaledb hypertable
SELECT create_hypertable('api.metrics', 'time');
---------------------------------------------------------------------------
-- Metadata from signalk
CREATE TABLE IF NOT EXISTS api.metadata(
id SERIAL PRIMARY KEY,
name VARCHAR(150) NULL,
mmsi VARCHAR(10) NULL,
client_id VARCHAR(255) UNIQUE NOT NULL,
length DOUBLE PRECISION NULL,
beam DOUBLE PRECISION NULL,
height DOUBLE PRECISION NULL,
ship_type VARCHAR(255) NULL,
plugin_version VARCHAR(10) NOT NULL,
signalk_version VARCHAR(10) NOT NULL,
time TIMESTAMP WITHOUT TIME ZONE NOT NULL, -- last_update
active BOOLEAN DEFAULT True -- monitor online/offline
);
-- Description
COMMENT ON TABLE
api.metadata
IS 'Stores metadata from vessel';
-- Index todo!
CREATE INDEX metadata_client_id_idx ON api.metadata (client_id);
---------------------------------------------------------------------------
-- Logbook
-- todo add clientid ref
-- todo add cosumption fuel?
-- todo add engine hour?
-- todo add geom object http://epsg.io/4326 EPSG:4326 Unit: degres
-- todo add geog object http://epsg.io/3857 EPSG:3857 Unit: meters
-- https://postgis.net/workshops/postgis-intro/geography.html#using-geography
-- https://medium.com/coord/postgis-performance-showdown-geometry-vs-geography-ec99967da4f0
-- virtual logbook by boat by client_id impossible?
-- https://www.postgresql.org/docs/current/ddl-partitioning.html
-- Issue:
-- https://www.reddit.com/r/PostgreSQL/comments/di5mbr/postgresql_12_foreign_keys_and_partitioned_tables/f3tsoop/
CREATE TABLE IF NOT EXISTS api.logbook(
id SERIAL PRIMARY KEY,
client_id VARCHAR(255) NOT NULL REFERENCES api.metadata(client_id) ON DELETE RESTRICT,
-- client_id VARCHAR(255) NOT NULL,
active BOOLEAN DEFAULT false,
name VARCHAR(255),
_from VARCHAR(255),
_from_lat DOUBLE PRECISION NULL,
_from_lng DOUBLE PRECISION NULL,
_to VARCHAR(255),
_to_lat DOUBLE PRECISION NULL,
_to_lng DOUBLE PRECISION NULL,
--track_geom Geometry(LINESTRING)
track_geom geometry(LINESTRING,4326) NULL,
track_geog geography(LINESTRING) NULL,
_from_time TIMESTAMP WITHOUT TIME ZONE NOT NULL,
_to_time TIMESTAMP WITHOUT TIME ZONE NULL,
distance NUMERIC, -- meters?
duration INTERVAL, -- duration in days and hours?
avg_speed DOUBLE PRECISION NULL,
max_speed DOUBLE PRECISION NULL,
max_wind_speed DOUBLE PRECISION NULL,
notes TEXT NULL
);
-- Description
COMMENT ON TABLE
api.logbook
IS 'Stores generated logbook';
COMMENT ON COLUMN api.logbook.distance IS 'in NM';
-- Index todo!
CREATE INDEX logbook_client_id_idx ON api.logbook (client_id);
CREATE INDEX ON api.logbook USING GIST ( track_geom );
COMMENT ON COLUMN api.logbook.track_geom IS 'postgis geometry type EPSG:4326 Unit: degres';
CREATE INDEX ON api.logbook USING GIST ( track_geog );
COMMENT ON COLUMN api.logbook.track_geog IS 'postgis geography type default SRID 4326 Unit: degres';
-- Otherwise -- ERROR: Only lon/lat coordinate systems are supported in geography.
---------------------------------------------------------------------------
-- Stays
-- todo add clientid ref
-- todo add FOREIGN KEY?
-- virtual logbook by boat?
CREATE TABLE IF NOT EXISTS api.stays(
id SERIAL PRIMARY KEY,
client_id VARCHAR(255) NOT NULL REFERENCES api.metadata(client_id) ON DELETE RESTRICT,
-- client_id VARCHAR(255) NOT NULL,
active BOOLEAN DEFAULT false,
name VARCHAR(255),
latitude DOUBLE PRECISION NULL,
longitude DOUBLE PRECISION NULL,
geog GEOGRAPHY(POINT) NULL,
arrived TIMESTAMP WITHOUT TIME ZONE NOT NULL,
departed TIMESTAMP WITHOUT TIME ZONE,
duration INTERVAL, -- duration in days and hours?
stay_code INT DEFAULT 1, -- REFERENCES api.stays_at(stay_code),
notes TEXT NULL
);
-- Description
COMMENT ON TABLE
api.stays
IS 'Stores generated stays';
-- Index
CREATE INDEX stays_client_id_idx ON api.stays (client_id);
CREATE INDEX ON api.stays USING GIST ( geog );
COMMENT ON COLUMN api.stays.geog IS 'postgis geography type default SRID 4326 Unit: degres';
-- With other SRID ERROR: Only lon/lat coordinate systems are supported in geography.
---------------------------------------------------------------------------
-- Moorages
-- todo add clientid ref
-- virtual logbook by boat?
CREATE TABLE IF NOT EXISTS api.moorages(
id SERIAL PRIMARY KEY,
client_id VARCHAR(255) NOT NULL REFERENCES api.metadata(client_id) ON DELETE RESTRICT,
-- client_id VARCHAR(255) NOT NULL,
name VARCHAR(255),
country VARCHAR(255), -- todo need to update reverse_geocode_py_fn
stay_id INT NOT NULL, -- needed?
stay_code INT DEFAULT 1, -- needed? REFERENCES api.stays_at(stay_code)
stay_duration INTERVAL NULL,
reference_count INT DEFAULT 1,
latitude DOUBLE PRECISION NULL,
longitude DOUBLE PRECISION NULL,
geog GEOGRAPHY(POINT) NULL,
home_flag BOOLEAN DEFAULT false,
notes TEXT NULL
);
-- Description
COMMENT ON TABLE
api.moorages
IS 'Stores generated moorages';
-- Index
CREATE INDEX moorages_client_id_idx ON api.moorages (client_id);
CREATE INDEX ON api.moorages USING GIST ( geog );
COMMENT ON COLUMN api.moorages.geog IS 'postgis geography type default SRID 4326 Unit: degres';
-- With other SRID ERROR: Only lon/lat coordinate systems are supported in geography.
---------------------------------------------------------------------------
-- Stay Type
CREATE TABLE IF NOT EXISTS api.stays_at(
stay_code INTEGER,
description TEXT
);
-- Description
COMMENT ON TABLE api.stays_at IS 'Stay Type';
-- Insert default possible values
INSERT INTO api.stays_at(stay_code, description) VALUES
(1, 'Unknow'),
(2, 'Anchor'),
(3, 'Mooring Buoy'),
(4, 'Dock');
---------------------------------------------------------------------------
-- Trigger Functions Metadata table
--
-- UPSERT - Insert vs Update for Metadata
DROP FUNCTION IF EXISTS metadata_upsert_trigger_fn;
CREATE FUNCTION metadata_upsert_trigger_fn() RETURNS trigger AS $metadata_upsert$
DECLARE
metadata_id integer;
metadata_active boolean;
BEGIN
-- UPSERT - Insert vs Update for Metadata
RAISE NOTICE 'metadata_upsert_trigger_fn';
SELECT m.id,m.active INTO metadata_id,metadata_active
FROM api.metadata m
WHERE (m.mmsi IS NOT NULL AND m.mmsi = NEW.mmsi)
OR (m.client_id IS NOT NULL AND m.client_id = NEW.client_id);
RAISE NOTICE 'metadata_id %', metadata_id;
IF metadata_id IS NOT NULL THEN
-- send notifitacion if boat is back online
IF metadata_active is False THEN
-- Add monitor online entry to process queue for later notification
INSERT INTO process_queue (channel, payload, stored)
VALUES ('monitoring_online', metadata_id, now());
END IF;
-- Update vessel metadata
UPDATE api.metadata
SET
name = NEW.name,
mmsi = NEW.mmsi,
client_id = NEW.client_id,
length = NEW.length,
beam = NEW.beam,
height = NEW.height,
ship_type = NEW.ship_type,
plugin_version = NEW.plugin_version,
signalk_version = NEW.signalk_version,
time = NEW.time,
active = true
WHERE id = metadata_id;
RETURN NULL; -- Ignore insert
ELSE
-- Insert new vessel metadata
RETURN NEW; -- Insert new vessel metadata
END IF;
END;
$metadata_upsert$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.metadata_upsert_trigger_fn
IS 'process metadata from vessel, upsert';
-- Metadata notification for new vessel after insert
DROP FUNCTION IF EXISTS metadata_notification_trigger_fn;
CREATE FUNCTION metadata_notification_trigger_fn() RETURNS trigger AS $metadata_notification$
DECLARE
BEGIN
RAISE NOTICE 'metadata_notification_trigger_fn';
INSERT INTO process_queue (channel, payload, stored)
VALUES ('monitoring_online', NEW.id, now());
RETURN NULL;
END;
$metadata_notification$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.metadata_notification_trigger_fn
IS 'process metadata notification from vessel, monitoring_online';
-- Metadata trigger BEFORE INSERT
CREATE TRIGGER metadata_upsert_trigger BEFORE INSERT ON api.metadata
FOR EACH ROW EXECUTE FUNCTION metadata_upsert_trigger_fn();
-- Description
COMMENT ON TRIGGER
metadata_upsert_trigger ON api.metadata
IS 'BEFORE INSERT ON api.metadata run function metadata_upsert_trigger_fn';
-- Metadata trigger AFTER INSERT
CREATE TRIGGER metadata_notification_trigger AFTER INSERT ON api.metadata
FOR EACH ROW EXECUTE FUNCTION metadata_notification_trigger_fn();
-- Description
COMMENT ON TRIGGER
metadata_notification_trigger ON api.metadata
IS 'AFTER INSERT ON api.metadata run function metadata_update_trigger_fn for notification on new vessel';
---------------------------------------------------------------------------
-- Trigger Functions metrics table
--
-- Create a logbook or stay entry base on the vessel state, eg: navigation.state
-- https://github.com/meri-imperiumi/signalk-autostate
DROP FUNCTION IF EXISTS metrics_trigger_fn;
CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
DECLARE
previous_status varchar;
previous_time TIMESTAMP WITHOUT TIME ZONE;
stay_code integer;
logbook_id integer;
stay_id integer;
BEGIN
RAISE NOTICE 'metrics_trigger_fn';
-- todo: Check we have the boat metadata?
-- Do we have a log in progress?
-- Do we have a stay in progress?
-- Fetch the latest entry to compare status against the new status to be insert
SELECT coalesce(m.status, 'moored'), m.time INTO previous_status, previous_time
FROM api.metrics m
WHERE m.client_id IS NOT NULL
AND m.client_id = NEW.client_id
ORDER BY m.time DESC LIMIT 1;
RAISE NOTICE 'Metrics Status, New:[%] Previous:[%]', NEW.status, previous_status;
IF NEW.status IS NULL THEN
RAISE WARNING 'Invalid new status [%], update to default moored', NEW.status;
NEW.status := 'moored';
END IF;
IF previous_status IS NULL THEN
RAISE WARNING 'Invalid previous status [%], update to default moored', previous_status;
previous_status := 'moored';
-- Add new stay as no previous entry exist
INSERT INTO api.stays
(client_id, active, arrived, latitude, longitude, stay_code)
VALUES (NEW.client_id, true, NEW.time, NEW.latitude, NEW.longitude, stay_code)
RETURNING id INTO stay_id;
-- Add stay entry to process queue for further processing
INSERT INTO process_queue (channel, payload, stored) values ('new_stay', stay_id, now());
RAISE WARNING 'Insert first stay as no previous metrics exist, stay_id %', stay_id;
END IF;
IF previous_time = NEW.time THEN
-- Ignore entry if same time
RAISE WARNING 'Ignoring metric, duplicate time [%] = [%]', previous_time, NEW.time;
RETURN NULL;
END IF;
--
-- Check the state and if any previous/current entry
IF previous_status <> NEW.status AND (NEW.status = 'sailing' OR NEW.status = 'motoring') THEN
-- Start new log
RAISE WARNING 'Start new log, New:[%] Previous:[%]', NEW.status, previous_status;
RAISE NOTICE 'Inserting new trip [%]', NEW.status;
INSERT INTO api.logbook
(client_id, active, _from_time, _from_lat, _from_lng)
VALUES (NEW.client_id, true, NEW.time, NEW.latitude, NEW.longitude);
-- End current stay
-- Fetch stay_id by client_id
SELECT id INTO stay_id
FROM api.stays s
WHERE s.client_id IS NOT NULL
AND s.client_id = NEW.client_id
AND active IS true
LIMIT 1;
RAISE NOTICE 'Updating stay status [%] [%] [%]', stay_id, NEW.status, NEW.time;
IF stay_id IS NOT NULL THEN
UPDATE api.stays
SET
active = false,
departed = NEW.time
WHERE id = stay_id;
-- Add moorage entry to process queue for further processing
INSERT INTO process_queue (channel, payload, stored) values ('new_moorage', stay_id, now());
ELSE
RAISE WARNING 'Invalid stay_id [%] [%]', stay_id, NEW.time;
END IF;
ELSIF previous_status <> NEW.status AND (NEW.status = 'moored' OR NEW.status = 'anchored') THEN
-- Start new stays
RAISE WARNING 'Start new stay, New:[%] Previous:[%]', NEW.status, previous_status;
RAISE NOTICE 'Inserting new stay [%]', NEW.status;
-- if metric status is anchored set stay_code accordingly
stay_code = 1;
IF NEW.status = 'anchored' THEN
stay_code = 2;
END IF;
-- Add new stay
INSERT INTO api.stays
(client_id, active, arrived, latitude, longitude, stay_code)
VALUES (NEW.client_id, true, NEW.time, NEW.latitude, NEW.longitude, stay_code)
RETURNING id INTO stay_id;
-- Add stay entry to process queue for further processing
INSERT INTO process_queue (channel, payload, stored) values ('new_stay', stay_id, now());
-- End current log/trip
-- Fetch logbook_id by client_id
SELECT id INTO logbook_id
FROM api.logbook l
WHERE l.client_id IS NOT NULL
AND l.client_id = NEW.client_id
AND active IS true
LIMIT 1;
IF logbook_id IS NOT NULL THEN
-- todo check on time start vs end
RAISE NOTICE 'Updating trip status [%] [%] [%]', logbook_id, NEW.status, NEW.time;
UPDATE api.logbook
SET
active = false,
_to_time = NEW.time,
_to_lat = NEW.latitude,
_to_lng = NEW.longitude
WHERE id = logbook_id;
-- Add logbook entry to process queue for later processing
INSERT INTO process_queue (channel, payload, stored) values ('new_logbook', logbook_id, now());
ELSE
RAISE WARNING 'Invalid logbook_id [%] [%]', logbook_id, NEW.time;
END IF;
END IF;
RETURN NEW; -- Finally insert the actual new metric
END;
$metrics$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.metrics_trigger_fn
IS 'process metrics from vessel, generate new_logbook and new_stay';
--
-- Triggers logbook update on metrics insert
CREATE TRIGGER metrics_trigger BEFORE INSERT ON api.metrics
FOR EACH ROW EXECUTE FUNCTION metrics_trigger_fn();
-- Description
COMMENT ON TRIGGER
metrics_trigger ON api.metrics
IS 'BEFORE INSERT ON api.metrics run function metrics_trigger_fn';
---------------------------------------------------------------------------
-- Functions API schema
-- Export a log entry to geojson
DROP FUNCTION IF EXISTS api.export_logbook_geojson_point_fn;
CREATE OR REPLACE FUNCTION api.export_logbook_geojson_point_fn(IN _id INTEGER, OUT geojson JSON) RETURNS JSON AS $export_logbook_geojson_point$
DECLARE
logbook_rec record;
BEGIN
-- If _id is is not NULL and > 0
SELECT * INTO logbook_rec
FROM api.logbook WHERE id = _id;
WITH log AS (
SELECT m.time as time, m.latitude as lat, m.longitude as lng, m.courseOverGroundTrue as cog
FROM api.metrics m
WHERE m.latitude IS NOT null
AND m.longitude IS NOT null
AND m.time >= logbook_rec._from_time::timestamp without time zone
AND m.time <= logbook_rec._to_time::timestamp without time zone
GROUP by m.time,m.latitude,m.longitude,m.courseOverGroundTrue
ORDER BY m.time ASC)
SELECT json_build_object(
'type', 'FeatureCollection',
'crs', json_build_object(
'type', 'name',
'properties', json_build_object(
'name', 'EPSG:4326'
)
),
'features', json_agg(
json_build_object(
'type', 'Feature',
-- 'id', {id}, -- the GeoJson spec includes an 'id' field, but it is optional, replace {id} with your id field
'geometry', ST_AsGeoJSON(st_makepoint(lng,lat))::json,
'properties', json_build_object(
-- list of fields
'field1', time,
'field2', cog
)
)
)
) INTO geojson
FROM log;
END;
$export_logbook_geojson_point$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.export_logbook_geojson_point_fn
IS 'Export a log entry to geojson feature point with Time and courseOverGroundTrue properties';
-- Export a log entry to geojson
DROP FUNCTION IF EXISTS api.export_logbook_geojson_linestring_fn;
CREATE FUNCTION api.export_logbook_geojson_linestring_fn(IN _id INTEGER) RETURNS JSON AS $export_logbook_geojson_linestring$
DECLARE
geojson json;
BEGIN
-- If _id is is not NULL and > 0
SELECT ST_AsGeoJSON(l.track_geom) INTO geojson
FROM api.logbook l
WHERE l.id = _id;
RETURN geojson;
END;
$export_logbook_geojson_linestring$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.export_logbook_geojson_linestring_fn
IS 'Export a log entry to geojson feature linestring';
-- Find all log from and to moorage geopoint within 100m
DROP FUNCTION IF EXISTS api.find_log_from_moorage_fn;
CREATE FUNCTION api.find_log_from_moorage_fn(IN _id INTEGER) RETURNS void AS $find_log_from_moorage$
DECLARE
moorage_rec record;
logbook_rec record;
BEGIN
-- If _id is is not NULL and > 0
SELECT * INTO moorage_rec
FROM api.moorages m
WHERE m.id = _id;
-- find all log from and to moorage geopoint within 100m
--RETURN QUERY
SELECT id,name,_from,_to,_from_time,_to_time,distance,duration
FROM api.logbook
WHERE ST_DWithin(
Geography(ST_MakePoint(_from_lng, _from_lat)),
moorage_rec.geog,
100 -- in meters ?
)
OR ST_DWithin(
Geography(ST_MakePoint(_to_lng, _to_lat)),
moorage_rec.geog,
100 -- in meters ?
)
ORDER BY _from_time DESC;
END;
$find_log_from_moorage$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.find_log_from_moorage_fn
IS 'Find all log from and to moorage geopoint within 100m';
-- Find all stay within 100m of moorage geopoint
DROP FUNCTION IF EXISTS api.find_stay_from_moorage_fn;
CREATE FUNCTION api.find_stay_from_moorage_fn(IN _id INTEGER) RETURNS void AS $find_stay_from_moorage$
DECLARE
moorage_rec record;
stay_rec record;
BEGIN
-- If _id is is not NULL and > 0
SELECT * INTO moorage_rec
FROM api.moorages m
WHERE m.id = _id;
-- find all log from and to moorage geopoint within 100m
--RETURN QUERY
SELECT s.id,s.arrived,s.departed,s.duration,sa.description
FROM api.stays s, api.stays_at sa
WHERE ST_DWithin(
s.geog,
moorage_rec.geog,
100 -- in meters ?
)
AND departed IS NOT NULL
AND s.name IS NOT NULL
AND s.stay_code = sa.stay_code
ORDER BY s.arrived DESC;
END;
$find_stay_from_moorage$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
api.find_stay_from_moorage_fn
IS 'Find all stay within 100m of moorage geopoint';
---------------------------------------------------------------------------
-- Views
--
CREATE VIEW first_metric AS
SELECT *
FROM api.metrics
ORDER BY time ASC LIMIT 1;
CREATE VIEW last_metric AS
SELECT *
FROM api.metrics
ORDER BY time DESC LIMIT 1;
CREATE VIEW trip_in_progress AS
SELECT *
FROM api.logbook
WHERE active IS true;
CREATE VIEW stay_in_progress AS
SELECT *
FROM api.stays
WHERE active IS true;
-- TODO: Use materialized views instead as it is not live data
-- Logs web view
DROP VIEW IF EXISTS api.logs_view;
CREATE VIEW api.logs_view AS
SELECT id,name,_from,_to,_from_time,_to_time,distance,duration
FROM api.logbook
WHERE _to_time IS NOT NULL
ORDER BY _from_time DESC;
-- Description
COMMENT ON VIEW
api.logs_view
IS 'Logs web view';
-- Stays web view
-- TODO group by month
DROP VIEW IF EXISTS api.stays_view;
CREATE VIEW api.stays_view AS
SELECT
concat(
extract(DAYS FROM (s.departed-s.arrived)::interval),
' days',
--DATE_TRUNC('day', s.departed-s.arrived),
' stay at ',
s.name,
' in ',
RTRIM(TO_CHAR(s.departed, 'Month')),
' ',
TO_CHAR(s.departed, 'YYYY')
) as Name,
s.name AS Moorage,
s.arrived AS Arrived,
s.departed AS Departed,
sa.description AS "Stayed at",
(s.departed-s.arrived) AS Duration
FROM api.stays s, api.stays_at sa
WHERE departed is not null
AND s.name is not null
AND s.stay_code = sa.stay_code
ORDER BY s.arrived DESC;
-- Description
COMMENT ON VIEW
api.stays_view
IS 'Stays web view';
-- Moorages web view
-- TODO, this is wrong using distinct (m.name) should be using postgis geog feature
--DROP VIEW IF EXISTS api.moorages_view_old;
--CREATE VIEW api.moorages_view_old AS
-- SELECT
-- m.name AS Moorage,
-- sa.description AS "Default Stay",
-- sum((m.departed-m.arrived)) OVER (PARTITION by m.name) AS "Total Stay",
-- count(m.departed) OVER (PARTITION by m.name) AS "Arrivals & Departures"
-- FROM api.moorages m, api.stays_at sa
-- WHERE departed is not null
-- AND m.name is not null
-- AND m.stay_code = sa.stay_code
-- GROUP BY m.name,sa.description,m.departed,m.arrived
-- ORDER BY 4 DESC;
-- the good way?
DROP VIEW IF EXISTS api.moorages_view;
CREATE OR REPLACE VIEW api.moorages_view AS
SELECT
m.name AS Moorage,
sa.description AS "Default Stay",
EXTRACT(DAY FROM justify_hours ( m.stay_duration )) AS "Total Stay",
m.reference_count AS "Arrivals & Departures",
m.geog
-- m.stay_duration,
-- justify_hours ( m.stay_duration )
FROM api.moorages m, api.stays_at sa
WHERE m.name is not null
AND m.stay_code = sa.stay_code
GROUP BY m.name,sa.description,m.stay_duration,m.reference_count,m.geog
-- ORDER BY 4 DESC;
ORDER BY m.reference_count DESC;
-- Description
COMMENT ON VIEW
api.moorages_view
IS 'Moorages web view';
-- All moorage in 100 meters from the start of a logbook.
-- ST_DistanceSphere Returns minimum distance in meters between two lon/lat points.
--SELECT
-- m.name, ST_MakePoint(m._lng,m._lat),
-- l._from, ST_MakePoint(l._from_lng,l._from_lat),
-- ST_DistanceSphere(ST_MakePoint(m._lng,m._lat), ST_MakePoint(l._from_lng,l._from_lat))
-- FROM api.moorages m , api.logbook l
-- WHERE ST_DistanceSphere(ST_MakePoint(m._lng,m._lat), ST_MakePoint(l._from_lng,l._from_lat)) <= 100;
-- Stats web view
-- TODO....
-- first time entry from metrics
----> select * from api.metrics m ORDER BY m.time desc limit 1
-- last time entry from metrics
----> select * from api.metrics m ORDER BY m.time asc limit 1
-- max speed from logbook
-- max wind speed from logbook
----> select max(l.max_speed) as max_speed, max(l.max_wind_speed) as max_wind_speed from api.logbook l;
-- Total Distance from logbook
----> select sum(l.distance) as "Total Distance" from api.logbook l;
-- Total Time Underway from logbook
----> select sum(l.duration) as "Total Time Underway" from api.logbook l;
-- Longest Nonstop Sail from logbook, eg longest trip duration and distance
----> select max(l.duration),max(l.distance) from api.logbook l;
CREATE VIEW api.stats_logs_view AS -- todo
WITH
meta AS (
SELECT m.name FROM api.metadata m ),
last_metric AS (
SELECT m.time FROM api.metrics m ORDER BY m.time DESC limit 1),
first_metric AS (
SELECT m.time FROM api.metrics m ORDER BY m.time ASC limit 1),
logbook AS (
SELECT
count(*) AS "Number of Log Entries",
max(l.max_speed) AS "Max Speed",
max(l.max_wind_speed) AS "Max Wind Speed",
sum(l.distance) AS "Total Distance",
sum(l.duration) AS "Total Time Underway",
concat( max(l.distance), ' NM, ', max(l.duration), ' hours') AS "Longest Nonstop Sail"
FROM api.logbook l)
SELECT
m.name as Name,
fm.time AS first,
lm.time AS last,
l.*
FROM first_metric fm, last_metric lm, logbook l, meta m;
-- Home Ports / Unique Moorages
----> select count(*) as "Home Ports" from api.moorages m where home_flag is true;
-- Unique Moorages
----> select count(*) as "Home Ports" from api.moorages m;
-- Time Spent at Home Port(s)
----> select sum(m.stay_duration) as "Time Spent at Home Port(s)" from api.moorages m where home_flag is true;
-- OR
----> select m.stay_duration as "Time Spent at Home Port(s)" from api.moorages m where home_flag is true;
-- Time Spent Away
----> select sum(m.stay_duration) as "Time Spent Away" from api.moorages m where home_flag is false;
-- Time Spent Away order by, group by stay_code (Dock, Anchor, Mooring Buoys, Unclassified)
----> select sa.description,sum(m.stay_duration) as "Time Spent Away" from api.moorages m, api.stays_at sa where home_flag is false AND m.stay_code = sa.stay_code group by m.stay_code,sa.description order by m.stay_code;
CREATE VIEW api.stats_moorages_view AS -- todo
select *
from api.moorages;
--CREATE VIEW api.stats_view AS -- todo
-- WITH
-- logs AS (
-- SELECT * FROM api.stats_logs_view ),
-- moorages AS (
-- SELECT * FROM api.stats_moorages_view)
-- SELECT
-- l.*,
-- m.*
-- FROM logs l, moorages m;
-- global timelapse
-- TODO
CREATE VIEW timelapse AS -- todo
SELECT latitude, longitude from api.metrics;
-- View main monitoring for grafana
-- LAST Monitoring data from json!
CREATE VIEW api.monitoring AS
SELECT
time AS "time",
metrics-> 'environment.water.temperature' AS waterTemperature,
metrics-> 'environment.inside.temperature' AS insideTemperature,
metrics-> 'environment.outside.temperature' AS outsideTemperature,
metrics-> 'environment.wind.speedOverGround' AS windSpeedOverGround,
metrics-> 'environment.wind.directionGround' AS windDirectionGround,
metrics-> 'environment.inside.humidity' AS insideHumidity,
metrics-> 'environment.outside.humidity' AS outsideHumidity,
metrics-> 'environment.outside.pressure' AS outsidePressure,
metrics-> 'environment.inside.pressure' AS insidePressure
FROM api.metrics m
ORDER BY time DESC LIMIT 1;
CREATE VIEW api.monitoring_humidity AS
SELECT
time AS "time",
metrics-> 'environment.inside.humidity' AS insideHumidity,
metrics-> 'environment.outside.humidity' AS outsideHumidity
FROM api.metrics m
ORDER BY time DESC LIMIT 1;
-- View System RPI monitoring for grafana
-- View Electric monitoring for grafana
-- View main monitoring for grafana
-- LAST Monitoring data from json!
CREATE VIEW api.monitorin_temperatures AS
SELECT
time AS "time",
metrics-> 'environment.water.temperature' AS waterTemperature,
metrics-> 'environment.inside.temperature' AS insideTemperature,
metrics-> 'environment.outside.temperature' AS outsideTemperature
FROM api.metrics m
ORDER BY time DESC LIMIT 1;
-- json key regexp
-- https://stackoverflow.com/questions/38204467/selecting-for-a-jsonb-array-contains-regex-match
-- Last voltage data from json!
CREATE VIEW api.voltage AS
SELECT
time AS "time",
cast(metrics-> 'electrical.batteries.AUX2.voltage' AS numeric) AS AUX2,
cast(metrics-> 'electrical.batteries.House.voltage' AS numeric) AS House,
cast(metrics-> 'environment.rpi.pijuice.gpioVoltage' AS numeric) AS gpioVoltage,
cast(metrics-> 'electrical.batteries.Seatalk.voltage' AS numeric) AS SeatalkVoltage,
cast(metrics-> 'electrical.batteries.Starter.voltage' AS numeric) AS StarterVoltage,
cast(metrics-> 'environment.rpi.pijuice.batteryVoltage' AS numeric) AS RPIBatteryVoltage,
cast(metrics-> 'electrical.batteries.victronDevice.voltage' AS numeric) AS victronDeviceVoltage
FROM api.metrics m
ORDER BY time DESC LIMIT 1;
---------------------------------------------------------------------------
-- API helper functions
--
DROP FUNCTION IF EXISTS api.export_logbook_gpx_py_fn;
CREATE OR REPLACE FUNCTION api.export_logbook_gpx_py_fn(IN _id INTEGER) RETURNS XML
AS $export_logbook_gpx_py$
import uuid
# BEGIN GPX XML format
gpx_data = f"""<?xml version="1.0"?>
<gpx version="1.1" creator="PostgSAIL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns:opencpn="http://www.opencpn.org">
<trk>
<link href="https://openplotter.cloud/log/{_id}">
<text>openplotter trip log todo</text>
</link>
<extensions>
<opencpn:guid>{uuid.uuid4()}</opencpn:guid>
<opencpn:viz>1</opencpn:viz>
<opencpn:start>{mytrack[0]['time']}</opencpn:start>
<opencpn:end>{mytrack[-1]['time']}</opencpn:end>
</extensions>
<trkseg>\n""";
##print(gpx_data)
# LOOP through log entry
for entry in mytrack:
##print(entry['time'])
gpx_data += f""" <trkpt lat="{entry['lat']}" lon="{entry['lng']}">
<time>{entry['time']}</time>
</trkpt>\n""";
# END GPX XML format
gpx_data += """ </trkseg>
</trk>
</gpx>""";
return gpx_data
$export_logbook_gpx_py$ LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
api.export_logbook_gpx_py_fn
IS 'TODO, Export a log entry to GPX XML format using plpython3u';
--DROP FUNCTION IF EXISTS api.export_logbook_csv_fn;
--CREATE OR REPLACE FUNCTION api.export_logbook_csv_fn(IN _id INTEGER) RETURNS void
--AS $export_logbook_csv$
-- TODO
--$export_logbook_csv$ language plpgsql;
-- Description
--COMMENT ON FUNCTION
-- api.export_logbook_csv_fn
-- IS 'TODO, ...';

View File

@@ -8,19 +8,49 @@ select current_database();
-- connect to the DB
\c signalk
-- Check for new logbook pending validation
CREATE FUNCTION cron_process_pre_logbook_fn() RETURNS void AS $$
DECLARE
process_rec record;
BEGIN
-- Check for new logbook pending update
RAISE NOTICE 'cron_process_pre_logbook_fn init loop';
FOR process_rec in
SELECT * FROM process_queue
WHERE channel = 'pre_logbook' AND processed IS NULL
ORDER BY stored ASC LIMIT 100
LOOP
RAISE NOTICE 'cron_process_pre_logbook_fn processing queue [%] for logbook id [%]', process_rec.id, process_rec.payload;
-- update logbook
PERFORM process_pre_logbook_fn(process_rec.payload::INTEGER);
-- update process_queue table , processed
UPDATE process_queue
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE 'cron_process_pre_logbook_fn processed queue [%] for logbook id [%]', process_rec.id, process_rec.payload;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_pre_logbook_fn
IS 'init by pg_cron to check for new logbook pending update, if so perform process_logbook_valid_fn';
-- Check for new logbook pending update
CREATE FUNCTION cron_process_new_logbook_fn() RETURNS void AS $$
declare
process_rec record;
begin
-- Check for new logbook pending update
RAISE NOTICE 'cron_process_new_logbook_fn';
RAISE NOTICE 'cron_process_new_logbook_fn init loop';
FOR process_rec in
SELECT * FROM process_queue
WHERE channel = 'new_logbook' AND processed IS NULL
ORDER BY stored ASC
ORDER BY stored ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_process_new_logbook_fn [%]', process_rec.payload;
RAISE NOTICE 'cron_process_new_logbook_fn processing queue [%] for logbook id [%]', process_rec.id, process_rec.payload;
-- update logbook
PERFORM process_logbook_queue_fn(process_rec.payload::INTEGER);
-- update process_queue table , processed
@@ -28,7 +58,7 @@ begin
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> updated process_queue table [%]', process_rec.id;
RAISE NOTICE 'cron_process_new_logbook_fn processed queue [%] for logbook id [%]', process_rec.id, process_rec.payload;
END LOOP;
END;
$$ language plpgsql;
@@ -43,13 +73,13 @@ declare
process_rec record;
begin
-- Check for new stay pending update
RAISE NOTICE 'cron_process_new_stay_fn';
RAISE NOTICE 'cron_process_new_stay_fn init loop';
FOR process_rec in
SELECT * FROM process_queue
WHERE channel = 'new_stay' AND processed IS NULL
ORDER BY stored ASC
ORDER BY stored ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_process_new_stay_fn [%]', process_rec.payload;
RAISE NOTICE 'cron_process_new_stay_fn processing queue [%] for stay id [%]', process_rec.id, process_rec.payload;
-- update stay
PERFORM process_stay_queue_fn(process_rec.payload::INTEGER);
-- update process_queue table , processed
@@ -57,7 +87,7 @@ begin
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> updated process_queue table [%]', process_rec.id;
RAISE NOTICE 'cron_process_new_stay_fn processed queue [%] for stay id [%]', process_rec.id, process_rec.payload;
END LOOP;
END;
$$ language plpgsql;
@@ -73,13 +103,13 @@ declare
process_rec record;
begin
-- Check for new moorage pending update
RAISE NOTICE 'cron_process_new_moorage_fn';
RAISE NOTICE 'cron_process_new_moorage_fn init loop';
FOR process_rec in
SELECT * FROM process_queue
WHERE channel = 'new_moorage' AND processed IS NULL
ORDER BY stored ASC
ORDER BY stored ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_process_new_moorage_fn [%]', process_rec.payload;
RAISE NOTICE 'cron_process_new_moorage_fn processing queue [%] for moorage id [%]', process_rec.id, process_rec.payload;
-- update moorage
PERFORM process_moorage_queue_fn(process_rec.payload::INTEGER);
-- update process_queue table , processed
@@ -87,14 +117,14 @@ begin
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> updated process_queue table [%]', process_rec.id;
RAISE NOTICE 'cron_process_new_moorage_fn processed queue [%] for moorage id [%]', process_rec.id, process_rec.payload;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_new_moorage_fn
IS 'init by pg_cron to check for new moorage pending update, if so perform process_moorage_queue_fn';
IS 'Deprecated, init by pg_cron to check for new moorage pending update, if so perform process_moorage_queue_fn';
-- CRON Monitor offline pending notification
create function cron_process_monitor_offline_fn() RETURNS void AS $$
@@ -123,29 +153,38 @@ begin
SET
active = False
WHERE id = metadata_rec.id;
RAISE NOTICE '-> updated api.metadata table to inactive for [%]', metadata_rec.id;
IF metadata_rec.vessel_id IS NULL OR metadata_rec.vessel_id = '' THEN
RAISE WARNING '-> cron_process_monitor_offline_fn invalid metadata record vessel_id %', vessel_id;
RAISE EXCEPTION 'Invalid metadata'
USING HINT = 'Unknown vessel_id';
RETURN;
END IF;
PERFORM set_config('vessel.id', metadata_rec.vessel_id, false);
RAISE DEBUG '-> DEBUG cron_process_monitor_offline_fn vessel.id %', current_setting('vessel.id', false);
RAISE NOTICE 'cron_process_monitor_offline_fn updated api.metadata table to inactive for [%] [%]', metadata_rec.id, metadata_rec.vessel_id;
-- Gather email and pushover app settings
app_settings = get_app_settings_fn();
--app_settings = get_app_settings_fn();
-- Gather user settings
user_settings := get_user_settings_from_metadata_fn(metadata_rec.id::INTEGER);
--user_settings := get_user_settings_from_clientid_fn(metadata_rec.id::INTEGER);
RAISE DEBUG '-> debug monitor_offline get_user_settings_from_metadata_fn [%]', user_settings;
user_settings := get_user_settings_from_vesselid_fn(metadata_rec.vessel_id::TEXT);
RAISE DEBUG '-> cron_process_monitor_offline_fn get_user_settings_from_vesselid_fn [%]', user_settings;
-- Send notification
--PERFORM send_notification_fn('monitor_offline'::TEXT, metadata_rec::RECORD);
PERFORM send_email_py_fn('monitor_offline'::TEXT, user_settings::JSONB, app_settings::JSONB);
PERFORM send_notification_fn('monitor_offline'::TEXT, user_settings::JSONB);
--PERFORM send_email_py_fn('monitor_offline'::TEXT, user_settings::JSONB, app_settings::JSONB);
--PERFORM send_pushover_py_fn('monitor_offline'::TEXT, user_settings::JSONB, app_settings::JSONB);
-- log/insert/update process_queue table with processed
INSERT INTO process_queue
(channel, payload, stored, processed)
(channel, payload, stored, processed, ref_id)
VALUES
('monitoring_offline', metadata_rec.id, metadata_rec.interval, now())
('monitoring_offline', metadata_rec.id, metadata_rec.interval, now(), metadata_rec.vessel_id)
RETURNING id INTO process_id;
RAISE NOTICE '-> updated process_queue table [%]', process_id;
RAISE NOTICE '-> cron_process_monitor_offline_fn updated process_queue table [%]', process_id;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
COMMENT ON FUNCTION
public.cron_process_monitor_offline_fn
IS 'init by pg_cron to monitor offline pending notification, if so perform send_email o send_pushover base on user preferences';
@@ -169,27 +208,36 @@ begin
SELECT * INTO metadata_rec
FROM api.metadata
WHERE id = process_rec.payload::INTEGER;
IF metadata_rec.vessel_id IS NULL OR metadata_rec.vessel_id = '' THEN
RAISE WARNING '-> cron_process_monitor_online_fn invalid metadata record vessel_id %', vessel_id;
RAISE EXCEPTION 'Invalid metadata'
USING HINT = 'Unknown vessel_id';
RETURN;
END IF;
PERFORM set_config('vessel.id', metadata_rec.vessel_id, false);
RAISE DEBUG '-> DEBUG cron_process_monitor_online_fn vessel_id %', current_setting('vessel.id', false);
-- Gather email and pushover app settings
app_settings = get_app_settings_fn();
--app_settings = get_app_settings_fn();
-- Gather user settings
user_settings := get_user_settings_from_metadata_fn(metadata_rec.id::INTEGER);
--user_settings := get_user_settings_from_clientid_fn((metadata_rec.client_id::INTEGER, );
RAISE NOTICE '-> debug monitor_online get_user_settings_from_metadata_fn [%]', user_settings;
user_settings := get_user_settings_from_vesselid_fn(metadata_rec.vessel_id::TEXT);
RAISE DEBUG '-> DEBUG cron_process_monitor_online_fn get_user_settings_from_vesselid_fn [%]', user_settings;
-- Send notification
--PERFORM send_notification_fn('monitor_online'::TEXT, metadata_rec::RECORD);
PERFORM send_email_py_fn('monitor_online'::TEXT, user_settings::JSONB, app_settings::JSONB);
PERFORM send_notification_fn('monitor_online'::TEXT, user_settings::JSONB);
--PERFORM send_email_py_fn('monitor_online'::TEXT, user_settings::JSONB, app_settings::JSONB);
--PERFORM send_pushover_py_fn('monitor_online'::TEXT, user_settings::JSONB, app_settings::JSONB);
-- update process_queue entry as processed
UPDATE process_queue
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> updated process_queue table [%]', process_rec.id;
RAISE NOTICE '-> cron_process_monitor_online_fn updated process_queue table [%]', process_rec.id;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
COMMENT ON FUNCTION
public.cron_process_monitor_online_fn
IS 'init by pg_cron to monitor back online pending notification, if so perform send_email or send_pushover base on user preferences';
@@ -213,14 +261,43 @@ begin
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> updated process_queue table [%]', process_rec.id;
RAISE NOTICE '-> cron_process_new_account_fn updated process_queue table [%]', process_rec.id;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
COMMENT ON FUNCTION
public.cron_process_new_account_fn
IS 'init by pg_cron to check for new account pending update, if so perform process_account_queue_fn';
IS 'deprecated, init by pg_cron to check for new account pending update, if so perform process_account_queue_fn';
-- CRON for new account pending otp validation notification
CREATE FUNCTION cron_process_new_account_otp_validation_fn() RETURNS void AS $$
declare
process_rec record;
begin
-- Check for new account pending update
RAISE NOTICE 'cron_process_new_account_otp_validation_fn';
FOR process_rec in
SELECT * from process_queue
where channel = 'new_account_otp' and processed is null
order by stored asc
LOOP
RAISE NOTICE '-> cron_process_new_account_otp_validation_fn [%]', process_rec.payload;
-- update account
PERFORM process_account_otp_validation_queue_fn(process_rec.payload::TEXT);
-- update process_queue entry as processed
UPDATE process_queue
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> cron_process_new_account_otp_validation_fn updated process_queue table [%]', process_rec.id;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_new_account_otp_validation_fn
IS 'deprecated, init by pg_cron to check for new account otp pending update, if so perform process_account_otp_validation_queue_fn';
-- CRON for new vessel pending notification
CREATE FUNCTION cron_process_new_vessel_fn() RETURNS void AS $$
@@ -242,21 +319,191 @@ begin
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> updated process_queue table [%]', process_rec.id;
RAISE NOTICE '-> cron_process_new_vessel_fn updated process_queue table [%]', process_rec.id;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_new_vessel_fn
IS 'init by pg_cron to check for new vessel pending update, if so perform process_vessel_queue_fn';
IS 'deprecated, init by pg_cron to check for new vessel pending update, if so perform process_vessel_queue_fn';
-- CRON for new event notification
CREATE FUNCTION cron_process_new_notification_fn() RETURNS void AS $$
declare
process_rec record;
begin
-- Check for new event notification pending update
RAISE NOTICE 'cron_process_new_notification_fn';
FOR process_rec in
SELECT * FROM process_queue
WHERE
(channel = 'new_account' OR channel = 'new_vessel' OR channel = 'email_otp')
and processed is null
order by stored asc
LOOP
RAISE NOTICE '-> cron_process_new_notification_fn for [%]', process_rec.payload;
-- process_notification_queue
PERFORM process_notification_queue_fn(process_rec.payload::TEXT, process_rec.channel::TEXT);
-- update process_queue entry as processed
UPDATE process_queue
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> cron_process_new_notification_fn updated process_queue table [%]', process_rec.id;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_new_notification_fn
IS 'init by pg_cron to check for new event pending notifications, if so perform process_notification_queue_fn';
-- CRON for new vessel metadata pending grafana provisioning
CREATE FUNCTION cron_process_grafana_fn() RETURNS void AS $$
DECLARE
process_rec record;
data_rec record;
app_settings jsonb;
user_settings jsonb;
BEGIN
-- We run grafana provisioning only after the first received vessel metadata
-- Check for new vessel metadata pending grafana provisioning
RAISE NOTICE 'cron_process_grafana_fn';
FOR process_rec in
SELECT * from process_queue
where channel = 'grafana' and processed is null
order by stored asc
LOOP
RAISE NOTICE '-> cron_process_grafana_fn [%]', process_rec.payload;
-- Gather url from app settings
app_settings := get_app_settings_fn();
-- Get vessel details base on metadata id
SELECT * INTO data_rec
FROM api.metadata m, auth.vessels v
WHERE m.id = process_rec.payload::INTEGER
AND m.vessel_id = v.vessel_id;
-- as we got data from the vessel we can do the grafana provisioning.
PERFORM grafana_py_fn(data_rec.name, data_rec.vessel_id, data_rec.owner_email, app_settings);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(data_rec.vessel_id::TEXT);
--RAISE DEBUG '-> DEBUG cron_process_grafana_fn get_user_settings_from_vesselid_fn [%]', user_settings;
-- add user in keycloak
PERFORM keycloak_auth_py_fn(data_rec.vessel_id, user_settings, app_settings);
-- Send notification
PERFORM send_notification_fn('grafana'::TEXT, user_settings::JSONB);
-- update process_queue entry as processed
UPDATE process_queue
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> cron_process_grafana_fn updated process_queue table [%]', process_rec.id;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_grafana_fn
IS 'init by pg_cron to check for new vessel pending grafana provisioning, if so perform grafana_py_fn';
CREATE OR REPLACE FUNCTION public.cron_process_windy_fn() RETURNS void AS $$
DECLARE
windy_rec record;
default_last_metric TIMESTAMPTZ := NOW() - interval '1 day';
last_metric TIMESTAMPTZ;
metric_rec record;
windy_metric jsonb;
app_settings jsonb;
user_settings jsonb;
windy_pws jsonb;
BEGIN
-- Check for new observations pending update
RAISE NOTICE 'cron_windy_fn';
-- Gather url from app settings
app_settings := get_app_settings_fn();
-- Find users with Windy active and with an active vessel
-- Map account id to Windy Station ID
FOR windy_rec in
SELECT
a.id,a.email,v.vessel_id,v.name,
COALESCE((a.preferences->'windy_last_metric')::TEXT, default_last_metric::TEXT) as last_metric
FROM auth.accounts a
LEFT JOIN auth.vessels AS v ON v.owner_email = a.email
LEFT JOIN api.metadata AS m ON m.vessel_id = v.vessel_id
WHERE (a.preferences->'public_windy')::boolean = True
AND m.active = True
LOOP
RAISE NOTICE '-> cron_windy_fn for [%]', windy_rec;
PERFORM set_config('vessel.id', windy_rec.vessel_id, false);
--RAISE WARNING 'public.cron_process_windy_rec_fn() scheduler vessel.id %, user.id', current_setting('vessel.id', false), current_setting('user.id', false);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(windy_rec.vessel_id::TEXT);
RAISE NOTICE '-> cron_windy_fn checking user_settings [%]', user_settings;
-- Get all metrics from the last windy_last_metric avg by 5 minutes
-- TODO json_agg to send all data in once, but issue with py jsonb transformation decimal.
FOR metric_rec in
SELECT time_bucket('5 minutes', m.time) AS time_bucket,
avg((m.metrics->'environment.outside.temperature')::numeric) AS temperature,
avg((m.metrics->'environment.outside.pressure')::numeric) AS pressure,
avg((m.metrics->'environment.outside.relativeHumidity')::numeric) AS rh,
avg((m.metrics->'environment.wind.directionTrue')::numeric) AS winddir,
avg((m.metrics->'environment.wind.speedTrue')::numeric) AS wind,
max((m.metrics->'environment.wind.speedTrue')::numeric) AS gust,
last(latitude, time) AS lat,
last(longitude, time) AS lng
FROM api.metrics m
WHERE vessel_id = windy_rec.vessel_id
AND m.time >= windy_rec.last_metric::TIMESTAMPTZ
GROUP BY time_bucket
ORDER BY time_bucket ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_windy_fn checking metrics [%]', metric_rec;
-- https://community.windy.com/topic/8168/report-your-weather-station-data-to-windy
-- temp from kelvin to celcuis
-- winddir from radiant to degres
-- rh from ratio to percentage
SELECT jsonb_build_object(
'dateutc', metric_rec.time_bucket,
'station', windy_rec.id,
'name', windy_rec.name,
'lat', metric_rec.lat,
'lon', metric_rec.lng,
'wind', metric_rec.wind,
'gust', metric_rec.gust,
'pressure', metric_rec.pressure,
'winddir', radiantToDegrees(metric_rec.winddir::numeric),
'temp', kelvinToCel(metric_rec.temperature::numeric),
'rh', valToPercent(metric_rec.rh::numeric)
) INTO windy_metric;
RAISE NOTICE '-> cron_windy_fn checking windy_metrics [%]', windy_metric;
SELECT windy_pws_py_fn(windy_metric, user_settings, app_settings) into windy_pws;
RAISE NOTICE '-> cron_windy_fn Windy PWS [%]', ((windy_pws->'header')::JSONB ? 'id');
IF NOT((user_settings->'settings')::JSONB ? 'windy') and ((windy_pws->'header')::JSONB ? 'id') then
RAISE NOTICE '-> cron_windy_fn new Windy PWS [%]', (windy_pws->'header')::JSONB->>'id';
-- Send metrics to Windy
PERFORM api.update_user_preferences_fn('{windy}'::TEXT, ((windy_pws->'header')::JSONB->>'id')::TEXT);
-- Send notification
PERFORM send_notification_fn('windy'::TEXT, user_settings::JSONB);
END IF;
-- Record last metrics time
SELECT metric_rec.time_bucket INTO last_metric;
END LOOP;
PERFORM api.update_user_preferences_fn('{windy_last_metric}'::TEXT, last_metric::TEXT);
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_windy_fn
IS 'init by pg_cron to create (or update) station and uploading observations to Windy Personal Weather Station observations';
-- CRON for Vacuum database
CREATE FUNCTION cron_vaccum_fn() RETURNS void AS $$
CREATE FUNCTION cron_vacuum_fn() RETURNS void AS $$
-- ERROR: VACUUM cannot be executed from a function
declare
begin
-- Vacuum
RAISE NOTICE 'cron_vaccum_fn';
RAISE NOTICE 'cron_vacuum_fn';
VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) api.logbook;
VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) api.stays;
VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) api.moorages;
@@ -266,5 +513,480 @@ END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_vaccum_fn
IS 'init by pg_cron to full vaccum tables on schema api';
public.cron_vacuum_fn
IS 'init by pg_cron to full vacuum tables on schema api';
-- CRON for alerts notification
CREATE OR REPLACE FUNCTION public.cron_alerts_fn() RETURNS void AS $$
DECLARE
alert_rec record;
default_last_metric TIMESTAMPTZ := NOW() - interval '1 day';
last_metric TIMESTAMPTZ;
metric_rec record;
app_settings JSONB;
user_settings JSONB;
alerting JSONB;
_alarms JSONB;
alarms TEXT;
alert_default JSONB := '{
"low_pressure_threshold": 990,
"high_wind_speed_threshold": 30,
"low_water_depth_threshold": 1,
"min_notification_interval": 6,
"high_pressure_drop_threshold": 12,
"low_battery_charge_threshold": 90,
"low_battery_voltage_threshold": 12.5,
"low_water_temperature_threshold": 10,
"low_indoor_temperature_threshold": 7,
"low_outdoor_temperature_threshold": 3
}';
BEGIN
-- Check for new event notification pending update
RAISE NOTICE 'cron_alerts_fn';
FOR alert_rec in
SELECT
a.user_id,a.email,v.vessel_id,
COALESCE((a.preferences->'alert_last_metric')::TEXT, default_last_metric::TEXT) as last_metric,
(alert_default || (a.preferences->'alerting')::JSONB) as alerting,
(a.preferences->'alarms')::JSONB as alarms
FROM auth.accounts a
LEFT JOIN auth.vessels AS v ON v.owner_email = a.email
LEFT JOIN api.metadata AS m ON m.vessel_id = v.vessel_id
WHERE (a.preferences->'alerting'->'enabled')::boolean = True
AND m.active = True
LOOP
RAISE NOTICE '-> cron_alerts_fn for [%]', alert_rec;
PERFORM set_config('vessel.id', alert_rec.vessel_id, false);
PERFORM set_config('user.email', alert_rec.email, false);
--RAISE WARNING 'public.cron_process_alert_rec_fn() scheduler vessel.id %, user.id', current_setting('vessel.id', false), current_setting('user.id', false);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(alert_rec.vessel_id::TEXT);
RAISE NOTICE '-> cron_alerts_fn checking user_settings [%]', user_settings;
-- Get all metrics from the last last_metric avg by 5 minutes
FOR metric_rec in
SELECT time_bucket('5 minutes', m.time) AS time_bucket,
avg((m.metrics->'environment.inside.temperature')::numeric) AS intemp,
avg((m.metrics->'environment.outside.temperature')::numeric) AS outtemp,
avg((m.metrics->'environment.water.temperature')::numeric) AS wattemp,
avg((m.metrics->'environment.depth.belowTransducer')::numeric) AS watdepth,
avg((m.metrics->'environment.outside.pressure')::numeric) AS pressure,
avg((m.metrics->'environment.wind.speedTrue')::numeric) AS wind,
avg((m.metrics->'electrical.batteries.House.voltage')::numeric) AS voltage,
avg((m.metrics->'electrical.batteries.House.capacity.stateOfCharge')::numeric) AS charge
FROM api.metrics m
WHERE vessel_id = alert_rec.vessel_id
AND m.time >= alert_rec.last_metric::TIMESTAMPTZ
GROUP BY time_bucket
ORDER BY time_bucket ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_alerts_fn checking metrics [%]', metric_rec;
RAISE NOTICE '-> cron_alerts_fn checking alerting [%]', alert_rec.alerting;
--RAISE NOTICE '-> cron_alerts_fn checking debug [%] [%]', kelvinToCel(metric_rec.intemp), (alert_rec.alerting->'low_indoor_temperature_threshold');
IF kelvinToCel(metric_rec.intemp) < (alert_rec.alerting->'low_indoor_temperature_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_indoor_temperature_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_indoor_temperature_threshold'->>'date') IS NULL) OR
(((_alarms->'low_indoor_temperature_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_indoor_temperature_threshold": {"value": '|| kelvinToCel(metric_rec.intemp) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_outdoor_temperature_threshold value:'|| kelvinToCel(metric_rec.intemp) ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_indoor_temperature_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_indoor_temperature_threshold';
END IF;
IF kelvinToCel(metric_rec.outtemp) < (alert_rec.alerting->'low_outdoor_temperature_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_outdoor_temperature_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_outdoor_temperature_threshold'->>'date') IS NULL) OR
(((_alarms->'low_outdoor_temperature_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_outdoor_temperature_threshold": {"value": '|| kelvinToCel(metric_rec.outtemp) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_outdoor_temperature_threshold value:'|| kelvinToCel(metric_rec.outtemp) ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_outdoor_temperature_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_outdoor_temperature_threshold';
END IF;
IF kelvinToCel(metric_rec.wattemp) < (alert_rec.alerting->'low_water_temperature_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_water_temperature_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_water_temperature_threshold'->>'date') IS NULL) OR
(((_alarms->'low_water_temperature_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_water_temperature_threshold": {"value": '|| kelvinToCel(metric_rec.wattemp) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_water_temperature_threshold value:'|| kelvinToCel(metric_rec.wattemp) ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_temperature_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_temperature_threshold';
END IF;
IF metric_rec.watdepth < (alert_rec.alerting->'low_water_depth_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_water_depth_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_water_depth_threshold'->>'date') IS NULL) OR
(((_alarms->'low_water_depth_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_water_depth_threshold": {"value": '|| metric_rec.watdepth ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_water_depth_threshold value:'|| metric_rec.watdepth ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_depth_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_depth_threshold';
END IF;
if metric_rec.pressure < (alert_rec.alerting->'high_pressure_drop_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'high_pressure_drop_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'high_pressure_drop_threshold'->>'date') IS NULL) OR
(((_alarms->'high_pressure_drop_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"high_pressure_drop_threshold": {"value": '|| metric_rec.pressure ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "high_pressure_drop_threshold value:'|| metric_rec.pressure ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug high_pressure_drop_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug high_pressure_drop_threshold';
END IF;
IF metric_rec.wind > (alert_rec.alerting->'high_wind_speed_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'high_wind_speed_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'high_wind_speed_threshold'->>'date') IS NULL) OR
(((_alarms->'high_wind_speed_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"high_wind_speed_threshold": {"value": '|| metric_rec.wind ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "high_wind_speed_threshold value:'|| metric_rec.wind ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug high_wind_speed_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug high_wind_speed_threshold';
END IF;
if metric_rec.voltage < (alert_rec.alerting->'low_battery_voltage_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_battery_voltage_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = 'lacroix.francois@gmail.com';
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_battery_voltage_threshold'->>'date') IS NULL) OR
(((_alarms->'low_battery_voltage_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_battery_voltage_threshold": {"value": '|| metric_rec.voltage ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_battery_voltage_threshold value:'|| metric_rec.voltage ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_voltage_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_voltage_threshold';
END IF;
if (metric_rec.charge*100) < (alert_rec.alerting->'low_battery_charge_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_battery_charge_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_battery_charge_threshold'->>'date') IS NULL) OR
(((_alarms->'low_battery_charge_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_battery_charge_threshold": {"value": '|| (metric_rec.charge*100) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_battery_charge_threshold value:'|| (metric_rec.charge*100) ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_charge_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_charge_threshold';
END IF;
-- Record last metrics time
SELECT metric_rec.time_bucket INTO last_metric;
END LOOP;
PERFORM api.update_user_preferences_fn('{alert_last_metric}'::TEXT, last_metric::TEXT);
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_alerts_fn
IS 'init by pg_cron to check for alerts';
-- CRON for no vessel notification
CREATE FUNCTION cron_process_no_vessel_fn() RETURNS void AS $no_vessel$
DECLARE
no_vessel record;
user_settings jsonb;
BEGIN
-- Check for user with no vessel register
RAISE NOTICE 'cron_process_no_vessel_fn';
FOR no_vessel in
SELECT a.user_id,a.email,a.first
FROM auth.accounts a
WHERE NOT EXISTS (
SELECT *
FROM auth.vessels v
WHERE v.owner_email = a.email)
LOOP
RAISE NOTICE '-> cron_process_no_vessel_rec_fn for [%]', no_vessel;
SELECT json_build_object('email', no_vessel.email, 'recipient', no_vessel.first) into user_settings;
RAISE NOTICE '-> debug cron_process_no_vessel_rec_fn [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('no_vessel'::TEXT, user_settings::JSONB);
END LOOP;
END;
$no_vessel$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_no_vessel_fn
IS 'init by pg_cron, check for user with no vessel register then send notification';
-- CRON for no metadata notification
CREATE FUNCTION cron_process_no_metadata_fn() RETURNS void AS $no_metadata$
DECLARE
no_metadata_rec record;
user_settings jsonb;
BEGIN
-- Check for vessel register but with no metadata
RAISE NOTICE 'cron_process_no_metadata_fn';
FOR no_metadata_rec in
SELECT
a.user_id,a.email,a.first
FROM auth.accounts a, auth.vessels v
WHERE NOT EXISTS (
SELECT *
FROM api.metadata m
WHERE v.vessel_id = m.vessel_id) AND v.owner_email = a.email
LOOP
RAISE NOTICE '-> cron_process_no_metadata_rec_fn for [%]', no_metadata_rec;
SELECT json_build_object('email', no_metadata_rec.email, 'recipient', no_metadata_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_no_metadata_rec_fn [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('no_metadata'::TEXT, user_settings::JSONB);
END LOOP;
END;
$no_metadata$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_no_metadata_fn
IS 'init by pg_cron, check for vessel with no metadata then send notification';
-- CRON for no activity notification
CREATE FUNCTION cron_process_no_activity_fn() RETURNS void AS $no_activity$
DECLARE
no_activity_rec record;
user_settings jsonb;
BEGIN
-- Check for vessel with no activity for more than 230 days
RAISE NOTICE 'cron_process_no_activity_fn';
FOR no_activity_rec in
SELECT
v.owner_email,m.name,m.vessel_id,m.time,a.first
FROM auth.accounts a
LEFT JOIN auth.vessels v ON v.owner_email = a.email
LEFT JOIN api.metadata m ON v.vessel_id = m.vessel_id
WHERE m.time < NOW() AT TIME ZONE 'UTC' - INTERVAL '230 DAYS'
LOOP
RAISE NOTICE '-> cron_process_no_activity_rec_fn for [%]', no_activity_rec;
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_no_activity_rec_fn [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('no_activity'::TEXT, user_settings::JSONB);
END LOOP;
END;
$no_activity$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_no_activity_fn
IS 'init by pg_cron, check for vessel with no activity for more than 230 days then send notification';
-- CRON for deactivated/deletion
CREATE FUNCTION cron_process_deactivated_fn() RETURNS void AS $deactivated$
DECLARE
no_activity_rec record;
user_settings jsonb;
BEGIN
RAISE NOTICE 'cron_process_deactivated_fn';
-- List accounts with vessel inactivity for more than 1 YEAR
FOR no_activity_rec in
SELECT
v.owner_email,m.name,m.vessel_id,m.time,a.first
FROM auth.accounts a
LEFT JOIN auth.vessels v ON v.owner_email = a.email
LEFT JOIN api.metadata m ON v.vessel_id = m.vessel_id
WHERE m.time < NOW() AT TIME ZONE 'UTC' - INTERVAL '1 YEAR'
LOOP
RAISE NOTICE '-> cron_process_deactivated_rec_fn for inactivity [%]', no_activity_rec;
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_deactivated_rec_fn inactivity [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('deactivated'::TEXT, user_settings::JSONB);
--PERFORM public.delete_account_fn(no_activity_rec.owner_email::TEXT, no_activity_rec.vessel_id::TEXT);
END LOOP;
-- List accounts with no vessel metadata for more than 1 YEAR
FOR no_activity_rec in
SELECT
a.user_id,a.email,a.first,a.created_at
FROM auth.accounts a, auth.vessels v
WHERE NOT EXISTS (
SELECT *
FROM api.metadata m
WHERE v.vessel_id = m.vessel_id) AND v.owner_email = a.email
AND v.created_at < NOW() AT TIME ZONE 'UTC' - INTERVAL '1 YEAR'
LOOP
RAISE NOTICE '-> cron_process_deactivated_rec_fn for no metadata [%]', no_activity_rec;
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_deactivated_rec_fn no metadata [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('deactivated'::TEXT, user_settings::JSONB);
--PERFORM public.delete_account_fn(no_activity_rec.owner_email::TEXT, no_activity_rec.vessel_id::TEXT);
END LOOP;
-- List accounts with no vessel created for more than 1 YEAR
FOR no_activity_rec in
SELECT a.user_id,a.email,a.first,a.created_at
FROM auth.accounts a
WHERE NOT EXISTS (
SELECT *
FROM auth.vessels v
WHERE v.owner_email = a.email)
AND a.created_at < NOW() AT TIME ZONE 'UTC' - INTERVAL '1 YEAR'
LOOP
RAISE NOTICE '-> cron_process_deactivated_rec_fn for no vessel [%]', no_activity_rec;
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_deactivated_rec_fn no vessel [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('deactivated'::TEXT, user_settings::JSONB);
--PERFORM public.delete_account_fn(no_activity_rec.owner_email::TEXT, no_activity_rec.vessel_id::TEXT);
END LOOP;
END;
$deactivated$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_deactivated_fn
IS 'init by pg_cron, check for vessel with no activity for more than 1 year then send notification and delete data';
-- Need to be in the postgres database.
\c postgres
-- CRON for clean up job details logs
CREATE FUNCTION public.job_run_details_cleanup_fn() RETURNS void AS $$
DECLARE
BEGIN
-- Remove job run log older than 3 months
RAISE NOTICE 'job_run_details_cleanup_fn';
DELETE FROM cron.job_run_details
WHERE start_time <= NOW() AT TIME ZONE 'UTC' - INTERVAL '91 DAYS';
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.job_run_details_cleanup_fn
IS 'init by pg_cron to cleanup job_run_details table on schema public postgres db';

View File

@@ -0,0 +1,937 @@
---------------------------------------------------------------------------
-- singalk db public schema tables
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
CREATE SCHEMA IF NOT EXISTS public;
COMMENT ON SCHEMA public IS 'backend public functions and tables';
---------------------------------------------------------------------------
-- Table geocoders
--
-- https://github.com/CartoDB/labs-postgresql/blob/master/workshop/plpython.md
--
CREATE TABLE IF NOT EXISTS geocoders(
name TEXT UNIQUE,
url TEXT,
reverse_url TEXT
);
-- Description
COMMENT ON TABLE
public.geocoders
IS 'geo service nominatim url';
INSERT INTO geocoders VALUES
('nominatim',
NULL,
'https://nominatim.openstreetmap.org/reverse');
-- https://photon.komoot.io/reverse?lat=48.30587233333333&lon=14.3040525
-- https://docs.mapbox.com/playground/geocoding/?search_text=-3.1457869856990897,51.35921326434686&limit=1
---------------------------------------------------------------------------
-- Tables for message template email/pushover/telegram
--
DROP TABLE IF EXISTS public.email_templates;
CREATE TABLE IF NOT EXISTS public.email_templates(
name TEXT UNIQUE,
email_subject TEXT,
email_content TEXT,
pushover_title TEXT,
pushover_message TEXT
);
-- Description
COMMENT ON TABLE
public.email_templates
IS 'email/message templates for notifications';
-- with escape value, eg: E'A\nB\r\nC'
-- https://stackoverflow.com/questions/26638615/insert-line-break-in-postgresql-when-updating-text-field
-- TODO Update notification subject for log entry to 'logbook #NB ...'
INSERT INTO public.email_templates VALUES
('logbook',
'New Logbook Entry',
E'Hello __RECIPIENT__,\n\nWe just wanted to let you know that you have a new entry on openplotter.cloud: "__LOGBOOK_NAME__"\r\n\r\nSee more details at __APP_URL__/log/__LOGBOOK_LINK__\n\nHappy sailing!\nThe PostgSail Team',
'New Logbook Entry',
E'New entry on openplotter.cloud: "__LOGBOOK_NAME__"\r\nSee more details at __APP_URL__/log/__LOGBOOK_LINK__\n'),
('new_account',
'Welcome',
E'Hello __RECIPIENT__,\nCongratulations!\nYou successfully created an account.\nKeep in mind to register your vessel.\nHappy sailing!',
'Welcome',
E'Hi!\nYou successfully created an account\nKeep in mind to register your vessel.\n'),
('new_vessel',
'New boat',
E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.\n',
'New boat',
E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.\n'),
('monitor_offline',
'Boat went Offline',
E'__BOAT__ has been offline for more than an hour\r\nFind more details at __APP_URL__/boats\n',
'Boat went Offline',
E'__BOAT__ has been offline for more than an hour\r\nFind more details at __APP_URL__/boats\n'),
('monitor_online',
'Boat went Online',
E'__BOAT__ just came online\nFind more details at __APP_URL__/boats\n',
'Boat went Online',
E'__BOAT__ just came online\nFind more details at __APP_URL__/boats\n'),
('new_badge',
'New Badge!',
E'Hello __RECIPIENT__,\nCongratulations! You have just unlocked a new badge: __BADGE_NAME__\nSee more details at __APP_URL__/badges\nHappy sailing!\nThe PostgSail Team',
'New Badge!',
E'Congratulations!\nYou have just unlocked a new badge: __BADGE_NAME__\nSee more details at __APP_URL__/badges\n'),
('pushover_valid',
'Pushover integration',
E'Hello __RECIPIENT__,\nCongratulations! You have just connect your account to Pushover.\n\nThe PostgSail Team',
'Pushover integration!',
E'Congratulations!\nYou have just connect your account to Pushover.\n'),
('email_otp',
'Email verification',
E'Hello,\nPlease active your account using the following code: __OTP_CODE__.\nThe code is valid 15 minutes.\nThe PostgSail Team',
'Email verification',
E'Congratulations!\nPlease validate your account. Check your email!'),
('email_valid',
'Email verified',
E'Hello,\nCongratulations!\nYou successfully validate your account.\nThe PostgSail Team',
'Email verified',
E'Hi!\nYou successfully validate your account.\n'),
('email_reset',
'Password reset',
E'Hello,\nYou requested a password reset. To reset your password __APP_URL__/reset-password?__RESET_QS__.\nThe PostgSail Team',
'Password reset',
E'You requested a password recovery. Check your email!\n'),
('telegram_otp',
'Telegram bot',
E'Hello,\nTo connect your account to a @postgsail_bot. Please type this verification code __OTP_CODE__ back to the bot.\nThe code is valid 15 minutes.\nFrancois',
'Telegram bot',
E'Hello,\nTo connect your account to a @postgsail_bot. Check your email!\n'),
('telegram_valid',
'Telegram bot',
E'Hello __RECIPIENT__,\nCongratulations! You have just connect your account to your vessel, @postgsail_bot.\nFrancois',
'Telegram bot!',
E'Congratulations!\nYou have just connect your account to your vessel, @postgsail_bot.\n'),
('no_vessel',
'PostgSail add your boat',
E'Hello __RECIPIENT__,\nYou created an account on PostgSail but you have not added your boat yet.\nIf you need any assistance, I would be happy to help. It is free and an open-source.\nFrancois',
'PostgSail next step',
E'Hello,\nYou should create your vessel. Check your email!\n'),
('no_metadata',
'PostgSail connect your boat',
E'Hello __RECIPIENT__,\nYou created an account on PostgSail but you have not connected your boat yet.\nIf you need any assistance, I would be happy to help. It is free and an open-source.\nFrancois',
'PostgSail next step',
E'Hello,\nYou should connect your vessel. Check your email!\n'),
('no_activity',
'PostgSail boat inactivity',
E'Hello __RECIPIENT__,\nWe don\'t see any activity on your account, do you need any assistance?\nIf you need any assistance, I would be happy to help. It is free and an open-source.\nFrancois.',
'PostgSail inactivity!',
E'We detected inactivity. Check your email!\n'),
('deactivated',
'PostgSail account deactivated',
E'Hello __RECIPIENT__,\nYour account has been deactivated and all your data has been removed from PostgSail system.',
'PostgSail deactivated!',
E'We removed your account. Check your email!\n'),
('grafana',
'PostgSail Grafana integration',
E'Hello __RECIPIENT__,\nCongratulations! You unlocked Grafana dashboard.\nSee more details at https://app.openplotter.cloud\nHappy sailing!\nFrancois',
'PostgSail Grafana!',
E'Congratulations!\nYou unlocked Grafana dashboard.\nSee more details at https://app.openplotter.cloud\n'),
('windy',
'PostgSail Windy Weather station',
E'Hello __RECIPIENT__,\nCongratulations! Your boat is now a Windy Weather station.\nSee more details at __APP_URL__/windy\nHappy sailing!\nFrancois',
'PostgSail Windy!',
E'Congratulations!\nYour boat is now a Windy Weather station.\nSee more details at __APP_URL__/windy\n'),
('alert',
'PostgSail Alert',
E'Hello __RECIPIENT__,\nWe detected an alert __ALERT__.\nSee more details at __APP_URL__\nStay safe.\nFrancois',
'PostgSail Alert!',
E'We detected an alert __ALERT__.\n');
---------------------------------------------------------------------------
-- Queue handling
--
-- https://gist.github.com/kissgyorgy/beccba1291de962702ea9c237a900c79
-- https://www.depesz.com/2012/06/13/how-to-send-mail-from-database/
-- Listen/Notify way
--create function new_logbook_entry() returns trigger as $$
--begin
-- perform pg_notify('new_logbook_entry', NEW.id::text);
-- return NEW;
--END;
--$$ language plpgsql;
-- table way
CREATE TABLE IF NOT EXISTS public.process_queue (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
channel TEXT NOT NULL,
payload TEXT NOT NULL,
ref_id TEXT NOT NULL,
stored TIMESTAMPTZ NOT NULL,
processed TIMESTAMPTZ DEFAULT NULL
);
-- Description
COMMENT ON TABLE
public.process_queue
IS 'process queue for async job';
-- Index
CREATE INDEX ON public.process_queue (channel);
CREATE INDEX ON public.process_queue (stored);
CREATE INDEX ON public.process_queue (processed);
COMMENT ON COLUMN public.process_queue.ref_id IS 'either user_id or vessel_id';
-- Function process_queue helpers
create function new_account_entry_fn() returns trigger as $new_account_entry$
begin
insert into process_queue (channel, payload, stored, ref_id) values ('new_account', NEW.email, now(), NEW.user_id);
return NEW;
END;
$new_account_entry$ language plpgsql;
create function new_account_otp_validation_entry_fn() returns trigger as $new_account_otp_validation_entry$
begin
-- Add email_otp check only if not from oauth server
if (NEW.preferences->>'email_verified')::boolean IS NOT True then
insert into process_queue (channel, payload, stored, ref_id) values ('email_otp', NEW.email, now(), NEW.user_id);
end if;
return NEW;
END;
$new_account_otp_validation_entry$ language plpgsql;
create function new_vessel_entry_fn() returns trigger as $new_vessel_entry$
begin
insert into process_queue (channel, payload, stored, ref_id) values ('new_vessel', NEW.owner_email, now(), NEW.vessel_id);
return NEW;
END;
$new_vessel_entry$ language plpgsql;
create function new_vessel_public_fn() returns trigger as $new_vessel_public$
begin
-- Update user settings with a public vessel name
perform api.update_user_preferences_fn('{public_vessel}', regexp_replace(NEW.name, '\W+', '', 'g'));
return NEW;
END;
$new_vessel_public$ language plpgsql;
---------------------------------------------------------------------------
-- Tables Application Settings
-- https://dba.stackexchange.com/questions/27296/storing-application-settings-with-different-datatypes#27297
-- https://stackoverflow.com/questions/6893780/how-to-store-site-wide-settings-in-a-database
-- http://cvs.savannah.gnu.org/viewvc/*checkout*/gnumed/gnumed/gnumed/server/sql/gmconfiguration.sql
CREATE TABLE IF NOT EXISTS public.app_settings (
name TEXT NOT NULL UNIQUE,
value TEXT NOT NULL
);
-- Description
COMMENT ON TABLE public.app_settings IS 'application settings';
COMMENT ON COLUMN public.app_settings.name IS 'application settings name key';
COMMENT ON COLUMN public.app_settings.value IS 'application settings value';
---------------------------------------------------------------------------
-- Badges description
--
DROP TABLE IF EXISTS public.badges;
CREATE TABLE IF NOT EXISTS public.badges(
name TEXT UNIQUE,
description TEXT
);
-- Description
COMMENT ON TABLE
public.badges
IS 'Badges descriptions';
INSERT INTO badges VALUES
('Helmsman',
'Nice work logging your first sail! You are officially a helmsman now!'),
('Wake Maker',
'Yowzers! Welcome to the 15 knot+ club ya speed demon skipper!'),
('Explorer',
'It looks like home is where the helm is. Cheers to 10 days away from home port!'),
('Mooring Pro',
'It takes a lot of skill to "thread that floating needle" but seems like you have mastered mooring with 10 nights on buoy!'),
('Anchormaster',
'Hook, line and sinker, you have this anchoring thing down! 25 days on the hook for you!'),
('Traveler todo',
'Who needs to fly when one can sail! You are an international sailor. À votre santé!'),
('Stormtrooper',
'Just like the elite defenders of the Empire, here you are, our braving your own hydro-empire in windspeeds above 30kts. Nice work trooper! '),
('Club Alaska',
'Home to the bears, glaciers, midnight sun and high adventure. Welcome to the Club Alaska Captain!'),
('Tropical Traveler',
'Look at you with your suntan, tropical drink and southern latitude!'),
('Aloha Award',
'Ticking off over 2300 NM across the great blue Pacific makes you the rare recipient of the Aloha Award. Well done and Aloha sailor!'),
('Navigator Award',
'Woohoo! You made it, Ticking off over 100NM in one go, well done sailor!'),
('Captain Award',
'Congratulation, you reach over 1000NM, well done sailor!');
---------------------------------------------------------------------------
-- aistypes description
--
DROP TABLE IF EXISTS public.aistypes;
CREATE TABLE IF NOT EXISTS aistypes(
id NUMERIC UNIQUE,
description TEXT
);
-- Description
COMMENT ON TABLE
public.aistypes
IS 'aistypes AIS Ship Types, https://api.vesselfinder.com/docs/ref-aistypes.html';
INSERT INTO aistypes VALUES
(0, 'Not available (default)'),
(20, 'Wing in ground (WIG), all ships of this type'),
(21, 'Wing in ground (WIG), Hazardous category A'),
(22, 'Wing in ground (WIG), Hazardous category B'),
(23, 'Wing in ground (WIG), Hazardous category C'),
(24, 'Wing in ground (WIG), Hazardous category D'),
(25, 'Wing in ground (WIG), Reserved for future use'),
(26, 'Wing in ground (WIG), Reserved for future use'),
(27, 'Wing in ground (WIG), Reserved for future use'),
(28, 'Wing in ground (WIG), Reserved for future use'),
(29, 'Wing in ground (WIG), Reserved for future use'),
(30, 'Fishing'),
(31, 'Towing'),
(32, 'Towing: length exceeds 200m or breadth exceeds 25m'),
(33, 'Dredging or underwater ops'),
(34, 'Diving ops'),
(35, 'Military ops'),
(36, 'Sailing'),
(37, 'Pleasure Craft'),
(38, 'Reserved'),
(39, 'Reserved'),
(40, 'High speed craft (HSC), all ships of this type'),
(41, 'High speed craft (HSC), Hazardous category A'),
(42, 'High speed craft (HSC), Hazardous category B'),
(43, 'High speed craft (HSC), Hazardous category C'),
(44, 'High speed craft (HSC), Hazardous category D'),
(45, 'High speed craft (HSC), Reserved for future use'),
(46, 'High speed craft (HSC), Reserved for future use'),
(47, 'High speed craft (HSC), Reserved for future use'),
(48, 'High speed craft (HSC), Reserved for future use'),
(49, 'High speed craft (HSC), No additional information'),
(50, 'Pilot Vessel'),
(51, 'Search and Rescue vessel'),
(52, 'Tug'),
(53, 'Port Tender'),
(54, 'Anti-pollution equipment'),
(55, 'Law Enforcement'),
(56, 'Spare - Local Vessel'),
(57, 'Spare - Local Vessel'),
(58, 'Medical Transport'),
(59, 'Noncombatant ship according to RR Resolution No. 18'),
(60, 'Passenger, all ships of this type'),
(61, 'Passenger, Hazardous category A'),
(62, 'Passenger, Hazardous category B'),
(63, 'Passenger, Hazardous category C'),
(64, 'Passenger, Hazardous category D'),
(65, 'Passenger, Reserved for future use'),
(66, 'Passenger, Reserved for future use'),
(67, 'Passenger, Reserved for future use'),
(68, 'Passenger, Reserved for future use'),
(69, 'Passenger, No additional information'),
(70, 'Cargo, all ships of this type'),
(71, 'Cargo, Hazardous category A'),
(72, 'Cargo, Hazardous category B'),
(73, 'Cargo, Hazardous category C'),
(74, 'Cargo, Hazardous category D'),
(75, 'Cargo, Reserved for future use'),
(76, 'Cargo, Reserved for future use'),
(77, 'Cargo, Reserved for future use'),
(78, 'Cargo, Reserved for future use'),
(79, 'Cargo, No additional information'),
(80, 'Tanker, all ships of this type'),
(81, 'Tanker, Hazardous category A'),
(82, 'Tanker, Hazardous category B'),
(83, 'Tanker, Hazardous category C'),
(84, 'Tanker, Hazardous category D'),
(85, 'Tanker, Reserved for future use'),
(86, 'Tanker, Reserved for future use'),
(87, 'Tanker, Reserved for future use'),
(88, 'Tanker, Reserved for future use'),
(89, 'Tanker, No additional information'),
(90, 'Other Type, all ships of this type'),
(91, 'Other Type, Hazardous category A'),
(92, 'Other Type, Hazardous category B'),
(93, 'Other Type, Hazardous category C'),
(94, 'Other Type, Hazardous category D'),
(95, 'Other Type, Reserved for future use'),
(96, 'Other Type, Reserved for future use'),
(97, 'Other Type, Reserved for future use'),
(98, 'Other Type, Reserved for future use'),
(99, 'Other Type, no additional information');
---------------------------------------------------------------------------
-- MMSI MID Codes
--
DROP TABLE IF EXISTS public.mid;
CREATE TABLE IF NOT EXISTS public.mid(
country TEXT,
id NUMERIC UNIQUE,
country_id INTEGER
);
-- Description
COMMENT ON TABLE
public.mid
IS 'MMSI MID Codes (Maritime Mobile Service Identity) Filtered by Flag of Registration, https://www.marinevesseltraffic.com/2013/11/mmsi-mid-codes-by-flag.html';
INSERT INTO mid VALUES
('Adelie Land', 501, NULL),
('Afghanistan', 401, 4),
('Alaska', 303, 840),
('Albania', 201, 8),
('Algeria', 605, 12),
('American Samoa', 559, 16),
('Andorra', 202, 20),
('Angola', 603, 24),
('Anguilla', 301, 660),
('Antigua and Barbuda', 304, 28),
('Antigua and Barbuda', 305, 28),
('Argentina', 701, 32),
('Armenia', 216, 51),
('Aruba', 307, 533),
('Ascension Island', 608, NULL),
('Australia', 503, 36),
('Austria', 203, 40),
('Azerbaijan', 423, 31),
('Azores', 204, NULL),
('Bahamas', 308, 44),
('Bahamas', 309, 44),
('Bahamas', 311, 44),
('Bahrain', 408, 48),
('Bangladesh', 405, 50),
('Barbados', 314, 52),
('Belarus', 206, 112),
('Belgium', 205, 56),
('Belize', 312, 84),
('Benin', 610, 204),
('Bermuda', 310, 60),
('Bhutan', 410, 64),
('Bolivia', 720, 68),
('Bosnia and Herzegovina', 478, 70),
('Botswana', 611, 72),
('Brazil', 710, 76),
('British Virgin Islands', 378, 92),
('Brunei Darussalam', 508, 96),
('Bulgaria', 207, 100),
('Burkina Faso', 633, 854),
('Burundi', 609, 108),
('Cambodia', 514, 116),
('Cambodia', 515, 116),
('Cameroon', 613, 120),
('Canada', 316, 124),
('Cape Verde', 617, 132),
('Cayman Islands', 319, 136),
('Central African Republic', 612, 140),
('Chad', 670, 148),
('Chile', 725, 152),
('China', 412, 156),
('China', 413, 156),
('China', 414, 156),
('Christmas Island', 516, 162),
('Cocos Islands', 523, 166),
('Colombia', 730, 170),
('Comoros', 616, 174),
('Comoros', 620, 174),
('Congo', 615, 178),
('Cook Islands', 518, 184),
('Costa Rica', 321, 188),
(E'Côte d\'Ivoire', 619, 384),
('Croatia', 238, 191),
('Crozet Archipelago', 618, NULL),
('Cuba', 323, 192),
('Cyprus', 209, 196),
('Cyprus', 210, 196),
('Cyprus', 212, 196),
('Czech Republic', 270, 203),
('Denmark', 219, 208),
('Denmark', 220, 208),
('Djibouti', 621, 262),
('Dominica', 325, 212),
('Dominican Republic', 327, 214),
('DR Congo', 676, NULL),
('Ecuador', 735, 218),
('Egypt', 622, 818),
('El Salvador', 359, 222),
('Equatorial Guinea', 631, 226),
('Eritrea', 625, 232),
('Estonia', 276, 233),
('Ethiopia', 624, 231),
('Falkland Islands', 740, 234),
('Faroe Islands', 231, NULL),
('Fiji', 520, 242),
('Finland', 230, 246),
('France', 226, 250),
('France', 227, 250),
('France', 228, 250),
('French Polynesia', 546, 260),
('Gabonese Republic', 626, 266),
('Gambia', 629, 270),
('Georgia', 213, 268),
('Germany', 211, 276),
('Germany', 218, 276),
('Ghana', 627, 288),
('Gibraltar', 236, 292),
('Greece', 237, 300),
('Greece', 239, 300),
('Greece', 240, 300),
('Greece', 241, 300),
('Greenland', 331, 304),
('Grenada', 330, 308),
('Guadeloupe', 329, 312),
('Guatemala', 332, 320),
('Guiana', 745, 324),
('Guinea', 632, 324),
('Guinea-Bissau', 630, 624),
('Guyana', 750, 328),
('Haiti', 336, 332),
('Honduras', 334, 340),
('Hong Kong', 477, 344),
('Hungary', 243, 348),
('Iceland', 251, 352),
('India', 419, 356),
('Indonesia', 525, 360),
('Iran', 422, 364),
('Iraq', 425, 368),
('Ireland', 250, 372),
('Israel', 428, 376),
('Italy', 247, 380),
('Jamaica', 339, 388),
('Japan', 431, 392),
('Japan', 432, 392),
('Jordan', 438, 400),
('Kazakhstan', 436, 398),
('Kenya', 634, 404),
('Kerguelen Islands', 635, NULL),
('Kiribati', 529, 296),
('Kuwait', 447, 414),
('Kyrgyzstan', 451, 417),
('Lao', 531, 418),
('Latvia', 275, 428),
('Lebanon', 450, 422),
('Lesotho', 644, 426),
('Liberia', 636, 430),
('Liberia', 637, 430),
('Libya', 642, 434),
('Liechtenstein', 252, 438),
('Lithuania', 277, 440),
('Luxembourg', 253, 442),
('Macao', 453, 446),
('Madagascar', 647, 450),
('Madeira', 255, NULL),
('Makedonia', 274, NULL),
('Malawi', 655, 454),
('Malaysia', 533, 458),
('Maldives', 455, 462),
('Mali', 649, 466),
('Malta', 215, 470),
('Malta', 229, 470),
('Malta', 248, 470),
('Malta', 249, 470),
('Malta', 256, 470),
('Marshall Islands', 538, 584),
('Martinique', 347, 474),
('Mauritania', 654, 478),
('Mauritius', 645, 480),
('Mexico', 345, 484),
('Micronesia', 510, 583),
('Moldova', 214, 498),
('Monaco', 254, 492),
('Mongolia', 457, 496),
('Montenegro', 262, 499),
('Montserrat', 348, 500),
('Morocco', 242, 504),
('Mozambique', 650, 508),
('Myanmar', 506, 104),
('Namibia', 659, 516),
('Nauru', 544, 520),
('Nepal', 459, 524),
('Netherlands', 244, 528),
('Netherlands', 245, 528),
('Netherlands', 246, 528),
('Netherlands Antilles', 306, NULL),
('New Caledonia', 540, 540),
('New Zealand', 512, 554),
('Nicaragua', 350, 558),
('Niger', 656, 562),
('Nigeria', 657, 566),
('Niue', 542, 570),
('North Korea', 445, 408),
('Northern Mariana Islands', 536, 580),
('Norway', 257, 578),
('Norway', 258, 578),
('Norway', 259, 578),
('Oman', 461, 512),
('Pakistan', 463, 586),
('Palau', 511, 585),
('Palestine', 443, 275),
('Panama', 351, 591),
('Panama', 352, 591),
('Panama', 353, 591),
('Panama', 354, 591),
('Panama', 355, 591),
('Panama', 356, 591),
('Panama', 357, 591),
('Panama', 370, 591),
('Panama', 371, 591),
('Panama', 372, 591),
('Panama', 373, 591),
('Papua New Guinea', 553, 598),
('Paraguay', 755, 600),
('Peru', 760, 604),
('Philippines', 548, 608),
('Pitcairn Island', 555, 612),
('Poland', 261, 616),
('Portugal', 263, 620),
('Puerto Rico', 358, 630),
('Qatar', 466, 634),
('Reunion', 660, 638),
('Romania', 264, 642),
('Russian Federation', 273, 643),
('Rwanda', 661, 646),
('Saint Helena', 665, 654),
('Saint Kitts and Nevis', 341, 659),
('Saint Lucia', 343, 662),
('Saint Paul and Amsterdam Islands', 607, NULL),
('Saint Pierre and Miquelon', 361, 666),
('Samoa', 561, 882),
('San Marino', 268, 674),
('Sao Tome and Principe', 668, 678),
('Saudi Arabia', 403, 682),
('Senegal', 663, 686),
('Serbia', 279, 688),
('Seychelles', 664, 690),
('Sierra Leone', 667, 694),
('Singapore', 563, 702),
('Singapore', 564, 702),
('Singapore', 565, 702),
('Singapore', 566, 702),
('Slovakia', 267, 703),
('Slovenia', 278, 705),
('Solomon Islands', 557, 90),
('Somalia', 666, 706),
('South Africa', 601, 710),
('South Korea', 440, 410),
('South Korea', 441, 410),
('South Sudan', 638, 728),
('Spain', 224, 724),
('Spain', 225, 724),
('Sri Lanka', 417, 144),
('St Vincent and the Grenadines', 375, 670),
('St Vincent and the Grenadines', 376, 670),
('St Vincent and the Grenadines', 377, 670),
('Sudan', 662, 729),
('Suriname', 765, 740),
('Swaziland', 669, 748),
('Sweden', 265, 752),
('Sweden', 266, 752),
('Switzerland', 269, 756),
('Syria', 468, 760),
('Taiwan', 416, 158),
('Tajikistan', 472, 762),
('Tanzania', 674, 834),
('Tanzania', 677, 834),
('Thailand', 567, 764),
('Togolese', 671, 768),
('Tonga', 570, 776),
('Trinidad and Tobago', 362, 780),
('Tunisia', 672, 788),
('Turkey', 271, 792),
('Turkmenistan', 434, 795),
('Turks and Caicos Islands', 364, 796),
('Tuvalu', 572, 798),
('Uganda', 675, 800),
('Ukraine', 272, 804),
('United Arab Emirates', 470, 784),
('United Kingdom', 232, 826),
('United Kingdom', 233, 826),
('United Kingdom', 234, 826),
('United Kingdom', 235, 826),
('Uruguay', 770, 858),
('US Virgin Islands', 379, 850),
('USA', 338, 840),
('USA', 366, 840),
('USA', 367, 840),
('USA', 368, 840),
('USA', 369, 840),
('Uzbekistan', 437, 860),
('Vanuatu', 576, 548),
('Vanuatu', 577, 548),
('Vatican City', 208, NULL),
('Venezuela', 775, 862),
('Vietnam', 574, 704),
('Wallis and Futuna Islands', 578, 876),
('Yemen', 473, 887),
('Yemen', 475, 887),
('Zambia', 678, 894),
('Zimbabwe', 679, 716);
---------------------------------------------------------------------------
--
DROP TABLE IF EXISTS public.iso3166;
CREATE TABLE IF NOT EXISTS public.iso3166(
id INTEGER,
country TEXT,
alpha_2 TEXT,
alpha_3 TEXT
);
-- Description
COMMENT ON TABLE
public.iso3166
IS 'This is a complete list of all country ISO codes as described in the ISO 3166 international standard. Country Codes Alpha-2 & Alpha-3 https://www.iban.com/country-codes';
INSERT INTO iso3166 VALUES
(4,'Afghanistan','AF','AFG'),
(8,'Albania','AL','ALB'),
(12,'Algeria','DZ','DZA'),
(16,'American Samoa','AS','ASM'),
(20,'Andorra','AD','AND'),
(24,'Angola','AO','AGO'),
(660,'Anguilla','AI','AIA'),
(10,'Antarctica','AQ','ATA'),
(28,'Antigua and Barbuda','AG','ATG'),
(32,'Argentina','AR','ARG'),
(51,'Armenia','AM','ARM'),
(533,'Aruba','AW','ABW'),
(36,'Australia','AU','AUS'),
(40,'Austria','AT','AUT'),
(31,'Azerbaijan','AZ','AZE'),
(44,'Bahamas (the)','BS','BHS'),
(48,'Bahrain','BH','BHR'),
(50,'Bangladesh','BD','BGD'),
(52,'Barbados','BB','BRB'),
(112,'Belarus','BY','BLR'),
(56,'Belgium','BE','BEL'),
(84,'Belize','BZ','BLZ'),
(204,'Benin','BJ','BEN'),
(60,'Bermuda','BM','BMU'),
(64,'Bhutan','BT','BTN'),
(68,E'Bolivia (Plurinational State of)','BO','BOL'),
(535,'Bonaire, Sint Eustatius and Saba','BQ','BES'),
(70,'Bosnia and Herzegovina','BA','BIH'),
(72,'Botswana','BW','BWA'),
(74,'Bouvet Island','BV','BVT'),
(76,'Brazil','BR','BRA'),
(86,E'British Indian Ocean Territory (the)','IO','IOT'),
(96,'Brunei Darussalam','BN','BRN'),
(100,'Bulgaria','BG','BGR'),
(854,'Burkina Faso','BF','BFA'),
(108,'Burundi','BI','BDI'),
(132,'Cabo Verde','CV','CPV'),
(116,'Cambodia','KH','KHM'),
(120,'Cameroon','CM','CMR'),
(124,'Canada','CA','CAN'),
(136,E'Cayman Islands (the)','KY','CYM'),
(140,E'Central African Republic (the)','CF','CAF'),
(148,'Chad','TD','TCD'),
(152,'Chile','CL','CHL'),
(156,'China','CN','CHN'),
(162,'Christmas Island','CX','CXR'),
(166,E'Cocos (Keeling) Islands (the)','CC','CCK'),
(170,'Colombia','CO','COL'),
(174,'Comoros (the)','KM','COM'),
(180,E'Congo (the Democratic Republic of the)','CD','COD'),
(178,E'Congo (the)','CG','COG'),
(184,E'Cook Islands (the)','CK','COK'),
(188,'Costa Rica','CR','CRI'),
(191,'Croatia','HR','HRV'),
(192,'Cuba','CU','CUB'),
(531,'Curaçao','CW','CUW'),
(196,'Cyprus','CY','CYP'),
(203,'Czechia','CZ','CZE'),
(384,E'Côte d\'Ivoire','CI','CIV'),
(208,'Denmark','DK','DNK'),
(262,'Djibouti','DJ','DJI'),
(212,'Dominica','DM','DMA'),
(214,E'Dominican Republic (the)','DO','DOM'),
(218,'Ecuador','EC','ECU'),
(818,'Egypt','EG','EGY'),
(222,'El Salvador','SV','SLV'),
(226,'Equatorial Guinea','GQ','GNQ'),
(232,'Eritrea','ER','ERI'),
(233,'Estonia','EE','EST'),
(748,'Eswatini','SZ','SWZ'),
(231,'Ethiopia','ET','ETH'),
(238,E'Falkland Islands (the) [Malvinas]','FK','FLK'),
(234,E'Faroe Islands (the)','FO','FRO'),
(242,'Fiji','FJ','FJI'),
(246,'Finland','FI','FIN'),
(250,'France','FR','FRA'),
(254,'French Guiana','GF','GUF'),
(258,'French Polynesia','PF','PYF'),
(260,E'French Southern Territories (the)','TF','ATF'),
(266,'Gabon','GA','GAB'),
(270,E'Gambia (the)','GM','GMB'),
(268,'Georgia','GE','GEO'),
(276,'Germany','DE','DEU'),
(288,'Ghana','GH','GHA'),
(292,'Gibraltar','GI','GIB'),
(300,'Greece','GR','GRC'),
(304,'Greenland','GL','GRL'),
(308,'Grenada','GD','GRD'),
(312,'Guadeloupe','GP','GLP'),
(316,'Guam','GU','GUM'),
(320,'Guatemala','GT','GTM'),
(831,'Guernsey','GG','GGY'),
(324,'Guinea','GN','GIN'),
(624,'Guinea-Bissau','GW','GNB'),
(328,'Guyana','GY','GUY'),
(332,'Haiti','HT','HTI'),
(334,'Heard Island and McDonald Islands','HM','HMD'),
(336,E'Holy See (the)','VA','VAT'),
(340,'Honduras','HN','HND'),
(344,'Hong Kong','HK','HKG'),
(348,'Hungary','HU','HUN'),
(352,'Iceland','IS','ISL'),
(356,'India','IN','IND'),
(360,'Indonesia','ID','IDN'),
(364,E'Iran (Islamic Republic of)','IR','IRN'),
(368,'Iraq','IQ','IRQ'),
(372,'Ireland','IE','IRL'),
(833,'Isle of Man','IM','IMN'),
(376,'Israel','IL','ISR'),
(380,'Italy','IT','ITA'),
(388,'Jamaica','JM','JAM'),
(392,'Japan','JP','JPN'),
(832,'Jersey','JE','JEY'),
(400,'Jordan','JO','JOR'),
(398,'Kazakhstan','KZ','KAZ'),
(404,'Kenya','KE','KEN'),
(296,'Kiribati','KI','KIR'),
(408,E'Korea (the Democratic People\'s Republic of)','KP','PRK'),
(410,E'Korea (the Republic of)','KR','KOR'),
(414,'Kuwait','KW','KWT'),
(417,'Kyrgyzstan','KG','KGZ'),
(418,E'Lao People\'s Democratic Republic (the)','LA','LAO'),
(428,'Latvia','LV','LVA'),
(422,'Lebanon','LB','LBN'),
(426,'Lesotho','LS','LSO'),
(430,'Liberia','LR','LBR'),
(434,'Libya','LY','LBY'),
(438,'Liechtenstein','LI','LIE'),
(440,'Lithuania','LT','LTU'),
(442,'Luxembourg','LU','LUX'),
(446,'Macao','MO','MAC'),
(450,'Madagascar','MG','MDG'),
(454,'Malawi','MW','MWI'),
(458,'Malaysia','MY','MYS'),
(462,'Maldives','MV','MDV'),
(466,'Mali','ML','MLI'),
(470,'Malta','MT','MLT'),
(584,E'Marshall Islands (the)','MH','MHL'),
(474,'Martinique','MQ','MTQ'),
(478,'Mauritania','MR','MRT'),
(480,'Mauritius','MU','MUS'),
(175,'Mayotte','YT','MYT'),
(484,'Mexico','MX','MEX'),
(583,E'Micronesia (Federated States of)','FM','FSM'),
(498,E'Moldova (the Republic of)','MD','MDA'),
(492,'Monaco','MC','MCO'),
(496,'Mongolia','MN','MNG'),
(499,'Montenegro','ME','MNE'),
(500,'Montserrat','MS','MSR'),
(504,'Morocco','MA','MAR'),
(508,'Mozambique','MZ','MOZ'),
(104,'Myanmar','MM','MMR'),
(516,'Namibia','NA','NAM'),
(520,'Nauru','NR','NRU'),
(524,'Nepal','NP','NPL'),
(528,E'Netherlands (the)','NL','NLD'),
(540,'New Caledonia','NC','NCL'),
(554,'New Zealand','NZ','NZL'),
(558,'Nicaragua','NI','NIC'),
(562,E'Niger (the)','NE','NER'),
(566,'Nigeria','NG','NGA'),
(570,'Niue','NU','NIU'),
(574,'Norfolk Island','NF','NFK'),
(580,E'Northern Mariana Islands (the)','MP','MNP'),
(578,'Norway','NO','NOR'),
(512,'Oman','OM','OMN'),
(586,'Pakistan','PK','PAK'),
(585,'Palau','PW','PLW'),
(275,'Palestine, State of','PS','PSE'),
(591,'Panama','PA','PAN'),
(598,'Papua New Guinea','PG','PNG'),
(600,'Paraguay','PY','PRY'),
(604,'Peru','PE','PER'),
(608,E'Philippines (the)','PH','PHL'),
(612,'Pitcairn','PN','PCN'),
(616,'Poland','PL','POL'),
(620,'Portugal','PT','PRT'),
(630,'Puerto Rico','PR','PRI'),
(634,'Qatar','QA','QAT'),
(807,'Republic of North Macedonia','MK','MKD'),
(642,'Romania','RO','ROU'),
(643,'Russian Federation (the)','RU','RUS'),
(646,'Rwanda','RW','RWA'),
(638,'Réunion','RE','REU'),
(652,'Saint Barthélemy','BL','BLM'),
(654,'Saint Helena, Ascension and Tristan da Cunha','SH','SHN'),
(659,'Saint Kitts and Nevis','KN','KNA'),
(662,'Saint Lucia','LC','LCA'),
(663,'Saint Martin (French part)','MF','MAF'),
(666,'Saint Pierre and Miquelon','PM','SPM'),
(670,'Saint Vincent and the Grenadines','VC','VCT'),
(882,'Samoa','WS','WSM'),
(674,'San Marino','SM','SMR'),
(678,'Sao Tome and Principe','ST','STP'),
(682,'Saudi Arabia','SA','SAU'),
(686,'Senegal','SN','SEN'),
(688,'Serbia','RS','SRB'),
(690,'Seychelles','SC','SYC'),
(694,'Sierra Leone','SL','SLE'),
(702,'Singapore','SG','SGP'),
(534,'Sint Maarten (Dutch part)','SX','SXM'),
(703,'Slovakia','SK','SVK'),
(705,'Slovenia','SI','SVN'),
(90,'Solomon Islands','SB','SLB'),
(706,'Somalia','SO','SOM'),
(710,'South Africa','ZA','ZAF'),
(239,'South Georgia and the South Sandwich Islands','GS','SGS'),
(728,'South Sudan','SS','SSD'),
(724,'Spain','ES','ESP'),
(144,'Sri Lanka','LK','LKA'),
(729,'Sudan (the)','SD','SDN'),
(740,'Suriname','SR','SUR'),
(744,'Svalbard and Jan Mayen','SJ','SJM'),
(752,'Sweden','SE','SWE'),
(756,'Switzerland','CH','CHE'),
(760,'Syrian Arab Republic','SY','SYR'),
(158,'Taiwan (Province of China)','TW','TWN'),
(762,'Tajikistan','TJ','TJK'),
(834,'Tanzania, United Republic of','TZ','TZA'),
(764,'Thailand','TH','THA'),
(626,'Timor-Leste','TL','TLS'),
(768,'Togo','TG','TGO'),
(772,'Tokelau','TK','TKL'),
(776,'Tonga','TO','TON'),
(780,'Trinidad and Tobago','TT','TTO'),
(788,'Tunisia','TN','TUN'),
(792,'Turkey','TR','TUR'),
(795,'Turkmenistan','TM','TKM'),
(796,'Turks and Caicos Islands (the)','TC','TCA'),
(798,'Tuvalu','TV','TUV'),
(800,'Uganda','UG','UGA'),
(804,'Ukraine','UA','UKR'),
(784,'United Arab Emirates (the)','AE','ARE'),
(826,'United Kingdom of Great Britain and Northern Ireland (the)','GB','GBR'),
(581,'United States Minor Outlying Islands (the)','UM','UMI'),
(840,'United States of America (the)','US','USA'),
(858,'Uruguay','UY','URY'),
(860,'Uzbekistan','UZ','UZB'),
(548,'Vanuatu','VU','VUT'),
(862,'Venezuela (Bolivarian Republic of)','VE','VEN'),
(704,'Viet Nam','VN','VNM'),
(92,'Virgin Islands (British)','VG','VGB'),
(850,'Virgin Islands (U.S.)','VI','VIR'),
(876,'Wallis and Futuna','WF','WLF'),
(732,'Western Sahara','EH','ESH'),
(887,'Yemen','YE','YEM'),
(894,'Zambia','ZM','ZMB'),
(716,'Zimbabwe','ZW','ZWE'),
(248,E'Åland Islands','AX','ALA');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,240 @@
---------------------------------------------------------------------------
-- singalk db public schema
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
CREATE SCHEMA IF NOT EXISTS public;
---------------------------------------------------------------------------
-- basic helpers to check type and more
--
CREATE OR REPLACE FUNCTION public.isdouble(text) RETURNS BOOLEAN AS
$isdouble$
DECLARE x DOUBLE PRECISION;
BEGIN
x = $1::DOUBLE PRECISION;
RETURN TRUE;
EXCEPTION WHEN others THEN
RETURN FALSE;
END;
$isdouble$
STRICT
LANGUAGE plpgsql IMMUTABLE;
-- Description
COMMENT ON FUNCTION
public.isdouble
IS 'Check typeof value is double';
CREATE OR REPLACE FUNCTION public.isnumeric(text) RETURNS BOOLEAN AS
$isnumeric$
DECLARE x NUMERIC;
BEGIN
x = $1::NUMERIC;
RETURN TRUE;
EXCEPTION WHEN others THEN
RETURN FALSE;
END;
$isnumeric$
STRICT
LANGUAGE plpgsql IMMUTABLE;
-- Description
COMMENT ON FUNCTION
public.isnumeric
IS 'Check typeof value is numeric';
CREATE OR REPLACE FUNCTION public.isboolean(text) RETURNS BOOLEAN AS
$isboolean$
DECLARE x BOOLEAN;
BEGIN
x = $1::BOOLEAN;
RETURN TRUE;
EXCEPTION WHEN others THEN
RETURN FALSE;
END;
$isboolean$
STRICT
LANGUAGE plpgsql IMMUTABLE;
-- Description
COMMENT ON FUNCTION
public.isboolean
IS 'Check typeof value is boolean';
CREATE OR REPLACE FUNCTION public.isdate(s varchar) returns boolean as $$
BEGIN
perform s::date;
return true;
exception when others then
return false;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.isdate
IS 'Check typeof value is date';
CREATE OR REPLACE FUNCTION public.istimestamptz(text) RETURNS BOOLEAN AS
$isdate$
DECLARE x TIMESTAMPTZ;
BEGIN
x = $1::TIMESTAMPTZ;
RETURN TRUE;
EXCEPTION WHEN others THEN
RETURN FALSE;
END;
$isdate$
STRICT
LANGUAGE plpgsql IMMUTABLE;
-- Description
COMMENT ON FUNCTION
public.istimestamptz
IS 'Check typeof value is TIMESTAMPTZ';
---------------------------------------------------------------------------
-- JSON helpers
--
CREATE FUNCTION jsonb_key_exists(some_json jsonb, outer_key text)
RETURNS BOOLEAN AS $$
BEGIN
RETURN (some_json->outer_key) IS NOT NULL;
END;
$$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.jsonb_key_exists
IS 'function that checks if an outer key exists in some_json and returns a boolean';
-- https://stackoverflow.com/questions/42944888/merging-jsonb-values-in-postgresql
CREATE OR REPLACE FUNCTION public.jsonb_recursive_merge(A jsonb, B jsonb)
RETURNS jsonb LANGUAGE SQL AS $$
SELECT
jsonb_object_agg(
coalesce(ka, kb),
CASE
WHEN va isnull THEN vb
WHEN vb isnull THEN va
WHEN jsonb_typeof(va) <> 'object' OR jsonb_typeof(vb) <> 'object' THEN vb
ELSE jsonb_recursive_merge(va, vb) END
)
FROM jsonb_each(A) temptable1(ka, va)
FULL JOIN jsonb_each(B) temptable2(kb, vb) ON ka = kb
$$;
-- Description
COMMENT ON FUNCTION
public.jsonb_recursive_merge
IS 'Merging JSONB values';
-- https://stackoverflow.com/questions/36041784/postgresql-compare-two-jsonb-objects
CREATE OR REPLACE FUNCTION public.jsonb_diff_val(val1 JSONB,val2 JSONB)
RETURNS JSONB AS $jsonb_diff_val$
DECLARE
result JSONB;
v RECORD;
BEGIN
result = val1;
FOR v IN SELECT * FROM jsonb_each(val2) LOOP
IF result @> jsonb_build_object(v.key,v.value)
THEN result = result - v.key;
ELSIF result ? v.key THEN CONTINUE;
ELSE
result = result || jsonb_build_object(v.key,'null');
END IF;
END LOOP;
RETURN result;
END;
$jsonb_diff_val$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.jsonb_diff_val
IS 'Compare two jsonb objects';
---------------------------------------------------------------------------
-- uuid v7 helpers
--
-- https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74
CREATE OR REPLACE FUNCTION public.timestamp_from_uuid_v7(_uuid uuid)
RETURNS timestamp without time zone
LANGUAGE sql
-- Based off IETF draft, https://datatracker.ietf.org/doc/draft-peabody-dispatch-new-uuid-format/
IMMUTABLE PARALLEL SAFE STRICT LEAKPROOF
AS $$
SELECT to_timestamp(('x0000' || substr(_uuid::text, 1, 8) || substr(_uuid::text, 10, 4))::bit(64)::bigint::numeric / 1000);
$$
;
-- Description
COMMENT ON FUNCTION
public.timestamp_from_uuid_v7
IS 'extract the timestamp from the uuid.';
create or replace function public.uuid_generate_v7()
returns uuid
as $$
begin
-- use random v4 uuid as starting point (which has the same variant we need)
-- then overlay timestamp
-- then set version 7 by flipping the 2 and 1 bit in the version 4 string
return encode(
set_bit(
set_bit(
overlay(uuid_send(gen_random_uuid())
placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3)
from 1 for 6
),
52, 1
),
53, 1
),
'hex')::uuid;
end
$$
language plpgsql volatile;
-- Description
COMMENT ON FUNCTION
public.uuid_generate_v7
IS 'Generate UUID v7, Based off IETF draft, https://datatracker.ietf.org/doc/draft-peabody-dispatch-new-uuid-format/';
---------------------------------------------------------------------------
-- Conversion helpers
--
CREATE OR REPLACE FUNCTION public.kelvinToCel(IN temperature NUMERIC)
RETURNS NUMERIC
AS $$
BEGIN
RETURN ROUND((((temperature)::numeric - 273.15) * 10) / 10);
END
$$
LANGUAGE plpgsql IMMUTABLE;
-- Description
COMMENT ON FUNCTION
public.kelvinToCel
IS 'convert kelvin To Celsius';
CREATE OR REPLACE FUNCTION public.radiantToDegrees(IN angle NUMERIC)
RETURNS NUMERIC
AS $$
BEGIN
RETURN ROUND((((angle)::numeric * 57.2958) * 10) / 10);
END
$$
LANGUAGE plpgsql IMMUTABLE;
-- Description
COMMENT ON FUNCTION
public.radiantToDegrees
IS 'convert radiant To Degrees';
CREATE OR REPLACE FUNCTION public.valToPercent(IN val NUMERIC)
RETURNS NUMERIC
AS $$
BEGIN
RETURN (val * 100);
END
$$
LANGUAGE plpgsql IMMUTABLE;
-- Description
COMMENT ON FUNCTION
public.valToPercent
IS 'convert radiant To Degrees';

View File

@@ -0,0 +1,859 @@
---------------------------------------------------------------------------
-- singalk db public schema
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
CREATE SCHEMA IF NOT EXISTS public;
---------------------------------------------------------------------------
-- python reverse_geocode
--
-- https://github.com/CartoDB/labs-postgresql/blob/master/workshop/plpython.md
--
DROP FUNCTION IF EXISTS reverse_geocode_py_fn;
CREATE OR REPLACE FUNCTION reverse_geocode_py_fn(IN geocoder TEXT, IN lon NUMERIC, IN lat NUMERIC,
OUT geo JSONB)
AS $reverse_geocode_py$
import requests
# Use the shared cache to avoid preparing the geocoder metadata
if geocoder in SD:
plan = SD[geocoder]
# A prepared statement from Python
else:
plan = plpy.prepare("SELECT reverse_url AS url FROM geocoders WHERE name = $1", ["text"])
SD[geocoder] = plan
# Execute the statement with the geocoder param and limit to 1 result
rv = plpy.execute(plan, [geocoder], 1)
url = rv[0]['url']
# Validate input
if not lon or not lat:
plpy.notice('reverse_geocode_py_fn Parameters [{}] [{}]'.format(lon, lat))
plpy.error('Error missing parameters')
return None
def georeverse(geocoder, lon, lat, zoom="18"):
# Make the request to the geocoder API
# https://operations.osmfoundation.org/policies/nominatim/
headers = {"Accept-Language": "en-US,en;q=0.5", "User-Agent": "PostgSail", "From": "xbgmsharp@gmail.com"}
payload = {"lon": lon, "lat": lat, "format": "jsonv2", "zoom": zoom, "accept-language": "en"}
# https://nominatim.org/release-docs/latest/api/Reverse/
r = requests.get(url, headers=headers, params=payload)
# Parse response
# If name is null fallback to address field tags: neighbourhood,suburb
# if none repeat with lower zoom level
if r.status_code == 200 and "name" in r.json():
r_dict = r.json()
#plpy.notice('reverse_geocode_py_fn Parameters [{}] [{}] Response'.format(lon, lat, r_dict))
output = None
country_code = None
if "country_code" in r_dict["address"] and r_dict["address"]["country_code"]:
country_code = r_dict["address"]["country_code"]
if r_dict["name"]:
return { "name": r_dict["name"], "country_code": country_code }
elif "address" in r_dict and r_dict["address"]:
if "neighbourhood" in r_dict["address"] and r_dict["address"]["neighbourhood"]:
return { "name": r_dict["address"]["neighbourhood"], "country_code": country_code }
elif "hamlet" in r_dict["address"] and r_dict["address"]["hamlet"]:
return { "name": r_dict["address"]["hamlet"], "country_code": country_code }
elif "suburb" in r_dict["address"] and r_dict["address"]["suburb"]:
return { "name": r_dict["address"]["suburb"], "country_code": country_code }
elif "residential" in r_dict["address"] and r_dict["address"]["residential"]:
return { "name": r_dict["address"]["residential"], "country_code": country_code }
elif "village" in r_dict["address"] and r_dict["address"]["village"]:
return { "name": r_dict["address"]["village"], "country_code": country_code }
elif "town" in r_dict["address"] and r_dict["address"]["town"]:
return { "name": r_dict["address"]["town"], "country_code": country_code }
elif "amenity" in r_dict["address"] and r_dict["address"]["amenity"]:
return { "name": r_dict["address"]["amenity"], "country_code": country_code }
else:
if (zoom == 15):
plpy.notice('georeverse recursive retry with lower zoom than:[{}], Response [{}]'.format(zoom , r.json()))
return { "name": "n/a", "country_code": country_code }
else:
plpy.notice('georeverse recursive retry with lower zoom than:[{}], Response [{}]'.format(zoom , r.json()))
return georeverse(geocoder, lon, lat, 15)
else:
return { "name": "n/a", "country_code": country_code }
else:
plpy.warning('Failed to received a geo full address %s', r.json())
#plpy.error('Failed to received a geo full address %s', r.json())
return { "name": "unknown", "country_code": "unknown" }
return georeverse(geocoder, lon, lat)
$reverse_geocode_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.reverse_geocode_py_fn
IS 'query reverse geo service to return location name using plpython3u';
---------------------------------------------------------------------------
-- python send email
--
-- https://www.programcreek.com/python/example/3684/email.utils.formatdate
DROP FUNCTION IF EXISTS send_email_py_fn;
CREATE OR REPLACE FUNCTION send_email_py_fn(IN email_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void
AS $send_email_py$
# Import smtplib for the actual sending function
import smtplib
# Import the email modules we need
#from email.message import EmailMessage
from email.utils import formatdate,make_msgid
from email.mime.text import MIMEText
# Use the shared cache to avoid preparing the email metadata
if email_type in SD:
plan = SD[email_type]
# A prepared statement from Python
else:
plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"])
SD[email_type] = plan
# Execute the statement with the email_type param and limit to 1 result
rv = plpy.execute(plan, [email_type], 1)
email_subject = rv[0]['email_subject']
email_content = rv[0]['email_content']
# Replace fields using input jsonb obj
if not _user or not app:
plpy.notice('send_email_py_fn Parameters [{}] [{}]'.format(_user, app))
plpy.error('Error missing parameters')
return None
if 'logbook_name' in _user and _user['logbook_name']:
email_content = email_content.replace('__LOGBOOK_NAME__', _user['logbook_name'])
if 'logbook_link' in _user and _user['logbook_link']:
email_content = email_content.replace('__LOGBOOK_LINK__', str(_user['logbook_link']))
if 'recipient' in _user and _user['recipient']:
email_content = email_content.replace('__RECIPIENT__', _user['recipient'])
if 'boat' in _user and _user['boat']:
email_content = email_content.replace('__BOAT__', _user['boat'])
if 'badge' in _user and _user['badge']:
email_content = email_content.replace('__BADGE_NAME__', _user['badge'])
if 'otp_code' in _user and _user['otp_code']:
email_content = email_content.replace('__OTP_CODE__', _user['otp_code'])
if 'reset_qs' in _user and _user['reset_qs']:
email_content = email_content.replace('__RESET_QS__', _user['reset_qs'])
if 'alert' in _user and _user['alert']:
email_content = email_content.replace('__ALERT__', _user['alert'])
if 'app.url' in app and app['app.url']:
email_content = email_content.replace('__APP_URL__', app['app.url'])
email_from = 'root@localhost'
if 'app.email_from' in app and app['app.email_from']:
email_from = 'PostgSail <' + app['app.email_from'] + '>'
#plpy.notice('Sending email from [{}] [{}]'.format(email_from, app['app.email_from']))
email_to = 'root@localhost'
if 'email' in _user and _user['email']:
email_to = _user['email']
#plpy.notice('Sending email to [{}] [{}]'.format(email_to, _user['email']))
else:
plpy.error('Error email to')
return None
msg = MIMEText(email_content, 'plain', 'utf-8')
msg["Subject"] = email_subject
msg["From"] = email_from
msg["To"] = email_to
msg["Date"] = formatdate()
msg["Message-ID"] = make_msgid()
server_smtp = 'localhost'
if 'app.email_server' in app and app['app.email_server']:
server_smtp = app['app.email_server']
#plpy.notice('Sending server [{}] [{}]'.format(server_smtp, app['app.email_server']))
# Send the message via our own SMTP server.
try:
# send your message with credentials specified above
with smtplib.SMTP(server_smtp, 587) as server:
if 'app.email_user' in app and app['app.email_user'] \
and 'app.email_pass' in app and app['app.email_pass']:
server.starttls()
server.login(app['app.email_user'], app['app.email_pass'])
#server.send_message(msg)
server.sendmail(msg["From"], msg["To"], msg.as_string())
server.quit()
# tell the script to report if your message was sent or which errors need to be fixed
plpy.notice('Sent email successfully to [{}] [{}]'.format(msg["To"], msg["Subject"]))
return None
except OSError as error:
plpy.error('OS Error occurred: ' + str(error))
except smtplib.SMTPConnectError:
plpy.error('Failed to connect to the server. Bad connection settings?')
except smtplib.SMTPServerDisconnected:
plpy.error('Failed to connect to the server. Wrong user/password?')
except smtplib.SMTPException as e:
plpy.error('SMTP error occurred: ' + str(e))
$send_email_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.send_email_py_fn
IS 'Send email notification using plpython3u';
---------------------------------------------------------------------------
-- python send pushover message
-- https://pushover.net/
DROP FUNCTION IF EXISTS send_pushover_py_fn;
CREATE OR REPLACE FUNCTION send_pushover_py_fn(IN message_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void
AS $send_pushover_py$
import requests
# Use the shared cache to avoid preparing the email metadata
if message_type in SD:
plan = SD[message_type]
# A prepared statement from Python
else:
plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"])
SD[message_type] = plan
# Execute the statement with the message_type param and limit to 1 result
rv = plpy.execute(plan, [message_type], 1)
pushover_title = rv[0]['pushover_title']
pushover_message = rv[0]['pushover_message']
# Replace fields using input jsonb obj
if 'logbook_name' in _user and _user['logbook_name']:
pushover_message = pushover_message.replace('__LOGBOOK_NAME__', _user['logbook_name'])
if 'logbook_link' in _user and _user['logbook_link']:
pushover_message = pushover_message.replace('__LOGBOOK_LINK__', str(_user['logbook_link']))
if 'recipient' in _user and _user['recipient']:
pushover_message = pushover_message.replace('__RECIPIENT__', _user['recipient'])
if 'boat' in _user and _user['boat']:
pushover_message = pushover_message.replace('__BOAT__', _user['boat'])
if 'badge' in _user and _user['badge']:
pushover_message = pushover_message.replace('__BADGE_NAME__', _user['badge'])
if 'alert' in _user and _user['alert']:
pushover_message = pushover_message.replace('__ALERT__', _user['alert'])
if 'app.url' in app and app['app.url']:
pushover_message = pushover_message.replace('__APP_URL__', app['app.url'])
pushover_token = None
if 'app.pushover_app_token' in app and app['app.pushover_app_token']:
pushover_token = app['app.pushover_app_token']
else:
plpy.error('Error no pushover token defined, check app settings')
return None
pushover_user = None
if 'pushover_user_key' in _user and _user['pushover_user_key']:
pushover_user = _user['pushover_user_key']
else:
plpy.error('Error no pushover user token defined, check user settings')
return None
# requests
r = requests.post("https://api.pushover.net/1/messages.json", data = {
"token": pushover_token,
"user": pushover_user,
"title": pushover_title,
"message": pushover_message
})
#print(r.text)
# Return ?? or None if not found
#plpy.notice('Sent pushover successfully to [{}] [{}]'.format(r.text, r.status_code))
if r.status_code == 200:
plpy.notice('Sent pushover successfully to [{}] [{}] [{}]'.format(pushover_user, pushover_title, r.text))
else:
plpy.error('Failed to send pushover')
return None
$send_pushover_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.send_pushover_py_fn
IS 'Send pushover notification using plpython3u';
---------------------------------------------------------------------------
-- python send telegram message
-- https://core.telegram.org/
DROP FUNCTION IF EXISTS send_telegram_py_fn;
CREATE OR REPLACE FUNCTION send_telegram_py_fn(IN message_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void
AS $send_telegram_py$
"""
Send a message to a telegram user or group specified on chatId
chat_id must be a number!
"""
import requests
import json
# Use the shared cache to avoid preparing the email metadata
if message_type in SD:
plan = SD[message_type]
# A prepared statement from Python
else:
plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"])
SD[message_type] = plan
# Execute the statement with the message_type param and limit to 1 result
rv = plpy.execute(plan, [message_type], 1)
telegram_title = rv[0]['pushover_title']
telegram_message = rv[0]['pushover_message']
# Replace fields using input jsonb obj
if 'logbook_name' in _user and _user['logbook_name']:
telegram_message = telegram_message.replace('__LOGBOOK_NAME__', _user['logbook_name'])
if 'logbook_link' in _user and _user['logbook_link']:
telegram_message = telegram_message.replace('__LOGBOOK_LINK__', str(_user['logbook_link']))
if 'recipient' in _user and _user['recipient']:
telegram_message = telegram_message.replace('__RECIPIENT__', _user['recipient'])
if 'boat' in _user and _user['boat']:
telegram_message = telegram_message.replace('__BOAT__', _user['boat'])
if 'badge' in _user and _user['badge']:
telegram_message = telegram_message.replace('__BADGE_NAME__', _user['badge'])
if 'alert' in _user and _user['alert']:
telegram_message = telegram_message.replace('__ALERT__', _user['alert'])
if 'app.url' in app and app['app.url']:
telegram_message = telegram_message.replace('__APP_URL__', app['app.url'])
telegram_token = None
if 'app.telegram_bot_token' in app and app['app.telegram_bot_token']:
telegram_token = app['app.telegram_bot_token']
else:
plpy.error('Error no telegram token defined, check app settings')
return None
telegram_chat_id = None
if 'telegram_chat_id' in _user and _user['telegram_chat_id']:
telegram_chat_id = _user['telegram_chat_id']
else:
plpy.error('Error no telegram user token defined, check user settings')
return None
# requests
headers = {'Content-Type': 'application/json',
'Proxy-Authorization': 'Basic base64'}
data_dict = {'chat_id': telegram_chat_id,
'text': telegram_message,
'parse_mode': 'HTML',
'disable_notification': False}
data = json.dumps(data_dict)
url = f'https://api.telegram.org/bot{telegram_token}/sendMessage'
r = requests.post(url,
data=data,
headers=headers)
#print(r.text)
# Return something boolean?
#plpy.notice('Sent telegram successfully to [{}] [{}]'.format(r.text, r.status_code))
if r.status_code == 200:
plpy.notice('Sent telegram successfully to [{}] [{}] [{}]'.format(telegram_chat_id, telegram_title, r.text))
else:
plpy.error('Failed to send telegram')
return None
$send_telegram_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.send_telegram_py_fn
IS 'Send a message to a telegram user or group specified on chatId using plpython3u';
---------------------------------------------------------------------------
-- python url encode
CREATE OR REPLACE FUNCTION urlencode_py_fn(uri text) RETURNS text
AS $urlencode_py$
import urllib.parse
return urllib.parse.quote(uri, safe="");
$urlencode_py$ LANGUAGE plpython3u IMMUTABLE STRICT;
-- Description
COMMENT ON FUNCTION
public.urlencode_py_fn
IS 'python url encode using plpython3u';
---------------------------------------------------------------------------
-- python
-- https://ipapi.co/
DROP FUNCTION IF EXISTS reverse_geoip_py_fn;
CREATE OR REPLACE FUNCTION reverse_geoip_py_fn(IN _ip TEXT) RETURNS JSONB
AS $reverse_geoip_py$
"""
Return ipapi.co ip details
"""
import requests
import json
# requests
url = f'https://ipapi.co/{_ip}/json/'
r = requests.get(url)
#print(r.text)
#plpy.notice('IP [{}] [{}]'.format(_ip, r.status_code))
if r.status_code == 200:
#plpy.notice('Got [{}] [{}]'.format(r.text, r.status_code))
return r.json()
else:
plpy.error('Failed to get ip details')
return {}
$reverse_geoip_py$ IMMUTABLE strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.reverse_geoip_py_fn
IS 'Retrieve reverse geo IP location via ipapi.co using plpython3u';
---------------------------------------------------------------------------
-- python url escape
--
DROP FUNCTION IF EXISTS urlescape_py_fn;
CREATE OR REPLACE FUNCTION urlescape_py_fn(original text) RETURNS text LANGUAGE plpython3u AS $$
import urllib.parse
return urllib.parse.quote(original);
$$
IMMUTABLE STRICT;
-- Description
COMMENT ON FUNCTION
public.urlescape_py_fn
IS 'URL-encoding VARCHAR and TEXT values using plpython3u';
---------------------------------------------------------------------------
-- python geojson parser
--
--CREATE TYPE geometry_type AS ENUM ('LineString', 'Point');
DROP FUNCTION IF EXISTS geojson_py_fn;
CREATE OR REPLACE FUNCTION geojson_py_fn(IN original JSONB, IN geometry_type TEXT) RETURNS JSONB LANGUAGE plpython3u
AS $geojson_py$
import json
parsed = json.loads(original)
output = []
#plpy.notice(parsed)
# [None, None]
if None not in parsed:
for idx, x in enumerate(parsed):
#plpy.notice(idx, x)
for feature in x:
#plpy.notice(feature)
if (feature['geometry']['type'] != geometry_type):
output.append(feature)
#elif (feature['properties']['id']): TODO
# output.append(feature)
#else:
# plpy.notice('ignoring')
return json.dumps(output)
$geojson_py$ -- TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
IMMUTABLE STRICT;
-- Description
COMMENT ON FUNCTION
public.geojson_py_fn
IS 'Parse geojson using plpython3u (should be done in PGSQL), deprecated';
DROP FUNCTION IF EXISTS overpass_py_fn;
CREATE OR REPLACE FUNCTION overpass_py_fn(IN lon NUMERIC, IN lat NUMERIC,
OUT geo JSONB) RETURNS JSONB
AS $overpass_py$
"""
Return https://overpass-turbo.eu seamark details within 400m
https://overpass-turbo.eu/s/1EaG
https://wiki.openstreetmap.org/wiki/Key:seamark:type
"""
import requests
import json
import urllib.parse
headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com'}
payload = """
[out:json][timeout:20];
is_in({0},{1})->.result_areas;
(
area.result_areas["seamark:type"~"(mooring|harbour)"][~"^seamark:.*:category$"~"."];
area.result_areas["leisure"="marina"][~"name"~"."];
);
out tags;
nwr(around:400.0,{0},{1})->.all;
(
nwr.all["seamark:type"~"(mooring|harbour)"][~"^seamark:.*:category$"~"."];
nwr.all["seamark:type"~"(anchorage|anchor_berth|berth)"];
nwr.all["leisure"="marina"];
nwr.all["natural"~"(bay|beach)"];
);
out tags;
""".format(lat, lon)
data = urllib.parse.quote(payload, safe="");
url = f'https://overpass-api.de/api/interpreter?data={data}'.format(data)
r = requests.get(url, headers)
#print(r.text)
#plpy.notice(url)
plpy.notice('overpass-api coord lon[{}] lat[{}] [{}]'.format(lon, lat, r.status_code))
if r.status_code == 200 and "elements" in r.json():
r_dict = r.json()
plpy.notice('overpass-api Got [{}]'.format(r_dict["elements"]))
if r_dict["elements"]:
if "tags" in r_dict["elements"][0] and r_dict["elements"][0]["tags"]:
return r_dict["elements"][0]["tags"]; # return the first element
return {}
else:
plpy.notice('overpass-api Failed to get overpass-api details')
return {}
$overpass_py$ IMMUTABLE strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.overpass_py_fn
IS 'Return https://overpass-turbo.eu seamark details within 400m using plpython3u';
---------------------------------------------------------------------------
-- Provision Grafana SQL
--
CREATE OR REPLACE FUNCTION grafana_py_fn(IN _v_name TEXT, IN _v_id TEXT,
IN _u_email TEXT, IN app JSONB) RETURNS VOID
AS $grafana_py$
"""
https://grafana.com/docs/grafana/latest/developers/http_api/
Create organization base on vessel name
Create user base on user email
Add user to organization
Add data_source to organization
Add dashboard to organization
Update organization preferences
"""
import requests
import json
import re
grafana_uri = None
if 'app.grafana_admin_uri' in app and app['app.grafana_admin_uri']:
grafana_uri = app['app.grafana_admin_uri']
else:
plpy.error('Error no grafana_admin_uri defined, check app settings')
return None
b_name = None
if not _v_name:
b_name = _v_id
else:
b_name = _v_name
# add vessel org
headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com',
'Accept': 'application/json', 'Content-Type': 'application/json'}
path = 'api/orgs'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
data_dict = {'name':b_name}
data = json.dumps(data_dict)
r = requests.post(url, data=data, headers=headers)
#print(r.text)
#plpy.notice(r.json())
if r.status_code == 200 and "orgId" in r.json():
org_id = r.json()['orgId']
else:
plpy.error('Error grafana add vessel org %', r.json())
return None
# add user to vessel org
path = 'api/admin/users'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
data_dict = {'orgId':org_id, 'email':_u_email, 'password':'asupersecretpassword'}
data = json.dumps(data_dict)
r = requests.post(url, data=data, headers=headers)
#print(r.text)
#plpy.notice(r.json())
if r.status_code == 200 and "id" in r.json():
user_id = r.json()['id']
else:
plpy.error('Error grafana add user to vessel org')
return
# read data_source
path = 'api/datasources/1'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
r = requests.get(url, headers=headers)
#print(r.text)
#plpy.notice(r.json())
data_source = r.json()
data_source['id'] = 0
data_source['orgId'] = org_id
data_source['uid'] = "ds_" + _v_id
data_source['name'] = "ds_" + _v_id
data_source['secureJsonData'] = {}
data_source['secureJsonData']['password'] = 'password'
data_source['readOnly'] = True
del data_source['secureJsonFields']
# add data_source to vessel org
path = 'api/datasources'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
data = json.dumps(data_source)
headers['X-Grafana-Org-Id'] = str(org_id)
r = requests.post(url, data=data, headers=headers)
#plpy.notice(r.json())
del headers['X-Grafana-Org-Id']
if r.status_code != 200 and "id" not in r.json():
plpy.error('Error grafana add data_source to vessel org')
return
dashboards_tpl = [ 'pgsail_tpl_electrical', 'pgsail_tpl_logbook', 'pgsail_tpl_monitor', 'pgsail_tpl_rpi', 'pgsail_tpl_solar', 'pgsail_tpl_weather', 'pgsail_tpl_home']
for dashboard in dashboards_tpl:
# read dashboard template by uid
path = 'api/dashboards/uid'
url = f'{grafana_uri}/{path}/{dashboard}'.format(grafana_uri,path,dashboard)
if 'X-Grafana-Org-Id' in headers:
del headers['X-Grafana-Org-Id']
r = requests.get(url, headers=headers)
#plpy.notice(r.json())
if r.status_code != 200 and "id" not in r.json():
plpy.error('Error grafana read dashboard template')
return
new_dashboard = r.json()
del new_dashboard['meta']
new_dashboard['dashboard']['version'] = 0
new_dashboard['dashboard']['id'] = 0
new_uid = re.sub(r'pgsail_tpl_(.*)', r'postgsail_\1', new_dashboard['dashboard']['uid'])
new_dashboard['dashboard']['uid'] = f'{new_uid}_{_v_id}'.format(new_uid,_v_id)
# add dashboard to vessel org
path = 'api/dashboards/db'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
data = json.dumps(new_dashboard)
new_data = data.replace('PCC52D03280B7034C', data_source['uid'])
headers['X-Grafana-Org-Id'] = str(org_id)
r = requests.post(url, data=new_data, headers=headers)
#plpy.notice(r.json())
if r.status_code != 200 and "id" not in r.json():
plpy.error('Error grafana add dashboard to vessel org')
return
# Update Org Prefs
path = 'api/org/preferences'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
home_dashboard = {}
home_dashboard['timezone'] = 'utc'
home_dashboard['homeDashboardUID'] = f'postgsail_home_{_v_id}'.format(_v_id)
data = json.dumps(home_dashboard)
headers['X-Grafana-Org-Id'] = str(org_id)
r = requests.patch(url, data=data, headers=headers)
#plpy.notice(r.json())
if r.status_code != 200:
plpy.error('Error grafana update org preferences')
return
plpy.notice('Done')
$grafana_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.grafana_py_fn
IS 'Grafana Organization,User,data_source,dashboards provisioning via HTTP API using plpython3u';
-- https://stackoverflow.com/questions/65517230/how-to-set-user-attribute-value-in-keycloak-using-api
DROP FUNCTION IF EXISTS keycloak_py_fn;
CREATE OR REPLACE FUNCTION keycloak_py_fn(IN user_id TEXT, IN vessel_id TEXT,
IN app JSONB) RETURNS JSONB
AS $keycloak_py$
"""
Add vessel_id user attribute to keycloak user {user_id}
"""
import requests
import json
import urllib.parse
safe_uri = host = user = pwd = None
if 'app.keycloak_uri' in app and app['app.keycloak_uri']:
#safe_uri = urllib.parse.quote(app['app.keycloak_uri'], safe=':/?&=')
_ = urllib.parse.urlparse(app['app.keycloak_uri'])
host = _.netloc.split('@')[-1]
user = _.netloc.split(':')[0]
pwd = _.netloc.split(':')[1].split('@')[0]
else:
plpy.error('Error no keycloak_uri defined, check app settings')
return None
if not host or not user or not pwd:
plpy.error('Error parsing keycloak_uri, check app settings')
return None
_headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com'}
_payload = {'client_id':'admin-cli','grant_type':'password','username':user,'password':pwd}
url = f'{_.scheme}://{host}/realms/master/protocol/openid-connect/token'.format(_.scheme, host)
r = requests.post(url, headers=_headers, data=_payload, timeout=(5, 60))
#print(r.text)
#plpy.notice(url)
if r.status_code == 200 and 'access_token' in r.json():
response = r.json()
plpy.notice(response)
_headers['Authorization'] = 'Bearer '+ response['access_token']
_headers['Content-Type'] = 'application/json'
_payload = { 'attributes': {'vessel_id': vessel_id} }
url = f'{keycloak_uri}/admin/realms/postgsail/users/{user_id}'.format(keycloak_uri,user_id)
#plpy.notice(url)
#plpy.notice(_payload)
data = json.dumps(_payload)
r = requests.put(url, headers=_headers, data=data, timeout=(5, 60))
if r.status_code != 204:
plpy.notice("Error updating user: {status} [{text}]".format(
status=r.status_code, text=r.text))
return None
else:
plpy.notice("Updated user : {user} [{text}]".format(user=user_id, text=r.text))
else:
plpy.notice(f'Error getting admin access_token: {status} [{text}]'.format(
status=r.status_code, text=r.text))
return None
$keycloak_py$ strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.keycloak_py_fn
IS 'Set oauth user attribute into keycloak using plpython3u';
DROP FUNCTION IF EXISTS keycloak_auth_py_fn;
CREATE OR REPLACE FUNCTION keycloak_auth_py_fn(IN _v_id TEXT,
IN _user JSONB, IN app JSONB) RETURNS JSONB
AS $keycloak_auth_py$
"""
Add keycloak user
"""
import requests
import json
import urllib.parse
safe_uri = host = user = pwd = None
if 'app.keycloak_uri' in app and app['app.keycloak_uri']:
#safe_uri = urllib.parse.quote(app['app.keycloak_uri'], safe=':/?&=')
_ = urllib.parse.urlparse(app['app.keycloak_uri'])
host = _.netloc.split('@')[-1]
user = _.netloc.split(':')[0]
pwd = _.netloc.split(':')[1].split('@')[0]
else:
plpy.error('Error no keycloak_uri defined, check app settings')
return none
if not host or not user or not pwd:
plpy.error('Error parsing keycloak_uri, check app settings')
return None
if not 'email' in _user and _user['email']:
plpy.error('Error parsing user email, check user settings')
return none
if not _v_id:
plpy.error('Error parsing vessel_id')
return none
_headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com'}
_payload = {'client_id':'admin-cli','grant_type':'password','username':user,'password':pwd}
url = f'{_.scheme}://{host}/realms/master/protocol/openid-connect/token'.format(_.scheme, host)
r = requests.post(url, headers=_headers, data=_payload, timeout=(5, 60))
#print(r.text)
#plpy.notice(url)
if r.status_code == 200 and 'access_token' in r.json():
response = r.json()
#plpy.notice(response)
_headers['Authorization'] = 'Bearer '+ response['access_token']
_headers['Content-Type'] = 'application/json'
url = f'{_.scheme}://{host}/admin/realms/postgsail/users'.format(_.scheme, host)
_payload = {
"enabled": "true",
"email": _user['email'],
"firstName": _user['recipient'],
"attributes": {"vessel_id": _v_id},
"emailVerified": True,
"requiredActions":["UPDATE_PROFILE", "UPDATE_PASSWORD"]
}
#plpy.notice(_payload)
data = json.dumps(_payload)
r = requests.post(url, headers=_headers, data=data, timeout=(5, 60))
if r.status_code != 201:
#print("Error creating user: {status}".format(status=r.status_code))
plpy.error(f'Error creating user: {user} {status}'.format(user=_payload['email'], status=r.status_code))
return None
else:
#print("Created user : {u}]".format(u=_payload['email']))
plpy.notice('Created user : {u} {t}, {l}'.format(u=_payload['email'], t=r.text, l=r.headers['location']))
user_url = "{user_url}/execute-actions-email".format(user_url=r.headers['location'])
_payload = ["UPDATE_PASSWORD"]
#plpy.notice(_payload)
data = json.dumps(_payload)
r = requests.put(user_url, headers=_headers, data=data, timeout=(5, 60))
if r.status_code != 204:
plpy.error('Error execute-actions-email: {u} {s}'.format(u=_user['email'], s=r.status_code))
else:
plpy.notice('execute-actions-email: {u} {s}'.format(u=_user['email'], s=r.status_code))
return None
else:
plpy.error(f'Error getting admin access_token: {status}'.format(status=r.status_code))
return None
$keycloak_auth_py$ strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.keycloak_auth_py_fn
IS 'Create an oauth user into keycloak using plpython3u';
CREATE OR REPLACE FUNCTION windy_pws_py_fn(IN metric JSONB,
IN _user JSONB, IN app JSONB) RETURNS JSONB
AS $windy_pws_py$
"""
Send environment data from boat instruments to Windy as a Personal Weather Station (PWS)
https://community.windy.com/topic/8168/report-your-weather-station-data-to-windy
"""
import requests
import json
import decimal
if not 'app.windy_apikey' in app and not app['app.windy_apikey']:
plpy.error('Error no windy_apikey defined, check app settings')
return none
if not 'station' in metric and not metric['station']:
plpy.error('Error no metrics defined')
return none
if not 'temp' in metric and not metric['temp']:
plpy.error('Error no metrics defined')
return none
if not _user:
plpy.error('Error no user defined, check user settings')
return none
_headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com', 'Content-Type': 'application/json'}
_payload = {
'stations': [
{ 'station': int(decimal.Decimal(metric['station'])),
'name': metric['name'],
'shareOption': 'Open',
'type': 'SignalK PostgSail Plugin',
'provider': 'PostgSail',
'url': 'https://iot.openplotter.cloud/{name}/monitoring'.format(name=metric['name']),
'lat': float(decimal.Decimal(metric['lat'])),
'lon': float(decimal.Decimal(metric['lon'])),
'elevation': 1 }
],
'observations': [
{ 'station': int(decimal.Decimal(metric['station'])),
'temp': float(decimal.Decimal(metric['temp'])),
'wind': round(float(decimal.Decimal(metric['wind']))),
'gust': round(float(decimal.Decimal(metric['wind']))),
'winddir': int(decimal.Decimal(metric['winddir'])),
'pressure': int(decimal.Decimal(metric['pressure'])),
'rh': float(decimal.Decimal(metric['rh'])) }
]}
#print(_payload)
#plpy.notice(_payload)
data = json.dumps(_payload)
api_url = 'https://stations.windy.com/pws/update/{api_key}'.format(api_key=app['app.windy_apikey'])
r = requests.post(api_url, data=data, headers=_headers, timeout=(5, 60))
#print(r.text)
#plpy.notice(api_url)
if r.status_code == 200:
#print('Data sent successfully!')
plpy.notice('Data sent successfully to Windy!')
#plpy.notice(api_url)
if not 'windy' in _user['settings']:
api_url = 'https://stations.windy.com/pws/station/{api_key}/{station}'.format(api_key=app['app.windy_apikey'], station=metric['station'])
#print(r.text)
#plpy.notice(api_url)
r = requests.get(api_url, timeout=(5, 60))
if r.status_code == 200:
#print('Windy Personal Weather Station created successfully in Windy Stations!')
plpy.notice('Windy Personal Weather Station created successfully in Windy Stations!')
return r.json()
else:
plpy.error(f'Failed to gather PWS details. Status code: {r.status_code}')
else:
plpy.error(f'Failed to send data. Status code: {r.status_code}')
#print(f'Failed to send data. Status code: {r.status_code}')
#print(r.text)
return {}
$windy_pws_py$ strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.windy_pws_py_fn
IS 'Forward vessel data to Windy as a Personal Weather Station using plpython3u';

View File

@@ -1,910 +0,0 @@
---------------------------------------------------------------------------
-- singalk db public schema
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
CREATE SCHEMA IF NOT EXISTS public;
COMMENT ON SCHEMA public IS 'backend functions';
---------------------------------------------------------------------------
-- python reverse_geocode
--
-- https://github.com/CartoDB/labs-postgresql/blob/master/workshop/plpython.md
--
CREATE TABLE IF NOT EXISTS geocoders(
name TEXT UNIQUE,
url TEXT,
reverse_url TEXT
);
-- Description
COMMENT ON TABLE
public.geocoders
IS 'geo service nominatim url';
INSERT INTO geocoders VALUES
('nominatim',
NULL,
'https://nominatim.openstreetmap.org/reverse');
DROP FUNCTION IF EXISTS reverse_geocode_py_fn;
CREATE OR REPLACE FUNCTION reverse_geocode_py_fn(IN geocoder TEXT, IN lon NUMERIC, IN lat NUMERIC,
OUT geo_name TEXT)
AS $reverse_geocode_py$
import requests
# Use the shared cache to avoid preparing the geocoder metadata
if geocoder in SD:
plan = SD[geocoder]
# A prepared statement from Python
else:
plan = plpy.prepare("SELECT reverse_url AS url FROM geocoders WHERE name = $1", ["text"])
SD[geocoder] = plan
# Execute the statement with the geocoder param and limit to 1 result
rv = plpy.execute(plan, [geocoder], 1)
url = rv[0]['url']
# Make the request to the geocoder API
payload = {"lon": lon, "lat": lat, "format": "jsonv2", "zoom": 18}
r = requests.get(url, params=payload)
# Return the full address or nothing if not found
if r.status_code == 200 and "name" in r.json():
return r.json()["name"]
else:
plpy.error('Failed to received a geo full address %s', r.json())
return 'unknow'
$reverse_geocode_py$ LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.reverse_geocode_py_fn
IS 'query reverse geo service to return location name';
---------------------------------------------------------------------------
-- python template email/pushover
--
CREATE TABLE IF NOT EXISTS email_templates(
name TEXT UNIQUE,
email_subject TEXT,
email_content TEXT,
pushover_title TEXT,
pushover_message TEXT
);
-- Description
COMMENT ON TABLE
public.email_templates
IS 'email/message templates for notifications';
-- with escape value, eg: E'A\nB\r\nC'
-- https://stackoverflow.com/questions/26638615/insert-line-break-in-postgresql-when-updating-text-field
INSERT INTO email_templates VALUES
('logbook',
'New Logbook Entry',
E'Hello __RECIPIENT__,\n\nWe just wanted to let you know that you have a new entry on openplotter.cloud: "__LOGBOOK_NAME__"\r\n\r\nSee more details at __LOGBOOK_LINK__\n\nHappy sailing!\nThe Saillogger Team',
'New Logbook Entry',
E'We just wanted to let you know that you have a new entry on openplotter.cloud: "__LOGBOOK_NAME__"\r\n\r\nSee more details at __LOGBOOK_LINK__\n\nHappy sailing!\nThe Saillogger Team'),
('user',
'Welcome',
E'Hello __RECIPIENT__,\nCongratulations!\nYou successfully created an account.\nKeep in mind to register your vessel.\nHappy sailing!',
'Welcome',
E'Hi!\nYou successfully created an account\nKeep in mind to register your vessel.\nHappy sailing!'),
('vessel',
'New vessel',
E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.',
'New vessel',
E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.'),
('monitor_offline',
'Offline',
E'__BOAT__ has been offline for more than an hour\r\nFind more details at https://openplotter.cloud/boats/\n',
'Offline',
E'__BOAT__ has been offline for more than an hour\r\nFind more details at https://openplotter.cloud/boats/\n'),
('monitor_online',
'Online',
E'__BOAT__ just came online\nFind more details at https://openplotter.cloud/boats/\n',
'Online',
E'__BOAT__ just came online\nFind more details at https://openplotter.cloud/boats/\n'),
('badge',
'New Badge!',
E'Hello __RECIPIENT__,\nCongratulations! You have just unlocked a new badge: __BADGE_NAME__\nSee more details at https://openplotter.cloud/badges\nHappy sailing!\nThe Saillogger Team',
'New Badge!',
E'Congratulations!\nYou have just unlocked a new badge: __BADGE_NAME__\nSee more details at https://openplotter.cloud/badges\nHappy sailing!\nThe Saillogger Team');
---------------------------------------------------------------------------
-- python send email
--
-- TODO read table from python or send email data as params?
-- https://www.programcreek.com/python/example/3684/email.utils.formatdate
DROP FUNCTION IF EXISTS send_email_py_fn;
CREATE OR REPLACE FUNCTION send_email_py_fn(IN email_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void
AS $send_email_py$
# Import smtplib for the actual sending function
import smtplib
# Import the email modules we need
#from email.message import EmailMessage
from email.utils import formatdate,make_msgid
from email.mime.text import MIMEText
# Use the shared cache to avoid preparing the email metadata
if email_type in SD:
plan = SD[email_type]
# A prepared statement from Python
else:
plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"])
SD[email_type] = plan
# Execute the statement with the email_type param and limit to 1 result
rv = plpy.execute(plan, [email_type], 1)
email_subject = rv[0]['email_subject']
email_content = rv[0]['email_content']
# Replace fields using input jsonb obj
plpy.notice('Parameters [{}] [{}]'.format(_user, app))
if not _user or not app:
plpy.error('Error no parameters')
return None
if 'logbook_name' in _user and _user['logbook_name']:
email_content = email_content.replace('__LOGBOOK_NAME__', _user['logbook_name'])
if 'recipient' in _user and _user['recipient']:
email_content = email_content.replace('__RECIPIENT__', _user['recipient'])
if 'boat' in _user and _user['boat']:
email_content = email_content.replace('__BOAT__', _user['boat'])
if 'badge' in _user and _user['badge']:
email_content = email_content.replace('__BADGE_NAME__', _user['badge'])
email_from = 'root@localhost'
if 'app.email_from' in app and app['app.email_from']:
email_from = app['app.email_from']
#plpy.notice('Sending email from [{}] [{}]'.format(email_from, app['app.email_from']))
email_to = 'root@localhost'
if 'email' in _user and _user['email']:
email_to = _user['email']
#plpy.notice('Sending email to [{}] [{}]'.format(email_to, _user['email']))
else:
plpy.error('Error email to')
return None
msg = MIMEText(email_content, 'plain', 'utf-8')
msg["Subject"] = email_subject
msg["From"] = email_from
msg["To"] = email_to
msg["Date"] = formatdate()
msg["Message-ID"] = make_msgid()
server_smtp = 'localhost'
if 'app.email_server' in app and app['app.email_server']:
server_smtp = app['app.email_server']
# Send the message via our own SMTP server.
try:
# send your message with credentials specified above
with smtplib.SMTP(server_smtp, 25) as server:
if 'app.email_user' in app and app['app.email_user'] \
and 'app.email_pass' in app and app['app.email_pass']:
server.starttls()
server.login(app['app.email_user'], app['app.email_pass'])
#server.send_message(msg)
server.sendmail(msg["To"], msg["From"], msg.as_string())
server.quit()
# tell the script to report if your message was sent or which errors need to be fixed
plpy.notice('Sent email successfully to [{}] [{}]'.format(msg["To"], msg["Subject"]))
return None
except OSError as error :
plpy.error(error)
except smtplib.SMTPConnectError:
plpy.error('Failed to connect to the server. Bad connection settings?')
except smtplib.SMTPServerDisconnected:
plpy.error('Failed to connect to the server. Wrong user/password?')
except smtplib.SMTPException as e:
plpy.error('SMTP error occurred: ' + str(e))
$send_email_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.send_email_py_fn
IS 'Send email notification using plpython3u';
---------------------------------------------------------------------------
-- python send pushover
--
-- TODO read app and user key from table?
-- https://pushover.net/
DROP FUNCTION IF EXISTS send_pushover_py_fn;
CREATE OR REPLACE FUNCTION send_pushover_py_fn(IN message_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void
AS $send_pushover_py$
import requests
# Use the shared cache to avoid preparing the email metadata
if message_type in SD:
plan = SD[message_type]
# A prepared statement from Python
else:
plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"])
SD[message_type] = plan
# Execute the statement with the message_type param and limit to 1 result
rv = plpy.execute(plan, [message_type], 1)
pushover_title = rv[0]['pushover_title']
pushover_message = rv[0]['pushover_message']
# Replace fields using input jsonb obj
if 'logbook_name' in _user and _user['logbook_name']:
pushover_message = pushover_message.replace('__LOGBOOK_NAME__', _user['logbook_name'])
if 'recipient' in _user and _user['recipient']:
pushover_message = pushover_message.replace('__RECIPIENT__', _user['recipient'])
if 'boat' in _user and _user['boat']:
pushover_message = pushover_message.replace('__BOAT__', _user['boat'])
if 'badge' in _user and _user['badge']:
pushover_message = pushover_message.replace('__BADGE_NAME__', _user['badge'])
pushover_token = None
if 'app.pushover_token' in app and app['app.pushover_token']:
pushover_token = app['app.pushover_token']
else:
plpy.error('Error no pushover token defined, check app settings')
return None
pushover_user = None
if 'pushover_key' in _user and _user['pushover_key']:
pushover_user = _user['pushover_key']
else:
plpy.error('Error no pushover user token defined, check user settings')
return None
# requests
r = requests.post("https://api.pushover.net/1/messages.json", data = {
"token": pushover_token,
"user": pushover_user,
"title": pushover_title,
"message": pushover_message
})
#print(r.text)
# Return the full address or None if not found
plpy.notice('Sent pushover successfully to [{}] [{}]'.format(r.text, r.status_code))
if r.status_code == 200:
plpy.notice('Sent pushover successfully to [{}] [{}] [{}]'.format("__USER__", pushover_title, r.text))
else:
plpy.error('Failed to send pushover')
return None
$send_pushover_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.send_pushover_py_fn
IS 'Send pushover notification using plpython3u';
---------------------------------------------------------------------------
-- Functions public schema
--
-- Update a logbook with avg data
-- TODO using timescale function
CREATE OR REPLACE FUNCTION logbook_update_avg_fn(
IN _id integer,
IN _start TEXT,
IN _end TEXT,
OUT avg_speed double precision,
OUT max_speed double precision,
OUT max_wind_speed double precision
) AS $logbook_update_avg$
BEGIN
RAISE NOTICE '-> Updating avg for logbook id=%, start: "%", end: "%"', _id, _start, _end;
SELECT AVG(speedOverGround), MAX(speedOverGround), MAX(windspeedapparent) INTO
avg_speed, max_speed, max_wind_speed
FROM api.metrics
WHERE time >= _start::TIMESTAMP WITHOUT TIME ZONE AND
time <= _end::TIMESTAMP WITHOUT TIME ZONE;
RAISE NOTICE '-> Updated avg for logbook id=%, avg_speed:%, max_speed:%, max_wind_speed:%', _id, avg_speed, max_speed, max_wind_speed;
END;
$logbook_update_avg$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.logbook_update_avg_fn
IS 'Update logbook details with calculate average and max data, AVG(speedOverGround), MAX(speedOverGround), MAX(windspeedapparent)';
-- Create a LINESTRING for Geometry
-- Todo validate st_length unit?
-- https://postgis.net/docs/ST_Length.html
CREATE FUNCTION logbook_update_geom_distance_fn(IN _id integer, IN _start text, IN _end text,
OUT _track_geom Geometry(LINESTRING),
OUT _track_distance double precision
) AS $logbook_geo_distance$
BEGIN
SELECT ST_MakeLine(
ARRAY(
--SELECT ST_SetSRID(ST_MakePoint(longitude,latitude),4326) as geo_point
SELECT st_makepoint(longitude,latitude) AS geo_point
FROM api.metrics m
WHERE m.latitude IS NOT NULL
AND m.longitude IS NOT NULL
AND m.time >= _start::TIMESTAMP WITHOUT TIME ZONE
AND m.time <= _end::TIMESTAMP WITHOUT TIME ZONE
ORDER BY m.time ASC
)
) INTO _track_geom;
RAISE NOTICE '-> GIS LINESTRING %', _track_geom;
-- SELECT ST_Length(_track_geom,false) INTO _track_distance;
-- SELECT TRUNC (st_length(st_transform(track_geom,4326)::geography)::INT / 1.852) from logbook where id = 209; -- in NM
SELECT TRUNC (ST_Length(_track_geom,false)::INT / 1.852) INTO _track_distance; -- in NM
RAISE NOTICE '-> GIS Length %', _track_distance;
END;
$logbook_geo_distance$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.logbook_update_geom_distance_fn
IS 'Update logbook details with geometry data an distance, ST_Length';
-- Update pending new logbook from process queue
DROP FUNCTION IF EXISTS process_logbook_queue_fn;
CREATE OR REPLACE FUNCTION process_logbook_queue_fn(IN _id integer) RETURNS void AS $process_logbook_queue$
DECLARE
logbook_rec record;
from_name varchar;
to_name varchar;
log_name varchar;
avg_rec record;
geo_rec record;
user_settings jsonb;
app_settings jsonb;
BEGIN
-- If _id is not NULL
SELECT * INTO logbook_rec
FROM api.logbook
WHERE active IS false
AND id = _id;
-- geo reverse _from_lng _from_lat
-- geo reverse _to_lng _to_lat
from_name := reverse_geocode_py_fn('nominatim', logbook_rec._from_lng::NUMERIC, logbook_rec._from_lat::NUMERIC);
to_name := reverse_geocode_py_fn('nominatim', logbook_rec._to_lng::NUMERIC, logbook_rec._to_lat::NUMERIC);
SELECT CONCAT(from_name, ' to ' , to_name) INTO log_name;
-- SELECT CONCAT("_from" , ' to ' ,"_to") from api.logbook where id = 1;
-- Generate logbook name, concat _from_location and to _to_locacion
-- Update logbook entry with the latest metric data and calculate data
avg_rec := logbook_update_avg_fn(logbook_rec.id, logbook_rec._from_time::TEXT, logbook_rec._to_time::TEXT);
geo_rec := logbook_update_geom_distance_fn(logbook_rec.id, logbook_rec._from_time::TEXT, logbook_rec._to_time::TEXT);
-- todo check on time start vs end
RAISE NOTICE 'Updating logbook entry [%] [%] [%]', logbook_rec.id, logbook_rec._from_time, logbook_rec._to_time;
UPDATE api.logbook
SET
duration = (logbook_rec._to_time::timestamp without time zone - logbook_rec._from_time::timestamp without time zone),
avg_speed = avg_rec.avg_speed,
max_speed = avg_rec.max_speed,
max_wind_speed = avg_rec.max_wind_speed,
_from = from_name,
_to = to_name,
name = log_name,
track_geom = geo_rec._track_geom,
distance = geo_rec._track_distance
WHERE id = logbook_rec.id;
-- Gather email and pushover app settings
app_settings := get_app_settings_fn();
-- Gather user settings
user_settings := get_user_settings_from_log_fn(logbook_rec::RECORD);
--user_settings := '{"logbook_name": "' || log_name || '"}, "{"email": "' || account_rec.email || '", "recipient": "' || account_rec.first || '}';
--user_settings := '{"logbook_name": "' || log_name || '"}';
-- Send notification email, pushover
--PERFORM send_notification('logbook'::TEXT, logbook_rec::RECORD);
PERFORM send_email_py_fn('logbook'::TEXT, user_settings::JSONB, app_settings::JSONB);
--PERFORM send_pushover_py_fn('logbook'::TEXT, user_settings::JSONB, app_settings::JSONB);
END;
$process_logbook_queue$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.process_logbook_queue_fn
IS 'Update logbook details when completed, logbook_update_avg_fn, logbook_update_geom_distance_fn, reverse_geocode_py_fn';
-- Update pending new stay from process queue
DROP FUNCTION IF EXISTS process_stay_queue_fn;
CREATE OR REPLACE FUNCTION process_stay_queue_fn(IN _id integer) RETURNS void AS $process_stay_queue$
DECLARE
stay_rec record;
_name varchar;
BEGIN
RAISE WARNING 'process_stay_queue_fn';
RAISE WARNING 'jwt %', current_setting('request.jwt.claims', true);
RAISE WARNING 'cur_user %', current_user;
-- If _id is not NULL
SELECT * INTO stay_rec
FROM api.stays
WHERE id = _id;
-- AND client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%';
-- geo reverse _lng _lat
_name := reverse_geocode_py_fn('nominatim', stay_rec.longitude::NUMERIC, stay_rec.latitude::NUMERIC);
RAISE NOTICE 'Updating stay entry [%]', stay_rec.id;
UPDATE api.stays
SET
name = _name,
geog = Geography(ST_MakePoint(stay_rec.longitude, stay_rec.latitude))
WHERE id = stay_rec.id;
-- Notification email/pushover?
END;
$process_stay_queue$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.process_stay_queue_fn
IS 'Update stay details, reverse_geocode_py_fn';
-- Handle moorage insert or update from stays
-- todo valide geography unit
-- https://postgis.net/docs/ST_DWithin.html
DROP FUNCTION IF EXISTS process_moorage_queue_fn;
CREATE OR REPLACE FUNCTION process_moorage_queue_fn(IN _id integer) RETURNS void AS $process_moorage_queue$
DECLARE
stay_rec record;
moorage_rec record;
BEGIN
-- If _id is not NULL
SELECT * INTO stay_rec
FROM api.stays
WHERE active IS false
AND departed IS NOT NULL
AND id = _id;
FOR moorage_rec in
SELECT
*
FROM api.moorages
WHERE
latitude IS NOT NULL
AND longitude IS NOT NULL
AND ST_DWithin(
-- Geography(ST_MakePoint(stay_rec._lng, stay_rec._lat)),
stay_rec.geog,
-- Geography(ST_MakePoint(longitude, latitude)),
geog,
100 -- in meters ?
)
ORDER BY id ASC
LOOP
-- found previous stay within 100m of the new moorage
IF moorage_rec.id IS NOT NULL AND moorage_rec.id > 0 THEN
RAISE NOTICE 'Found previous stay within 100m of moorage %', moorage_rec;
EXIT; -- exit loop
END IF;
END LOOP;
-- if with in 100m update reference count and stay duration
-- else insert new entry
IF moorage_rec.id IS NOT NULL AND moorage_rec.id > 0 THEN
RAISE NOTICE 'Update moorage %', moorage_rec;
UPDATE api.moorages
SET
reference_count = moorage_rec.reference_count + 1,
stay_duration =
moorage_rec.stay_duration +
(stay_rec.departed::timestamp without time zone - stay_rec.arrived::timestamp without time zone)
WHERE id = moorage_rec.id;
else
RAISE NOTICE 'Insert new moorage entry from stay %', stay_rec;
-- Ensure the stay as a name
IF stay_rec.name IS NULL THEN
stay_rec.name := reverse_geocode_py_fn('nominatim', stay_rec.longitude::NUMERIC, stay_rec.latitude::NUMERIC);
END IF;
-- Insert new moorage from stay
INSERT INTO api.moorages
(client_id, name, stay_id, stay_code, stay_duration, reference_count, latitude, longitude, geog)
VALUES (
stay_rec.client_id,
stay_rec.name,
stay_rec.id,
stay_rec.stay_code,
(stay_rec.departed::timestamp without time zone - stay_rec.arrived::timestamp without time zone),
1, -- default reference_count
stay_rec.latitude,
stay_rec.longitude,
Geography(ST_MakePoint(stay_rec.longitude, stay_rec.latitude))
);
END IF;
END;
$process_moorage_queue$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.process_moorage_queue_fn
IS 'Handle moorage insert or update from stays';
-- process new account notification
DROP FUNCTION IF EXISTS process_account_queue_fn;
CREATE OR REPLACE FUNCTION process_account_queue_fn(IN _email TEXT) RETURNS void AS $process_account_queue$
DECLARE
account_rec record;
user_settings jsonb;
app_settings jsonb;
BEGIN
-- If _email is not NULL
SELECT * INTO account_rec
FROM auth.accounts
WHERE email = _email;
-- Gather email and pushover app settings
app_settings := get_app_settings_fn();
-- Gather user settings
user_settings := '{"email": "' || account_rec.email || '", "recipient": "' || account_rec.first || '"}';
-- Send notification email, pushover
--PERFORM send_notification_fn('user'::TEXT, account_rec::RECORD);
PERFORM send_email_py_fn('user'::TEXT, user_settings::JSONB, app_settings::JSONB);
--PERFORM send_pushover_py_fn('user'::TEXT, user_settings::JSONB, app_settings::JSONB);
END;
$process_account_queue$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.process_account_queue_fn
IS 'process new account notification';
-- process new vessel notification
DROP FUNCTION IF EXISTS process_vessel_queue_fn;
CREATE OR REPLACE FUNCTION process_vessel_queue_fn(IN _email TEXT) RETURNS void AS $process_vessel_queue$
DECLARE
vessel_rec record;
user_settings jsonb;
app_settings jsonb;
BEGIN
-- If _email is not NULL
SELECT * INTO vessel_rec
FROM auth.vessels
WHERE owner_email = _email;
-- Gather user_settings from
-- if notification email
-- -- Send email
--
-- Gather email and pushover app settings
app_settings := get_app_settings_fn();
-- Gather user settings
user_settings := '{"email": "' || vessel_rec.owner_email || '", "boat": "' || vessel_rec.name || '"}';
--user_settings := get_user_settings_from_clientid_fn();
-- Send notification email, pushover
--PERFORM send_notification_fn('vessel'::TEXT, vessel_rec::RECORD);
PERFORM send_email_py_fn('vessel'::TEXT, user_settings::JSONB, app_settings::JSONB);
--PERFORM send_pushover_py_fn('vessel'::TEXT, user_settings::JSONB, app_settings::JSONB);
END;
$process_vessel_queue$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.process_vessel_queue_fn
IS 'process new vessel notification';
-- Get user settings details from a log entry
DROP FUNCTION IF EXISTS get_app_settings_fn;
CREATE OR REPLACE FUNCTION get_app_settings_fn(OUT app_settings JSON) RETURNS JSON
AS $get_app_settings$
DECLARE
BEGIN
SELECT jsonb_object_agg(name,value) INTO app_settings
FROM public.app_settings
WHERE name LIKE '%app.email%' OR name LIKE '%app.pushover%';
END;
$get_app_settings$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.get_app_settings_fn
IS 'get app settings details, email, pushover';
-- Get user settings details from a log entry
DROP FUNCTION IF EXISTS get_user_settings_from_log_fn;
CREATE OR REPLACE FUNCTION get_user_settings_from_log_fn(IN logbook_rec RECORD, OUT user_settings JSON) RETURNS JSON
AS $get_user_settings_from_log$
DECLARE
BEGIN
-- If client_id is not NULL
IF logbook_rec.client_id IS NULL OR logbook_rec.client_id = '' THEN
RAISE WARNING '-> get_user_settings_from_log_fn invalid input %', logbook_rec.client_id;
END IF;
SELECT
json_build_object(
'boat' , v.name,
'recipient', a.first,
'email', v.owner_email,
'logbook_name', l.name) INTO user_settings
FROM auth.accounts a, auth.vessels v, api.metadata m, api.logbook l
WHERE lower(a.email) = lower(v.owner_email)
-- AND lower(v.name) = lower(m.name)
AND m.client_id = l.client_id
AND l.client_id = logbook_rec.client_id
AND l.id = logbook_rec.id;
END;
$get_user_settings_from_log$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.get_user_settings_from_log_fn
IS 'get user settings details from a log entry, initiate for logbook entry notification';
-- Get user settings details from a metadata entry
DROP FUNCTION IF EXISTS get_user_settings_from_metadata;
CREATE OR REPLACE FUNCTION get_user_settings_from_metadata_fn(IN meta_id INTEGER, OUT user_settings JSON) RETURNS JSON
AS $get_user_settings_from_metadata$
DECLARE
BEGIN
-- If meta_id is not NULL
IF meta_id IS NULL OR meta_id < 1 THEN
RAISE WARNING '-> get_user_settings_from_metadata_fn invalid input %', meta_id;
END IF;
SELECT json_build_object(
'boat' , v.name,
'email', v.owner_email) INTO user_settings
FROM auth.vessels v, api.metadata m
WHERE
--lower(v.name) = lower(m.name) AND
m.id = meta_id;
END;
$get_user_settings_from_metadata$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.get_user_settings_from_metadata_fn
IS 'get user settings details from a metadata entry, initiate for monitoring offline,online notification';
-- Get user settings details from a metadata entry
DROP FUNCTION IF EXISTS send_notification_fn;
CREATE OR REPLACE FUNCTION send_notification_fn(IN email_type TEXT, IN notification_rec RECORD) RETURNS JSON
AS $send_notification$
DECLARE
app_settings JSONB;
BEGIN
-- Gather email and pushover app settings
app_settings := get_app_settings_fn();
-- Gather user settings
--user_settings := '{"email": "' || vessel_rec.owner_email || '", "boat": "' || vessel_rec.name || '}';
--user_settings := get_user_settings_from_clientid_fn();
--user_settings := '{"email": "' || account_rec.email || '", "recipient": "' || account_rec.first || '}';
--user_settings := get_user_settings_from_metadata_fn();
--user_settings := '{"logbook_name": "' || log_name || '"}';
--user_settings := get_user_settings_from_log_fn();
-- Send notification email
PERFORM send_email_py_fn(email_type::TEXT, user_settings::JSONB, app_settings::JSONB);
-- Send notification pushover
--PERFORM send_pushover_py_fn(email_type::TEXT, user_settings::JSONB, app_settings::JSONB);
END;
$send_notification$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.send_notification_fn
IS 'TODO';
DROP FUNCTION IF EXISTS get_user_settings_from_clientid_fn;
CREATE OR REPLACE FUNCTION get_user_settings_from_clientid_fn(
IN clientid TEXT,
IN logbook_name TEXT,
OUT user_settings JSON
) RETURNS JSON
AS $get_user_settings_from_clientid$
DECLARE
BEGIN
-- If client_id is not NULL
IF clientid IS NULL OR clientid <> '' THEN
RAISE WARNING '-> get_user_settings_from_clientid_fn invalid input %', clientid;
END IF;
SELECT
json_build_object(
'boat' , v.name,
'recipient', a.first,
'email', v.owner_email ,
'settings', a.preferences,
'pushover_key', a.preferences->'pushover_key',
'badges', a.preferences->'badges',
'logbook_name', logbook_name ) INTO user_settings
FROM auth.accounts a, auth.vessels v, api.metadata m
WHERE lower(a.email) = lower(v.owner_email)
--AND lower(v.name) = lower(m.name)
AND m.mmsi = v.mmsi
AND m.client_id = clientid;
END;
$get_user_settings_from_clientid$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION
public.get_user_settings_from_clientid_fn
IS 'get user settings details from a clientid, initiate for badge entry notification';
---------------------------------------------------------------------------
-- Queue handling
--
-- https://gist.github.com/kissgyorgy/beccba1291de962702ea9c237a900c79
-- https://www.depesz.com/2012/06/13/how-to-send-mail-from-database/
-- Listen/Notify way
--create function new_logbook_entry() returns trigger as $$
--begin
-- perform pg_notify('new_logbook_entry', NEW.id::text);
-- return NEW;
--END;
--$$ language plpgsql;
-- table way
CREATE TABLE IF NOT EXISTS public.process_queue (
id SERIAL PRIMARY KEY,
channel TEXT NOT NULL,
payload TEXT NOT NULL,
stored timestamptz NOT NULL,
processed timestamptz
);
COMMENT ON TABLE
public.process_queue
IS 'process queue for async job';
CREATE INDEX ON public.process_queue (channel);
CREATE INDEX ON public.process_queue (processed);
create function new_account_entry_fn() returns trigger as $new_account_entry$
begin
insert into process_queue (channel, payload, stored) values ('new_account', NEW.email, now());
return NEW;
END;
$new_account_entry$ language plpgsql;
create function new_vessel_entry_fn() returns trigger as $new_vessel_entry$
begin
insert into process_queue (channel, payload, stored) values ('new_vessel', NEW.owner_email, now());
return NEW;
END;
$new_vessel_entry$ language plpgsql;
---------------------------------------------------------------------------
-- App settings
-- https://dba.stackexchange.com/questions/27296/storing-application-settings-with-different-datatypes#27297
-- https://stackoverflow.com/questions/6893780/how-to-store-site-wide-settings-in-a-database
-- http://cvs.savannah.gnu.org/viewvc/*checkout*/gnumed/gnumed/gnumed/server/sql/gmconfiguration.sql
CREATE TABLE IF NOT EXISTS public.app_settings (
name TEXT NOT NULL UNIQUE,
value TEXT NOT NULL
);
COMMENT ON TABLE public.app_settings IS 'application settings';
COMMENT ON COLUMN public.app_settings.name IS 'application settings name key';
COMMENT ON COLUMN public.app_settings.value IS 'application settings value';
---------------------------------------------------------------------------
-- Badges descriptions
-- TODO add contiditions
--
CREATE TABLE IF NOT EXISTS badges(
name TEXT UNIQUE,
description TEXT
);
-- Description
COMMENT ON TABLE
public.badges
IS 'Badges descriptions';
INSERT INTO badges VALUES
('Helmsman',
'Nice work logging your first sail! You are officially a helmsman now!'),
('Wake Maker',
'Yowzers! Welcome to the 15 knot+ club ya speed demon skipper!'),
('Explorer',
'It looks like home is where the helm is. Cheers to 10 days away from home port!'),
('Mooring Pro',
'It takes a lot of skill to "thread that floating needle" but seems like you have mastered mooring with 10 nights on buoy!'),
('Anchormaster',
'Hook, line and sinker, you have this anchoring thing down! 25 days on the hook for you!'),
('Traveler',
'Who needs to fly when one can sail! You are an international sailor. À votre santé!'),
('Stormtrooper',
'Just like the elite defenders of the Empire, here you are, our braving your own hydro-empire in windspeeds above 30kts. Nice work trooper! '),
('Club Alaska',
'Home to the bears, glaciers, midnight sun and high adventure. Welcome to the Club Alaska Captain!'),
('Tropical Traveler',
'Look at you with your suntan, tropical drink and southern latitude!'),
('Aloha Award',
'Ticking off over 2300 NM across the great blue Pacific makes you the rare recipient of the Aloha Award. Well done and Aloha sailor!'),
('Tyee',
'You made it to the Tyee Outstation, the friendliest dock in Pacific Northwest!'),
-- TODO the sea is big and the world is not limited to the US
('Mediterranean Traveler',
'You made it trought the Mediterranean!');
create function public.process_badge_queue_fn() RETURNS void AS $process_badge_queue$
declare
badge_rec record;
badges_arr record;
begin
SELECT json_array_elements_text((a.preferences->'badges')::json) from auth.accounts a;
FOR badge_rec in
SELECT
name
FROM badges
LOOP
-- found previous stay within 100m of the new moorage
IF moorage_rec.id IS NOT NULL AND moorage_rec.id > 0 THEN
RAISE NOTICE 'Found previous stay within 100m of moorage %', moorage_rec;
EXIT; -- exit loop
END IF;
END LOOP;
-- Helmsman
-- select count(l.id) api.logbook l where count(l.id) = 1;
-- Wake Maker
-- select max(l.max_wind_speed) api.logbook l where l.max_wind_speed >= 15;
-- Explorer
-- select sum(m.stay_duration) api.stays s where home_flag is false;
-- Mooring Pro
-- select sum(m.stay_duration) api.stays s where stay_code = 3;
-- Anchormaster
-- select sum(m.stay_duration) api.stays s where stay_code = 2;
-- Traveler
-- todo country to country.
-- Stormtrooper
-- select max(l.max_wind_speed) api.logbook l where l.max_wind_speed >= 30;
-- Club Alaska
-- todo country zone
-- Tropical Traveler
-- todo country zone
-- Aloha Award
-- todo pacific zone
-- TODO the sea is big and the world is not limited to the US
END
$process_badge_queue$ language plpgsql;
---------------------------------------------------------------------------
-- TODO add alert monitoring for Battery
---------------------------------------------------------------------------
-- TODO db-pre-request = "public.check_jwt"
-- Prevent unregister user or unregister vessel access
CREATE OR REPLACE FUNCTION public.check_jwt() RETURNS void AS $$
DECLARE
_role name;
_email name;
_mmsi name;
account_rec record;
vessel_rec record;
BEGIN
RAISE WARNING 'jwt %', current_setting('request.jwt.claims', true);
SELECT current_setting('request.jwt.claims', true)::json->>'email' INTO _email;
SELECT current_setting('request.jwt.claims', true)::json->>'role' INTO _role;
--RAISE WARNING 'jwt email %', current_setting('request.jwt.claims', true)::json->>'email';
--RAISE WARNING 'jwt role %', current_setting('request.jwt.claims', true)::json->>'role';
--RAISE WARNING 'cur_user %', current_user;
IF _role = 'user_role' THEN
-- Check the user exist in the accounts table
SELECT * INTO account_rec
FROM auth.accounts
WHERE auth.accounts.email = _email;
IF account_rec.email IS NULL THEN
RAISE EXCEPTION 'Invalid user'
USING HINT = 'Unkown user';
END IF;
ELSIF _role = 'vessel_role' THEN
-- Check the vessel and user exist
SELECT * INTO vessel_rec
FROM auth.vessels, auth.accounts
WHERE auth.vessels.owner_email = _email
AND auth.accounts.email = _email;
IF vessel_rec.owner_email IS NULL THEN
RAISE EXCEPTION 'Invalid vessel'
USING HINT = 'Unkown vessel owner_email';
END IF;
SELECT current_setting('request.jwt.claims', true)::json->>'mmsi' INTO _mmsi;
IF vessel_rec.mmsi IS NULL OR vessel_rec.mmsi <> _mmsi THEN
RAISE EXCEPTION 'Invalid vessel'
USING HINT = 'Unkown vessel mmsi';
END IF;
PERFORM set_config('vessel.mmsi', vessel_rec.mmsi, false);
RAISE WARNING 'vessel.mmsi %', current_setting('vessel.mmsi', false);
ELSIF _role <> 'api_anonymous' THEN
RAISE EXCEPTION 'Invalid role'
USING HINT = 'Stop being so evil and maybe you can log in';
END IF;
END
$$ language plpgsql security definer;
-- Function to trigger cron_jobs using API for tests.
-- Todo limit access and permision
-- Run con jobs
CREATE OR REPLACE FUNCTION api.run_cron_jobs() RETURNS void AS $$
BEGIN
-- In correct order
perform public.cron_process_new_account_fn();
perform public.cron_process_new_vessel_fn();
perform public.cron_process_monitor_online_fn();
perform public.cron_process_new_logbook_fn();
perform public.cron_process_new_stay_fn();
perform public.cron_process_new_moorage_fn();
perform public.cron_process_monitor_offline_fn();
END
$$ language plpgsql security definer;

View File

@@ -15,41 +15,101 @@ CREATE SCHEMA IF NOT EXISTS auth;
COMMENT ON SCHEMA auth IS 'auth postgrest for users and vessels';
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- provides functions to generate universally unique identifiers (UUIDs)
CREATE EXTENSION IF NOT EXISTS "moddatetime"; -- provides functions for tracking last modification time
CREATE EXTENSION IF NOT EXISTS "citext"; -- provides data type for case-insensitive character strings
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- provides cryptographic functions
DROP TABLE IF EXISTS auth.accounts CASCADE;
CREATE TABLE IF NOT EXISTS auth.accounts (
-- id UUID DEFAULT uuid_generate_v4() NOT NULL,
email text primary key check ( email ~* '^.+@.+\..+$' ),
first text not null check (length(pass) < 512),
last text not null check (length(pass) < 512),
pass text not null check (length(pass) < 512),
role name not null check (length(role) < 512),
preferences JSONB null,
created_at TIMESTAMP WITHOUT TIME ZONE default NOW()
id INT UNIQUE GENERATED ALWAYS AS IDENTITY,
--id TEXT NOT NULL UNIQUE DEFAULT uuid_generate_v7(),
user_id TEXT NOT NULL UNIQUE DEFAULT RIGHT(gen_random_uuid()::text, 12),
email CITEXT PRIMARY KEY CHECK ( email ~* '^.+@.+\..+$' ),
first TEXT NOT NULL CHECK (length(pass) < 512),
last TEXT NOT NULL CHECK (length(pass) < 512),
pass TEXT NOT NULL CHECK (length(pass) < 512),
role name NOT NULL CHECK (length(role) < 512),
preferences JSONB NULL DEFAULT '{"email_notifications":true}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
connected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT valid_email CHECK (length(email) > 5), -- Enforce at least 5 char, eg: a@b.io
CONSTRAINT valid_first CHECK (length(first) > 1),
CONSTRAINT valid_last CHECK (length(last) > 1),
CONSTRAINT valid_pass CHECK (length(pass) > 4)
);
-- Preferences jsonb
---- PushOver Notification, bool
---- PushOver user key, varchar
---- Email notification, bool
---- Instagram Handle, varchar
---- Timezone, TZ
---- Unit, bool
---- Preferred Homepage
---- Website, varchar or text
---- Public Profile
---- References to users ?
-- Description
COMMENT ON TABLE
auth.accounts
IS 'users account table';
-- Indexes
CREATE INDEX accounts_preferences_idx ON auth.accounts USING GIN (preferences);
COMMENT ON COLUMN auth.accounts.first IS 'User first name with CONSTRAINT CHECK';
COMMENT ON COLUMN auth.accounts.last IS 'User last name with CONSTRAINT CHECK';
CREATE TRIGGER accounts_moddatetime
BEFORE UPDATE ON auth.accounts
FOR EACH ROW
EXECUTE PROCEDURE moddatetime (updated_at);
-- Description
COMMENT ON TRIGGER accounts_moddatetime
ON auth.accounts
IS 'Automatic update of updated_at on table modification';
DROP TABLE IF EXISTS auth.vessels;
CREATE TABLE IF NOT EXISTS auth.vessels (
-- vesselId UUID PRIMARY KEY REFERENCES auth.accounts(id) ON DELETE RESTRICT,
owner_email TEXT PRIMARY KEY REFERENCES auth.accounts(email) ON DELETE RESTRICT,
mmsi TEXT UNIQUE,
name TEXT,
-- owner_email TEXT,
pass UUID,
vessel_id TEXT NOT NULL UNIQUE DEFAULT RIGHT(gen_random_uuid()::text, 12),
-- user_id TEXT NOT NULL REFERENCES auth.accounts(user_id) ON DELETE RESTRICT,
owner_email CITEXT PRIMARY KEY REFERENCES auth.accounts(email) ON DELETE RESTRICT,
mmsi NUMERIC UNIQUE, -- MMSI can be optional but if present must be a valid one and unique
name TEXT NOT NULL CHECK (length(name) >= 3 AND length(name) < 512),
role name not null check (length(role) < 512),
created_at TIMESTAMP WITHOUT TIME ZONE default NOW()
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
CONSTRAINT valid_range_mmsi CHECK (mmsi > 100000000 AND mmsi < 800000000)
);
-- Description
COMMENT ON TABLE
auth.vessels
IS 'vessels table link to accounts email user_id column';
COMMENT ON COLUMN
auth.vessels.mmsi
IS 'MMSI can be optional but if present must be a valid one and unique but must be in numeric range between 100000000 and 800000000';
CREATE TRIGGER vessels_moddatetime
BEFORE UPDATE ON auth.vessels
FOR EACH ROW
EXECUTE PROCEDURE moddatetime (updated_at);
-- Description
COMMENT ON TRIGGER vessels_moddatetime
ON auth.vessels
IS 'Automatic update of updated_at on table modification';
CREATE TABLE auth.users (
id NAME PRIMARY KEY DEFAULT current_setting('request.jwt.claims', true)::json->>'sub',
email NAME NOT NULL DEFAULT current_setting('request.jwt.claims', true)::json->>'email',
user_id TEXT NOT NULL UNIQUE DEFAULT RIGHT(gen_random_uuid()::text, 12),
first TEXT NOT NULL DEFAULT current_setting('request.jwt.claims', true)::json->>'given_name',
last TEXT NOT NULL DEFAULT current_setting('request.jwt.claims', true)::json->>'family_name',
role NAME NOT NULL DEFAULT 'user_role' CHECK (length(role) < 512),
preferences JSONB NULL DEFAULT '{"email_notifications":true, "email_valid": true, "email_verified": true}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
connected_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Description
COMMENT ON TABLE
auth.users
IS 'Keycloak Oauth user, map user details from access token';
CREATE TRIGGER user_moddatetime
BEFORE UPDATE ON auth.users
FOR EACH ROW
EXECUTE PROCEDURE moddatetime (updated_at);
-- Description
COMMENT ON TRIGGER user_moddatetime
ON auth.users
IS 'Automatic update of updated_at on table modification';
create or replace function
auth.check_role_exists() returns trigger as $$
@@ -69,21 +129,48 @@ create constraint trigger ensure_user_role_exists
after insert or update on auth.accounts
for each row
execute procedure auth.check_role_exists();
-- Description
COMMENT ON TRIGGER ensure_user_role_exists
ON auth.accounts
IS 'ensure user role exists';
-- trigger add queue new account
CREATE TRIGGER new_account_entry AFTER INSERT ON auth.accounts
FOR EACH ROW EXECUTE FUNCTION public.new_account_entry_fn();
-- Description
COMMENT ON TRIGGER new_account_entry
ON auth.accounts
IS 'Add new account in process_queue for further processing';
-- trigger add queue new account OTP validation
CREATE TRIGGER new_account_otp_validation_entry AFTER INSERT ON auth.accounts
FOR EACH ROW EXECUTE FUNCTION public.new_account_otp_validation_entry_fn();
-- Description
COMMENT ON TRIGGER new_account_otp_validation_entry
ON auth.accounts
IS 'Add new account OTP validation in process_queue for further processing';
-- trigger check role on vessel
drop trigger if exists ensure_user_role_exists on auth.vessels;
create constraint trigger ensure_user_role_exists
drop trigger if exists ensure_vessel_role_exists on auth.vessels;
create constraint trigger ensure_vessel_role_exists
after insert or update on auth.vessels
for each row
execute procedure auth.check_role_exists();
-- trigger add queue new vessel
CREATE TRIGGER new_vessel_entry AFTER INSERT ON auth.vessels
FOR EACH ROW EXECUTE FUNCTION public.new_vessel_entry_fn();
-- Description
COMMENT ON TRIGGER new_vessel_entry
ON auth.vessels
IS 'Add new vessel in process_queue for further processing';
create extension if not exists pgcrypto;
-- trigger add new vessel name as public_vessel user configuration
CREATE TRIGGER new_vessel_public AFTER INSERT ON auth.vessels
FOR EACH ROW EXECUTE FUNCTION public.new_vessel_public_fn();
-- Description
COMMENT ON TRIGGER new_vessel_public
ON auth.vessels
IS 'Add new vessel name as public_vessel user configuration';
create or replace function
auth.encrypt_pass() returns trigger as $$
@@ -113,6 +200,7 @@ begin
return (
select role from auth.accounts
where accounts.email = user_role.email
and user_role.pass is NOT NULL
and accounts.pass = crypt(user_role.pass, accounts.pass)
);
end;
@@ -133,11 +221,17 @@ declare
_role name;
result auth.jwt_token;
app_jwt_secret text;
_email_valid boolean := false;
_email text := email;
_user_id text := null;
begin
-- check email and password
select auth.user_role(email, pass) into _role;
if _role is null then
raise invalid_password using message = 'invalid user or password';
-- HTTP/403
--raise invalid_password using message = 'invalid user or password';
-- HTTP/401
raise insufficient_privilege using message = 'invalid user or password';
end if;
-- Get app_jwt_secret
@@ -145,13 +239,25 @@ begin
FROM app_settings
WHERE name = 'app.jwt_secret';
-- Check email_valid and generate OTP
SELECT preferences['email_valid'],user_id INTO _email_valid,_user_id
FROM auth.accounts a
WHERE a.email = _email;
IF _email_valid is null or _email_valid is False THEN
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('email_otp', email, now(), _user_id);
END IF;
--RAISE WARNING 'api.login debug: [%],[%],[%]', app_jwt_secret, _role, login.email;
-- Generate jwt
select jwt.sign(
-- row_to_json(r), ''
-- row_to_json(r)::json, current_setting('app.jwt_secret')::text
row_to_json(r)::json, app_jwt_secret
) as token
from (
select _role as role, login.email as email,
select _role as role, login.email as email, -- TODO replace with user_id
-- select _role as role, user_id as uid, -- add support in check_jwt
extract(epoch from now())::integer + 60*60 as exp
) r
into result;
@@ -165,17 +271,113 @@ api.signup(in email text, in pass text, in firstname text, in lastname text) ret
declare
_role name;
begin
IF email IS NULL OR email = ''
OR pass IS NULL OR pass = '' THEN
RAISE EXCEPTION 'Invalid input'
USING HINT = 'Check your parameter';
END IF;
-- check email and password
select auth.user_role(email, pass) into _role;
if _role is null then
RAISE WARNING 'Register new account email:[%]', email;
INSERT INTO auth.accounts ( email, pass, first, last, role)
VALUES (email, pass, firstname, lastname, 'user_role');
-- TODO replace preferences default into table rather than trigger
INSERT INTO auth.accounts ( email, pass, first, last, role, preferences)
VALUES (email, pass, firstname, lastname, 'user_role', '{"email_notifications":true}');
end if;
return ( api.login(email, pass) );
end;
$$ language plpgsql security definer;
---------------------------------------------------------------------------
-- API account Oauth functions
--
-- oauth is on your exposed schema
create or replace function
api.oauth() returns void as $$
declare
_exist boolean;
begin
-- Ensure we have the required key/value in the access token
if current_setting('request.jwt.claims', true)::json->>'sub' is null OR
current_setting('request.jwt.claims', true)::json->>'email' is null THEN
return;
end if;
-- check email exist
select exists( select email from auth.users
where id = current_setting('request.jwt.claims', true)::json->>'sub'
) INTO _exist;
if NOT FOUND then
RAISE WARNING 'Register new oauth user email:[%]', current_setting('request.jwt.claims', true)::json->>'email';
-- insert new user, default value from the oauth access token
INSERT INTO auth.users (role, preferences)
VALUES ('user_role', '{"email_notifications":true, "email_valid": true, "email_verified": true}');
end if;
end;
$$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.oauth
IS 'openid/oauth user register entry point';
create or replace function
api.oauth_vessel(in _mmsi text, in _name text) returns void as $$
declare
_exist boolean;
vessel_name text := _name;
vessel_mmsi text := _mmsi;
_vessel_id text := null;
vessel_rec record;
app_settings jsonb;
_user_id text := null;
begin
RAISE WARNING 'oauth_vessel:[%]', current_setting('user.email', true);
RAISE WARNING 'oauth_vessel:[%]', current_setting('request.jwt.claims', true)::json->>'email';
-- Ensure we have the required key/value in the access token
if current_setting('request.jwt.claims', true)::json->>'sub' is null OR
current_setting('request.jwt.claims', true)::json->>'email' is null THEN
return;
end if;
-- check email exist
select exists( select email from auth.accounts
where email = current_setting('request.jwt.claims', true)::json->>'email'
) INTO _exist;
if _exist is False then
RAISE WARNING 'Register new oauth user email:[%]', current_setting('request.jwt.claims', true)::json->>'email';
-- insert new user, default value from the oauth access token
INSERT INTO auth.users VALUES(DEFAULT) RETURNING user_id INTO _user_id;
-- insert new user to account table from the oauth access token
INSERT INTO auth.accounts (email, first, last, pass, user_id, role, preferences)
VALUES (current_setting('request.jwt.claims', true)::json->>'email',
current_setting('request.jwt.claims', true)::json->>'given_name',
current_setting('request.jwt.claims', true)::json->>'family_name',
current_setting('request.jwt.claims', true)::json->>'sub',
_user_id, 'user_role', '{"email_notifications":true, "email_valid": true, "email_verified": true}');
end if;
IF public.isnumeric(vessel_mmsi) IS False THEN
vessel_mmsi = NULL;
END IF;
-- check vessel exist
SELECT * INTO vessel_rec
FROM auth.vessels vessel
WHERE vessel.owner_email = current_setting('request.jwt.claims', true)::json->>'email';
IF vessel_rec IS NULL THEN
RAISE WARNING 'Register new vessel name:[%] mmsi:[%] for [%]', vessel_name, vessel_mmsi, current_setting('request.jwt.claims', true)::json->>'email';
INSERT INTO auth.vessels (owner_email, mmsi, name, role)
VALUES (current_setting('request.jwt.claims', true)::json->>'email', vessel_mmsi::NUMERIC, vessel_name, 'vessel_role') RETURNING vessel_id INTO _vessel_id;
-- Gather url from app settings
app_settings := get_app_settings_fn();
-- set oauth user vessel_id attributes for token generation
PERFORM keycloak_py_fn(current_setting('request.jwt.claims', true)::json->>'sub'::TEXT, _vessel_id::TEXT, app_settings);
END IF;
end;
$$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.oauth_vessel
IS 'user and vessel register entry point from signalk plugin';
---------------------------------------------------------------------------
-- API vessel helper functions
-- register_vessel should be on your exposed schema
@@ -185,21 +387,28 @@ declare
result auth.jwt_token;
app_jwt_secret text;
vessel_rec record;
_vessel_id text;
begin
IF vessel_email IS NULL OR vessel_email = ''
OR vessel_name IS NULL OR vessel_name = '' THEN
RAISE EXCEPTION 'Invalid input'
USING HINT = 'Check your parameter';
END IF;
IF public.isnumeric(vessel_mmsi) IS False THEN
vessel_mmsi = NULL;
END IF;
-- check vessel exist
SELECT * INTO vessel_rec
FROM auth.vessels vessel
WHERE vessel.owner_email = vessel_email
AND vessel.mmsi = vessel_mmsi
AND LOWER(vessel.name) = LOWER(vessel_name);
if vessel_rec is null then
WHERE vessel.owner_email = vessel_email;
IF vessel_rec IS NULL THEN
RAISE WARNING 'Register new vessel name:[%] mmsi:[%] for [%]', vessel_name, vessel_mmsi, vessel_email;
INSERT INTO auth.vessels (owner_email, mmsi, name, role)
VALUES (vessel_email, vessel_mmsi, vessel_name, 'vessel_role');
VALUES (vessel_email, vessel_mmsi::NUMERIC, vessel_name, 'vessel_role') RETURNING vessel_id INTO _vessel_id;
vessel_rec.role := 'vessel_role';
vessel_rec.owner_email = vessel_email;
vessel_rec.mmsi = vessel_mmsi;
end if;
vessel_rec.vessel_id = _vessel_id;
END IF;
-- Get app_jwt_secret
SELECT value INTO app_jwt_secret
@@ -211,8 +420,9 @@ begin
) as token
from (
select vessel_rec.role as role,
vessel_rec.owner_email as email,
vessel_rec.mmsi as mmsi
vessel_rec.owner_email as email, -- TODO replace with user_id
-- vessel_rec.user_id as uid
vessel_rec.vessel_id as vid
) r
into result;
return result;

View File

@@ -0,0 +1,356 @@
---------------------------------------------------------------------------
-- signalk db api schema
-- View and Function that have dependency with auth schema
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- Link auth.vessels with api.metadata
--ALTER TABLE api.metadata ADD vessel_id TEXT NOT NULL REFERENCES auth.vessels(vessel_id) ON DELETE RESTRICT;
ALTER TABLE api.metadata ADD FOREIGN KEY (vessel_id) REFERENCES auth.vessels(vessel_id) ON DELETE RESTRICT;
COMMENT ON COLUMN api.metadata.vessel_id IS 'Link auth.vessels with api.metadata via FOREIGN KEY and REFERENCES';
-- Link auth.vessels with auth.accounts
--ALTER TABLE auth.vessels ADD user_id TEXT NOT NULL REFERENCES auth.accounts(user_id) ON DELETE RESTRICT;
--COMMENT ON COLUMN auth.vessels.user_id IS 'Link auth.vessels with auth.accounts';
--COMMENT ON COLUMN auth.vessels.vessel_id IS 'Vessel identifier. Link auth.vessels with api.metadata';
-- REFERENCE ship type with AIS type ?
-- REFERENCE mmsi MID with country ?
ALTER TABLE api.logbook ADD FOREIGN KEY (_from_moorage_id) REFERENCES api.moorages(id) ON DELETE RESTRICT;
COMMENT ON COLUMN api.logbook._from_moorage_id IS 'Link api.moorages with api.logbook via FOREIGN KEY and REFERENCES';
ALTER TABLE api.logbook ADD FOREIGN KEY (_to_moorage_id) REFERENCES api.moorages(id) ON DELETE RESTRICT;
COMMENT ON COLUMN api.logbook._to_moorage_id IS 'Link api.moorages with api.logbook via FOREIGN KEY and REFERENCES';
ALTER TABLE api.stays ADD FOREIGN KEY (moorage_id) REFERENCES api.moorages(id) ON DELETE RESTRICT;
COMMENT ON COLUMN api.stays.moorage_id IS 'Link api.moorages with api.stays via FOREIGN KEY and REFERENCES';
ALTER TABLE api.stays ADD FOREIGN KEY (stay_code) REFERENCES api.stays_at(stay_code) ON DELETE RESTRICT;
COMMENT ON COLUMN api.stays.stay_code IS 'Link api.stays_at with api.stays via FOREIGN KEY and REFERENCES';
ALTER TABLE api.moorages ADD FOREIGN KEY (stay_code) REFERENCES api.stays_at(stay_code) ON DELETE RESTRICT;
COMMENT ON COLUMN api.moorages.stay_code IS 'Link api.stays_at with api.moorages via FOREIGN KEY and REFERENCES';
-- List vessel
--TODO add geojson with position
DROP VIEW IF EXISTS api.vessels_view;
CREATE OR REPLACE VIEW api.vessels_view WITH (security_invoker=true,security_barrier=true) AS
WITH metadata AS (
SELECT COALESCE(
(SELECT m.time
FROM api.metadata m
WHERE m.vessel_id = current_setting('vessel.id')
)::TEXT ,
NULL ) as last_contact
)
SELECT
v.name as name,
v.mmsi as mmsi,
v.created_at as created_at,
m.last_contact as last_contact,
((NOW() AT TIME ZONE 'UTC' - m.last_contact::TIMESTAMPTZ) > INTERVAL '70 MINUTES') as offline,
(NOW() AT TIME ZONE 'UTC' - m.last_contact::TIMESTAMPTZ) as duration
FROM auth.vessels v, metadata m
WHERE v.owner_email = current_setting('user.email');
-- Description
COMMENT ON VIEW
api.vessels_view
IS 'Expose vessels listing to web api';
DROP FUNCTION IF EXISTS public.has_vessel_fn;
CREATE OR REPLACE FUNCTION public.has_vessel_fn() RETURNS BOOLEAN
AS $has_vessel$
DECLARE
BEGIN
-- Check a vessel and user exist
RETURN (
SELECT auth.vessels.name
FROM auth.vessels, auth.accounts
WHERE auth.vessels.owner_email = auth.accounts.email
AND auth.accounts.email = current_setting('user.email')
) IS NOT NULL;
END;
$has_vessel$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
public.has_vessel_fn
IS 'Check if user has a vessel register';
DROP FUNCTION IF EXISTS public.has_vessel_metadata_fn;
CREATE OR REPLACE FUNCTION public.has_vessel_metadata_fn() RETURNS BOOLEAN
AS $has_vessel_metadata$
DECLARE
BEGIN
-- Check a vessel metadata
RETURN (
SELECT m.vessel_id
FROM auth.accounts a, auth.vessels v, api.metadata m
WHERE m.vessel_id = v.vessel_id
AND auth.vessels.owner_email = auth.accounts.email
AND auth.accounts.email = current_setting('user.email')
) IS NOT NULL;
END;
$has_vessel_metadata$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
public.has_vessel_metadata_fn
IS 'Check if user has a vessel register';
-- Or function?
-- TODO Improve: return null until the vessel has sent metadata?
DROP FUNCTION IF EXISTS api.vessel_fn;
CREATE OR REPLACE FUNCTION api.vessel_fn(OUT vessel JSON) RETURNS JSON
AS $vessel$
DECLARE
BEGIN
SELECT
jsonb_build_object(
'name', coalesce(m.name, null),
'mmsi', coalesce(m.mmsi, null),
'created_at', v.created_at,
'first_contact', coalesce(m.created_at, null),
'last_contact', coalesce(m.time, null),
'geojson', coalesce(ST_AsGeoJSON(geojson_t.*)::json, null)
)::jsonb || api.vessel_details_fn()::jsonb
INTO vessel
FROM auth.vessels v, api.metadata m,
( select
current_setting('vessel.name') as name,
time,
courseovergroundtrue,
speedoverground,
anglespeedapparent,
longitude,latitude,
st_makepoint(longitude,latitude) AS geo_point
FROM api.metrics
WHERE
latitude IS NOT NULL
AND longitude IS NOT NULL
AND vessel_id = current_setting('vessel.id', false)
ORDER BY time DESC LIMIT 1
) AS geojson_t
WHERE
m.vessel_id = current_setting('vessel.id')
AND m.vessel_id = v.vessel_id;
--RAISE notice 'api.vessel_fn %', obj;
END;
$vessel$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.vessel_fn
IS 'Expose vessel details to API';
-- Export user settings
DROP FUNCTION IF EXISTS api.settings_fn;
CREATE OR REPLACE FUNCTION api.settings_fn(out settings json) RETURNS JSON
AS $user_settings$
BEGIN
select row_to_json(row)::json INTO settings
from (
select a.email, a.first, a.last, a.preferences, a.created_at,
INITCAP(CONCAT (LEFT(first, 1), ' ', last)) AS username,
public.has_vessel_fn() as has_vessel
--public.has_vessel_metadata_fn() as has_vessel_metadata,
from auth.accounts a
where email = current_setting('user.email')
) row;
END;
$user_settings$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.settings_fn
IS 'Expose user settings to API';
DROP FUNCTION IF EXISTS api.versions_fn;
CREATE OR REPLACE FUNCTION api.versions_fn() RETURNS JSON
AS $version$
DECLARE
_appv TEXT;
_sysv TEXT;
BEGIN
SELECT
value, rtrim(substring(version(), 0, 17)) AS sys_version into _appv,_sysv
FROM app_settings
WHERE name = 'app.version';
RETURN json_build_object('api_version', _appv,
'sys_version', _sysv,
'timescaledb', (SELECT extversion as timescaledb FROM pg_extension WHERE extname='timescaledb'),
'postgis', (SELECT extversion as postgis FROM pg_extension WHERE extname='postgis'),
'postgrest', (SELECT rtrim(substring(application_name from 'PostgREST [0-9.]+')) as postgrest FROM pg_stat_activity WHERE application_name ilike '%postgrest%' LIMIT 1));
END;
$version$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.versions_fn
IS 'Expose as a function, app and system version to API';
DROP VIEW IF EXISTS api.versions_view;
CREATE OR REPLACE VIEW api.versions_view AS
SELECT
value AS api_version,
--version() as sys_version
rtrim(substring(version(), 0, 17)) AS sys_version,
(SELECT extversion as timescaledb FROM pg_extension WHERE extname='timescaledb'),
(SELECT extversion as postgis FROM pg_extension WHERE extname='postgis'),
(SELECT rtrim(substring(application_name from 'PostgREST [0-9.]+')) as postgrest FROM pg_stat_activity WHERE application_name ilike '%postgrest%' limit 1)
FROM app_settings
WHERE name = 'app.version';
-- Description
COMMENT ON VIEW
api.versions_view
IS 'Expose as a table view app and system version to API';
DROP FUNCTION IF EXISTS api.update_user_preferences_fn;
-- Update/Add a specific user setting into preferences
CREATE OR REPLACE FUNCTION api.update_user_preferences_fn(IN key TEXT, IN value TEXT) RETURNS BOOLEAN AS
$update_user_preferences$
DECLARE
first_c TEXT := NULL;
last_c TEXT := NULL;
_value TEXT := value;
BEGIN
-- Is it the only way to check variable type?
-- Convert string to jsonb and skip type of json obj or integer or boolean
SELECT SUBSTRING(value, 1, 1),RIGHT(value, 1) INTO first_c,last_c;
IF first_c <> '{' AND last_c <> '}' AND public.isnumeric(value) IS False
AND public.isboolean(value) IS False THEN
--RAISE WARNING '-> first_c:[%] last_c:[%] pg_typeof:[%]', first_c,last_c,pg_typeof(value);
_value := to_jsonb(value)::jsonb;
END IF;
--RAISE WARNING '-> update_user_preferences_fn update preferences for user [%]', current_setting('request.jwt.claims', true)::json->>'email';
UPDATE auth.accounts
SET preferences =
jsonb_set(preferences::jsonb, key::text[], _value::jsonb)
WHERE
email = current_setting('user.email', true);
IF FOUND THEN
--RAISE WARNING '-> update_user_preferences_fn True';
RETURN True;
END IF;
--RAISE WARNING '-> update_user_preferences_fn False';
RETURN False;
END;
$update_user_preferences$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.update_user_preferences_fn
IS 'Update user preferences jsonb key pair value';
DROP FUNCTION IF EXISTS api.vessel_details_fn;
CREATE OR REPLACE FUNCTION api.vessel_details_fn() RETURNS JSON AS
$vessel_details$
DECLARE
BEGIN
RETURN ( WITH tbl AS (
SELECT mmsi,ship_type,length,beam,height,plugin_version,platform FROM api.metadata WHERE vessel_id = current_setting('vessel.id', false)
)
SELECT json_build_object(
'ship_type', (SELECT ais.description FROM aistypes ais, tbl t WHERE t.ship_type = ais.id),
'country', (SELECT mid.country FROM mid, tbl t WHERE LEFT(cast(t.mmsi as text), 3)::NUMERIC = mid.id),
'alpha_2', (SELECT o.alpha_2 FROM mid m, iso3166 o, tbl t WHERE LEFT(cast(t.mmsi as text), 3)::NUMERIC = m.id AND m.country_id = o.id),
'length', t.length,
'beam', t.beam,
'height', t.height,
'plugin_version', t.plugin_version,
'platform', t.platform)
FROM tbl t
);
END;
$vessel_details$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.vessel_details_fn
IS 'Return vessel details such as metadata (length,beam,height), ais type and country name and country iso3166-alpha-2';
DROP VIEW IF EXISTS api.eventlogs_view;
CREATE VIEW api.eventlogs_view WITH (security_invoker=true,security_barrier=true) AS
SELECT pq.*
FROM public.process_queue pq
WHERE channel <> 'pre_logbook' AND (ref_id = current_setting('user.id', true)
OR ref_id = current_setting('vessel.id', true))
ORDER BY id ASC;
-- Description
COMMENT ON VIEW
api.eventlogs_view
IS 'Event logs view';
DROP FUNCTION IF EXISTS api.update_logbook_observations_fn;
-- Update/Add a specific user observations into logbook
CREATE OR REPLACE FUNCTION api.update_logbook_observations_fn(IN _id INT, IN observations TEXT) RETURNS BOOLEAN AS
$update_logbook_observations$
DECLARE
BEGIN
-- Merge existing observations with the new observations objects
RAISE NOTICE '-> update_logbook_extra_fn id:[%] observations:[%]', _id, observations;
-- { 'observations': { 'seaState': -1, 'cloudCoverage': -1, 'visibility': -1 } }
UPDATE api.logbook SET extra = public.jsonb_recursive_merge(extra, observations::jsonb) WHERE id = _id;
IF FOUND IS True THEN
RETURN True;
END IF;
RETURN False;
END;
$update_logbook_observations$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.update_logbook_observations_fn
IS 'Update/Add logbook observations jsonb key pair value';
CREATE TYPE public_type AS ENUM ('public_logs', 'public_logs_list', 'public_timelapse', 'public_monitoring', 'public_stats');
CREATE or replace FUNCTION api.ispublic_fn(IN boat TEXT, IN _type TEXT, IN _id INTEGER DEFAULT NULL) RETURNS BOOLEAN AS $ispublic$
DECLARE
vessel TEXT := '^' || boat || '$';
anonymous BOOLEAN := False;
valid_public_type BOOLEAN := False;
public_logs BOOLEAN := False;
BEGIN
-- If boat is not NULL
IF boat IS NULL THEN
RAISE WARNING '-> ispublic_fn invalid input %', boat;
RETURN False;
END IF;
-- Check if public_type is valid enum
SELECT _type::name = any(enum_range(null::public_type)::name[]) INTO valid_public_type;
IF valid_public_type IS False THEN
-- Ignore entry if type is invalid
RAISE WARNING '-> ispublic_fn invalid input type %', _type;
RETURN False;
END IF;
RAISE WARNING '-> ispublic_fn _type [%], _id [%]', _type, _id;
IF _type ~ '^public_(logs|timelapse)$' AND _id > 0 THEN
WITH log as (
SELECT vessel_id from api.logbook l where l.id = _id
)
SELECT EXISTS (
SELECT l.vessel_id
FROM auth.accounts a, auth.vessels v, jsonb_each_text(a.preferences) as prefs, log l
WHERE v.vessel_id = l.vessel_id
AND a.email = v.owner_email
AND a.preferences->>'public_vessel'::text ~* vessel
AND prefs.key = _type::TEXT
AND prefs.value::BOOLEAN = true
) into anonymous;
RAISE WARNING '-> ispublic_fn public_logs output boat:[%], type:[%], result:[%]', boat, _type, anonymous;
IF anonymous IS True THEN
RETURN True;
END IF;
ELSE
SELECT EXISTS (
SELECT a.email
FROM auth.accounts a, jsonb_each_text(a.preferences) as prefs
WHERE a.preferences->>'public_vessel'::text ~* vessel
AND prefs.key = _type::TEXT
AND prefs.value::BOOLEAN = true
) into anonymous;
RAISE WARNING '-> ispublic_fn output boat:[%], type:[%], result:[%]', boat, _type, anonymous;
IF anonymous IS True THEN
RETURN True;
END IF;
END IF;
RETURN False;
END
$ispublic$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.ispublic_fn
IS 'Is web page publicly accessible by register boat name and/or logbook id';

View File

@@ -0,0 +1,513 @@
---------------------------------------------------------------------------
-- signalk db auth schema
-- View and Function that have dependency with auth schema
-- List current database
select current_database();
-- connect to the DB
\c signalk
DROP TABLE IF EXISTS auth.otp;
CREATE TABLE IF NOT EXISTS auth.otp (
-- update email type to CITEXT, https://www.postgresql.org/docs/current/citext.html
user_email CITEXT NOT NULL PRIMARY KEY REFERENCES auth.accounts(email) ON DELETE RESTRICT,
otp_pass TEXT NOT NULL,
otp_timestamp TIMESTAMPTZ DEFAULT NOW(),
otp_tries SMALLINT NOT NULL DEFAULT '0'
);
-- Description
COMMENT ON TABLE
auth.otp
IS 'Stores temporal otp code for up to 15 minutes';
-- Indexes
CREATE INDEX otp_pass_idx ON auth.otp (otp_pass);
-- Duplicate Indexes
--CREATE INDEX otp_user_email_idx ON auth.otp (user_email);
DROP FUNCTION IF EXISTS public.generate_uid_fn;
CREATE OR REPLACE FUNCTION public.generate_uid_fn(size INT) RETURNS TEXT
AS $generate_uid_fn$
DECLARE
characters TEXT := '0123456789';
bytes BYTEA := gen_random_bytes(size);
l INT := length(characters);
i INT := 0;
output TEXT := '';
BEGIN
WHILE i < size LOOP
output := output || substr(characters, get_byte(bytes, i) % l + 1, 1);
i := i + 1;
END LOOP;
RETURN output;
END;
$generate_uid_fn$ LANGUAGE plpgsql VOLATILE;
-- Description
COMMENT ON FUNCTION
public.generate_uid_fn
IS 'Generate a random digit';
-- gerenate a OTP code by email
-- Expose as an API endpoint
DROP FUNCTION IF EXISTS api.generate_otp_fn;
CREATE OR REPLACE FUNCTION api.generate_otp_fn(IN email TEXT) RETURNS TEXT
AS $generate_otp$
DECLARE
_email CITEXT := email;
_email_check TEXT := NULL;
_otp_pass VARCHAR(10) := NULL;
BEGIN
IF email IS NULL OR _email IS NULL OR _email = '' THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
SELECT lower(a.email) INTO _email_check FROM auth.accounts a WHERE a.email = _email;
IF _email_check IS NULL THEN
RETURN NULL;
END IF;
--SELECT substr(gen_random_uuid()::text, 1, 6) INTO otp_pass;
SELECT generate_uid_fn(6) INTO _otp_pass;
-- upsert - Insert or update otp code on conflit
INSERT INTO auth.otp (user_email, otp_pass)
VALUES (_email_check, _otp_pass)
ON CONFLICT (user_email) DO UPDATE SET otp_pass = _otp_pass, otp_timestamp = NOW();
RETURN _otp_pass;
END;
$generate_otp$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.generate_otp_fn
IS 'Generate otp code';
DROP FUNCTION IF EXISTS api.recover;
CREATE OR REPLACE FUNCTION api.recover(in email text) returns BOOLEAN
AS $recover_fn$
DECLARE
_email CITEXT := email;
_user_id TEXT := NULL;
otp_pass TEXT := NULL;
_reset_qs TEXT := NULL;
user_settings jsonb := NULL;
BEGIN
IF _email IS NULL OR _email = '' THEN
RAISE EXCEPTION 'Invalid input'
USING HINT = 'Check your parameter';
END IF;
SELECT user_id INTO _user_id FROM auth.accounts a WHERE a.email = _email;
IF NOT FOUND THEN
RAISE EXCEPTION 'Invalid input'
USING HINT = 'Check your parameter';
END IF;
-- Generate OTP
otp_pass := api.generate_otp_fn(email);
SELECT CONCAT('uuid=', _user_id, '&token=', otp_pass) INTO _reset_qs;
-- Enable email_notifications
PERFORM api.update_user_preferences_fn('{email_notifications}'::TEXT, True::TEXT);
-- Send email/notifications
user_settings := '{"email": "' || _email || '", "reset_qs": "' || _reset_qs || '"}';
PERFORM send_notification_fn('email_reset'::TEXT, user_settings::JSONB);
RETURN TRUE;
END;
$recover_fn$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.recover
IS 'Send recover password email to reset password';
DROP FUNCTION IF EXISTS api.reset;
CREATE OR REPLACE FUNCTION api.reset(in pass text, in token text, in uuid text) returns BOOLEAN
AS $reset_fn$
DECLARE
_email TEXT := NULL;
_pass TEXT := pass;
BEGIN
-- Check parameters
IF token IS NULL OR uuid IS NULL OR pass IS NULL THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
-- Verify token
SELECT auth.verify_otp_fn(token) INTO _email;
IF _email IS NOT NULL THEN
SELECT email INTO _email FROM auth.accounts WHERE user_id = uuid;
IF _email IS NULL THEN
RETURN False;
END IF;
-- Set user new password
UPDATE auth.accounts
SET pass = _pass
WHERE email = _email;
-- Enable email_validation into user preferences
PERFORM api.update_user_preferences_fn('{email_valid}'::TEXT, True::TEXT);
-- Enable email_notifications
PERFORM api.update_user_preferences_fn('{email_notifications}'::TEXT, True::TEXT);
-- Delete token when validated
DELETE FROM auth.otp
WHERE user_email = _email;
RETURN True;
END IF;
RETURN False;
END;
$reset_fn$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.reset
IS 'Reset user password base on otp code and user_id send by email from api.recover';
DROP FUNCTION IF EXISTS auth.verify_otp_fn;
CREATE OR REPLACE FUNCTION auth.verify_otp_fn(IN token TEXT) RETURNS TEXT
AS $verify_otp$
DECLARE
email TEXT := NULL;
BEGIN
IF token IS NULL THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
-- Token is valid 15 minutes
SELECT user_email INTO email
FROM auth.otp
WHERE otp_timestamp > NOW() AT TIME ZONE 'UTC' - INTERVAL '15 MINUTES'
AND otp_tries < 3
AND otp_pass = token;
RETURN email;
END;
$verify_otp$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
auth.verify_otp_fn
IS 'Verify OTP';
-- CRON to purge OTP older than 15 minutes
DROP FUNCTION IF EXISTS public.cron_process_prune_otp_fn;
CREATE OR REPLACE FUNCTION public.cron_process_prune_otp_fn() RETURNS void
AS $$
DECLARE
otp_rec record;
BEGIN
-- Purge OTP older than 15 minutes
RAISE NOTICE 'cron_process_prune_otp_fn';
FOR otp_rec in
SELECT *
FROM auth.otp
WHERE otp_timestamp < NOW() AT TIME ZONE 'UTC' - INTERVAL '15 MINUTES'
ORDER BY otp_timestamp desc
LOOP
RAISE NOTICE '-> cron_process_prune_otp_fn deleting expired otp for user [%]', otp_rec.user_email;
-- remove entry
DELETE FROM auth.otp
WHERE user_email = otp_rec.user_email;
RAISE NOTICE '-> cron_process_prune_otp_fn deleted expire otp for user [%]', otp_rec.user_email;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_process_prune_otp_fn
IS 'init by pg_cron to purge older than 15 minutes OTP token';
-- Email OTP validation
-- Expose as an API endpoint
DROP FUNCTION IF EXISTS api.email_fn;
CREATE OR REPLACE FUNCTION api.email_fn(IN token TEXT) RETURNS BOOLEAN
AS $email_validation$
DECLARE
_email TEXT := NULL;
BEGIN
-- Check parameters
IF token IS NULL THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
-- Verify token
SELECT auth.verify_otp_fn(token) INTO _email;
IF _email IS NOT NULL THEN
-- Check the email JWT token match the OTP email
IF current_setting('user.email', true) <> _email THEN
RETURN False;
END IF;
-- Set user email into env to allow RLS update
--PERFORM set_config('user.email', _email, false);
-- Enable email_validation into user preferences
PERFORM api.update_user_preferences_fn('{email_valid}'::TEXT, True::TEXT);
-- Enable email_notifications
PERFORM api.update_user_preferences_fn('{email_notifications}'::TEXT, True::TEXT);
-- Delete token when validated
DELETE FROM auth.otp
WHERE user_email = _email;
-- Disable to reduce spam
-- Send Notification async
--INSERT INTO process_queue (channel, payload, stored)
-- VALUES ('email_valid', _email, now());
RETURN True;
END IF;
RETURN False;
END;
$email_validation$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.email_fn
IS 'Store email_valid into user preferences if valid token/otp';
-- Pushover Subscription API
-- Web-Based Subscription Process
-- https://pushover.net/api/subscriptions#web
-- Expose as an API endpoint
CREATE OR REPLACE FUNCTION api.pushover_subscribe_link_fn(OUT pushover_link JSON) RETURNS JSON
AS $pushover_subscribe_link$
DECLARE
app_url text;
otp_code text;
pushover_app_url text;
success text;
failure text;
email text := current_setting('user.email', true);
BEGIN
--https://pushover.net/api/subscriptions#web
-- "https://pushover.net/subscribe/PostgSail-23uvrho1d5y6n3e"
-- + "?success=" + urlencode("https://beta.openplotter.cloud/api/rpc/pushover_fn?token=" + generate_otp_fn({{email}}))
-- + "&failure=" + urlencode("https://beta.openplotter.cloud/settings");
-- get app_url
SELECT
value INTO app_url
FROM
public.app_settings
WHERE
name = 'app.url';
-- get pushover url subscribe
SELECT
value INTO pushover_app_url
FROM
public.app_settings
WHERE
name = 'app.pushover_app_url';
-- Generate OTP
otp_code := api.generate_otp_fn(email);
-- On success redirect to API endpoint
SELECT CONCAT(
'?success=',
public.urlescape_py_fn(CONCAT(app_url,'/pushover?token=')),
otp_code)
INTO success;
-- On failure redirect to user settings, where he does come from
SELECT CONCAT(
'&failure=',
public.urlescape_py_fn(CONCAT(app_url,'/profile'))
) INTO failure;
SELECT json_build_object('link', CONCAT(pushover_app_url, success, failure)) INTO pushover_link;
END;
$pushover_subscribe_link$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.pushover_subscribe_link_fn
IS 'Generate Pushover subscription link';
-- Confirm Pushover Subscription
-- Web-Based Subscription Process
-- https://pushover.net/api/subscriptions#web
-- Expose as an API endpoint
DROP FUNCTION IF EXISTS api.pushover_fn;
CREATE OR REPLACE FUNCTION api.pushover_fn(IN token TEXT, IN pushover_user_key TEXT) RETURNS BOOLEAN
AS $pushover$
DECLARE
_email TEXT := NULL;
BEGIN
-- Check parameters
IF token IS NULL OR pushover_user_key IS NULL THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
-- Verify token
SELECT auth.verify_otp_fn(token) INTO _email;
IF _email IS NOT NULL THEN
-- Set user email into env to allow RLS update
PERFORM set_config('user.email', _email, false);
-- Add pushover_user_key into user preferences
PERFORM api.update_user_preferences_fn('{pushover_user_key}'::TEXT, pushover_user_key::TEXT);
-- Enable phone_notifications
PERFORM api.update_user_preferences_fn('{phone_notifications}'::TEXT, True::TEXT);
-- Delete token when validated
DELETE FROM auth.otp
WHERE user_email = _email;
-- Disable Notification because
-- Pushover send a notification when sucesssful with the description of the app
--
-- Send Notification async
--INSERT INTO process_queue (channel, payload, stored)
-- VALUES ('pushover_valid', _email, now());
RETURN True;
END IF;
RETURN False;
END;
$pushover$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.pushover_fn
IS 'Confirm Pushover Subscription and store pushover_user_key into user preferences if provide a valid OTP token';
-- Telegram OTP Validation
-- Expose as an API endpoint
DROP FUNCTION IF EXISTS api.telegram_fn;
CREATE OR REPLACE FUNCTION api.telegram_fn(IN token TEXT, IN telegram_obj TEXT) RETURNS BOOLEAN
AS $telegram$
DECLARE
_email TEXT := NULL;
user_settings jsonb;
BEGIN
-- Check parameters
IF token IS NULL OR telegram_obj IS NULL THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
-- Verify token
SELECT auth.verify_otp_fn(token) INTO _email;
IF _email IS NOT NULL THEN
-- Set user email into env to allow RLS update
PERFORM set_config('user.email', _email, false);
-- Add telegram obj into user preferences
PERFORM api.update_user_preferences_fn('{telegram}'::TEXT, telegram_obj::TEXT);
-- Delete token when validated
DELETE FROM auth.otp
WHERE user_email = _email;
-- Send Notification async
--INSERT INTO process_queue (channel, payload, stored)
-- VALUES ('telegram_valid', _email, now());
RETURN True;
END IF;
RETURN False;
END;
$telegram$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.telegram_fn
IS 'Confirm telegram user and store telegram chat details into user preferences if provide a valid OTP token';
-- Telegram user validation
DROP FUNCTION IF EXISTS auth.telegram_user_exists_fn;
CREATE OR REPLACE FUNCTION auth.telegram_user_exists_fn(IN email TEXT, IN user_id BIGINT) RETURNS BOOLEAN
AS $telegram_user_exists$
DECLARE
_email CITEXT := email;
_user_id BIGINT := user_id;
BEGIN
IF _email IS NULL OR _chat_id IS NULL THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
-- Does user and telegram obj
SELECT preferences->'telegram'->'from'->'id' INTO _user_id
FROM auth.accounts a
WHERE a.email = _email
AND cast(preferences->'telegram'->'from'->'id' as BIGINT) = _user_id::BIGINT;
IF FOUND THEN
RETURN TRUE;
END IF;
RETURN FALSE;
END;
$telegram_user_exists$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
auth.telegram_user_exists_fn
IS 'Check if user exist based on email and user_id';
-- Telegram otp validation
DROP FUNCTION IF EXISTS api.telegram_otp_fn;
CREATE OR REPLACE FUNCTION api.telegram_otp_fn(IN email TEXT, OUT otp_code TEXT) RETURNS TEXT
AS $telegram_otp$
DECLARE
_email CITEXT := email;
user_settings jsonb := NULL;
BEGIN
IF _email IS NULL THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
-- Generate token
otp_code := api.generate_otp_fn(_email);
IF otp_code IS NOT NULL THEN
-- Set user email into env to allow RLS update
PERFORM set_config('user.email', _email, false);
-- Send Notification
user_settings := '{"email": "' || _email || '", "otp_code": "' || otp_code || '"}';
PERFORM send_notification_fn('telegram_otp'::TEXT, user_settings::JSONB);
END IF;
END;
$telegram_otp$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.telegram_otp_fn
IS 'Telegram otp generation';
-- Telegram JWT auth
-- Expose as an API endpoint
-- Avoid sending a password so use email and chat_id as key pair
DROP FUNCTION IF EXISTS api.telegram;
CREATE OR REPLACE FUNCTION api.telegram(IN user_id BIGINT, IN email TEXT DEFAULT NULL) RETURNS auth.jwt_token
AS $telegram_jwt$
DECLARE
_email TEXT := email;
_user_id BIGINT := user_id;
_uid TEXT := NULL;
_exist BOOLEAN := False;
result auth.jwt_token;
app_jwt_secret text;
BEGIN
IF _user_id IS NULL THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
-- Check _user_id
SELECT auth.telegram_session_exists_fn(_user_id) into _exist;
IF _exist IS NULL OR _exist <> True THEN
--RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
RETURN NULL;
END IF;
-- Get email and user_id
SELECT a.email,a.user_id INTO _email,_uid
FROM auth.accounts a
WHERE cast(preferences->'telegram'->'from'->'id' as BIGINT) = _user_id::BIGINT;
-- Get app_jwt_secret
SELECT value INTO app_jwt_secret
FROM app_settings
WHERE name = 'app.jwt_secret';
-- Generate JWT token, force user_role
select jwt.sign(
row_to_json(r)::json, app_jwt_secret
) as token
from (
select 'user_role' as role,
(select lower(_email)) as email,
_uid as uid,
extract(epoch from now())::integer + 60*60 as exp
) r
into result;
return result;
END;
$telegram_jwt$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
api.telegram
IS 'Generate a JWT user_role token based on chat_id from telegram';
-- Telegram chat_id session validation
DROP FUNCTION IF EXISTS auth.telegram_session_exists_fn;
CREATE OR REPLACE FUNCTION auth.telegram_session_exists_fn(IN user_id BIGINT) RETURNS BOOLEAN
AS $telegram_session_exists$
DECLARE
_id BIGINT := NULL;
_user_id BIGINT := user_id;
_email TEXT := NULL;
BEGIN
IF user_id IS NULL THEN
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
END IF;
-- Find user email based on telegram chat_id
SELECT preferences->'telegram'->'from'->'id' INTO _id
FROM auth.accounts a
WHERE cast(preferences->'telegram'->'from'->'id' as BIGINT) = _user_id::BIGINT;
IF FOUND THEN
RETURN True;
END IF;
RETURN FALSE;
END;
$telegram_session_exists$ language plpgsql security definer;
-- Description
COMMENT ON FUNCTION
auth.telegram_session_exists_fn
IS 'Check if session/user exist based on user_id';

View File

@@ -1,156 +0,0 @@
---------------------------------------------------------------------------
-- singalk db permissions
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
---------------------------------------------------------------------------
-- Permissions roles
-- Users Sharing Role
-- https://postgrest.org/en/stable/auth.html#web-users-sharing-role
--
-- api_anonymous
-- nologin
-- api_anonymous role in the database with which to execute anonymous web requests.
-- api_anonymous allows JWT token generation with an expiration time via function api.login() from auth.accounts table
create role api_anonymous nologin noinherit;
grant usage on schema api to api_anonymous;
-- explicitly limit EXECUTE privileges to only signup and login functions
grant execute on function api.login(text,text) to api_anonymous;
grant execute on function api.signup(text,text,text,text) to api_anonymous;
-- explicitly limit EXECUTE privileges to pgrest db-pre-request function
grant execute on function public.check_jwt() to api_anonymous;
-- authenticator
-- login role
create role authenticator noinherit login password 'mysecretpassword';
grant api_anonymous to authenticator;
-- Grafana user and role with login, read-only
CREATE ROLE grafana WITH LOGIN PASSWORD 'mysecretpassword';
GRANT USAGE ON SCHEMA api TO grafana;
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO grafana;
GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata TO grafana;
-- User:
-- nologin
-- read-only for all and Read-Write on logbook, stays and moorage except for name COLUMN ?
CREATE ROLE user_role WITH NOLOGIN;
GRANT user_role to authenticator;
GRANT USAGE ON SCHEMA api TO user_role;
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO user_role;
GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata TO user_role;
-- Allow update on table for notes
GRANT UPDATE ON TABLE api.logbook,api.moorages,api.stays TO user_role;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO user_role;
-- explicitly limit EXECUTE privileges to pgrest db-pre-request function
GRANT EXECUTE ON FUNCTION public.check_jwt() to user_role;
-- Allow read on VIEWS
GRANT SELECT ON TABLE api.logs_view,api.moorages_view,api.stays_view TO user_role;
-- Vessel:
-- nologin
-- insert-update-only for api.metrics,api.logbook,api.moorages,api.stays,api.metadata and sequences and process_queue
CREATE ROLE vessel_role WITH NOLOGIN;
GRANT vessel_role to authenticator;
GRANT USAGE ON SCHEMA api TO vessel_role;
GRANT INSERT, UPDATE, SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata TO vessel_role;
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO vessel_role;
GRANT INSERT ON TABLE public.process_queue TO vessel_role;
GRANT USAGE, SELECT ON SEQUENCE public.process_queue_id_seq TO vessel_role;
-- explicitly limit EXECUTE privileges to pgrest db-pre-request function
GRANT EXECUTE ON FUNCTION public.check_jwt() to vessel_role;
-- TODO: currently cron function are run as super user, switch to scheduler role.
-- Scheduler read-only all, and write on logbook, stays, moorage, process_queue
-- Crons
CREATE ROLE scheduler WITH NOLOGIN;
GRANT scheduler to authenticator;
GRANT EXECUTE ON FUNCTION api.run_cron_jobs() to scheduler;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO scheduler;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO scheduler;
GRANT SELECT,UPDATE ON TABLE process_queue TO scheduler;
GRANT USAGE ON SCHEMA auth TO scheduler;
GRANT SELECT ON ALL TABLES IN SCHEMA auth TO scheduler;
---------------------------------------------------------------------------
-- Security policy
-- ROW LEVEL Security policy
ALTER TABLE api.metadata ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.metadata TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.metadata TO vessel_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.metadata TO user_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%');
ALTER TABLE api.metrics ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.metrics TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.metrics TO vessel_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.metrics TO user_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%');
-- Be sure to enable row level security on the table
ALTER TABLE api.logbook ENABLE ROW LEVEL SECURITY;
-- Create policies
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.logbook TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.logbook TO vessel_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.logbook TO user_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%');
-- Be sure to enable row level security on the table
ALTER TABLE api.stays ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.stays TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.stays TO vessel_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.stays TO user_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%');
-- Be sure to enable row level security on the table
ALTER TABLE api.moorages ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.moorages TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.moorages TO vessel_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.moorages TO user_role
USING (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%')
WITH CHECK (client_id LIKE '%' || current_setting('vessel.mmsi', false) || '%');

View File

@@ -0,0 +1,379 @@
---------------------------------------------------------------------------
-- singalk db permissions
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
---------------------------------------------------------------------------
-- Permissions roles
-- Users Sharing Role
-- https://postgrest.org/en/stable/auth.html#web-users-sharing-role
--
-- api_anonymous
-- nologin
-- api_anonymous role in the database with which to execute anonymous web requests, limit 20 connections
-- api_anonymous allows JWT token generation with an expiration time via function api.login() from auth.accounts table
create role api_anonymous WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOLOGIN NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 20;
comment on role api_anonymous is
'The role that PostgREST will switch to when a user is not authenticated.';
-- Limit to 20 connections
--alter user api_anonymous connection limit 20;
grant usage on schema api to api_anonymous;
-- explicitly limit EXECUTE privileges to only signup and login and reset functions
grant execute on function api.login(text,text) to api_anonymous;
grant execute on function api.signup(text,text,text,text) to api_anonymous;
grant execute on function api.recover(text) to api_anonymous;
grant execute on function api.reset(text,text,text) to api_anonymous;
-- explicitly limit EXECUTE privileges to pgrest db-pre-request function
grant execute on function public.check_jwt() to api_anonymous;
-- explicitly limit EXECUTE privileges to only telegram jwt auth function
grant execute on function api.telegram(bigint,text) to api_anonymous;
-- explicitly limit EXECUTE privileges to only pushover subscription validation function
grant execute on function api.email_fn(text) to api_anonymous;
grant execute on function api.pushover_fn(text,text) to api_anonymous;
grant execute on function api.telegram_fn(text,text) to api_anonymous;
grant execute on function api.telegram_otp_fn(text) to api_anonymous;
--grant execute on function api.generate_otp_fn(text) to api_anonymous;
grant execute on function api.ispublic_fn(text,text,integer) to api_anonymous;
grant execute on function api.timelapse_fn to api_anonymous;
grant execute on function api.stats_logs_fn to api_anonymous;
grant execute on function api.stats_stays_fn to api_anonymous;
grant execute on function api.status_fn to api_anonymous;
-- Allow read on TABLES on API schema
--GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata,api.stays_at TO api_anonymous;
-- Allow read on VIEWS on API schema
--GRANT SELECT ON TABLE api.logs_view,api.moorages_view,api.stays_view TO api_anonymous;
--GRANT SELECT ON TABLE api.log_view,api.moorage_view,api.stay_view,api.vessels_view TO api_anonymous;
GRANT SELECT ON ALL TABLES IN SCHEMA api TO api_anonymous;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO api_anonymous;
--grant execute on function public.st_asgeojson(record,text,integer,boolean) to api_anonymous;
--grant execute on function public.st_makepoint(float,float) to api_anonymous;
-- authenticator
-- login role
create role authenticator NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT login password 'mysecretpassword';
comment on role authenticator is
'Role that serves as an entry-point for API servers such as PostgREST.';
grant api_anonymous to authenticator;
-- Grafana user and role with login, read-only, limit 20 connections
CREATE ROLE grafana WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 20 LOGIN PASSWORD 'mysecretpassword';
comment on role grafana is
'Role that grafana will use for authenticated web users.';
-- Allow API schema and Tables
GRANT USAGE ON SCHEMA api TO grafana;
-- Allow read on SEQUENCE on API schema
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO grafana;
-- Allow read on TABLES on API schema
GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata,api.stays_at TO grafana;
-- Allow read on VIEWS on API schema
GRANT SELECT ON TABLE api.logs_view,api.moorages_view,api.stays_view TO grafana;
GRANT SELECT ON TABLE api.log_view,api.moorage_view,api.stay_view,api.vessels_view TO grafana;
GRANT SELECT ON TABLE api.monitoring_view,api.monitoring_view2,api.monitoring_view3 TO grafana;
GRANT SELECT ON TABLE api.monitoring_humidity,api.monitoring_voltage,api.monitoring_temperatures TO grafana;
-- Allow Auth schema and Tables
GRANT USAGE ON SCHEMA auth TO grafana;
GRANT SELECT ON TABLE auth.vessels TO grafana;
GRANT EXECUTE ON FUNCTION public.citext_eq(citext, citext) TO grafana;
-- Grafana_auth authenticator user and role with login, read-only on auth.accounts, limit 20 connections
CREATE ROLE grafana_auth WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 20 LOGIN PASSWORD 'mysecretpassword';
comment on role grafana_auth is
'Role that grafana auth proxy authenticator via apache.';
-- Allow read on VIEWS on API schema
GRANT USAGE ON SCHEMA api TO grafana_auth;
GRANT SELECT ON TABLE api.metadata TO grafana_auth;
-- Allow Auth schema and Tables
GRANT USAGE ON SCHEMA auth TO grafana_auth;
GRANT SELECT ON TABLE auth.accounts TO grafana_auth;
GRANT SELECT ON TABLE auth.vessels TO grafana_auth;
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO grafana_auth;
GRANT EXECUTE ON FUNCTION public.citext_eq(citext, citext) TO grafana_auth;
GRANT ALL ON SCHEMA public TO grafana_auth; -- Important if grafana database in pg
-- User:
-- nologin, web api only
-- read-only for all and Read on logbook, stays and moorage and Write only for specific (name, notes) COLUMNS
CREATE ROLE user_role WITH NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION;
comment on role user_role is
'Role that PostgREST will switch to for authenticated web users.';
GRANT user_role to authenticator;
GRANT USAGE ON SCHEMA api TO user_role;
-- Allow read on SEQUENCE on API schema
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO user_role;
-- Allow read on TABLES on API schema
GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata,api.stays_at TO user_role;
GRANT SELECT ON TABLE public.process_queue TO user_role;
-- To check?
GRANT SELECT ON TABLE auth.vessels TO user_role;
-- Allow users to update certain columns on specific TABLES on API schema
GRANT UPDATE (name, _from, _to, notes) ON api.logbook TO user_role;
GRANT UPDATE (name, notes, stay_code, active, departed) ON api.stays TO user_role;
GRANT UPDATE (name, notes, stay_code, home_flag) ON api.moorages TO user_role;
-- Allow users to remove logs and stays
GRANT DELETE ON api.logbook,api.stays,api.moorages TO user_role;
-- Allow EXECUTE on all FUNCTIONS on API and public schema
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO user_role;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO user_role;
-- pg15 feature security_invoker=true,security_barrier=true
GRANT SELECT ON TABLE api.logs_view,api.moorages_view,api.stays_view TO user_role;
GRANT SELECT ON TABLE api.log_view,api.moorage_view,api.stay_view,api.vessels_view TO user_role;
GRANT SELECT ON TABLE api.monitoring_view,api.monitoring_view2,api.monitoring_view3,api.explore_view TO user_role;
GRANT SELECT ON TABLE api.monitoring_humidity,api.monitoring_voltage,api.monitoring_temperatures TO user_role;
GRANT SELECT ON TABLE api.stats_moorages_away_view,api.versions_view TO user_role;
GRANT SELECT ON TABLE api.total_info_view TO user_role;
GRANT SELECT ON TABLE api.stats_logs_view TO user_role;
GRANT SELECT ON TABLE api.stats_moorages_view TO user_role;
GRANT SELECT ON TABLE api.eventlogs_view TO user_role;
GRANT SELECT ON TABLE api.vessels_view TO user_role;
GRANT SELECT ON TABLE api.moorages_stays_view TO user_role;
-- Vessel:
-- nologin
-- insert-update-only for api.metrics,api.logbook,api.moorages,api.stays,api.metadata and sequences and process_queue
CREATE ROLE vessel_role WITH NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION;
comment on role vessel_role is
'Role that PostgREST will switch to for authenticated web vessels.';
GRANT vessel_role to authenticator;
GRANT USAGE ON SCHEMA api TO vessel_role;
-- Allow read on SEQUENCE on API schema
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO vessel_role;
-- Allow read/write on TABLES on API schema
GRANT INSERT, UPDATE, SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata TO vessel_role;
GRANT INSERT ON TABLE public.process_queue TO vessel_role;
GRANT USAGE, SELECT ON SEQUENCE public.process_queue_id_seq TO vessel_role;
-- explicitly limit EXECUTE privileges to pgrest db-pre-request function
GRANT EXECUTE ON FUNCTION public.check_jwt() to vessel_role;
-- explicitly limit EXECUTE privileges to api.metrics triggers function
GRANT EXECUTE ON FUNCTION public.trip_in_progress_fn(text) to vessel_role;
GRANT EXECUTE ON FUNCTION public.stay_in_progress_fn(text) to vessel_role;
-- hypertable get_partition_hash ?!?
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA _timescaledb_internal TO vessel_role;
-- on metrics st_makepoint
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO vessel_role;
-- Oauth registration
GRANT EXECUTE ON FUNCTION api.oauth() TO vessel_role;
GRANT EXECUTE ON FUNCTION api.oauth_vessel(text,text) TO vessel_role;
--- Scheduler:
-- TODO: currently cron function are run as super user, switch to scheduler role.
-- Scheduler read-only all, and write on api.logbook, api.stays, api.moorages, public.process_queue, auth.otp
-- Crons
--CREATE ROLE scheduler WITH NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION;
CREATE ROLE scheduler WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 10 LOGIN;
comment on role scheduler is
'Role that pgcron will use to process logbook,moorages,stays,monitoring and notification.';
GRANT scheduler to authenticator;
GRANT USAGE ON SCHEMA api TO scheduler;
GRANT SELECT ON TABLE api.metrics,api.metadata TO scheduler;
GRANT INSERT, UPDATE, SELECT ON TABLE api.logbook,api.moorages,api.stays TO scheduler;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO scheduler;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO scheduler;
GRANT SELECT,UPDATE ON TABLE public.process_queue TO scheduler;
GRANT USAGE ON SCHEMA auth TO scheduler;
GRANT SELECT ON ALL TABLES IN SCHEMA auth TO scheduler;
GRANT SELECT,UPDATE,DELETE ON TABLE auth.otp TO scheduler;
---------------------------------------------------------------------------
-- Security policy
-- ROW LEVEL Security policy
ALTER TABLE api.metadata ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.metadata TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.metadata TO vessel_role
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.metadata TO user_role
USING (vessel_id = current_setting('vessel.id', true))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow scheduler to update and select based on the vessel.id
CREATE POLICY api_scheduler_role ON api.metadata TO scheduler
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow grafana to select based on email
CREATE POLICY grafana_role ON api.metadata TO grafana
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (false);
-- Allow grafana_auth to select
CREATE POLICY grafana_proxy_role ON api.metadata TO grafana_auth
USING (true)
WITH CHECK (false);
ALTER TABLE api.metrics ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.metrics TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.metrics TO vessel_role
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.metrics TO user_role
USING (vessel_id = current_setting('vessel.id', true))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow scheduler to update and select based on the vessel.id
CREATE POLICY api_scheduler_role ON api.metrics TO scheduler
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow grafana to select based on the vessel.id
CREATE POLICY grafana_role ON api.metrics TO grafana
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (false);
-- Allow anonymous to select based on the vessel.id
CREATE POLICY api_anonymous_role ON api.metrics TO api_anonymous
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (false);
-- Be sure to enable row level security on the table
ALTER TABLE api.logbook ENABLE ROW LEVEL SECURITY;
-- Create policies
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.logbook TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.logbook TO vessel_role
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.logbook TO user_role
USING (vessel_id = current_setting('vessel.id', true))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow scheduler to update and select based on the vessel.id
CREATE POLICY api_scheduler_role ON api.logbook TO scheduler
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow grafana to select based on the vessel.id
CREATE POLICY grafana_role ON api.logbook TO grafana
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (false);
-- Allow anonymous to select based on the vessel.id
CREATE POLICY api_anonymous_role ON api.logbook TO api_anonymous
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (false);
-- Be sure to enable row level security on the table
ALTER TABLE api.stays ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.stays TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.stays TO vessel_role
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.stays TO user_role
USING (vessel_id = current_setting('vessel.id', true))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow scheduler to update and select based on the vessel_id
CREATE POLICY api_scheduler_role ON api.stays TO scheduler
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow grafana to select based on the vessel_id
CREATE POLICY grafana_role ON api.stays TO grafana
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (false);
-- Allow anonymous to select based on the vessel.id
CREATE POLICY api_anonymous_role ON api.stays TO api_anonymous
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (false);
-- Be sure to enable row level security on the table
ALTER TABLE api.moorages ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON api.moorages TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON api.moorages TO vessel_role
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON api.moorages TO user_role
USING (vessel_id = current_setting('vessel.id', true))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow scheduler to update and select based on the vessel_id
CREATE POLICY api_scheduler_role ON api.moorages TO scheduler
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Allow grafana to select based on the vessel_id
CREATE POLICY grafana_role ON api.moorages TO grafana
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (false);
-- Allow anonymous to select based on the vessel.id
CREATE POLICY api_anonymous_role ON api.moorages TO api_anonymous
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (false);
-- Be sure to enable row level security on the table
ALTER TABLE auth.vessels ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON auth.vessels TO current_user
USING (true)
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON auth.vessels TO user_role
USING (vessel_id = current_setting('vessel.id', true)
AND owner_email = current_setting('user.email', true)
)
WITH CHECK (vessel_id = current_setting('vessel.id', true)
AND owner_email = current_setting('user.email', true)
);
-- Allow grafana to select based on email
CREATE POLICY grafana_role ON auth.vessels TO grafana
USING (owner_email = current_setting('user.email', true))
WITH CHECK (false);
-- Allow grafana to select
CREATE POLICY grafana_proxy_role ON auth.vessels TO grafana_auth
USING (true)
WITH CHECK (false);
-- Be sure to enable row level security on the table
ALTER TABLE auth.accounts ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON auth.accounts TO current_user
USING (true)
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON auth.accounts TO user_role
USING (email = current_setting('user.email', true))
WITH CHECK (email = current_setting('user.email', true));
-- Allow scheduler see all rows and add any rows
CREATE POLICY api_scheduler_role ON auth.accounts TO scheduler
USING (email = current_setting('user.email', true))
WITH CHECK (email = current_setting('user.email', true));
-- Allow grafana_auth to select
CREATE POLICY grafana_proxy_role ON auth.accounts TO grafana_auth
USING (true)
WITH CHECK (false);
-- Be sure to enable row level security on the table
ALTER TABLE public.process_queue ENABLE ROW LEVEL SECURITY;
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON public.process_queue TO current_user
USING (true)
WITH CHECK (true);
-- Allow vessel_role to insert and select on their own records
CREATE POLICY api_vessel_role ON public.process_queue TO vessel_role
USING (ref_id = current_setting('user.id', true) OR ref_id = current_setting('vessel.id', true))
WITH CHECK (true);
-- Allow user_role to update and select on their own records
CREATE POLICY api_user_role ON public.process_queue TO user_role
USING (ref_id = current_setting('user.id', true) OR ref_id = current_setting('vessel.id', true))
WITH CHECK (ref_id = current_setting('user.id', true) OR ref_id = current_setting('vessel.id', true));
-- Allow scheduler see all rows and updates any rows
CREATE POLICY api_scheduler_role ON public.process_queue TO scheduler
USING (true)
WITH CHECK (false);

View File

@@ -8,7 +8,7 @@
-- List current database
select current_database();
-- connext to the DB
-- connect to the DB
\c signalk
CREATE SCHEMA IF NOT EXISTS jwt;

View File

@@ -8,22 +8,26 @@
CREATE EXTENSION IF NOT EXISTS pg_cron; -- provides a simple cron-based job scheduler for PostgreSQL
-- TRUNCATE table jobs
TRUNCATE TABLE cron.job CONTINUE IDENTITY RESTRICT;
--TRUNCATE TABLE cron.job CONTINUE IDENTITY RESTRICT;
-- Create a every 5 minutes or minute job cron_process_new_logbook_fn ??
SELECT cron.schedule('cron_new_logbook', '*/5 * * * *', 'select public.cron_process_new_logbook_fn()') ;
-- Create a every 5 minutes or minute job cron_process_pre_logbook_fn ??
SELECT cron.schedule('cron_pre_logbook', '*/5 * * * *', 'select public.cron_process_pre_logbook_fn()');
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_pre_logbook';
-- Create a every 6 minutes or minute job cron_process_new_logbook_fn ??
SELECT cron.schedule('cron_new_logbook', '*/6 * * * *', 'select public.cron_process_new_logbook_fn()');
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_logbook';
-- Create a every 5 minute job cron_process_new_stay_fn
SELECT cron.schedule('cron_new_stay', '*/5 * * * *', 'select public.cron_process_new_stay_fn()');
-- Create a every 7 minute job cron_process_new_stay_fn
SELECT cron.schedule('cron_new_stay', '*/7 * * * *', 'select public.cron_process_new_stay_fn()');
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_stay';
-- Create a every 6 minute job cron_process_new_moorage_fn, delay from stay to give time to generate geo reverse location, eg: name
SELECT cron.schedule('cron_new_moorage', '*/6 * * * *', 'select public.cron_process_new_moorage_fn()');
--SELECT cron.schedule('cron_new_moorage', '*/7 * * * *', 'select public.cron_process_new_moorage_fn()');
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_moorage';
-- Create a every 10 minute job cron_process_monitor_offline_fn
SELECT cron.schedule('cron_monitor_offline', '*/10 * * * *', 'select public.cron_process_monitor_offline_fn()');
-- Create a every 11 minute job cron_process_monitor_offline_fn
SELECT cron.schedule('cron_monitor_offline', '*/11 * * * *', 'select public.cron_process_monitor_offline_fn()');
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_monitor_offline';
-- Create a every 10 minute job cron_process_monitor_online_fn
@@ -31,21 +35,62 @@ SELECT cron.schedule('cron_monitor_online', '*/10 * * * *', 'select public.cron_
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_monitor_online';
-- Create a every 5 minute job cron_process_new_account_fn
SELECT cron.schedule('cron_new_account', '*/5 * * * *', 'select public.cron_process_new_account_fn()');
--SELECT cron.schedule('cron_new_account', '*/5 * * * *', 'select public.cron_process_new_account_fn()');
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_account';
-- Create a every 5 minute job cron_process_new_vessel_fn
SELECT cron.schedule('cron_new_vessel', '*/5 * * * *', 'select public.cron_process_new_vessel_fn()');
--SELECT cron.schedule('cron_new_vessel', '*/5 * * * *', 'select public.cron_process_new_vessel_fn()');
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_vessel';
-- Create a every 6 minute job cron_process_new_account_otp_validation_queue_fn, delay from cron_new_account
--SELECT cron.schedule('cron_new_account_otp', '*/6 * * * *', 'select public.cron_process_new_account_otp_validation_fn()');
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_account_otp';
-- Create a every 5 minute job cron_process_grafana_fn
SELECT cron.schedule('cron_grafana', '*/5 * * * *', 'select public.cron_process_grafana_fn()');
-- Create a every 5 minute job cron_process_windy_fn
SELECT cron.schedule('cron_windy', '*/5 * * * *', 'select public.cron_windy_fn()');
-- Notification
-- Create a every 1 minute job cron_process_new_notification_queue_fn, new_account, new_vessel, _new_account_otp
SELECT cron.schedule('cron_new_notification', '*/1 * * * *', 'select public.cron_process_new_notification_fn()');
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_notification';
-- Maintenance
-- Vacuum database at At 01:01 on Sunday.
SELECT cron.schedule('cron_vacumm', '1 1 * * 0', 'select public.cron_vaccum_fn()');
-- Vacuum database schema api at "At 01:31 on Sunday."
SELECT cron.schedule('cron_vacuum_api', '31 1 * * 0', 'VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) api.logbook,api.stays,api.moorages,api.metadata,api.metrics;');
-- Vacuum database schema auth at "At 01:01 on Sunday."
SELECT cron.schedule('cron_vacuum_auth', '1 1 * * 0', 'VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) auth.accounts,auth.vessels,auth.otp;');
-- Remove old jobs log at "At 02:02 on Sunday."
SELECT cron.schedule('job_run_details_cleanup', '2 2 * * 0', 'select public.job_run_details_cleanup_fn()');
-- Rebuilding indexes schema api at "first day of each month at 23:15."
SELECT cron.schedule('cron_reindex_api', '15 23 1 * *', 'REINDEX TABLE CONCURRENTLY api.logbook; REINDEX TABLE CONCURRENTLY api.stays; REINDEX TABLE CONCURRENTLY api.moorages; REINDEX TABLE CONCURRENTLY api.metadata;');
-- Rebuilding indexes schema auth at "first day of each month at 23:01."
SELECT cron.schedule('cron_reindex_auth', '1 23 1 * *', 'REINDEX TABLE CONCURRENTLY auth.accounts; REINDEX TABLE CONCURRENTLY auth.vessels; REINDEX TABLE CONCURRENTLY auth.otp;');
-- Any other maintenance require?
-- OTP
-- Create a every 15 minute job cron_prune_otp_fn
SELECT cron.schedule('cron_prune_otp', '*/15 * * * *', 'select public.cron_prune_otp_fn()');
-- Alerts
-- Create a every 11 minute job cron_alerts_fn
SELECT cron.schedule('cron_alerts', '*/11 * * * *', 'select public.cron_alerts_fn()');
-- Notifications/Reminders of no vessel & no metadata & no activity
-- At 08:05 on Sunday.
-- At 08:05 on every 4th day-of-month if it's on Sunday.
SELECT cron.schedule('cron_no_vessel', '5 8 */4 * 0', 'select public.cron_no_vessel_fn()');
SELECT cron.schedule('cron_no_metadata', '5 8 */4 * 0', 'select public.cron_no_metadata_fn()');
SELECT cron.schedule('cron_no_activity', '5 8 */4 * 0', 'select public.cron_no_activity_fn()');
-- Cron job settings
UPDATE cron.job SET database = 'signalk';
UPDATE cron.job SET username = current_user; -- TODO update to scheduler, pending process_queue update
--UPDATE cron.job SET username = 'username' where jobname = 'cron_vacuum'; -- TODO Update to superuser for vacuum permissions
UPDATE cron.job SET nodename = '/var/run/postgresql/'; -- VS default localhost ??
UPDATE cron.job SET database = 'postgres' WHERE jobname = 'job_run_details_cleanup';
-- check job lists
SELECT * FROM cron.job;
-- unschedule by job id
@@ -53,6 +98,8 @@ SELECT * FROM cron.job;
-- unschedule by job name
--SELECT cron.unschedule('cron_new_logbook');
-- TRUNCATE TABLE cron.job_run_details
TRUNCATE TABLE cron.job_run_details CONTINUE IDENTITY RESTRICT;
--TRUNCATE TABLE cron.job_run_details CONTINUE IDENTITY RESTRICT;
-- check job log
select * from cron.job_run_details ORDER BY end_time DESC LIMIT 10;
SELECT * FROM cron.job_run_details ORDER BY end_time DESC;
-- DEBUG Disable all
UPDATE cron.job SET active = False;

View File

@@ -0,0 +1,76 @@
---------------------------------------------------------------------------
-- https://www.naturalearthdata.com
--
-- https://naciscdn.org/naturalearth/10m/physical/ne_10m_geography_marine_polys.zip
--
-- https://github.com/nvkelso/natural-earth-vector/raw/master/10m_physical/ne_10m_geography_marine_polys.shp
--
-- Import from shapefile
-- # shp2pgsql ne_10m_geography_marine_polys.shp public.ne_10m_geography_marine_polys | psql -U ${POSTGRES_USER} signalk
--
-- PostgSail Customization, add tropics and alaska area.
-- List current database
select current_database();
-- connect to the DB
\c signalk
CREATE TABLE public.ne_10m_geography_marine_polys (
gid INT GENERATED ALWAYS AS IDENTITY NOT NULL,
featurecla TEXT NULL,
"name" TEXT NULL,
namealt TEXT NULL,
changed TEXT NULL,
note TEXT NULL,
name_fr TEXT NULL,
min_label float8 NULL,
max_label float8 NULL,
scalerank int2 NULL,
"label" TEXT NULL,
wikidataid TEXT NULL,
name_ar TEXT NULL,
name_bn TEXT NULL,
name_de TEXT NULL,
name_en TEXT NULL,
name_es TEXT NULL,
name_el TEXT NULL,
name_hi TEXT NULL,
name_hu TEXT NULL,
name_id TEXT NULL,
name_it TEXT NULL,
name_ja TEXT NULL,
name_ko TEXT NULL,
name_nl TEXT NULL,
name_pl TEXT NULL,
name_pt TEXT NULL,
name_ru TEXT NULL,
name_sv TEXT NULL,
name_tr TEXT NULL,
name_vi TEXT NULL,
name_zh TEXT NULL,
ne_id int8 NULL,
name_fa TEXT NULL,
name_he TEXT NULL,
name_uk TEXT NULL,
name_ur TEXT NULL,
name_zht TEXT NULL,
geom geometry(multipolygon,4326) NULL,
CONSTRAINT ne_10m_geography_marine_polys_pkey PRIMARY KEY (gid)
);
-- Add GIST index
CREATE INDEX ne_10m_geography_marine_polys_geom_idx
ON public.ne_10m_geography_marine_polys
USING GIST (geom);
-- Description
COMMENT ON TABLE
public.ne_10m_geography_marine_polys
IS 'imperfect but light weight geographic marine areas from https://www.naturalearthdata.com';
-- Import data
COPY public.ne_10m_geography_marine_polys(gid,featurecla,"name",namealt,changed,note,name_fr,min_label,max_label,scalerank,"label",wikidataid,name_ar,name_bn,name_de,name_en,name_es,name_el,name_hi,name_hu,name_id,name_it,name_ja,name_ko,name_nl,name_pl,name_pt,name_ru,name_sv,name_tr,name_vi,name_zh,ne_id,name_fa,name_he,name_uk,name_ur,name_zht,geom)
FROM '/docker-entrypoint-initdb.d/ne_10m_geography_marine_polys.csv'
DELIMITER ','
CSV HEADER;

View File

@@ -0,0 +1,457 @@
---------------------------------------------------------------------------
-- Copyright 2021-2024 Francois Lacroix <xbgmsharp@gmail.com>
-- This file is part of PostgSail which is released under Apache License, Version 2.0 (the "License").
-- See file LICENSE or go to http://www.apache.org/licenses/LICENSE-2.0 for full license details.
--
-- Migration January 2024
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
\echo 'Force timezone, just in case'
set timezone to 'UTC';
COMMENT ON FUNCTION
public.cron_process_new_moorage_fn
IS 'Deprecated, init by pg_cron to check for new moorage pending update, if so perform process_moorage_queue_fn';
DROP FUNCTION IF EXISTS reverse_geoip_py_fn;
CREATE OR REPLACE FUNCTION reverse_geoip_py_fn(IN _ip TEXT) RETURNS JSONB
AS $reverse_geoip_py$
"""
Return ipapi.co ip details
"""
import requests
import json
# requests
url = f'https://ipapi.co/{_ip}/json/'
r = requests.get(url)
#print(r.text)
#plpy.notice('IP [{}] [{}]'.format(_ip, r.status_code))
if r.status_code == 200:
#plpy.notice('Got [{}] [{}]'.format(r.text, r.status_code))
return r.json()
else:
plpy.error('Failed to get ip details')
return {}
$reverse_geoip_py$ IMMUTABLE strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.reverse_geoip_py_fn
IS 'Retrieve reverse geo IP location via ipapi.co using plpython3u';
DROP FUNCTION IF EXISTS overpass_py_fn;
CREATE OR REPLACE FUNCTION overpass_py_fn(IN lon NUMERIC, IN lat NUMERIC,
OUT geo JSONB) RETURNS JSONB
AS $overpass_py$
"""
Return https://overpass-turbo.eu seamark details within 400m
https://overpass-turbo.eu/s/1EaG
https://wiki.openstreetmap.org/wiki/Key:seamark:type
"""
import requests
import json
import urllib.parse
headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com'}
payload = """
[out:json][timeout:20];
is_in({0},{1})->.result_areas;
(
area.result_areas["seamark:type"~"(mooring|harbour)"][~"^seamark:.*:category$"~"."];
area.result_areas["leisure"="marina"][~"name"~"."];
);
out tags;
nwr(around:400.0,{0},{1})->.all;
(
nwr.all["seamark:type"~"(mooring|harbour)"][~"^seamark:.*:category$"~"."];
nwr.all["seamark:type"~"(anchorage|anchor_berth|berth)"];
nwr.all["leisure"="marina"];
nwr.all["natural"~"(bay|beach)"];
);
out tags;
""".format(lat, lon)
data = urllib.parse.quote(payload, safe="");
url = f'https://overpass-api.de/api/interpreter?data={data}'.format(data)
r = requests.get(url, headers)
#print(r.text)
#plpy.notice(url)
plpy.notice('overpass-api coord lon[{}] lat[{}] [{}]'.format(lon, lat, r.status_code))
if r.status_code == 200 and "elements" in r.json():
r_dict = r.json()
plpy.notice('overpass-api Got [{}]'.format(r_dict["elements"]))
if r_dict["elements"]:
if "tags" in r_dict["elements"][0] and r_dict["elements"][0]["tags"]:
return r_dict["elements"][0]["tags"]; # return the first element
return {}
else:
plpy.notice('overpass-api Failed to get overpass-api details')
return {}
$overpass_py$ IMMUTABLE strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.overpass_py_fn
IS 'Return https://overpass-turbo.eu seamark details within 400m using plpython3u';
CREATE OR REPLACE FUNCTION get_app_settings_fn(OUT app_settings jsonb)
RETURNS jsonb
AS $get_app_settings$
DECLARE
BEGIN
SELECT
jsonb_object_agg(name, value) INTO app_settings
FROM
public.app_settings
WHERE
name LIKE 'app.email%'
OR name LIKE 'app.pushover%'
OR name LIKE 'app.url'
OR name LIKE 'app.telegram%'
OR name LIKE 'app.grafana_admin_uri'
OR name LIKE 'app.keycloak_uri';
END;
$get_app_settings$
LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION keycloak_auth_py_fn(IN _v_id TEXT,
IN _user JSONB, IN app JSONB) RETURNS JSONB
AS $keycloak_auth_py$
"""
Addkeycloak user
"""
import requests
import json
import urllib.parse
safe_uri = host = user = pwd = None
if 'app.keycloak_uri' in app and app['app.keycloak_uri']:
#safe_uri = urllib.parse.quote(app['app.keycloak_uri'], safe=':/?&=')
_ = urllib.parse.urlparse(app['app.keycloak_uri'])
host = _.netloc.split('@')[-1]
user = _.netloc.split(':')[0]
pwd = _.netloc.split(':')[1].split('@')[0]
else:
plpy.error('Error no keycloak_uri defined, check app settings')
return none
if not host or not user or not pwd:
plpy.error('Error parsing keycloak_uri, check app settings')
return None
if not 'email' in _user and _user['email']:
plpy.error('Error parsing user email, check user settings')
return none
if not _v_id:
plpy.error('Error parsing vessel_id')
return none
_headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com'}
_payload = {'client_id':'admin-cli','grant_type':'password','username':user,'password':pwd}
url = f'{_.scheme}://{host}/realms/master/protocol/openid-connect/token'.format(_.scheme, host)
r = requests.post(url, headers=_headers, data=_payload, timeout=(5, 60))
#print(r.text)
#plpy.notice(url)
if r.status_code == 200 and 'access_token' in r.json():
response = r.json()
plpy.notice(response)
_headers['Authorization'] = 'Bearer '+ response['access_token']
_headers['Content-Type'] = 'application/json'
url = f'{_.scheme}://{host}/admin/realms/postgsail/users'.format(_.scheme, host)
_payload = {
"enabled": "true",
"email": _user['email'],
"firstName": _user['recipient'],
"attributes": {"vessel_id": _v_id},
"emailVerified": True,
"requiredActions":["UPDATE_PROFILE", "UPDATE_PASSWORD"]
}
plpy.notice(_payload)
data = json.dumps(_payload)
r = requests.post(url, headers=_headers, data=data, timeout=(5, 60))
if r.status_code != 201:
#print("Error creating user: {status}".format(status=r.status_code))
plpy.error(f'Error creating user: {user} {status}'.format(user=_payload['email'], status=r.status_code))
return None
else:
#print("Created user : {u}]".format(u=_payload['email']))
plpy.notice('Created user : {u} {t}, {l}'.format(u=_payload['email'], t=r.text, l=r.headers['location']))
user_url = "{user_url}/execute-actions-email".format(user_url=r.headers['location'])
_payload = ["UPDATE_PASSWORD"]
plpy.notice(_payload)
data = json.dumps(_payload)
r = requests.put(user_url, headers=_headers, data=data, timeout=(5, 60))
if r.status_code != 204:
plpy.error('Error execute-actions-email: {u} {s}'.format(u=_user['email'], s=r.status_code))
else:
plpy.notice('execute-actions-email: {u} {s}'.format(u=_user['email'], s=r.status_code))
return None
else:
plpy.error(f'Error getting admin access_token: {status}'.format(status=r.status_code))
return None
$keycloak_auth_py$ strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.keycloak_auth_py_fn
IS 'Return set oauth user attribute into keycloak using plpython3u';
CREATE OR REPLACE FUNCTION keycloak_py_fn(IN user_id TEXT, IN vessel_id TEXT,
IN app JSONB) RETURNS JSONB
AS $keycloak_py$
"""
Add vessel_id user attribute to keycloak user {user_id}
"""
import requests
import json
import urllib.parse
safe_uri = host = user = pwd = None
if 'app.keycloak_uri' in app and app['app.keycloak_uri']:
#safe_uri = urllib.parse.quote(app['app.keycloak_uri'], safe=':/?&=')
_ = urllib.parse.urlparse(app['app.keycloak_uri'])
host = _.netloc.split('@')[-1]
user = _.netloc.split(':')[0]
pwd = _.netloc.split(':')[1].split('@')[0]
else:
plpy.error('Error no keycloak_uri defined, check app settings')
return None
if not host or not user or not pwd:
plpy.error('Error parsing keycloak_uri, check app settings')
return None
_headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com'}
_payload = {'client_id':'admin-cli','grant_type':'password','username':user,'password':pwd}
url = f'{_.scheme}://{host}/realms/master/protocol/openid-connect/token'.format(_.scheme, host)
r = requests.post(url, headers=_headers, data=_payload, timeout=(5, 60))
#print(r.text)
#plpy.notice(url)
if r.status_code == 200 and 'access_token' in r.json():
response = r.json()
plpy.notice(response)
_headers['Authorization'] = 'Bearer '+ response['access_token']
_headers['Content-Type'] = 'application/json'
_payload = { 'attributes': {'vessel_id': vessel_id} }
url = f'{keycloak_uri}/admin/realms/postgsail/users/{user_id}'.format(keycloak_uri,user_id)
#plpy.notice(url)
#plpy.notice(_payload)
data = json.dumps(_payload)
r = requests.put(url, headers=_headers, data=data, timeout=(5, 60))
if r.status_code != 204:
plpy.notice("Error updating user: {status} [{text}]".format(
status=r.status_code, text=r.text))
return None
else:
plpy.notice("Updated user : {user} [{text}]".format(user=user_id, text=r.text))
else:
plpy.notice(f'Error getting admin access_token: {status} [{text}]'.format(
status=r.status_code, text=r.text))
return None
$keycloak_py$ strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
UPDATE public.email_templates
SET pushover_message='Congratulations!
You unlocked Grafana dashboard.
See more details at https://app.openplotter.cloud
',email_content='Hello __RECIPIENT__,
Congratulations! You unlocked Grafana dashboard.
See more details at https://app.openplotter.cloud
Happy sailing!
Francois'
WHERE "name"='grafana';
CREATE OR REPLACE FUNCTION public.cron_process_grafana_fn()
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
process_rec record;
data_rec record;
app_settings jsonb;
user_settings jsonb;
BEGIN
-- We run grafana provisioning only after the first received vessel metadata
-- Check for new vessel metadata pending grafana provisioning
RAISE NOTICE 'cron_process_grafana_fn';
FOR process_rec in
SELECT * from process_queue
where channel = 'grafana' and processed is null
order by stored asc
LOOP
RAISE NOTICE '-> cron_process_grafana_fn [%]', process_rec.payload;
-- Gather url from app settings
app_settings := get_app_settings_fn();
-- Get vessel details base on metadata id
SELECT * INTO data_rec
FROM api.metadata m, auth.vessels v
WHERE m.id = process_rec.payload::INTEGER
AND m.vessel_id = v.vessel_id;
-- as we got data from the vessel we can do the grafana provisioning.
PERFORM grafana_py_fn(data_rec.name, data_rec.vessel_id, data_rec.owner_email, app_settings);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(data_rec.vessel_id::TEXT);
RAISE DEBUG '-> DEBUG cron_process_grafana_fn get_user_settings_from_vesselid_fn [%]', user_settings;
-- add user in keycloak
PERFORM keycloak_auth_py_fn(data_rec.vessel_id, user_settings, app_settings);
-- Send notification
PERFORM send_notification_fn('grafana'::TEXT, user_settings::JSONB);
-- update process_queue entry as processed
UPDATE process_queue
SET
processed = NOW()
WHERE id = process_rec.id;
RAISE NOTICE '-> cron_process_grafana_fn updated process_queue table [%]', process_rec.id;
END LOOP;
END;
$function$
;
COMMENT ON FUNCTION public.cron_process_grafana_fn() IS 'init by pg_cron to check for new vessel pending grafana provisioning, if so perform grafana_py_fn';
-- DROP FUNCTION public.grafana_py_fn(text, text, text, jsonb);
CREATE OR REPLACE FUNCTION public.grafana_py_fn(_v_name text, _v_id text, _u_email text, app jsonb)
RETURNS void
TRANSFORM FOR TYPE jsonb
LANGUAGE plpython3u
AS $function$
"""
https://grafana.com/docs/grafana/latest/developers/http_api/
Create organization base on vessel name
Create user base on user email
Add user to organization
Add data_source to organization
Add dashboard to organization
Update organization preferences
"""
import requests
import json
import re
grafana_uri = None
if 'app.grafana_admin_uri' in app and app['app.grafana_admin_uri']:
grafana_uri = app['app.grafana_admin_uri']
else:
plpy.error('Error no grafana_admin_uri defined, check app settings')
return None
b_name = None
if not _v_name:
b_name = _v_id
else:
b_name = _v_name
# add vessel org
headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com',
'Accept': 'application/json', 'Content-Type': 'application/json'}
path = 'api/orgs'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
data_dict = {'name':b_name}
data = json.dumps(data_dict)
r = requests.post(url, data=data, headers=headers)
#print(r.text)
plpy.notice(r.json())
if r.status_code == 200 and "orgId" in r.json():
org_id = r.json()['orgId']
else:
plpy.error('Error grafana add vessel org %', r.json())
return none
# add user to vessel org
path = 'api/admin/users'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
data_dict = {'orgId':org_id, 'email':_u_email, 'password':'asupersecretpassword'}
data = json.dumps(data_dict)
r = requests.post(url, data=data, headers=headers)
#print(r.text)
plpy.notice(r.json())
if r.status_code == 200 and "id" in r.json():
user_id = r.json()['id']
else:
plpy.error('Error grafana add user to vessel org')
return
# read data_source
path = 'api/datasources/1'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
r = requests.get(url, headers=headers)
#print(r.text)
plpy.notice(r.json())
data_source = r.json()
data_source['id'] = 0
data_source['orgId'] = org_id
data_source['uid'] = "ds_" + _v_id
data_source['name'] = "ds_" + _v_id
data_source['secureJsonData'] = {}
data_source['secureJsonData']['password'] = 'mysecretpassword'
data_source['readOnly'] = True
del data_source['secureJsonFields']
# add data_source to vessel org
path = 'api/datasources'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
data = json.dumps(data_source)
headers['X-Grafana-Org-Id'] = str(org_id)
r = requests.post(url, data=data, headers=headers)
plpy.notice(r.json())
del headers['X-Grafana-Org-Id']
if r.status_code != 200 and "id" not in r.json():
plpy.error('Error grafana add data_source to vessel org')
return
dashboards_tpl = [ 'pgsail_tpl_electrical', 'pgsail_tpl_logbook', 'pgsail_tpl_monitor', 'pgsail_tpl_rpi', 'pgsail_tpl_solar', 'pgsail_tpl_weather', 'pgsail_tpl_home']
for dashboard in dashboards_tpl:
# read dashboard template by uid
path = 'api/dashboards/uid'
url = f'{grafana_uri}/{path}/{dashboard}'.format(grafana_uri,path,dashboard)
if 'X-Grafana-Org-Id' in headers:
del headers['X-Grafana-Org-Id']
r = requests.get(url, headers=headers)
plpy.notice(r.json())
if r.status_code != 200 and "id" not in r.json():
plpy.error('Error grafana read dashboard template')
return
new_dashboard = r.json()
del new_dashboard['meta']
new_dashboard['dashboard']['version'] = 0
new_dashboard['dashboard']['id'] = 0
new_uid = re.sub(r'pgsail_tpl_(.*)', r'postgsail_\1', new_dashboard['dashboard']['uid'])
new_dashboard['dashboard']['uid'] = f'{new_uid}_{_v_id}'.format(new_uid,_v_id)
# add dashboard to vessel org
path = 'api/dashboards/db'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
data = json.dumps(new_dashboard)
new_data = data.replace('PCC52D03280B7034C', data_source['uid'])
headers['X-Grafana-Org-Id'] = str(org_id)
r = requests.post(url, data=new_data, headers=headers)
plpy.notice(r.json())
if r.status_code != 200 and "id" not in r.json():
plpy.error('Error grafana add dashboard to vessel org')
return
# Update Org Prefs
path = 'api/org/preferences'
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
home_dashboard = {}
home_dashboard['timezone'] = 'utc'
home_dashboard['homeDashboardUID'] = f'postgsail_home_{_v_id}'.format(_v_id)
data = json.dumps(home_dashboard)
headers['X-Grafana-Org-Id'] = str(org_id)
r = requests.patch(url, data=data, headers=headers)
plpy.notice(r.json())
if r.status_code != 200:
plpy.error('Error grafana update org preferences')
return
plpy.notice('Done')
$function$
;
COMMENT ON FUNCTION public.grafana_py_fn(text, text, text, jsonb) IS 'Grafana Organization,User,data_source,dashboards provisioning via HTTP API using plpython3u';
UPDATE public.app_settings
SET value='0.6.1'
WHERE "name"='app.version';

View File

@@ -0,0 +1,919 @@
---------------------------------------------------------------------------
-- Copyright 2021-2024 Francois Lacroix <xbgmsharp@gmail.com>
-- This file is part of PostgSail which is released under Apache License, Version 2.0 (the "License").
-- See file LICENSE or go to http://www.apache.org/licenses/LICENSE-2.0 for full license details.
--
-- Migration February 2024
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
\echo 'Force timezone, just in case'
set timezone to 'UTC';
-- Update email_templates
--INSERT INTO public.email_templates ("name",email_subject,email_content,pushover_title,pushover_message)
-- VALUES ('windy','PostgSail Windy Weather station',E'Hello __RECIPIENT__,\nCongratulations! Your boat is now a Windy Weather station.\nSee more details at __APP_URL__/windy\nHappy sailing!\nFrancois','PostgSail Windy!',E'Congratulations!\nYour boat is now a Windy Weather station.\nSee more details at __APP_URL__/windy\n');
--INSERT INTO public.email_templates ("name",email_subject,email_content,pushover_title,pushover_message)
--VALUES ('alert','PostgSail Alert',E'Hello __RECIPIENT__,\nWe detected an alert __ALERT__.\nSee more details at __APP_URL__\nStay safe.\nFrancois','PostgSail Alert!',E'Congratulations!\nWe detected an alert __ALERT__.\n');
INSERT INTO public.email_templates ("name",email_subject,email_content,pushover_title,pushover_message)
VALUES ('windy_error','PostgSail Windy Weather station Error',E'Hello __RECIPIENT__,\nSorry!We could not convert your boat into a Windy Personal Weather Station due to missing data (temp or wind).\nWindy Personal Weather Station is now disable.','PostgSail Windy error!',E'Sorry!\nWe could not convert your boat into a Windy Personal Weather Station.');
-- Update app_settings
CREATE OR REPLACE FUNCTION public.get_app_settings_fn(OUT app_settings jsonb)
RETURNS jsonb
AS $get_app_settings$
DECLARE
BEGIN
SELECT
jsonb_object_agg(name, value) INTO app_settings
FROM
public.app_settings
WHERE
name LIKE 'app.email%'
OR name LIKE 'app.pushover%'
OR name LIKE 'app.url'
OR name LIKE 'app.telegram%'
OR name LIKE 'app.grafana_admin_uri'
OR name LIKE 'app.keycloak_uri'
OR name LIKE 'app.windy_apikey';
END;
$get_app_settings$
LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION public.get_user_settings_from_vesselid_fn(
IN vesselid TEXT,
OUT user_settings JSONB
) RETURNS JSONB
AS $get_user_settings_from_vesselid$
DECLARE
BEGIN
-- If vessel_id is not NULL
IF vesselid IS NULL OR vesselid = '' THEN
RAISE WARNING '-> get_user_settings_from_vesselid_fn invalid input %', vesselid;
END IF;
SELECT
json_build_object(
'boat' , v.name,
'recipient', a.first,
'email', v.owner_email,
'settings', a.preferences
) INTO user_settings
FROM auth.accounts a, auth.vessels v, api.metadata m
WHERE m.vessel_id = v.vessel_id
AND m.vessel_id = vesselid
AND a.email = v.owner_email;
PERFORM set_config('user.email', user_settings->>'email'::TEXT, false);
PERFORM set_config('user.recipient', user_settings->>'recipient'::TEXT, false);
END;
$get_user_settings_from_vesselid$ LANGUAGE plpgsql;
-- Create Windy PWS integration
CREATE OR REPLACE FUNCTION public.windy_pws_py_fn(IN metric JSONB,
IN _user JSONB, IN app JSONB) RETURNS JSONB
AS $windy_pws_py$
"""
Send environment data from boat instruments to Windy as a Personal Weather Station (PWS)
https://community.windy.com/topic/8168/report-your-weather-station-data-to-windy
"""
import requests
import json
import decimal
if not 'app.windy_apikey' in app and not app['app.windy_apikey']:
plpy.error('Error no windy_apikey defined, check app settings')
return none
if not 'station' in metric and not metric['station']:
plpy.error('Error no metrics defined')
return none
if not 'temp' in metric and not metric['temp']:
plpy.error('Error no metrics defined')
return none
if not _user:
plpy.error('Error no user defined, check user settings')
return none
_headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com', 'Content-Type': 'application/json'}
_payload = {
'stations': [
{ 'station': int(decimal.Decimal(metric['station'])),
'name': metric['name'],
'shareOption': 'Open',
'type': 'SignalK PostgSail Plugin',
'provider': 'PostgSail',
'url': 'https://iot.openplotter.cloud/{name}/monitoring'.format(name=metric['name']),
'lat': float(decimal.Decimal(metric['lat'])),
'lon': float(decimal.Decimal(metric['lon'])),
'elevation': 1 }
],
'observations': [
{ 'station': int(decimal.Decimal(metric['station'])),
'temp': float(decimal.Decimal(metric['temp'])),
'wind': round(float(decimal.Decimal(metric['wind']))),
'gust': round(float(decimal.Decimal(metric['gust']))),
'winddir': int(decimal.Decimal(metric['winddir'])),
'pressure': int(decimal.Decimal(metric['pressure'])),
'rh': float(decimal.Decimal(metric['rh'])) }
]}
#print(_payload)
#plpy.notice(_payload)
data = json.dumps(_payload)
api_url = 'https://stations.windy.com/pws/update/{api_key}'.format(api_key=app['app.windy_apikey'])
r = requests.post(api_url, data=data, headers=_headers, timeout=(5, 60))
#print(r.text)
#plpy.notice(api_url)
if r.status_code == 200:
#print('Data sent successfully!')
plpy.notice('Data sent successfully to Windy!')
#plpy.notice(api_url)
if not 'windy' in _user['settings']:
api_url = 'https://stations.windy.com/pws/station/{api_key}/{station}'.format(api_key=app['app.windy_apikey'], station=metric['station'])
#print(r.text)
#plpy.notice(api_url)
r = requests.get(api_url, timeout=(5, 60))
if r.status_code == 200:
#print('Windy Personal Weather Station created successfully in Windy Stations!')
plpy.notice('Windy Personal Weather Station created successfully in Windy Stations!')
return r.json()
else:
plpy.error(f'Failed to gather PWS details. Status code: {r.status_code}')
else:
plpy.error(f'Failed to send data. Status code: {r.status_code}')
#print(f'Failed to send data. Status code: {r.status_code}')
#print(r.text)
return {}
$windy_pws_py$ strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
-- Description
COMMENT ON FUNCTION
public.windy_pws_py_fn
IS 'Forward vessel data to Windy as a Personal Weather Station using plpython3u';
CREATE OR REPLACE FUNCTION public.cron_windy_fn() RETURNS void AS $cron_windy$
DECLARE
windy_rec record;
default_last_metric TIMESTAMPTZ := NOW() - interval '1 day';
last_metric TIMESTAMPTZ;
metric_rec record;
windy_metric jsonb;
app_settings jsonb;
user_settings jsonb;
windy_pws jsonb;
BEGIN
-- Check for new observations pending update
RAISE NOTICE 'cron_windy_fn';
-- Gather url from app settings
app_settings := get_app_settings_fn();
-- Find users with Windy active and with an active vessel
-- Map account id to Windy Station ID
FOR windy_rec in
SELECT
a.id,a.email,v.vessel_id,v.name,
COALESCE((a.preferences->'windy_last_metric')::TEXT, default_last_metric::TEXT) as last_metric
FROM auth.accounts a
LEFT JOIN auth.vessels AS v ON v.owner_email = a.email
LEFT JOIN api.metadata AS m ON m.vessel_id = v.vessel_id
WHERE (a.preferences->'public_windy')::boolean = True
AND m.active = True
LOOP
RAISE NOTICE '-> cron_windy_fn for [%]', windy_rec;
PERFORM set_config('vessel.id', windy_rec.vessel_id, false);
--RAISE WARNING 'public.cron_process_windy_rec_fn() scheduler vessel.id %, user.id', current_setting('vessel.id', false), current_setting('user.id', false);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(windy_rec.vessel_id::TEXT);
RAISE NOTICE '-> cron_windy_fn checking user_settings [%]', user_settings;
-- Get all metrics from the last windy_last_metric avg by 5 minutes
-- TODO json_agg to send all data in once, but issue with py jsonb transformation decimal.
FOR metric_rec in
SELECT time_bucket('5 minutes', m.time) AS time_bucket,
avg((m.metrics->'environment.outside.temperature')::numeric) AS temperature,
avg((m.metrics->'environment.outside.pressure')::numeric) AS pressure,
avg((m.metrics->'environment.outside.relativeHumidity')::numeric) AS rh,
avg((m.metrics->'environment.wind.directionTrue')::numeric) AS winddir,
avg((m.metrics->'environment.wind.speedTrue')::numeric) AS wind,
max((m.metrics->'environment.wind.speedTrue')::numeric) AS gust,
last(latitude, time) AS lat,
last(longitude, time) AS lng
FROM api.metrics m
WHERE vessel_id = windy_rec.vessel_id
AND m.time >= windy_rec.last_metric::TIMESTAMPTZ
GROUP BY time_bucket
ORDER BY time_bucket ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_windy_fn checking metrics [%]', metric_rec;
IF metric_rec.wind IS NULL OR metric_rec.temperature IS NULL THEN
-- Ignore when there is no metrics
-- Send notification
PERFORM send_notification_fn('windy_error'::TEXT, user_settings::JSONB);
-- Disable windy
PERFORM api.update_user_preferences_fn('{public_windy}'::TEXT, 'false'::TEXT);
RETURN;
END IF;
-- https://community.windy.com/topic/8168/report-your-weather-station-data-to-windy
-- temp from Kelvin to Celsius
-- winddir from radiant to Degrees
-- rh from ratio to percentage
SELECT jsonb_build_object(
'dateutc', metric_rec.time_bucket,
'station', windy_rec.id,
'name', windy_rec.name,
'lat', metric_rec.lat,
'lon', metric_rec.lng,
'wind', metric_rec.wind,
'gust', metric_rec.gust,
'pressure', metric_rec.pressure,
'winddir', radiantToDegrees(metric_rec.winddir::numeric),
'temp', kelvinToCel(metric_rec.temperature::numeric),
'rh', valToPercent(metric_rec.rh::numeric)
) INTO windy_metric;
RAISE NOTICE '-> cron_windy_fn checking windy_metrics [%]', windy_metric;
SELECT windy_pws_py_fn(windy_metric, user_settings, app_settings) into windy_pws;
RAISE NOTICE '-> cron_windy_fn Windy PWS [%]', ((windy_pws->'header')::JSONB ? 'id');
IF NOT((user_settings->'settings')::JSONB ? 'windy') and ((windy_pws->'header')::JSONB ? 'id') then
RAISE NOTICE '-> cron_windy_fn new Windy PWS [%]', (windy_pws->'header')::JSONB->>'id';
-- Send metrics to Windy
PERFORM api.update_user_preferences_fn('{windy}'::TEXT, ((windy_pws->'header')::JSONB->>'id')::TEXT);
-- Send notification
PERFORM send_notification_fn('windy'::TEXT, user_settings::JSONB);
END IF;
-- Record last metrics time
SELECT metric_rec.time_bucket INTO last_metric;
END LOOP;
PERFORM api.update_user_preferences_fn('{windy_last_metric}'::TEXT, last_metric::TEXT);
END LOOP;
END;
$cron_windy$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_windy_fn
IS 'init by pg_cron to create (or update) station and uploading observations to Windy Personal Weather Station observations';
CREATE OR REPLACE FUNCTION public.cron_alerts_fn() RETURNS void AS $$
DECLARE
alert_rec record;
default_last_metric TIMESTAMPTZ := NOW() - interval '1 day';
last_metric TIMESTAMPTZ;
metric_rec record;
app_settings JSONB;
user_settings JSONB;
alerting JSONB;
_alarms JSONB;
alarms TEXT;
alert_default JSONB := '{
"low_pressure_threshold": 990,
"high_wind_speed_threshold": 30,
"low_water_depth_threshold": 1,
"min_notification_interval": 6,
"high_pressure_drop_threshold": 12,
"low_battery_charge_threshold": 90,
"low_battery_voltage_threshold": 12.5,
"low_water_temperature_threshold": 10,
"low_indoor_temperature_threshold": 7,
"low_outdoor_temperature_threshold": 3
}';
BEGIN
-- Check for new event notification pending update
RAISE NOTICE 'cron_alerts_fn';
FOR alert_rec in
SELECT
a.user_id,a.email,v.vessel_id,
COALESCE((a.preferences->'alert_last_metric')::TEXT, default_last_metric::TEXT) as last_metric,
(alert_default || (a.preferences->'alerting')::JSONB) as alerting,
(a.preferences->'alarms')::JSONB as alarms
FROM auth.accounts a
LEFT JOIN auth.vessels AS v ON v.owner_email = a.email
LEFT JOIN api.metadata AS m ON m.vessel_id = v.vessel_id
WHERE (a.preferences->'alerting'->'enabled')::boolean = True
AND m.active = True
LOOP
RAISE NOTICE '-> cron_alerts_fn for [%]', alert_rec;
PERFORM set_config('vessel.id', alert_rec.vessel_id, false);
PERFORM set_config('user.email', alert_rec.email, false);
--RAISE WARNING 'public.cron_process_alert_rec_fn() scheduler vessel.id %, user.id', current_setting('vessel.id', false), current_setting('user.id', false);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(alert_rec.vessel_id::TEXT);
RAISE NOTICE '-> cron_alerts_fn checking user_settings [%]', user_settings;
-- Get all metrics from the last last_metric avg by 5 minutes
FOR metric_rec in
SELECT time_bucket('5 minutes', m.time) AS time_bucket,
avg((m.metrics->'environment.inside.temperature')::numeric) AS intemp,
avg((m.metrics->'environment.outside.temperature')::numeric) AS outtemp,
avg((m.metrics->'environment.water.temperature')::numeric) AS wattemp,
avg((m.metrics->'environment.depth.belowTransducer')::numeric) AS watdepth,
avg((m.metrics->'environment.outside.pressure')::numeric) AS pressure,
avg((m.metrics->'environment.wind.speedTrue')::numeric) AS wind,
avg((m.metrics->'electrical.batteries.House.voltage')::numeric) AS voltage,
avg((m.metrics->'electrical.batteries.House.capacity.stateOfCharge')::numeric) AS charge
FROM api.metrics m
WHERE vessel_id = alert_rec.vessel_id
AND m.time >= alert_rec.last_metric::TIMESTAMPTZ
GROUP BY time_bucket
ORDER BY time_bucket ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_alerts_fn checking metrics [%]', metric_rec;
RAISE NOTICE '-> cron_alerts_fn checking alerting [%]', alert_rec.alerting;
--RAISE NOTICE '-> cron_alerts_fn checking debug [%] [%]', kelvinToCel(metric_rec.intemp), (alert_rec.alerting->'low_indoor_temperature_threshold');
IF kelvinToCel(metric_rec.intemp) < (alert_rec.alerting->'low_indoor_temperature_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_indoor_temperature_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_indoor_temperature_threshold'->>'date') IS NULL) OR
(((_alarms->'low_indoor_temperature_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_indoor_temperature_threshold": {"value": '|| kelvinToCel(metric_rec.intemp) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_outdoor_temperature_threshold value:'|| kelvinToCel(metric_rec.intemp) ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_indoor_temperature_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_indoor_temperature_threshold';
END IF;
IF kelvinToCel(metric_rec.outtemp) < (alert_rec.alerting->'low_outdoor_temperature_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_outdoor_temperature_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_outdoor_temperature_threshold'->>'date') IS NULL) OR
(((_alarms->'low_outdoor_temperature_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_outdoor_temperature_threshold": {"value": '|| kelvinToCel(metric_rec.outtemp) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_outdoor_temperature_threshold value:'|| kelvinToCel(metric_rec.outtemp) ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_outdoor_temperature_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_outdoor_temperature_threshold';
END IF;
IF kelvinToCel(metric_rec.wattemp) < (alert_rec.alerting->'low_water_temperature_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_water_temperature_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_water_temperature_threshold'->>'date') IS NULL) OR
(((_alarms->'low_water_temperature_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_water_temperature_threshold": {"value": '|| kelvinToCel(metric_rec.wattemp) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_water_temperature_threshold value:'|| kelvinToCel(metric_rec.wattemp) ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_temperature_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_temperature_threshold';
END IF;
IF metric_rec.watdepth < (alert_rec.alerting->'low_water_depth_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_water_depth_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_water_depth_threshold'->>'date') IS NULL) OR
(((_alarms->'low_water_depth_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_water_depth_threshold": {"value": '|| metric_rec.watdepth ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_water_depth_threshold value:'|| metric_rec.watdepth ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_depth_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_depth_threshold';
END IF;
if metric_rec.pressure < (alert_rec.alerting->'high_pressure_drop_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'high_pressure_drop_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'high_pressure_drop_threshold'->>'date') IS NULL) OR
(((_alarms->'high_pressure_drop_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"high_pressure_drop_threshold": {"value": '|| metric_rec.pressure ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "high_pressure_drop_threshold value:'|| metric_rec.pressure ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug high_pressure_drop_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug high_pressure_drop_threshold';
END IF;
IF metric_rec.wind > (alert_rec.alerting->'high_wind_speed_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'high_wind_speed_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'high_wind_speed_threshold'->>'date') IS NULL) OR
(((_alarms->'high_wind_speed_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"high_wind_speed_threshold": {"value": '|| metric_rec.wind ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "high_wind_speed_threshold value:'|| metric_rec.wind ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug high_wind_speed_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug high_wind_speed_threshold';
END IF;
if metric_rec.voltage < (alert_rec.alerting->'low_battery_voltage_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_battery_voltage_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = 'lacroix.francois@gmail.com';
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_battery_voltage_threshold'->>'date') IS NULL) OR
(((_alarms->'low_battery_voltage_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_battery_voltage_threshold": {"value": '|| metric_rec.voltage ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_battery_voltage_threshold value:'|| metric_rec.voltage ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_voltage_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_voltage_threshold';
END IF;
if (metric_rec.charge*100) < (alert_rec.alerting->'low_battery_charge_threshold')::numeric then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_battery_charge_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_battery_charge_threshold'->>'date') IS NULL) OR
(((_alarms->'low_battery_charge_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_battery_charge_threshold": {"value": '|| (metric_rec.charge*100) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_battery_charge_threshold value:'|| (metric_rec.charge*100) ||'"}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_charge_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_charge_threshold';
END IF;
-- Record last metrics time
SELECT metric_rec.time_bucket INTO last_metric;
END LOOP;
PERFORM api.update_user_preferences_fn('{alert_last_metric}'::TEXT, last_metric::TEXT);
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_alerts_fn
IS 'init by pg_cron to check for alerts';
-- CRON for no vessel notification
CREATE FUNCTION public.cron_no_vessel_fn() RETURNS void AS $no_vessel$
DECLARE
no_vessel record;
user_settings jsonb;
BEGIN
-- Check for user with no vessel register
RAISE NOTICE 'cron_no_vessel_fn';
FOR no_vessel in
SELECT a.user_id,a.email,a.first
FROM auth.accounts a
WHERE NOT EXISTS (
SELECT *
FROM auth.vessels v
WHERE v.owner_email = a.email)
LOOP
RAISE NOTICE '-> cron_no_vessel_rec_fn for [%]', no_vessel;
SELECT json_build_object('email', no_vessel.email, 'recipient', no_vessel.first) into user_settings;
RAISE NOTICE '-> debug cron_no_vessel_rec_fn [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('no_vessel'::TEXT, user_settings::JSONB);
END LOOP;
END;
$no_vessel$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_no_vessel_fn
IS 'init by pg_cron, check for user with no vessel register then send notification';
CREATE FUNCTION public.cron_no_metadata_fn() RETURNS void AS $no_metadata$
DECLARE
no_metadata_rec record;
user_settings jsonb;
BEGIN
-- Check for vessel register but with no metadata
RAISE NOTICE 'cron_no_metadata_fn';
FOR no_metadata_rec in
SELECT
a.user_id,a.email,a.first
FROM auth.accounts a, auth.vessels v
WHERE NOT EXISTS (
SELECT *
FROM api.metadata m
WHERE v.vessel_id = m.vessel_id) AND v.owner_email = a.email
LOOP
RAISE NOTICE '-> cron_process_no_metadata_rec_fn for [%]', no_metadata_rec;
SELECT json_build_object('email', no_metadata_rec.email, 'recipient', no_metadata_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_no_metadata_rec_fn [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('no_metadata'::TEXT, user_settings::JSONB);
END LOOP;
END;
$no_metadata$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_no_metadata_fn
IS 'init by pg_cron, check for vessel with no metadata then send notification';
CREATE FUNCTION public.cron_no_activity_fn() RETURNS void AS $no_activity$
DECLARE
no_activity_rec record;
user_settings jsonb;
BEGIN
-- Check for vessel with no activity for more than 230 days
RAISE NOTICE 'cron_no_activity_fn';
FOR no_activity_rec in
SELECT
v.owner_email,m.name,m.vessel_id,m.time,a.first
FROM auth.accounts a
LEFT JOIN auth.vessels v ON v.owner_email = a.email
LEFT JOIN api.metadata m ON v.vessel_id = m.vessel_id
WHERE m.time < NOW() AT TIME ZONE 'UTC' - INTERVAL '230 DAYS'
LOOP
RAISE NOTICE '-> cron_process_no_activity_rec_fn for [%]', no_activity_rec;
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_no_activity_rec_fn [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('no_activity'::TEXT, user_settings::JSONB);
END LOOP;
END;
$no_activity$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_no_activity_fn
IS 'init by pg_cron, check for vessel with no activity for more than 230 days then send notification';
CREATE FUNCTION public.cron_deactivated_fn() RETURNS void AS $deactivated$
DECLARE
no_activity_rec record;
user_settings jsonb;
BEGIN
RAISE NOTICE 'cron_deactivated_fn';
-- List accounts with vessel inactivity for more than 1 YEAR
FOR no_activity_rec in
SELECT
v.owner_email,m.name,m.vessel_id,m.time,a.first
FROM auth.accounts a
LEFT JOIN auth.vessels v ON v.owner_email = a.email
LEFT JOIN api.metadata m ON v.vessel_id = m.vessel_id
WHERE m.time < NOW() AT TIME ZONE 'UTC' - INTERVAL '1 YEAR'
LOOP
RAISE NOTICE '-> cron_process_deactivated_rec_fn for inactivity [%]', no_activity_rec;
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_deactivated_rec_fn inactivity [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('deactivated'::TEXT, user_settings::JSONB);
--PERFORM public.delete_account_fn(no_activity_rec.owner_email::TEXT, no_activity_rec.vessel_id::TEXT);
END LOOP;
-- List accounts with no vessel metadata for more than 1 YEAR
FOR no_activity_rec in
SELECT
a.user_id,a.email,a.first,a.created_at
FROM auth.accounts a, auth.vessels v
WHERE NOT EXISTS (
SELECT *
FROM api.metadata m
WHERE v.vessel_id = m.vessel_id) AND v.owner_email = a.email
AND v.created_at < NOW() AT TIME ZONE 'UTC' - INTERVAL '1 YEAR'
LOOP
RAISE NOTICE '-> cron_process_deactivated_rec_fn for no metadata [%]', no_activity_rec;
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_deactivated_rec_fn no metadata [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('deactivated'::TEXT, user_settings::JSONB);
--PERFORM public.delete_account_fn(no_activity_rec.owner_email::TEXT, no_activity_rec.vessel_id::TEXT);
END LOOP;
-- List accounts with no vessel created for more than 1 YEAR
FOR no_activity_rec in
SELECT a.user_id,a.email,a.first,a.created_at
FROM auth.accounts a
WHERE NOT EXISTS (
SELECT *
FROM auth.vessels v
WHERE v.owner_email = a.email)
AND a.created_at < NOW() AT TIME ZONE 'UTC' - INTERVAL '1 YEAR'
LOOP
RAISE NOTICE '-> cron_process_deactivated_rec_fn for no vessel [%]', no_activity_rec;
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_deactivated_rec_fn no vessel [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('deactivated'::TEXT, user_settings::JSONB);
--PERFORM public.delete_account_fn(no_activity_rec.owner_email::TEXT, no_activity_rec.vessel_id::TEXT);
END LOOP;
END;
$deactivated$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_deactivated_fn
IS 'init by pg_cron, check for vessel with no activity for more than 1 year then send notification and delete data';
DROP FUNCTION IF EXISTS public.cron_prune_otp_fn;
CREATE OR REPLACE FUNCTION public.cron_prune_otp_fn() RETURNS void
AS $$
DECLARE
otp_rec record;
BEGIN
-- Purge OTP older than 15 minutes
RAISE NOTICE 'cron_prune_otp_fn';
FOR otp_rec in
SELECT *
FROM auth.otp
WHERE otp_timestamp < NOW() AT TIME ZONE 'UTC' - INTERVAL '15 MINUTES'
ORDER BY otp_timestamp desc
LOOP
RAISE NOTICE '-> cron_prune_otp_fn deleting expired otp for user [%]', otp_rec.user_email;
-- remove entry
DELETE FROM auth.otp
WHERE user_email = otp_rec.user_email;
RAISE NOTICE '-> cron_prune_otp_fn deleted expire otp for user [%]', otp_rec.user_email;
END LOOP;
END;
$$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.cron_prune_otp_fn
IS 'init by pg_cron to purge older than 15 minutes OTP token';
DROP FUNCTION IF EXISTS public.cron_process_prune_otp_fn();
DROP FUNCTION IF EXISTS public.cron_process_no_vessel_fn();
DROP FUNCTION IF EXISTS public.cron_process_no_metadata_fn();
DROP FUNCTION IF EXISTS public.cron_process_no_activity_fn();
DROP FUNCTION IF EXISTS public.cron_process_deactivated_fn();
DROP FUNCTION IF EXISTS public.cron_process_windy_fn();
DROP FUNCTION IF EXISTS public.cron_process_alerts_fn();
-- Remove deprecated fn
DROP FUNCTION IF EXISTS public.cron_process_new_account_fn();
DROP FUNCTION IF EXISTS public.cron_process_new_account_otp_validation_fn();
DROP FUNCTION IF EXISTS public.cron_process_new_moorage_fn();
DROP FUNCTION IF EXISTS public.cron_process_new_vessel_fn();
CREATE OR REPLACE FUNCTION send_notification_fn(
IN email_type TEXT,
IN user_settings JSONB) RETURNS VOID
AS $send_notification$
DECLARE
app_settings JSONB;
_email_notifications BOOLEAN := False;
_phone_notifications BOOLEAN := False;
_pushover_user_key TEXT := NULL;
pushover_settings JSONB := NULL;
_telegram_notifications BOOLEAN := False;
_telegram_chat_id TEXT := NULL;
telegram_settings JSONB := NULL;
_email TEXT := NULL;
BEGIN
-- TODO input check
--RAISE NOTICE '--> send_notification_fn type [%]', email_type;
-- Gather notification app settings, eg: email, pushover, telegram
app_settings := get_app_settings_fn();
--RAISE NOTICE '--> send_notification_fn app_settings [%]', app_settings;
--RAISE NOTICE '--> user_settings [%]', user_settings->>'email'::TEXT;
-- Gather notifications settings and merge with user settings
-- Send notification email
SELECT preferences['email_notifications'] INTO _email_notifications
FROM auth.accounts a
WHERE a.email = user_settings->>'email'::TEXT;
RAISE NOTICE '--> send_notification_fn email_notifications [%]', _email_notifications;
-- If email server app settings set and if email user settings set
IF app_settings['app.email_server'] IS NOT NULL AND _email_notifications IS True THEN
PERFORM send_email_py_fn(email_type::TEXT, user_settings::JSONB, app_settings::JSONB);
END IF;
-- Send notification pushover
SELECT preferences['phone_notifications'],preferences->>'pushover_user_key' INTO _phone_notifications,_pushover_user_key
FROM auth.accounts a
WHERE a.email = user_settings->>'email'::TEXT;
RAISE NOTICE '--> send_notification_fn phone_notifications [%]', _phone_notifications;
-- If pushover app settings set and if pushover user settings set
IF app_settings['app.pushover_app_token'] IS NOT NULL AND _phone_notifications IS True AND _pushover_user_key IS NOT NULL THEN
SELECT json_build_object('pushover_user_key', _pushover_user_key) into pushover_settings;
SELECT user_settings::JSONB || pushover_settings::JSONB into user_settings;
--RAISE NOTICE '--> send_notification_fn user_settings + pushover [%]', user_settings;
PERFORM send_pushover_py_fn(email_type::TEXT, user_settings::JSONB, app_settings::JSONB);
END IF;
-- Send notification telegram
SELECT (preferences->'telegram'->'chat'->'id') IS NOT NULL,preferences['telegram']['chat']['id'] INTO _telegram_notifications,_telegram_chat_id
FROM auth.accounts a
WHERE a.email = user_settings->>'email'::TEXT;
RAISE NOTICE '--> send_notification_fn telegram_notifications [%]', _telegram_notifications;
-- If telegram app settings set and if telegram user settings set
IF app_settings['app.telegram_bot_token'] IS NOT NULL AND _telegram_notifications IS True AND _phone_notifications IS True THEN
SELECT json_build_object('telegram_chat_id', _telegram_chat_id) into telegram_settings;
SELECT user_settings::JSONB || telegram_settings::JSONB into user_settings;
--RAISE NOTICE '--> send_notification_fn user_settings + telegram [%]', user_settings;
PERFORM send_telegram_py_fn(email_type::TEXT, user_settings::JSONB, app_settings::JSONB);
END IF;
END;
$send_notification$ LANGUAGE plpgsql;
-- fn to trim new vessel name
CREATE FUNCTION new_vessel_trim_fn() RETURNS trigger AS $new_vessel_trim_fn$
BEGIN
NEW.name := TRIM(NEW.name);
RETURN NEW;
END;
$new_vessel_trim_fn$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.new_vessel_trim_fn
IS 'Trim space vessel name';
-- Trigger trim new vessel name
CREATE TRIGGER new_vessel_trim BEFORE INSERT ON auth.vessels
FOR EACH ROW EXECUTE FUNCTION public.new_vessel_trim_fn();
-- Description
COMMENT ON TRIGGER new_vessel_trim
ON auth.vessels
IS 'Trim space vessel name';
CREATE or REPLACE FUNCTION public.logbook_update_geojson_fn(IN _id integer, IN _start text, IN _end text,
OUT _track_geojson JSON
) AS $logbook_geojson$
declare
log_geojson jsonb;
metrics_geojson jsonb;
_map jsonb;
begin
-- GeoJson Feature Logbook linestring
SELECT
ST_AsGeoJSON(log.*) into log_geojson
FROM
( SELECT
id,name,
distance,
duration,
avg_speed,
max_speed,
max_wind_speed,
_from_time,
_to_time
_from_moorage_id,
_to_moorage_id,
notes,
track_geom
FROM api.logbook
WHERE id = _id
) AS log;
-- GeoJson Feature Metrics point
SELECT
json_agg(ST_AsGeoJSON(t.*)::json) into metrics_geojson
FROM (
( SELECT
time,
courseovergroundtrue,
speedoverground,
windspeedapparent,
longitude,latitude,
'' AS notes,
coalesce(metrics->'environment.wind.speedTrue', null) as truewindspeed,
coalesce(metrics->'environment.wind.directionTrue', null) as truewinddirection,
coalesce(status, null) as status,
st_makepoint(longitude,latitude) AS geo_point
FROM api.metrics m
WHERE m.latitude IS NOT NULL
AND m.longitude IS NOT NULL
AND time >= _start::TIMESTAMPTZ
AND time <= _end::TIMESTAMPTZ
AND vessel_id = current_setting('vessel.id', false)
ORDER BY m.time ASC
)
) AS t;
-- Merge jsonb
SELECT log_geojson::jsonb || metrics_geojson::jsonb into _map;
-- output
SELECT
json_build_object(
'type', 'FeatureCollection',
'features', _map
) into _track_geojson;
END;
$logbook_geojson$ LANGUAGE plpgsql;
-- Update version
UPDATE public.app_settings
SET value='0.7.0'
WHERE "name"='app.version';
-- Create a cron job
\c postgres
UPDATE cron.job
SET command='select public.cron_prune_otp_fn()'
WHERE jobname = 'cron_prune_otp';
UPDATE cron.job
SET command='select public.cron_no_vessel_fn()'
WHERE jobname = 'cron_no_vessel';
UPDATE cron.job
SET command='select public.cron_no_metadata_fn()'
WHERE jobname = 'cron_no_metadata';
UPDATE cron.job
SET command='select public.cron_no_activity_fn()'
WHERE jobname = 'cron_no_activity';
UPDATE cron.job
SET command='select public.cron_windy_fn()'
WHERE jobname = 'cron_windy';
UPDATE cron.job
SET command='select public.cron_alerts_fn()'
WHERE jobname = 'cron_alerts';

View File

@@ -0,0 +1,528 @@
---------------------------------------------------------------------------
-- Copyright 2021-2024 Francois Lacroix <xbgmsharp@gmail.com>
-- This file is part of PostgSail which is released under Apache License, Version 2.0 (the "License").
-- See file LICENSE or go to http://www.apache.org/licenses/LICENSE-2.0 for full license details.
--
-- Migration March 2024
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
\echo 'Force timezone, just in case'
set timezone to 'UTC';
CREATE OR REPLACE FUNCTION public.process_lat_lon_fn(IN lon NUMERIC, IN lat NUMERIC,
OUT moorage_id INTEGER,
OUT moorage_type INTEGER,
OUT moorage_name TEXT,
OUT moorage_country TEXT
) AS $process_lat_lon$
DECLARE
stay_rec record;
--moorage_id INTEGER := NULL;
--moorage_type INTEGER := 1; -- Unknown
--moorage_name TEXT := NULL;
--moorage_country TEXT := NULL;
existing_rec record;
geo jsonb;
overpass jsonb;
BEGIN
RAISE NOTICE '-> process_lat_lon_fn';
IF lon IS NULL OR lat IS NULL THEN
RAISE WARNING '-> process_lat_lon_fn invalid input lon %, lat %', lon, lat;
--return NULL;
END IF;
-- Do we have an existing moorages within 300m of the new stay
FOR existing_rec in
SELECT
*
FROM api.moorages m
WHERE
m.latitude IS NOT NULL
AND m.longitude IS NOT NULL
AND m.geog IS NOT NULL
AND ST_DWithin(
Geography(ST_MakePoint(m.longitude, m.latitude)),
Geography(ST_MakePoint(lon, lat)),
300 -- in meters
)
AND m.vessel_id = current_setting('vessel.id', false)
ORDER BY id ASC
LOOP
-- found previous stay within 300m of the new moorage
IF existing_rec.id IS NOT NULL AND existing_rec.id > 0 THEN
RAISE NOTICE '-> process_lat_lon_fn found previous moorages within 300m %', existing_rec;
EXIT; -- exit loop
END IF;
END LOOP;
-- if with in 300m use existing name and stay_code
-- else insert new entry
IF existing_rec.id IS NOT NULL AND existing_rec.id > 0 THEN
RAISE NOTICE '-> process_lat_lon_fn found close by moorage using existing name and stay_code %', existing_rec;
moorage_id := existing_rec.id;
moorage_name := existing_rec.name;
moorage_type := existing_rec.stay_code;
ELSE
RAISE NOTICE '-> process_lat_lon_fn create new moorage';
-- query overpass api to guess moorage type
overpass := overpass_py_fn(lon::NUMERIC, lat::NUMERIC);
RAISE NOTICE '-> process_lat_lon_fn overpass name:[%] seamark:type:[%]', overpass->'name', overpass->'seamark:type';
moorage_type = 1; -- Unknown
IF overpass->>'seamark:type' = 'harbour' AND overpass->>'seamark:harbour:category' = 'marina' then
moorage_type = 4; -- Dock
ELSIF overpass->>'seamark:type' = 'mooring' AND overpass->>'seamark:mooring:category' = 'buoy' then
moorage_type = 3; -- Mooring Buoy
ELSIF overpass->>'seamark:type' ~ '(anchorage|anchor_berth|berth)' OR overpass->>'natural' ~ '(bay|beach)' then
moorage_type = 2; -- Anchor
ELSIF overpass->>'seamark:type' = 'mooring' then
moorage_type = 3; -- Mooring Buoy
ELSIF overpass->>'leisure' = 'marina' then
moorage_type = 4; -- Dock
END IF;
-- geo reverse _lng _lat
geo := reverse_geocode_py_fn('nominatim', lon::NUMERIC, lat::NUMERIC);
moorage_country := geo->>'country_code';
IF overpass->>'name:en' IS NOT NULL then
moorage_name = overpass->>'name:en';
ELSIF overpass->>'name' IS NOT NULL then
moorage_name = overpass->>'name';
ELSE
moorage_name := geo->>'name';
END IF;
RAISE NOTICE '-> process_lat_lon_fn output name:[%] type:[%]', moorage_name, moorage_type;
RAISE NOTICE '-> process_lat_lon_fn insert new moorage for [%] name:[%] type:[%]', current_setting('vessel.id', false), moorage_name, moorage_type;
-- Insert new moorage from stay
INSERT INTO api.moorages
(vessel_id, name, country, stay_code, reference_count, latitude, longitude, geog, overpass, nominatim)
VALUES (
current_setting('vessel.id', false),
coalesce(moorage_name, null),
coalesce(moorage_country, null),
moorage_type,
1,
lat,
lon,
Geography(ST_MakePoint(lon, lat)),
coalesce(overpass, null),
coalesce(geo, null)
) returning id into moorage_id;
-- Add moorage entry to process queue for reference
--INSERT INTO process_queue (channel, payload, stored, ref_id, processed)
-- VALUES ('new_moorage', moorage_id, now(), current_setting('vessel.id', true), now());
END IF;
--return json_build_object(
-- 'id', moorage_id,
-- 'name', moorage_name,
-- 'type', moorage_type
-- )::jsonb;
END;
$process_lat_lon$ LANGUAGE plpgsql;
CREATE or replace FUNCTION public.logbook_update_geojson_fn(IN _id integer, IN _start text, IN _end text,
OUT _track_geojson JSON
) AS $logbook_geojson$
declare
log_geojson jsonb;
metrics_geojson jsonb;
_map jsonb;
begin
-- GeoJson Feature Logbook linestring
SELECT
ST_AsGeoJSON(log.*) into log_geojson
FROM
( SELECT
id,name,
distance,
duration,
avg_speed,
max_speed,
max_wind_speed,
_from_time,
_to_time
_from_moorage_id,
_to_moorage_id,
notes,
track_geom
FROM api.logbook
WHERE id = _id
) AS log;
-- GeoJson Feature Metrics point
SELECT
json_agg(ST_AsGeoJSON(t.*)::json) into metrics_geojson
FROM (
( SELECT
time,
courseovergroundtrue,
speedoverground,
windspeedapparent,
longitude,latitude,
'' AS notes,
coalesce(metersToKnots((metrics->'environment.wind.speedTrue')::NUMERIC), null) as truewindspeed,
coalesce(radiantToDegrees((metrics->'environment.wind.directionTrue')::NUMERIC), null) as truewinddirection,
coalesce(status, null) as status,
st_makepoint(longitude,latitude) AS geo_point
FROM api.metrics m
WHERE m.latitude IS NOT NULL
AND m.longitude IS NOT NULL
AND time >= _start::TIMESTAMPTZ
AND time <= _end::TIMESTAMPTZ
AND vessel_id = current_setting('vessel.id', false)
ORDER BY m.time ASC
)
) AS t;
-- Merge jsonb
SELECT log_geojson::jsonb || metrics_geojson::jsonb into _map;
-- output
SELECT
json_build_object(
'type', 'FeatureCollection',
'features', _map
) into _track_geojson;
END;
$logbook_geojson$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION public.metersToKnots(IN meters NUMERIC)
RETURNS NUMERIC
AS $$
BEGIN
RETURN ROUND(((meters * 1.9438445) * 10) / 10, 2);
END
$$
LANGUAGE plpgsql IMMUTABLE;
-- Description
COMMENT ON FUNCTION
public.metersToKnots
IS 'convert speed meters/s To Knots';
CREATE OR REPLACE FUNCTION logbook_update_extra_json_fn(IN _id integer, IN _start text, IN _end text,
OUT _extra_json JSON
) AS $logbook_extra_json$
declare
obs_json jsonb default '{ "seaState": -1, "cloudCoverage": -1, "visibility": -1}'::jsonb;
log_json jsonb default '{}'::jsonb;
runtime_json jsonb default '{}'::jsonb;
metrics_json jsonb default '{}'::jsonb;
metric_rec record;
BEGIN
-- Calculate 'navigation.log' metrics
WITH
start_trip as (
-- Fetch 'navigation.log' start, first entry
SELECT key, value
FROM api.metrics m,
jsonb_each_text(m.metrics)
WHERE key ILIKE 'navigation.log'
AND time = _start::TIMESTAMPTZ
AND vessel_id = current_setting('vessel.id', false)
),
end_trip as (
-- Fetch 'navigation.log' end, last entry
SELECT key, value
FROM api.metrics m,
jsonb_each_text(m.metrics)
WHERE key ILIKE 'navigation.log'
AND time = _end::TIMESTAMPTZ
AND vessel_id = current_setting('vessel.id', false)
),
nm as (
-- calculate distance and convert meter to nautical miles
SELECT ((end_trip.value::NUMERIC - start_trip.value::numeric) * 0.00053996) as trip from start_trip,end_trip
)
-- Generate JSON
SELECT jsonb_build_object('navigation.log', trip) INTO log_json FROM nm;
RAISE NOTICE '-> logbook_update_extra_json_fn navigation.log: %', log_json;
-- Calculate engine hours from propulsion.%.runTime first entry
FOR metric_rec IN
SELECT key, value
FROM api.metrics m,
jsonb_each_text(m.metrics)
WHERE key ILIKE 'propulsion.%.runTime'
AND time = _start::TIMESTAMPTZ
AND vessel_id = current_setting('vessel.id', false)
LOOP
-- Engine Hours in seconds
RAISE NOTICE '-> logbook_update_extra_json_fn propulsion.*.runTime: %', metric_rec;
with
end_runtime AS (
-- Fetch 'propulsion.*.runTime' last entry
SELECT key, value
FROM api.metrics m,
jsonb_each_text(m.metrics)
WHERE key ILIKE metric_rec.key
AND time = _end::TIMESTAMPTZ
AND vessel_id = current_setting('vessel.id', false)
),
runtime AS (
-- calculate runTime Engine Hours as ISO duration
--SELECT (end_runtime.value::numeric - metric_rec.value::numeric) AS value FROM end_runtime
SELECT (((end_runtime.value::numeric - metric_rec.value::numeric) / 3600) * '1 hour'::interval)::interval as value FROM end_runtime
)
-- Generate JSON
SELECT jsonb_build_object(metric_rec.key, runtime.value) INTO runtime_json FROM runtime;
RAISE NOTICE '-> logbook_update_extra_json_fn key: %, value: %', metric_rec.key, runtime_json;
END LOOP;
-- Update logbook with extra value and return json
SELECT COALESCE(log_json::JSONB, '{}'::jsonb) || COALESCE(runtime_json::JSONB, '{}'::jsonb) INTO metrics_json;
SELECT jsonb_build_object('metrics', metrics_json, 'observations', obs_json) INTO _extra_json;
RAISE NOTICE '-> logbook_update_extra_json_fn log_json: %, runtime_json: %, _extra_json: %', log_json, runtime_json, _extra_json;
END;
$logbook_extra_json$ LANGUAGE plpgsql;
DROP FUNCTION IF EXISTS public.logbook_update_gpx_fn();
CREATE OR REPLACE FUNCTION metadata_upsert_trigger_fn() RETURNS trigger AS $metadata_upsert$
DECLARE
metadata_id integer;
metadata_active boolean;
BEGIN
-- Set client_id to new value to allow RLS
--PERFORM set_config('vessel.client_id', NEW.client_id, false);
-- UPSERT - Insert vs Update for Metadata
--RAISE NOTICE 'metadata_upsert_trigger_fn';
--PERFORM set_config('vessel.id', NEW.vessel_id, true);
--RAISE WARNING 'metadata_upsert_trigger_fn [%] [%]', current_setting('vessel.id', true), NEW;
SELECT m.id,m.active INTO metadata_id, metadata_active
FROM api.metadata m
WHERE m.vessel_id IS NOT NULL AND m.vessel_id = current_setting('vessel.id', true);
--RAISE NOTICE 'metadata_id is [%]', metadata_id;
IF metadata_id IS NOT NULL THEN
-- send notification if boat is back online
IF metadata_active is False THEN
-- Add monitor online entry to process queue for later notification
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('monitoring_online', metadata_id, now(), current_setting('vessel.id', true));
END IF;
-- Update vessel metadata
UPDATE api.metadata
SET
name = NEW.name,
mmsi = NEW.mmsi,
client_id = NEW.client_id,
length = NEW.length,
beam = NEW.beam,
height = NEW.height,
ship_type = NEW.ship_type,
plugin_version = NEW.plugin_version,
signalk_version = NEW.signalk_version,
platform = REGEXP_REPLACE(NEW.platform, '[^a-zA-Z0-9\(\) ]', '', 'g'),
configuration = NEW.configuration,
-- time = NEW.time, ignore the time sent by the vessel as it is out of sync sometimes.
time = NOW(), -- overwrite the time sent by the vessel
active = true
WHERE id = metadata_id;
RETURN NULL; -- Ignore insert
ELSE
IF NEW.vessel_id IS NULL THEN
-- set vessel_id from jwt if not present in INSERT query
NEW.vessel_id := current_setting('vessel.id');
END IF;
-- Ignore and overwrite the time sent by the vessel
NEW.time := NOW();
-- Insert new vessel metadata
RETURN NEW; -- Insert new vessel metadata
END IF;
END;
$metadata_upsert$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION public.cron_windy_fn() RETURNS void AS $$
DECLARE
windy_rec record;
default_last_metric TIMESTAMPTZ := NOW() - interval '1 day';
last_metric TIMESTAMPTZ := NOW();
metric_rec record;
windy_metric jsonb;
app_settings jsonb;
user_settings jsonb;
windy_pws jsonb;
BEGIN
-- Check for new observations pending update
RAISE NOTICE 'cron_process_windy_fn';
-- Gather url from app settings
app_settings := get_app_settings_fn();
-- Find users with Windy active and with an active vessel
-- Map account id to Windy Station ID
FOR windy_rec in
SELECT
a.id,a.email,v.vessel_id,v.name,
COALESCE((a.preferences->'windy_last_metric')::TEXT, default_last_metric::TEXT) as last_metric
FROM auth.accounts a
LEFT JOIN auth.vessels AS v ON v.owner_email = a.email
LEFT JOIN api.metadata AS m ON m.vessel_id = v.vessel_id
WHERE (a.preferences->'public_windy')::boolean = True
AND m.active = True
LOOP
RAISE NOTICE '-> cron_process_windy_fn for [%]', windy_rec;
PERFORM set_config('vessel.id', windy_rec.vessel_id, false);
--RAISE WARNING 'public.cron_process_windy_rec_fn() scheduler vessel.id %, user.id', current_setting('vessel.id', false), current_setting('user.id', false);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(windy_rec.vessel_id::TEXT);
RAISE NOTICE '-> cron_process_windy_fn checking user_settings [%]', user_settings;
-- Get all metrics from the last windy_last_metric avg by 5 minutes
-- TODO json_agg to send all data in once, but issue with py jsonb transformation decimal.
FOR metric_rec in
SELECT time_bucket('5 minutes', m.time) AS time_bucket,
avg((m.metrics->'environment.outside.temperature')::numeric) AS temperature,
avg((m.metrics->'environment.outside.pressure')::numeric) AS pressure,
avg((m.metrics->'environment.outside.relativeHumidity')::numeric) AS rh,
avg((m.metrics->'environment.wind.directionTrue')::numeric) AS winddir,
avg((m.metrics->'environment.wind.speedTrue')::numeric) AS wind,
max((m.metrics->'environment.wind.speedTrue')::numeric) AS gust,
last(latitude, time) AS lat,
last(longitude, time) AS lng
FROM api.metrics m
WHERE vessel_id = windy_rec.vessel_id
AND m.time >= windy_rec.last_metric::TIMESTAMPTZ
GROUP BY time_bucket
ORDER BY time_bucket ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_process_windy_fn checking metrics [%]', metric_rec;
IF metric_rec.wind is null or metric_rec.temperature is null THEN
-- Ignore when there is no metrics.
-- Send notification
PERFORM send_notification_fn('windy_error'::TEXT, user_settings::JSONB);
-- Disable windy
PERFORM api.update_user_preferences_fn('{public_windy}'::TEXT, 'false'::TEXT);
RETURN;
END IF;
-- https://community.windy.com/topic/8168/report-your-weather-station-data-to-windy
-- temp from kelvin to celcuis
-- winddir from radiant to degres
-- rh from ratio to percentage
SELECT jsonb_build_object(
'dateutc', metric_rec.time_bucket,
'station', windy_rec.id,
'name', windy_rec.name,
'lat', metric_rec.lat,
'lon', metric_rec.lng,
'wind', metric_rec.wind,
'gust', metric_rec.gust,
'pressure', metric_rec.pressure,
'winddir', radiantToDegrees(metric_rec.winddir::numeric),
'temp', kelvinToCel(metric_rec.temperature::numeric),
'rh', valToPercent(metric_rec.rh::numeric)
) INTO windy_metric;
RAISE NOTICE '-> cron_process_windy_fn checking windy_metrics [%]', windy_metric;
SELECT windy_pws_py_fn(windy_metric, user_settings, app_settings) into windy_pws;
RAISE NOTICE '-> cron_process_windy_fn Windy PWS [%]', ((windy_pws->'header')::JSONB ? 'id');
IF NOT((user_settings->'settings')::JSONB ? 'windy') and ((windy_pws->'header')::JSONB ? 'id') then
RAISE NOTICE '-> cron_process_windy_fn new Windy PWS [%]', (windy_pws->'header')::JSONB->>'id';
-- Send metrics to Windy
PERFORM api.update_user_preferences_fn('{windy}'::TEXT, ((windy_pws->'header')::JSONB->>'id')::TEXT);
-- Send notification
PERFORM send_notification_fn('windy'::TEXT, user_settings::JSONB);
-- Refresh user settings after first success
user_settings := get_user_settings_from_vesselid_fn(windy_rec.vessel_id::TEXT);
END IF;
-- Record last metrics time
SELECT metric_rec.time_bucket INTO last_metric;
END LOOP;
PERFORM api.update_user_preferences_fn('{windy_last_metric}'::TEXT, last_metric::TEXT);
END LOOP;
END;
$$ language plpgsql;
DROP FUNCTION public.delete_vessel_fn;
CREATE OR REPLACE FUNCTION public.delete_vessel_fn(IN _vessel_id TEXT) RETURNS JSONB
AS $delete_vessel$
DECLARE
total_metrics INTEGER;
del_metrics INTEGER;
del_logs INTEGER;
del_stays INTEGER;
del_moorages INTEGER;
del_queue INTEGER;
out_json JSONB;
BEGIN
select count(*) INTO total_metrics from api.metrics m where vessel_id = _vessel_id;
WITH deleted AS (delete from api.metrics m where vessel_id = _vessel_id RETURNING *) SELECT count(*) INTO del_metrics FROM deleted;
WITH deleted AS (delete from api.logbook l where vessel_id = _vessel_id RETURNING *) SELECT count(*) INTO del_logs FROM deleted;
WITH deleted AS (delete from api.stays s where vessel_id = _vessel_id RETURNING *) SELECT count(*) INTO del_stays FROM deleted;
WITH deleted AS (delete from api.moorages m where vessel_id = _vessel_id RETURNING *) SELECT count(*) INTO del_moorages FROM deleted;
WITH deleted AS (delete from public.process_queue m where ref_id = _vessel_id RETURNING *) SELECT count(*) INTO del_queue FROM deleted;
SELECT jsonb_build_object('total_metrics', total_metrics,
'del_metrics', del_metrics,
'del_logs', del_logs,
'del_stays', del_stays,
'del_moorages', del_moorages,
'del_queue', del_queue) INTO out_json;
RETURN out_json;
END
$delete_vessel$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.delete_vessel_fn
IS 'Delete all vessel data (metrics,logbook,stays,moorages,process_queue) for a vessel_id';
DROP FUNCTION IF EXISTS public.cron_process_no_activity_fn();
CREATE OR REPLACE FUNCTION public.cron_process_no_activity_fn() RETURNS void AS $no_activity$
DECLARE
no_activity_rec record;
user_settings jsonb;
total_metrics INTEGER;
total_logs INTEGER;
del_metrics INTEGER;
out_json JSONB;
BEGIN
-- Check for vessel with no activity for more than 230 days
RAISE NOTICE 'cron_process_no_activity_fn';
FOR no_activity_rec in
SELECT
v.owner_email,m.name,m.vessel_id,m.time,a.first
FROM auth.accounts a
LEFT JOIN auth.vessels v ON v.owner_email = a.email
LEFT JOIN api.metadata m ON v.vessel_id = m.vessel_id
WHERE m.time < NOW() AT TIME ZONE 'UTC' - INTERVAL '230 DAYS'
AND v.owner_email <> 'demo@openplotter.cloud'
ORDER BY m.time DESC
LOOP
RAISE NOTICE '-> cron_process_no_activity_rec_fn for [%]', no_activity_rec;
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
RAISE NOTICE '-> debug cron_process_no_activity_rec_fn [%]', user_settings;
-- Send notification
PERFORM send_notification_fn('no_activity'::TEXT, user_settings::JSONB);
SELECT count(*) INTO total_metrics from api.metrics where vessel_id = no_activity_rec.vessel_id;
WITH deleted AS (delete from api.metrics m where vessel_id = no_activity_rec.vessel_id RETURNING *) SELECT count(*) INTO del_metrics FROM deleted;
SELECT count(*) INTO total_logs from api.logbook where vessel_id = no_activity_rec.vessel_id;
SELECT jsonb_build_object('total_metrics', total_metrics, 'total_logs', total_logs, 'del_metrics', del_metrics) INTO out_json;
RAISE NOTICE '-> debug cron_process_no_activity_rec_fn [%]', out_json;
END LOOP;
END;
$no_activity$ language plpgsql;
DROP FUNCTION public.delete_account_fn(text,text);
CREATE OR REPLACE FUNCTION public.delete_account_fn(IN _email TEXT, IN _vessel_id TEXT) RETURNS JSONB
AS $delete_account$
DECLARE
del_vessel_data JSONB;
del_meta INTEGER;
del_vessel INTEGER;
del_account INTEGER;
out_json JSONB;
BEGIN
SELECT public.delete_vessel_fn(_vessel_id) INTO del_vessel_data;
WITH deleted AS (delete from api.metadata where vessel_id = _vessel_id RETURNING *) SELECT count(*) INTO del_meta FROM deleted;
WITH deleted AS (delete from auth.vessels where vessel_id = _vessel_id RETURNING *) SELECT count(*) INTO del_vessel FROM deleted;
WITH deleted AS (delete from auth.accounts where email = _email RETURNING *) SELECT count(*) INTO del_account FROM deleted;
SELECT jsonb_build_object('del_metadata', del_meta,
'del_vessel', del_vessel,
'del_account', del_account) || del_vessel_data INTO out_json;
RETURN out_json;
END
$delete_account$ language plpgsql;
-- Description
COMMENT ON FUNCTION
public.delete_account_fn
IS 'Delete all data for a account by email and vessel_id';
-- Update version
UPDATE public.app_settings
SET value='0.7.1'
WHERE "name"='app.version';

View File

@@ -14,12 +14,17 @@ INSERT INTO app_settings (name, value) VALUES
('app.email_user', '${PGSAIL_EMAIL_USER}'),
('app.email_pass', '${PGSAIL_EMAIL_PASS}'),
('app.email_from', '${PGSAIL_EMAIL_FROM}'),
('app.pushover_token', '${PGSAIL_PUSHOVER_TOKEN}'),
('app.pushover_app', '_todo_'),
('app.pushover_app_token', '${PGSAIL_PUSHOVER_APP_TOKEN}'),
('app.pushover_app_url', '${PGSAIL_PUSHOVER_APP_URL}'),
('app.telegram_bot_token', '${PGSAIL_TELEGRAM_BOT_TOKEN}'),
('app.grafana_admin_uri', '${PGSAIL_GRAFANA_ADMIN_URI}'),
('app.keycloak_uri', '${PGSAIL_KEYCLOAK_URI}'),
('app.url', '${PGSAIL_APP_URL}'),
('app.version', '${PGSAIL_VERSION}');
-- Update comment with version
COMMENT ON DATABASE signalk IS 'version ${PGSAIL_VERSION}';
COMMENT ON DATABASE signalk IS 'PostgSail version ${PGSAIL_VERSION}';
-- Update password from env
ALTER ROLE authenticator WITH PASSWORD '${PGSAIL_AUTHENTICATOR_PASSWORD}';
ALTER ROLE grafana WITH PASSWORD '${PGSAIL_GRAFANA_PASSWORD}';
ALTER ROLE grafana_auth WITH PASSWORD '${PGSAIL_GRAFANA_AUTH_PASSWORD}';
END

View File

@@ -1 +1 @@
0.0.4
0.7.1

File diff suppressed because one or more lines are too long

1
openapi.json Normal file

File diff suppressed because one or more lines are too long

13
pgadmin_servers.json Normal file
View File

@@ -0,0 +1,13 @@
{
"Servers": {
"dev": {
"Name": "PostgSail dev db",
"Group": "Servers",
"Port": 5432,
"Host": "db",
"SSLMode": "prefer",
"MaintenanceDB": "postgres",
"Username": "postgres"
}
}
}

7
tests/Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM node:lts
ENV DEBIAN_FRONTEND=noninteractive
# Install and update the system
RUN apt-get -q update && apt-get -qy upgrade && apt-get -qy install postgresql-client
# Clean up APT when done.
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

18
tests/README.md Normal file
View File

@@ -0,0 +1,18 @@
# PostgSail Unit Tests
The Unit Tests allow to automatically validate api workflow.
## A global overview
Based on `mocha` & `psql`
## get started
```bash
$ npm i
$ alias mocha="./node_modules/.bin/mocha"
$ bash tests.sh
```
## docker
```bash
$ docker-compose up -d db && sleep 15 && docker-compose up -d api && sleep 5
$ docker-compose -f docker-compose.dev.yml -f docker-compose.yml up tests
```

826
tests/index.js Normal file
View File

@@ -0,0 +1,826 @@
'use strict';
/*
* Unit test #1
* Create 2 users and 2 associate vessel with metrics
*
* process.env.PGSAIL_API_URI = from inside the docker
*
* npm install supertest should mocha mochawesome moment
* alias mocha="./node_modules/mocha/bin/_mocha"
* mocha index.js --reporter mochawesome --reporter-options reportDir=/mnt/postgsail/,reportFilename=report_api.html
*
*/
const sleep = ms => new Promise(r => setTimeout(r, ms));
const supertest = require("supertest");
// Deprecated
const should = require("should");
//const chai = require("chai");
//const should = chai.should();
let request = null;
let user_jwt = null;
let vessel_jwt = null;
var moment = require('moment');
const metrics_kapla = require('./metrics_sample_kapla.json');
const metrics_aava = require('./metrics_sample_aava.json');
const fs = require('fs');
// CNAMEs Array
[
{ cname: process.env.PGSAIL_API_URI, name: "PostgSail unit test kapla",
signin: { email: 'demo+kapla@openplotter.cloud', pass: 'test', firstname:'First_kapla', lastname:'Last_kapla'},
login: { email: 'demo+kapla@openplotter.cloud', pass: 'test'},
vessel: { vessel_email: "demo+kapla@openplotter.cloud", vessel_mmsi: "test", vessel_name: " kapla "},
preferences: { key: '{email_notifications}', value: false }, /* Disable email_notifications */
vessel_metadata: {
name: "kapla",
mmsi: "123456789",
client_id: "vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd",
length: "12",
beam: "10",
height: "24",
ship_type: "36",
plugin_version: "0.0.1",
signalk_version: "signalk_version",
time: moment.utc().subtract(69, 'minutes').format()
/* To trigger monitor_offline quickly */
},
vessel_metrics: metrics_kapla,
user_tables: [
{ url: '/stays', res_body_length: 3},
// not processed yet, { url: '/moorages', res_body_length: 2},
{ url: '/logbook', res_body_length: 2},
{ url: '/metadata', res_body_length: 1}
],
user_views: [
// not processed yet, { url: '/stays_view', res_body_length: 1},
// not processed yet, { url: '/moorages_view', res_body_length: 1},
{ url: '/logs_view', res_body_length: 0},
{ url: '/log_view', res_body_length: 2},
//{ url: '/stats_view', res_body_length: 1},
{ url: '/vessels_view', res_body_length: 1},
],
user_patchs: [
{ url: '/logbook?id=eq.1',
patch: {
name: "patch log name",
notes: "new log note"
},
},
{ url: '/stays?id=eq.1',
patch: {
name: "patch stay name",
stay_code: 2,
notes: "new stay note"
},
},
/* not processed yet, { url: '/moorages?id=eq.1',
patch: {
name: "patch moorage name",
home_flag: true,
stay_code: 2,
notes: "new moorage note"
},
}
*/
],
user_fn: [
{ url: '/rpc/timelapse_fn',
payload: {
start_log: 1
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_geojson_fn',
payload: {
_id: 1
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_gpx_fn',
payload: {
_id: 1
},
res: {
obj_name: null
}
},
{ url: '/rpc/vessel_fn',
payload: null,
res: {
obj_name: 'vessel'
}
},
{ url: '/rpc/settings_fn',
payload: null,
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/versions_fn',
payload: null,
res: {
obj_name: 'versions'
}
}
],
otp_fn: [
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+kapla@openplotter.cloud' },
res: {
otp: 0
}
},
{ url: '/rpc/email_fn',
payload: { token: null },
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+kapla@openplotter.cloud' },
res: {
otp: 0
}
},
{ url: '/rpc/pushover_fn',
payload: { token: null, pushover_user_key: '1234567890azerty!'},
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+kapla@openplotter.cloud' },
res: {
otp: 0
}
},
{ url: '/rpc/telegram_fn',
payload: { token: null, telegram_obj: {"chat": {"id": 1234567890, "type": "private", "title": null, "all_members_are_administrators": null}, "date": "NOW", "from": {"id": 1234567890, "is_bot": false, "first_name": "Kapla", "language_code": "en"}} },
res: {
obj_name: 'settings'
}
}
]
},
{ cname: process.env.PGSAIL_API_URI, name: "PostgSail unit test, aava",
signin: { email: 'demo+aava@openplotter.cloud', pass: 'test', firstname:'first_aava', lastname:'last_aava'},
login: { email: 'demo+aava@openplotter.cloud', pass: 'test'},
vessel: { vessel_email: "demo+aava@openplotter.cloud", vessel_mmsi: "787654321", vessel_name: " aava "},
preferences: { key: '{email_notifications}', value: false }, /* Disable email_notifications */
vessel_metadata: {
name: "aava",
mmsi: "787654321",
client_id: "vessels.urn:mrn:imo:mmsi:787654321",
length: "12",
beam: "10",
height: "24",
ship_type: "37",
plugin_version: "1.0.2",
signalk_version: "1.20.0",
time: moment().subtract(69, 'minutes').format()
},
vessel_metrics: metrics_aava,
user_tables: [
{ url: '/stays', res_body_length: 2},
// not processed yet, { url: '/moorages', res_body_length: 2},
{ url: '/logbook', res_body_length: 1},
{ url: '/metadata', res_body_length: 1}
],
user_views: [
// not processed yet, { url: '/stays_view', res_body_length: 1},
// not processed yet, { url: '/moorages_view', res_body_length: 1},
{ url: '/logs_view', res_body_length: 0},
{ url: '/log_view', res_body_length: 1},
//{ url: '/stats_view', res_body_length: 1},
{ url: '/vessels_view', res_body_length: 1},
],
user_patchs: [
{ url: '/logbook?id=eq.3',
patch: {
name: "patch log name",
notes: "new log note"
},
},
{ url: '/stays?id=eq.4',
patch: {
name: "patch stay name",
stay_code: 2,
notes: "new stay note"
},
},
/* not processed yet, { url: '/moorages?id=eq.1',
patch: {
name: "patch moorage name",
home_flag: true,
stay_code: 2,
notes: "new moorage note"
},
}
*/
],
user_fn: [
{ url: '/rpc/timelapse_fn',
payload: {
start_log: 3
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_geojson_fn',
payload: {
_id: 3
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_gpx_fn',
payload: {
_id: 3
},
res: {
obj_name: null
}
},
{ url: '/rpc/vessel_fn',
payload: null,
res: {
obj_name: 'vessel'
}
},
{ url: '/rpc/settings_fn',
payload: null,
res: {
obj_name: 'settings'
}
}
],
otp_fn: [
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+aava@openplotter.cloud' },
res: {
otp: 0
}
},
{ url: '/rpc/email_fn',
payload: { token: null },
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+aava@openplotter.cloud' },
res: {
otp: 0
}
},
{ url: '/rpc/pushover_fn',
payload: { token: null, pushover_user_key: '0987654321qwerty!'},
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+aava@openplotter.cloud' },
res: {
otp: 0
}
},
{ url: '/rpc/telegram_fn',
payload: { token: null, telegram_obj: {"chat": {"id": 9876543210, "type": "private", "title": null, "all_members_are_administrators": null}, "date": "NOW", "from": {"id": 9876543210, "is_bot": false, "first_name": "Aava", "language_code": "en"}} },
res: {
obj_name: 'settings'
}
},
]
}
].forEach( function(test){
//console.log(`${test.cname}`);
describe(`${test.name}`, function(){
request = supertest.agent(test.cname);
request.set('User-Agent', 'PostgSail unit tests');
describe("OpenAPI description", function(){
it('/', function(done) {
request = supertest.agent(test.cname);
request
.get('/')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.paths['/rpc/signup']);
should.exist(res.body.paths['/rpc/login']);
should.exist(res.body.paths['/rpc/reset']);
should.exist(res.body.paths['/rpc/recover']);
//should.exist(res.body.paths['/rpc/generate_otp_fn']);
should.exist(res.body.paths['/rpc/pushover_fn']);
should.exist(res.body.paths['/rpc/telegram_fn']);
should.exist(res.body.paths['/rpc/telegram']);
done(err);
});
});
}); // OpenAPI description
describe("Get JWT user_role", function(){
it('/rpc/signup return user_role jwt token', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/signup')
.send(test.signin)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.token);
user_jwt = res.body.token;
should.exist(user_jwt);
done(err);
});
});
it('/rpc/login return user_role jwt token', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/login')
.send(test.login)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.token);
//res.body.token.should.match(user_jwt);
console.log(user_jwt);
should.exist(user_jwt);
done(err);
});
});
}); // JWT user_role
describe("OpenAPI with JWT user_role", function(){
it('/', function(done) {
request = supertest.agent(test.cname);
request
.get('/')
.set('Authorization', `Bearer ${user_jwt}`)
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
// Functions
should.exist(res.body.paths['/rpc/register_vessel']);
should.exist(res.body.paths['/rpc/update_user_preferences_fn']);
should.exist(res.body.paths['/rpc/settings_fn']);
should.exist(res.body.paths['/rpc/versions_fn']);
// Tables
should.exist(res.body.paths['/metadata']);
should.exist(res.body.paths['/metrics']);
should.exist(res.body.paths['/logbook']);
should.exist(res.body.paths['/stays']);
should.exist(res.body.paths['/moorages']);
// Views
should.exist(res.body.paths['/logs_view']);
should.exist(res.body.paths['/moorages_view']);
should.exist(res.body.paths['/stays_view']);
should.exist(res.body.paths['/vessels_view']);
//should.exist(res.body.paths['/stats_view']);
should.exist(res.body.paths['/monitoring_view']);
done(err);
});
});
}); // OpenAPI JWT user_role
describe("Set preferences email_notifications, JWT user_role", function(){
it('/rpc/update_user_preferences_fn return true', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/update_user_preferences_fn')
.send(test.preferences)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//console.log(res.text);
should.exist(res.text);
res.text.should.match('true');
done(err);
});
});
}); // JWT user_role
describe("Get versions, JWT user_role", function(){
it('/rpc/versions_fn return json', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get('/rpc/versions_fn')
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//console.log(res.text);
should.exist(res.body.api_version);
should.exist(res.body.sys_version);
done(err);
});
});
}); // JWT user_role
describe("Get JWT vessel_role from user_role", function(){
it('/rpc/register_vessel return vessel_role jwt token', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/register_vessel')
.send(test.vessel)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.token);
vessel_jwt = res.body.token;
console.log(vessel_jwt);
should.exist(vessel_jwt);
// Save vessel JWT token for later use.
fs.writeFile(`vessel_jwt_${test.vessel.vessel_name}.txt`, vessel_jwt, (err) => {
// In case of a error throw err.
if (err) throw err;
})
done(err);
});
});
}); // JWT user_role
describe("OpenAPI with JWT vessel_role", function(){
it('/', function(done) {
request = supertest.agent(test.cname);
request
.get('/')
.set('Authorization', `Bearer ${vessel_jwt}`)
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.paths['/metadata']);
should.exist(res.body.paths['/metrics']);
should.exist(res.body.paths['/logbook']);
should.exist(res.body.paths['/stays']);
should.exist(res.body.paths['/moorages']);
done(err);
});
});
}); // OpenAPI JWT vessel_role
describe("Get vessel details view, JWT user_role", function(){
it('/vessels_view return json', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get('/vessels_view')
.set('Authorization', `Bearer ${user_jwt}`)
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
console.log(res.body);
//res.body.length.should.match(0);
res.body.length.should.match(1);
//res.body[0].last_contact.should.match('Never');
should.equal(res.body[0].last_contact, null);
done(err);
});
});
}); // JWT user_role
describe("Get vessel details function, JWT user_role", function(){
// no metadata from vessel so error - unrecognized configuration parameter "vessel.client_id"
it('/rpc/vessel_fn return json', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get('/rpc/vessel_fn')
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.end(function(err,res){
console.log(res.body)
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body);
//body = res.body;
//console.log(res.text);
done(err);
});
});
}); // JWT user_role
describe("Vessel POST metadata, JWT vessel_role", function(){
it('/metadata?on_conflict=vessel_id', function(done) {
request = supertest.agent(test.cname);
request
.post('/metadata?on_conflict=vessel_id')
.send(test.vessel_metadata)
.set('Authorization', `Bearer ${vessel_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Prefer', 'return=headers-only,resolution=merge-duplicates')
.end(function(err,res){
res.status.should.equal(201);
//console.log(res.header);
should.exist(res.header['server']);
res.header['server'].should.match(new RegExp('postgrest','g'));
done(err);
});
});
}); // Vessel metadata JWT vessel_role
describe("Vessel POST metrics, JWT vessel_role", function(){
let data = [];
//console.log(vessel_metrics['metrics'][0]);
let i;
for (i = 0; i < test.vessel_metrics['metrics'].length; i++) {
data[i] = test.vessel_metrics['metrics'][i];
// Override time, -2h to allow to new data later without delay.
data[i]['time'] = moment.utc().subtract(2, 'hours').add(i, 'minutes').format();
// Override client_id
data[i]['client_id'] = test.vessel_metadata.client_id;
}
// The last entry are invalid and should be ignore.
// - Invalid status
// - Invalid speedoverground
// - Invalid time previous time is duplicate
// Force last valid entry to be back in time from previous, it should be ignore silently
data.at(-1).time = moment.utc(data.at(-3).time).subtract(1, 'minutes').format();
//console.log(data[0]);
it('/metrics?select=time', function(done) {
request = supertest.agent(test.cname);
request
.post('/metrics?select=time')
.send(data)
.set('Authorization', `Bearer ${vessel_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Prefer', 'return=representation')
.end(function(err,res){
//console.log(res.body);
res.status.should.equal(201);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body);
res.body.length.should.match(test.vessel_metrics['metrics'].length-3);
done(err);
});
});
}); // Vessel POST metrics JWT vessel_role
/*
describe("run_cron_jobs() JWT vessel_role", function(){
it('/rpc/run_cron_jobs', function(done) {
request = supertest.agent(test.cname);
request
.get('/rpc/run_cron_jobs')
.set('Authorization', `Bearer ${vessel_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
done();
});
});
}); // run_cron_jobs() JWT vessel_role
*/
describe("Table endpoint, JWT user_role", function(){
test.user_tables.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(`${subtest.url}`)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body);
res.body.length.should.match(subtest.res_body_length);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Table endpoint
describe("Views endpoint, JWT user_role", function(){
test.user_views.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(`${subtest.url}`)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body);
res.body.length.should.match(subtest.res_body_length);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Views endpoint
describe("Patch endpoint, JWT user_role", function(){
test.user_patchs.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.patch(subtest.url)
.send(subtest.patch)
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(204);
should.exist(res.header['server']);
res.header['server'].should.match(new RegExp('postgrest','g'));
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Patch endpoint
describe("Function endpoint, JWT user_role", function(){
test.user_fn.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//should.exist(res.body);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Function endpoint
/*
describe("Function OTP endpoint, JWT user_role", function(){
let otp = null;
test.otp_fn.forEach(function (subtest) {
otp = null;
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res}`);
if (otp) {
subtest.payload.token = otp;
}
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
console.log(res.body);
should.exist(res.body);
if (subtest.url == '/rpc/generate_otp_fn') {
otp = res.body;
} else {
res.text.should.match('true');
otp = null;
}
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Function OTP endpoint
*/
}); // OpenAPI description
}); // CNAMEs Array

645
tests/index2.js Normal file
View File

@@ -0,0 +1,645 @@
'use strict';
/*
* Unit test #2, additional aava sample vessel metrics
*
* process.env.PGSAIL_API_URI = from inside the docker
*
* npm install supertest should mocha mochawesome moment
* alias mocha="./node_modules/mocha/bin/_mocha"
* mocha index.js --reporter mochawesome --reporter-options reportDir=/mnt/postgsail/,reportFilename=report_api.html
*
*/
const supertest = require("supertest");
const should = require("should");
let request = null;
let user_jwt = null;
let vessel_jwt = null;
var moment = require('moment');
const metrics_simulator = require('./metrics_sample_simulator.json');
// CNAMEs Array
[
{ cname: process.env.PGSAIL_API_URI, name: "PostgSail unit test 2, aava",
signin: { email: 'demo+aava@openplotter.cloud', pass: 'test', firstname:'first_aava', lastname:'last_aava'},
login: { email: 'demo+aava@openplotter.cloud', pass: 'test'},
vessel: { vessel_email: "demo+aava@openplotter.cloud", vessel_mmsi: "787654321", vessel_name: "aava"},
preferences: { key: '{email_notifications}', value: false }, /* Disable email_notifications */
vessel_metadata: {
name: "aava",
mmsi: "787654321",
client_id: "vessels.urn:mrn:imo:mmsi:787654321",
length: "12",
beam: "10",
height: "24",
ship_type: "37",
plugin_version: "1.0.2",
signalk_version: "1.20.0",
time: moment().subtract(69, 'minutes').format()
},
vessel_metrics: metrics_simulator,
user_tables: [
{ url: '/stays', res_body_length: 3},
// not processed yet, { url: '/moorages', res_body_length: 2},
{ url: '/logbook', res_body_length: 2},
{ url: '/metadata', res_body_length: 1}
],
user_views: [
// not processed yet, { url: '/stays_view', res_body_length: 1},
// not processed yet, { url: '/moorages_view', res_body_length: 1},
{ url: '/logs_view', res_body_length: 1},
{ url: '/log_view', res_body_length: 2},
//{ url: '/stats_view', res_body_length: 1},
{ url: '/vessels_view', res_body_length: 1},
],
user_patchs: [
{ url: '/logbook?id=eq.4',
patch: {
name: "patch log name 2",
notes: "new log note 2"
},
},
{ url: '/stays?id=eq.5',
patch: {
name: "patch stay name 2",
stay_code: 2,
notes: "new stay note 2"
},
},
/* not processed yet, { url: '/moorages?id=eq.1',
patch: {
name: "patch moorage name",
home_flag: true,
stay_code: 2,
notes: "new moorage note"
},
}
*/
],
user_fn: [
{ url: '/rpc/timelapse_fn',
payload: {
start_log: 4
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_geojson_fn',
payload: {
_id: 4
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_gpx_fn',
payload: {
_id: 4
},
res: {
obj_name: null
}
},
{ url: '/rpc/vessel_fn',
payload: null,
res: {
obj_name: 'vessel'
}
},
{ url: '/rpc/settings_fn',
payload: null,
res: {
obj_name: 'settings'
}
}
],
others_fn: [
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+aava@openplotter.cloud' },
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/pushover_fn',
// invalid key to avoid trigger notification
payload: { token: 'zxy', pushover_test_key: '987azerty#'},
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/update_user_preferences_fn',
//payload: { key: '{xyz}', value: '987azerty#'},
// invalid key to avoid trigger notification
payload: { key: '{telegram_test}', value: '{"id": 987654321, "is_bot": false, "first_name": "aaVa", "language_code": "en"}' },
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/bot',
payload: { email: 'demo+aava@openplotter.cloud', chat_id: 987654321},
res: {
obj_name: 'settings'
}
}
]
}
].forEach( function(test){
//console.log(`${test.cname}`);
describe(`${test.name}`, function(){
request = supertest.agent(test.cname);
request.set('User-Agent', 'PostgSail unit tests');
describe("OpenAPI description", function(){
it('/', function(done) {
request = supertest.agent(test.cname);
request
.get('/')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.paths['/rpc/signup']);
should.exist(res.body.paths['/rpc/login']);
//should.exist(res.body.paths['/rpc/generate_otp_fn']);
should.exist(res.body.paths['/rpc/pushover_fn']);
should.exist(res.body.paths['/rpc/telegram_fn']);
//should.exist(res.body.paths['/rpc/bot']);
done(err);
});
});
}); // OpenAPI description
describe("Get JWT user_role", function(){
it('/rpc/signup return user_role jwt token', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/signup')
.send(test.signin)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.token);
user_jwt = res.body.token;
should.exist(user_jwt);
done(err);
});
});
it('/rpc/login return user_role jwt token', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/login')
.send(test.login)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.token);
//res.body.token.should.match(user_jwt);
console.log(user_jwt);
should.exist(user_jwt);
done(err);
});
});
}); // JWT user_role
describe("OpenAPI with JWT user_role", function(){
it('/', function(done) {
request = supertest.agent(test.cname);
request
.get('/')
.set('Authorization', `Bearer ${user_jwt}`)
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
// Function
should.exist(res.body.paths['/rpc/register_vessel']);
should.exist(res.body.paths['/rpc/update_user_preferences_fn']);
should.exist(res.body.paths['/rpc/settings_fn']);
should.exist(res.body.paths['/rpc/versions_fn']);
// Tables
should.exist(res.body.paths['/metadata']);
should.exist(res.body.paths['/metrics']);
should.exist(res.body.paths['/logbook']);
should.exist(res.body.paths['/stays']);
should.exist(res.body.paths['/moorages']);
// Views
should.exist(res.body.paths['/logs_view']);
should.exist(res.body.paths['/moorages_view']);
should.exist(res.body.paths['/stays_view']);
should.exist(res.body.paths['/vessels_view']);
// should.exist(res.body.paths['/stats_view']);
should.exist(res.body.paths['/monitoring_view']);
done(err);
});
});
}); // OpenAPI JWT user_role
describe("Set preferences email_notifications, JWT user_role", function(){
it('/rpc/update_user_preferences_fn return true', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/update_user_preferences_fn')
.send(test.preferences)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//console.log(res.text);
should.exist(res.text);
res.text.should.match('true');
done(err);
});
});
}); // JWT user_role
describe("Get versions, JWT user_role", function(){
it('/rpc/versions_fn return json', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get('/rpc/versions_fn')
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//console.log(res.text);
should.exist(res.body.api_version);
should.exist(res.body.sys_version);
done(err);
});
});
}); // JWT user_role
describe("Get JWT vessel_role from user_role", function(){
it('/rpc/register_vessel return vessel_role jwt token', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/register_vessel')
.send(test.vessel)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.token);
vessel_jwt = res.body.token;
console.log(vessel_jwt);
should.exist(vessel_jwt);
done(err);
});
});
}); // JWT user_role
describe("OpenAPI with JWT vessel_role", function(){
it('/', function(done) {
request = supertest.agent(test.cname);
request
.get('/')
.set('Authorization', `Bearer ${vessel_jwt}`)
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.paths['/metadata']);
should.exist(res.body.paths['/metrics']);
should.exist(res.body.paths['/logbook']);
should.exist(res.body.paths['/stays']);
should.exist(res.body.paths['/moorages']);
done(err);
});
});
}); // OpenAPI JWT vessel_role
describe("Get vessel details view, JWT user_role", function(){
it('/vessels_view return json', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get('/vessels_view')
.set('Authorization', `Bearer ${user_jwt}`)
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
console.log(res.body);
//res.body.length.should.match(0);
res.body.length.should.match(1);
//res.body[0].last_contact.should.match('Never');
should.exist(res.body[0].last_contact);
done(err);
});
});
}); // JWT user_role
describe("Get vessel details function, JWT user_role", function(){
it('/rpc/vessel_fn return json', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/vessel_fn')
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//should.exist(res.body);
//body = res.body;
console.log(res.text);
done(err);
});
});
}); // JWT user_role
describe("Vessel POST metadata, JWT vessel_role", function(){
it('/metadata', function(done) {
request = supertest.agent(test.cname);
request
.post('/metadata')
.send(test.vessel_metadata)
.set('Authorization', `Bearer ${vessel_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Prefer', 'return=headers-only')
.end(function(err,res){
res.status.should.equal(201);
//console.log(res.header);
should.exist(res.header['server']);
res.header['server'].should.match(new RegExp('postgrest','g'));
done(err);
});
});
}); // Vessel metadata JWT vessel_role
describe("Vessel POST metrics, JWT vessel_role", function(){
let data = [];
//console.log(vessel_metrics['metrics'][0]);
let i;
for (i = 0; i < test.vessel_metrics['metrics'].length; i++) {
data[i] = test.vessel_metrics['metrics'][i];
// Override time, +1h because previous sample include 47 entry.
data[i]['time'] = moment().add(1, 'hour').add(i, 'minutes').format();
// Override client_id
data[i]['client_id'] = test.vessel_metadata.client_id;
}
console.log(data[0]);
it('/metrics?select=time', function(done) {
request = supertest.agent(test.cname);
request
.post('/metrics?select=time')
.send(data)
.set('Authorization', `Bearer ${vessel_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Prefer', 'return=representation')
.end(function(err,res){
//console.log(res.body);
res.status.should.equal(201);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body);
res.body.length.should.match(test.vessel_metrics['metrics'].length);
done(err);
});
});
}); // Vessel POST metrics JWT vessel_role
/*
describe("run_cron_jobs() JWT vessel_role", function(){
it('/rpc/run_cron_jobs', function(done) {
request = supertest.agent(test.cname);
request
.get('/rpc/run_cron_jobs')
.set('Authorization', `Bearer ${vessel_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
done();
});
});
}); // run_cron_jobs() JWT vessel_role
*/
describe("Table endpoint, JWT user_role", function(){
test.user_tables.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(`${subtest.url}`)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body);
res.body.length.should.match(subtest.res_body_length);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Table endpoint
describe("Views endpoint, JWT user_role", function(){
test.user_views.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(`${subtest.url}`)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body);
res.body.length.should.match(subtest.res_body_length);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Views endpoint
describe("Patch endpoint, JWT user_role", function(){
test.user_patchs.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.patch(subtest.url)
.send(subtest.patch)
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(204);
should.exist(res.header['server']);
res.header['server'].should.match(new RegExp('postgrest','g'));
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Patch endpoint
describe("Function user_fn endpoint, JWT user_role", function(){
test.user_fn.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//should.exist(res.body);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Function endpoint
/*
describe("Function others endpoint, JWT user_role", function(){
let otp = null;
test.others_fn.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//console.log(res.body);
should.exist(res.body);
if (subtest.url == '/rpc/generate_otp_fn') {
otp = res.body.text();
}
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Function endpoint
*/
}); // OpenAPI description
}); // CNAMEs Array

836
tests/index3.js Normal file
View File

@@ -0,0 +1,836 @@
'use strict';
/*
* Unit test #3, check post cron results, moorages, stays and stats
*
* process.env.PGSAIL_API_URI = from inside the docker
*
* npm install supertest should mocha mochawesome moment
* alias mocha="./node_modules/mocha/bin/_mocha"
* mocha index.js --reporter mochawesome --reporter-options reportDir=/mnt/postgsail/,reportFilename=report_api.html
*
*/
const supertest = require("supertest");
// Deprecated
const should = require("should");
//const chai = require("chai");
//const should = chai.should();
let request = null;
let user_jwt = null;
let vessel_jwt = null;
var moment = require('moment');
// CNAMEs Array
[
{ cname: process.env.PGSAIL_API_URI, name: "PostgSail unit test kapla",
signin: { email: 'demo+kapla@openplotter.cloud', pass: 'test', firstname:'First_kapla', lastname:'Last_kapla'},
login: { email: 'demo+kapla@openplotter.cloud', pass: 'test'},
vessel: { vessel_email: "demo+kapla@openplotter.cloud", vessel_mmsi: null, vessel_name: "kapla"},
preferences: { key: '{email_notifications}', value: false }, /* Disable email_notifications */
vessel_metadata: {
name: "kapla",
mmsi: "123456789",
client_id: "vessels.urn:mrn:imo:mmsi:123456789",
length: "12",
beam: "10",
height: "24",
ship_type: "36",
plugin_version: "0.0.1",
signalk_version: "1.12.0",
time: moment().subtract(69, 'minutes').format()
/* To trigger monitor_offline quickly */
},
user_tables: [
{ url: '/stays', res_body_length: 3},
{ url: '/moorages', res_body_length: 3},
{ url: '/logbook', res_body_length: 2},
{ url: '/metadata', res_body_length: 1}
],
user_views: [
{ url: '/stays_view', res_body_length: 2},
{ url: '/moorages_view', res_body_length: 2},
{ url: '/logs_view', res_body_length: 2},
{ url: '/log_view', res_body_length: 2},
//{ url: '/stats_view', res_body_length: 1},
{ url: '/vessels_view', res_body_length: 1},
],
user_patchs: [
{ url: '/logbook?id=eq.1',
patch: {
name: "patch log name 3",
notes: "new log note 3"
},
},
{ url: '/stays?id=eq.1',
patch: {
name: "patch stay name 3",
stay_code: 2,
notes: "new stay note 3"
},
},
{ url: '/moorages?id=eq.1',
patch: {
name: "patch moorage name 3",
home_flag: true,
stay_code: 2,
notes: "new moorage note 3"
},
}
],
user_fn: [
{ url: '/rpc/timelapse_fn',
payload: {
start_log: 2
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_geojson_fn',
payload: {
_id: 2
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_gpx_fn',
payload: {
_id: 2
},
res: {
obj_name: null
}
},
{ url: '/rpc/export_logbook_kml_fn',
payload: {
_id: 2
},
res: {
obj_name: null
}
},
{ url: '/rpc/export_moorages_geojson_fn',
payload: {},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_moorages_gpx_fn',
payload: {},
res: {
obj_name: null
}
},
{ url: '/rpc/find_log_from_moorage_fn',
payload: {
_id: 2
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/find_log_to_moorage_fn',
payload: {
_id: 2
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/vessel_fn',
payload: null,
res: {
obj_name: 'vessel'
}
},
{ url: '/rpc/settings_fn',
payload: null,
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/versions_fn',
payload: null,
res: {
obj_name: 'versions'
}
},
{ url: '/rpc/stats_logs_fn',
payload: {},
res: {
obj_name: 'stats'
}
},
{ url: '/rpc/stats_logs_fn',
payload: {
start_date: '2022-01-01',
end_date: '2022-06-12'
},
res: {
obj_name: null
}
},
],
email_otp_fn: [
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+kapla@openplotter.cloud' },
res: {
otp: 0
}
},
{ url: '/rpc/email_fn',
//payload: { token: 'abc', pushover_user_key: '123qwerty!'},
// invalid key to avoid trigger notification
payload: { token: '123456' },
res: {
obj_name: 'settings'
}
}
],
pushover_fn: [
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+kapla@openplotter.cloud' },
res: {
otp: 0
}
},
{ url: '/rpc/pushover_fn',
//payload: { token: 'abc', pushover_user_key: '123qwerty!'},
// invalid key to avoid trigger notification
payload: { token: null, pushover_test_key: '123qwerty!'},
res: {
obj_name: 'settings'
}
}
],
telegram_fn: [
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+kapla@openplotter.cloud' },
res: {
otp: 0
}
},
{ url: '/rpc/telegram_fn',
//payload: { key: '{abc}', value: {"a": "1", "b": 2, "c": true}},
// invalid key to avoid trigger notification
payload: { token: null, telegram_test: '{"id": 123456789, "is_bot": false, "first_name": "kaplA", "language_code": "en"}' },
res: {
obj_name: 'settings'
}
}
]
},
{ cname: process.env.PGSAIL_API_URI, name: "PostgSail unit test, aava",
signin: {email: 'demo+aava@openplotter.cloud', pass: 'test', firstname:'first_aava', lastname:'last_aava'},
login: {email: 'demo+aava@openplotter.cloud', pass: 'test'},
vessel: {vessel_email: "demo+aava@openplotter.cloud", vessel_mmsi: null, vessel_name: "aava"},
preferences: { key: '{email_notifications}', value: false }, /* Disable email_notifications */
vessel_metadata: {
name: "aava",
mmsi: "787654321",
client_id: "vessels.urn:mrn:imo:mmsi:787654321",
length: "12",
beam: "10",
height: "24",
ship_type: "37",
plugin_version: "1.0.2",
signalk_version: "1.20.0",
time: moment().subtract(69, 'minutes').format()
},
user_tables: [
{ url: '/stays', res_body_length: 3},
{ url: '/moorages', res_body_length: 4},
{ url: '/logbook', res_body_length: 2},
{ url: '/metadata', res_body_length: 1}
],
user_views: [
{ url: '/stays_view', res_body_length: 2},
{ url: '/moorages_view', res_body_length: 2},
{ url: '/logs_view', res_body_length: 2},
{ url: '/log_view', res_body_length: 2},
//{ url: '/stats_view', res_body_length: 1},
{ url: '/vessels_view', res_body_length: 1},
],
user_patchs: [
{ url: '/logbook?id=eq.4',
patch: {
name: "patch log name 4",
notes: "new log note 4"
},
},
{ url: '/stays?id=eq.4',
patch: {
name: "patch stay name 4",
stay_code: 2,
notes: "new stay note 4"
},
},
{ url: '/moorages?id=eq.4',
patch: {
name: "patch moorage name",
home_flag: true,
stay_code: 2,
notes: "new moorage note"
},
}
],
user_fn: [
{ url: '/rpc/timelapse_fn',
payload: {
start_log: 4
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_geojson_fn',
payload: {
_id: 4
},
res: {
obj_name: 'geojson'
}
},
{ url: '/rpc/export_logbook_gpx_fn',
payload: {
_id: 4
},
res: {
obj_name: null
}
},
{ url: '/rpc/export_logbook_kml_fn',
payload: {
_id: 4
},
res: {
obj_name: null
}
},
{ url: '/rpc/export_logbooks_gpx_fn',
payload: {
start_log: 3,
end_log: 4
},
res: {
obj_name: null
}
},
{ url: '/rpc/export_logbooks_kml_fn',
payload: {
start_log: 3,
end_log: 4
},
res: {
obj_name: null
}
},
{ url: '/rpc/export_moorages_geojson_fn',
payload: {},
res: {
geojson: { type: 'FeatureCollection', features: [ [Object], [Object] ] }
}
},
{ url: '/rpc/export_moorages_gpx_fn',
payload: {},
res: {
obj_name: null
}
},
{ url: '/rpc/find_log_from_moorage_fn',
payload: {
_id: 4
},
res: { geojson: { type: 'FeatureCollection', features: [ [Object] ] } }
},
{ url: '/rpc/find_log_to_moorage_fn',
payload: {
_id: 4
},
res: { geojson: { type: 'FeatureCollection', features: [ [Object] ] } }
},
{ url: '/rpc/vessel_fn',
payload: null,
res: {
vessel: {
beam: 10,
mmsi: 787654321,
name: 'aava',
height: 24,
length: 37,
alpha_2: null,
country: null,
geojson: { type: 'Feature', geometry: [Object], properties: [Object] },
ship_type: 'Pleasure Craft',
created_at: '2023-08-17T16:32:13',
last_contact: '2023-08-17T15:23:14'
}
}
},
{ url: '/rpc/settings_fn',
payload: null,
res: {
settings: {
email: 'demo+aava@openplotter.cloud',
first: 'first_aava',
last: 'last_aava',
preferences: { badges: [Object], email_notifications: false },
created_at: '2023-08-17T16:32:12.701788',
username: 'F Last_Aava',
has_vessel: true
}
}
},
{ url: '/rpc/stats_logs_fn',
payload: {},
res: { // Compare keys only
stats: {
count: 2,
max_speed: 7.1,
max_distance: 8.2365,
max_duration: '01:11:00',
max_speed_id: 3,
sum_duration: '01:54:00',
max_wind_speed: 44.2,
max_distance_id: 3,
max_wind_speed_id: 4
}
}
},
{ url: '/rpc/stats_logs_fn',
payload: {
start_date: '2022-01-01',
end_date: '2022-06-12'
},
res: { stats: null }
},
],
others_fn: [
{ url: '/rpc/generate_otp_fn',
payload: { email: 'demo+aava@openplotter.cloud' },
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/pushover_fn',
// invalid key to avoid trigger notification
payload: { token: 'zxy', pushover_test_key: '987azerty#'},
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/update_user_preferences_fn',
//payload: { key: '{xyz}', value: '987azerty#'},
// invalid key to avoid trigger notification
payload: { key: '{telegram_test}', value: '{"id": 987654321, "is_bot": false, "first_name": "aaVa", "language_code": "en"}' },
res: {
obj_name: 'settings'
}
},
{ url: '/rpc/bot',
payload: { email: 'demo+aava@openplotter.cloud', chat_id: 987654321},
res: {
obj_name: 'settings'
}
}
]
}
].forEach( function(test){
//console.log(`${test.cname}`);
describe(`${test.name}`, function(){
request = supertest.agent(test.cname);
request.set('User-Agent', 'PostgSail unit tests');
describe("OpenAPI description", function(){
it('/', function(done) {
request = supertest.agent(test.cname);
request
.get('/')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.paths['/rpc/signup']);
should.exist(res.body.paths['/rpc/login']);
//should.exist(res.body.paths['/rpc/generate_otp_fn']);
should.exist(res.body.paths['/rpc/pushover_fn']);
should.exist(res.body.paths['/rpc/telegram_fn']);
//should.exist(res.body.paths['/rpc/bot']);
done(err);
});
});
}); // OpenAPI description
describe("Get JWT user_role", function(){
it('/rpc/signup return user_role jwt token', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/signup')
.send(test.signin)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.token);
user_jwt = res.body.token;
should.exist(user_jwt);
done(err);
});
});
it('/rpc/login return user_role jwt token', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/login')
.send(test.login)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.token);
//res.body.token.should.match(user_jwt);
//console.log(user_jwt);
should.exist(user_jwt);
done(err);
});
});
}); // JWT user_role
describe("OpenAPI with JWT user_role", function(){
it('/', function(done) {
request = supertest.agent(test.cname);
request
.get('/')
.set('Authorization', `Bearer ${user_jwt}`)
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
// Function
should.exist(res.body.paths['/rpc/register_vessel']);
should.exist(res.body.paths['/rpc/update_user_preferences_fn']);
should.exist(res.body.paths['/rpc/settings_fn']);
should.exist(res.body.paths['/rpc/versions_fn']);
// Tables
should.exist(res.body.paths['/metadata']);
should.exist(res.body.paths['/metrics']);
should.exist(res.body.paths['/logbook']);
should.exist(res.body.paths['/stays']);
should.exist(res.body.paths['/moorages']);
// Views
should.exist(res.body.paths['/logs_view']);
should.exist(res.body.paths['/moorages_view']);
should.exist(res.body.paths['/stays_view']);
should.exist(res.body.paths['/vessels_view']);
//should.exist(res.body.paths['/stats_view']);
should.exist(res.body.paths['/monitoring_view']);
done(err);
});
});
}); // OpenAPI JWT user_role
describe("Set preferences email_notifications, JWT user_role", function(){
it('/rpc/update_user_preferences_fn return true', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/update_user_preferences_fn')
.send(test.preferences)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//console.log(res.text);
should.exist(res.text);
res.text.should.match('true');
done(err);
});
});
}); // JWT user_role
describe("Get versions, JWT user_role", function(){
it('/rpc/versions_fn return json', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get('/rpc/versions_fn')
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//console.log(res.text);
should.exist(res.body.api_version);
should.exist(res.body.sys_version);
done(err);
});
});
}); // JWT user_role
describe("Get JWT vessel_role from user_role", function(){
it('/rpc/register_vessel return vessel_role jwt token', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/register_vessel')
.send(test.vessel)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body.token);
vessel_jwt = res.body.token;
console.log(vessel_jwt);
should.exist(vessel_jwt);
done(err);
});
});
}); // JWT user_role
describe("Get vessel details view, JWT user_role", function(){
it('/vessels_view return json', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get('/vessels_view')
.set('Authorization', `Bearer ${user_jwt}`)
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
console.log(res.body);
//res.body.length.should.match(0);
res.body.length.should.match(1);
//res.body[0].last_contact.should.match('Never');
should.exist(res.body[0].last_contact);
done(err);
});
});
}); // JWT user_role
describe("Get vessel details function, JWT user_role", function(){
it('/rpc/vessel_fn return json', function(done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post('/rpc/vessel_fn')
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//should.exist(res.body);
//body = res.body;
console.log(res.text);
done(err);
});
});
}); // JWT user_role
describe("Table endpoint, JWT user_role", function(){
test.user_tables.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(`${subtest.url}`)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
console.log(res.body);
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body);
res.body.length.should.match(subtest.res_body_length);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Table endpoint
describe("Views endpoint, JWT user_role", function(){
test.user_views.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(`${subtest.url}`)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
//console.log(res.body);
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
should.exist(res.body);
res.body.length.should.match(subtest.res_body_length);
console.log(res.body);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Views endpoint
describe("Patch endpoint, JWT user_role", function(){
test.user_patchs.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.patch(subtest.url)
.send(subtest.patch)
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(204);
should.exist(res.header['server']);
res.header['server'].should.match(new RegExp('postgrest','g'));
console.log(res.body);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Patch endpoint
describe("Function user_fn endpoint, JWT user_role", function(){
test.user_fn.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//should.exist(res.body);
console.log(res.body);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Function endpoint
/*
describe("Function others endpoint, JWT user_role", function(){
let otp = null;
test.others_fn.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//console.log(res.body);
should.exist(res.body);
if (subtest.url == '/rpc/generate_otp_fn') {
otp = res.body.text();
}
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Function endpoint
*/
}); // OpenAPI description
}); // CNAMEs Array

727
tests/index4.js Normal file
View File

@@ -0,0 +1,727 @@
"use strict";
/*
* Unit test #4
* OTP for email, Pushover, Telegram
*
* process.env.PGSAIL_API_URI = from inside the docker
*
* npm install supertest should mocha mochawesome moment
* alias mocha="./node_modules/mocha/bin/_mocha"
* mocha index4.js --reporter mochawesome --reporter-options reportDir=/mnt/postgsail/,reportFilename=report_api.html
*
*/
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const supertest = require("supertest");
// Deprecated
const should = require("should");
//const chai = require("chai");
//const should = chai.should();
let request = null;
let user_jwt = null;
var moment = require("moment");
// Users Array
[
{
cname: process.env.PGSAIL_API_URI,
name: "PostgSail unit test kapla",
signin: {
email: "demo+kapla@openplotter.cloud",
pass: "test",
firstname: "First_kapla",
lastname: "Last_kapla",
},
login: { email: "demo+kapla@openplotter.cloud", pass: "test" },
preferences: { key: "{email_valid}", value: false },
email_otp: [
{
url: "/rpc/generate_otp_fn",
payload: { email: "demo+kapla@openplotter.cloud" },
res: {
otp: 0,
},
},
{
url: "/rpc/email_fn",
payload: { token: null },
res: {
obj_name: "settings",
},
},
],
pushover_otp: [
{
//url: '/rpc/generate_otp_fn',
url: "/rpc/pushover_subscribe_link_fn",
//payload: { email: 'demo+kapla@openplotter.cloud' },
res: {
obj_name: "pushover_link",
},
},
{
url: "/rpc/pushover_fn",
payload: { token: null, pushover_user_key: "1234567890azerty!" },
res: {
obj_name: "settings",
},
},
],
telegram_otp: [
{
url: "/rpc/update_user_preferences_fn",
payload: { key: "{email_notifications}", value: false },
},
{
url: "/rpc/update_user_preferences_fn",
payload: { key: "{phone_notifications}", value: false },
},
{
//url: '/rpc/generate_otp_fn',
url: "/rpc/telegram_otp_fn",
payload: { email: "demo+kapla@openplotter.cloud" },
res: {
otp: 0,
},
},
{
url: "/rpc/telegram_fn",
payload: {
token: null,
telegram_obj: {
chat: {
id: 1234567890,
type: "private",
title: null,
all_members_are_administrators: null,
},
date: "NOW",
from: {
id: 1234567890,
is_bot: false,
first_name: "Kapla",
language_code: "en",
},
},
},
res: {},
},
],
telegram: { payload: { user_id: 1234567890 } },
telegram_fn: [{ url: "/rpc/vessel_fn" }, { url: "/monitoring_view" }],
badges: {
url: "/rpc/settings_fn",
payload: null,
res: {
obj_name: "settings",
},
},
monitoring: [
{
url: "/monitoring_view",
payload: null,
res: {},
},
{
url: "/monitoring_view2",
payload: null,
res: {},
},
{
url: "/monitoring_view3",
payload: null,
res: {},
},
{
url: "/monitoring_voltage",
payload: null,
res: {},
},
{
url: "/monitoring_temperatures",
payload: null,
res: {},
},
{
url: "/monitoring_humidity",
payload: null,
res: {},
},
],
eventlogs: {
url: "/eventlogs_view",
payload: null,
res: {},
},
public: [
{
url: "/rpc/update_user_preferences_fn",
payload: { key: "{public_logs}", value: true },
},
{
url: "/rpc/update_user_preferences_fn",
payload: { key: "{public_monitoring}", value: true },
},
],
},
{
cname: process.env.PGSAIL_API_URI,
name: "PostgSail unit test, aava",
signin: {
email: "demo+aava@openplotter.cloud",
pass: "test",
firstname: "first_aava",
lastname: "last_aava",
},
login: { email: "demo+aava@openplotter.cloud", pass: "test" },
preferences: { key: "{email_valid}", value: false },
email_otp: [
{
url: "/rpc/generate_otp_fn",
payload: { email: "demo+aava@openplotter.cloud" },
res: {
otp: 0,
},
},
{
url: "/rpc/email_fn",
payload: { token: null },
res: {
obj_name: "settings",
},
},
],
pushover_otp: [
{
//url: '/rpc/generate_otp_fn',
url: "/rpc/pushover_subscribe_link_fn",
//payload: { email: 'demo+aava@openplotter.cloud' },
res: {
obj_name: "pushover_link",
},
},
{
url: "/rpc/pushover_fn",
payload: { token: null, pushover_user_key: "0987654321qwerty!" },
res: {
obj_name: "settings",
},
},
],
telegram_otp: [
{
url: "/rpc/update_user_preferences_fn",
payload: { key: "{email_notifications}", value: false },
},
{
url: "/rpc/update_user_preferences_fn",
payload: { key: "{phone_notifications}", value: false },
},
{
//url: '/rpc/generate_otp_fn',
url: "/rpc/telegram_otp_fn",
payload: { email: "demo+aava@openplotter.cloud" },
res: {
otp: 0,
},
},
{
url: "/rpc/telegram_fn",
payload: {
token: null,
telegram_obj: {
chat: {
id: 9876543210,
type: "private",
title: null,
all_members_are_administrators: null,
},
date: "NOW",
from: {
id: 9876543210,
is_bot: false,
first_name: "Aava",
language_code: "en",
},
},
},
res: {},
},
],
telegram: { payload: { user_id: 9876543210 } },
telegram_fn: [{ url: "/rpc/vessel_fn" }, { url: "/monitoring_view" }],
badges: {
url: "/rpc/settings_fn",
payload: null,
res: {
obj_name: "settings",
},
},
monitoring: [
{
url: "/monitoring_view",
payload: null,
res: {},
},
{
url: "/monitoring_view2",
payload: null,
res: {},
},
{
url: "/monitoring_view3",
payload: null,
res: {},
},
{
url: "/monitoring_voltage",
payload: null,
res: {},
},
{
url: "/monitoring_temperatures",
payload: null,
res: {},
},
{
url: "/monitoring_humidity",
payload: null,
res: {},
},
],
eventlogs: {
url: "/eventlogs_view",
payload: null,
res: {},
},
public: [
{
url: "/rpc/update_user_preferences_fn",
payload: { key: "{public_logs}", value: true },
},
{
url: "/rpc/update_user_preferences_fn",
payload: { key: "{public_monitoring}", value: true },
},
],
},
].forEach(function (test) {
//console.log(`${test.cname}`);
describe(`${test.name}`, function () {
request = supertest.agent(test.cname);
request.set("User-Agent", "PostgSail unit tests");
describe("Get JWT user_role", function () {
it("/rpc/signup return user_role jwt token", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post("/rpc/signup")
.send(test.signin)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
should.exist(res.body.token);
user_jwt = res.body.token;
should.exist(user_jwt);
done(err);
});
});
it("/rpc/login return user_role jwt token", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post("/rpc/login")
.send(test.login)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
should.exist(res.body.token);
//res.body.token.should.match(user_jwt);
console.log(user_jwt);
should.exist(user_jwt);
done(err);
});
});
}); // JWT user_role
describe("Set preferences email_notifications, JWT user_role", function () {
it("/rpc/update_user_preferences_fn return true", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post("/rpc/update_user_preferences_fn")
.send(test.preferences)
.set("Authorization", `Bearer ${user_jwt}`)
.set("Accept", "application/json")
.set("Content-Type", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
//console.log(res.text);
should.exist(res.text);
res.text.should.match("true");
done(err);
});
});
}); // JWT user_role
describe("Function email OTP endpoint, JWT user_role", function () {
let otp = null;
test.email_otp.forEach(function (subtest) {
it(`${subtest.url}`, function (done) {
try {
//console.log(`${subtest.url} ${subtest.payload}`);
if (otp) {
subtest.payload.token = otp;
}
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set("Authorization", `Bearer ${user_jwt}`)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(
new RegExp("json", "g")
);
res.header["server"].should.match(new RegExp("postgrest", "g"));
console.log(res.body);
should.exist(res.body);
if (subtest.url == "/rpc/generate_otp_fn") {
otp = res.body;
} else {
res.text.should.match("true");
}
done(err);
});
} catch (error) {
done();
}
});
});
}); // email OTP endpoint
describe("Function Pushover OTP endpoint, JWT user_role", function () {
let otp = null;
test.pushover_otp.forEach(function (subtest) {
it(`${subtest.url}`, function (done) {
try {
//console.log(`${subtest.url} ${subtest.payload}`);
if (otp) {
subtest.payload.token = otp;
}
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set("Authorization", `Bearer ${user_jwt}`)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(
new RegExp("json", "g")
);
res.header["server"].should.match(new RegExp("postgrest", "g"));
//console.log(res.body);
should.exist(res.body);
if (subtest.url == "/rpc/pushover_subscribe_link_fn") {
should.exist(res.body.pushover_link.link);
let rx = /3D(\d+)\&/g;
//console.log(rx.exec(res.body.pushover_link.link)[1]);
let arr = rx.exec(res.body.pushover_link.link);
//console.log(arr);
console.log(arr[1]);
otp = arr[1];
} else {
res.text.should.match("true");
}
done(err);
});
} catch (error) {
done();
}
});
});
}); // pushover OTP endpoint
describe("Function Telegram OTP endpoint, JWT user_role", function () {
let otp = null;
test.telegram_otp.forEach(function (subtest) {
it(`${subtest.url}`, function (done) {
try {
console.log(`${subtest.url} ${subtest.payload.email} ${otp}`);
if (otp) {
subtest.payload.token = otp;
console.log(subtest.payload.telegram_obj.date);
subtest.payload.telegram_obj.date = moment.utc().format();
}
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set("Authorization", `Bearer ${user_jwt}`)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(
new RegExp("json", "g")
);
res.header["server"].should.match(new RegExp("postgrest", "g"));
console.log(res.body);
should.exist(res.body);
if (subtest.url == "/rpc/telegram_otp_fn") {
console.log(res.body.otp_code);
otp = res.body.otp_code;
} else {
console.log(res.text);
res.text.should.match("true");
otp = null;
}
done(err);
});
} catch (error) {
done();
}
});
});
}); // telegram OTP endpoint
describe("telegram session, anonymous", function () {
it("/rpc/telegram return jwt token", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post("/rpc/telegram")
.send(test.telegram.payload)
.set("Accept", "application/json")
.set("Content-Type", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
should.exist(res.body.token);
user_jwt = res.body.token;
console.log(res.body.token);
done(err);
});
});
}); // anonymous JWT
describe("Telegram endpoint, JWT user_role", function () {
let otp = null;
test.telegram_fn.forEach(function (subtest) {
it(`${subtest.url}`, function (done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(subtest.url)
.set("Authorization", `Bearer ${user_jwt}`)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(
new RegExp("json", "g")
);
res.header["server"].should.match(new RegExp("postgrest", "g"));
console.log(res.body);
should.exist(res.body);
done(err);
});
} catch (error) {
done();
}
});
});
}); // Function endpoint
/*
describe("Function others endpoint, JWT user_role", function(){
let otp = null;
test.others_fn.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res_body_length}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set('Authorization', `Bearer ${user_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
//console.log(res.body);
should.exist(res.body);
if (subtest.url == '/rpc/generate_otp_fn') {
otp = res.body.text();
}
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Function endpoint
*/
describe("Badges, user jwt", function () {
it("/rpc/settings_fn return user settings", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post("/rpc/settings_fn")
.set("Authorization", `Bearer ${user_jwt}`)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
console.log(res.body);
should.exist(res.body.settings);
should.exist(res.body.settings.preferences.badges);
let badges = res.body.settings.preferences.badges;
//console.log(Object.keys(badges));
Object.keys(badges).length.should.be.aboveOrEqual(3);
badges.should.have.properties(
"Helmsman",
"Wake Maker",
"Stormtrooper"
);
done(err);
});
});
}); // user JWT
describe("Function monitoring endpoint, JWT user_role", function () {
let otp = null;
test.monitoring.forEach(function (subtest) {
it(`${subtest.url}`, function (done) {
try {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(subtest.url)
.set("Authorization", `Bearer ${user_jwt}`)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(
new RegExp("json", "g")
);
res.header["server"].should.match(new RegExp("postgrest", "g"));
//console.log(res.body);
should.exist(res.body);
//let monitoring = res.body;
//console.log(monitoring);
// minimum set for static monitoring page
// no value for humidity monitoring
//monitoring.length.should.be.aboveOrEqual(21);
done(err);
});
} catch (error) {
done();
}
});
});
}); // Monitoring endpoint
describe("Event Logs, user jwt", function () {
it("/eventlogs_view endpoint, list process_queue, JWT user_role", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get("/eventlogs_view")
.set("Authorization", `Bearer ${user_jwt}`)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
//console.log(res.body);
should.exist(res.body);
let event = res.body;
//console.log(event);
// minimum events log for kapla & aava 13 + 4 email_otp = 17
event.length.should.be.aboveOrEqual(13);
done(err);
});
});
}); // user JWT
describe("Function update preference for public access endpoint, JWT user_role", function () {
test.public.forEach(function (subtest) {
it(`${subtest.url}`, function (done) {
try {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(subtest.url)
.send(subtest.payload)
.set("Authorization", `Bearer ${user_jwt}`)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["server"].should.match(new RegExp("postgrest", "g"));
//console.log(res.body);
should.exist(res.body);
//let monitoring = res.body;
//console.log(monitoring);
// minimum set for static monitoring page
// no value for humidity monitoring
//monitoring.length.should.be.aboveOrEqual(21);
done(err);
});
} catch (error) {
done();
}
});
});
}); // Monitoring endpoint
}); // OpenAPI description
}); // Users Array

223
tests/index5.js Normal file
View File

@@ -0,0 +1,223 @@
"use strict";
/*
* Unit test #5
* Public/Anonymous access
*
* process.env.PGSAIL_API_URI = from inside the docker
*
* npm install supertest should mocha mochawesome moment
* alias mocha="./node_modules/mocha/bin/_mocha"
* mocha index5.js --reporter mochawesome --reporter-options reportDir=/mnt/postgsail/,reportFilename=report_api.html
*
*/
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const supertest = require("supertest");
// Deprecated
const should = require("should");
//const chai = require("chai");
//const should = chai.should();
let request = null;
var moment = require("moment");
// Users Array
[
{
cname: process.env.PGSAIL_API_URI,
name: "PostgSail unit test kapla",
logs: {
url: "/logs_view",
header: { name: "x-is-public", value: btoa("kapla,public_logs_list,0") },
payload: null,
res: {},
},
log: {
url: "/log_view?id=eq.1",
header: { name: "x-is-public", value: btoa("kapla,public_logs,1") },
payload: null,
res: {},
},
monitoring: {
url: "/monitoring_view",
header: { name: "x-is-public", value: btoa("kapla,public_monitoring,0") },
payload: null,
res: {},
},
timelapse: {
url: "/rpc/timelapse_fn",
header: { name: "x-is-public", value: btoa("kapla,public_timelapse,1") },
payload: null,
res: {},
},
timelapse_full: {
url: "/rpc/timelapse_fn",
header: { name: "x-is-public", value: btoa("kapla,public_timelapse,0") },
payload: null,
res: {},
},
stats_logs: {
url: "/rpc/stats_logs_fn",
header: { name: "x-is-public", value: btoa("kapla,public_stats,0") },
payload: null,
res: {},
},
stats_stays: {
url: "/rpc/stats_stay_fn",
header: { name: "x-is-public", value: btoa("kapla,public_stats,0") },
payload: null,
res: {},
},
export_gpx: {
url: "/rpc/export_logbook_gpx_fn",
header: { name: "x-is-public", value: btoa("kapla,public_logs,0") },
payload: null,
res: {},
},
},
{
cname: process.env.PGSAIL_API_URI,
name: "PostgSail unit test, aava",
logs: {
url: "/logs_view",
header: { name: "x-is-public", value: btoa("aava,public_logs_list,0") },
payload: null,
res: {},
},
log: {
url: "/log_view?id=eq.3",
header: { name: "x-is-public", value: btoa("aava,public_logs,3") },
payload: null,
res: {},
},
monitoring: {
url: "/monitoring_view",
header: { name: "x-is-public", value: btoa("aava,public_monitoring,0") },
payload: null,
res: {},
},
timelapse: {
url: "/rpc/timelapse_fn",
header: { name: "x-is-public", value: btoa("aava,public_timelapse,3") },
payload: null,
res: {},
},
timelapse_full: {
url: "/rpc/timelapse_fn",
header: { name: "x-is-public", value: btoa("aava,public_timelapse,0") },
payload: null,
res: {},
},
stats_logs: {
url: "/rpc/stats_logs_fn",
header: { name: "x-is-public", value: btoa("aava,public_stats,0") },
payload: null,
res: {},
},
stats_stays: {
url: "/rpc/stats_stay_fn",
header: { name: "x-is-public", value: btoa("kapla,public_stats,0") },
payload: null,
res: {},
},
export_gpx: {
url: "/rpc/export_logbook_gpx_fn",
header: { name: "x-is-public", value: btoa("aava,public_logs,0") },
payload: null,
res: {},
},
},
].forEach(function (test) {
//console.log(`${test.cname}`);
describe(`${test.name}`, function () {
request = supertest.agent(test.cname);
request.set("User-Agent", "PostgSail unit tests");
describe("With no JWT as api_anonymous", function () {
it("/logs_view, api_anonymous no jwt token", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(test.logs.url)
.set(test.logs.header.name, test.logs.header.value)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(404);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
done(err);
});
});
it("/log_view, api_anonymous no jwt token", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(test.log.url)
.set(test.log.header.name, test.log.header.value)
.set("Accept", "application/json")
.end(function (err, res) {
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
done(err);
});
});
it("/monitoring_view, api_anonymous no jwt token", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(test.monitoring.url)
.set(test.monitoring.header.name, test.monitoring.header.value)
.set("Accept", "application/json")
.end(function (err, res) {
console.log(res.text);
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
done(err);
});
});
it("/rpc/timelapse_fn, api_anonymous no jwt token", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(test.timelapse.url)
.set(test.timelapse.header.name, test.timelapse.header.value)
.set("Accept", "application/json")
.end(function (err, res) {
console.log(res.text);
res.status.should.equal(404); // return 404 as it is not enable in user settings.
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
done(err);
});
});
it("/rpc/export_logbook_gpx_fn, api_anonymous no jwt token", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.post(test.export_gpx.url)
.send({_id: 1})
.set(test.export_gpx.header.name, test.export_gpx.header.value)
.set("Accept", "application/json")
.end(function (err, res) {
console.log(res.text)
res.status.should.equal(401);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
done(err);
});
});
}); // user JWT
}); // OpenAPI description
}); // Users Array

View File

@@ -0,0 +1,603 @@
{
"metrics": [
{
"time" : "2022-07-31T11:28:13.331Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7231605,
"longitude" : 24.7358807,
"speedoverground" : 7.1,
"courseovergroundtrue" : 188.9,
"windspeedapparent" : 13.9,
"anglespeedapparent" : 56.0,
"status" : "moored",
"metrics" : {"navigation.log": 17441210, "navigation.trip.log": 80099, "navigation.headingTrue": 3.3179, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 36.219, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.48, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 19.6, "electrical.batteries.1.voltage": 13.38, "navigation.gnss.antennaAltitude": 2.21, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 36.22, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 89, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 9, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:29:13.340Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7213961,
"longitude" : 24.7349507,
"speedoverground" : 6.5,
"courseovergroundtrue" : 197.4,
"windspeedapparent" : 15.4,
"anglespeedapparent" : 43.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441395, "navigation.trip.log": 80284, "navigation.headingTrue": 3.4924, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 32.289, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 231, "electrical.batteries.1.voltage": 14.45, "navigation.gnss.antennaAltitude": -0.04, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 32.29, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 57, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 11, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:29:13.340Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7213961,
"longitude" : 24.7349507,
"speedoverground" : 6.5,
"courseovergroundtrue" : 197.4,
"windspeedapparent" : 15.4,
"anglespeedapparent" : 43.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441395, "navigation.trip.log": 80284, "navigation.headingTrue": 3.4924, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 32.289, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 231, "electrical.batteries.1.voltage": 14.45, "navigation.gnss.antennaAltitude": -0.04, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 32.29, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 57, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 11, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:30:28.338Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7190763,
"longitude" : 24.733775,
"speedoverground" : 7.1,
"courseovergroundtrue" : 194.4,
"windspeedapparent" : 16.3,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441580, "navigation.trip.log": 80544, "navigation.headingTrue": 3.4226, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 23.808999999999997, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.61, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 231.5, "electrical.batteries.1.voltage": 13.02, "navigation.gnss.antennaAltitude": 2.06, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 23.81, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 73, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 1, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 1, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:31:28.348Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7173052,
"longitude" : 24.7325741,
"speedoverground" : 6.5,
"courseovergroundtrue" : 198.8,
"windspeedapparent" : 15.8,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441766, "navigation.trip.log": 80747, "navigation.headingTrue": 3.5972, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 20.948999999999998, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.47, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 192.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 0.21, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 20.95, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 64, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:32:28.364Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7156422,
"longitude" : 24.731211599999998,
"speedoverground" : 6.4,
"courseovergroundtrue" : 203.3,
"windspeedapparent" : 13.7,
"anglespeedapparent" : 58.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442136, "navigation.trip.log": 80951, "navigation.headingTrue": 3.5361, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 17.529, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 2.93, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 105.2, "electrical.batteries.1.voltage": 14.55, "navigation.gnss.antennaAltitude": 2.49, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 17.53, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 43, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 11, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:33:28.377Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7139966,
"longitude" : 24.7299656,
"speedoverground" : 6.5,
"courseovergroundtrue" : 200.4,
"windspeedapparent" : 15.6,
"anglespeedapparent" : 44.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442321, "navigation.trip.log": 81136, "navigation.headingTrue": 3.571, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 13.999, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 70.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 2.69, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 34, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:34:28.396Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7124464,
"longitude" : 24.728924,
"speedoverground" : 5.8,
"courseovergroundtrue" : 197.8,
"windspeedapparent" : 12.2,
"anglespeedapparent" : 31.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442506, "navigation.trip.log": 81321, "navigation.headingTrue": 3.5535, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 13.999, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.07, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 43.9, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 2.11, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 66, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:35:28.413Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7106379,
"longitude" : 24.728088,
"speedoverground" : 6.7,
"courseovergroundtrue" : 187.8,
"windspeedapparent" : 13.7,
"anglespeedapparent" : 42.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442692, "navigation.trip.log": 81507, "navigation.headingTrue": 3.4313, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 14.029, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 36.1, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 2.78, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14.03, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 69, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:36:28.424Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7087125,
"longitude" : 24.7276837,
"speedoverground" : 7.1,
"courseovergroundtrue" : 185.4,
"windspeedapparent" : 13.5,
"anglespeedapparent" : 51.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442877, "navigation.trip.log": 81692, "navigation.headingTrue": 3.3004, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 22.689, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 25.2, "electrical.batteries.1.voltage": 14.55, "navigation.gnss.antennaAltitude": 3.24, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 22.69, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 45, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:37:28.444Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7245499,
"longitude" : 24.736394999999998,
"speedoverground" : 6.7,
"courseovergroundtrue" : 196.5,
"windspeedapparent" : 14.1,
"anglespeedapparent" : 42.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441025, "navigation.trip.log": 79951, "navigation.headingTrue": 3.475, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 71.039, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 16.4, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 2.32, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 71.04, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 64, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:38:28.483Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7227198,
"longitude" : 24.735699,
"speedoverground" : 6.4,
"courseovergroundtrue" : 186.4,
"windspeedapparent" : 13.9,
"anglespeedapparent" : 44.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441210, "navigation.trip.log": 80136, "navigation.headingTrue": 3.4401, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 33.169000000000004, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 172, "electrical.batteries.1.voltage": 13.35, "navigation.gnss.antennaAltitude": 2.11, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 33.17, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 32, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 5, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 6, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 13, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 1, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 1, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:39:28.509Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7209298,
"longitude" : 24.73472,
"speedoverground" : 6.8,
"courseovergroundtrue" : 190.7,
"windspeedapparent" : 15.4,
"anglespeedapparent" : 38.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441395, "navigation.trip.log": 80340, "navigation.headingTrue": 3.4663, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 30.738999999999997, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.47, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 211.6, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 0.76, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 30.74, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 52, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 9, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:40:28.539Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7190763,
"longitude" : 24.733774999999998,
"speedoverground" : 7.1,
"courseovergroundtrue" : 194.4,
"windspeedapparent" : 16.3,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441580, "navigation.trip.log": 80544, "navigation.headingTrue": 3.4226, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 23.808999999999997, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.61, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 231.5, "electrical.batteries.1.voltage": 13.02, "navigation.gnss.antennaAltitude": 2.06, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 23.81, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 73, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 1, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 1, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:41:28.561Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7173052,
"longitude" : 24.7325741,
"speedoverground" : 6.5,
"courseovergroundtrue" : 198.8,
"windspeedapparent" : 15.8,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441766, "navigation.trip.log": 80747, "navigation.headingTrue": 3.5972, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 20.948999999999998, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.47, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 192.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 0.39, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 20.95, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 64, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:42:28.569Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7156422,
"longitude" : 24.7312116,
"speedoverground" : 6.4,
"courseovergroundtrue" : 203.3,
"windspeedapparent" : 13.7,
"anglespeedapparent" : 58.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442136, "navigation.trip.log": 80951, "navigation.headingTrue": 3.5361, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 17.529, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 2.93, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 107.6, "electrical.batteries.1.voltage": 14.55, "navigation.gnss.antennaAltitude": 2.49, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 17.53, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 43, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 11, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:43:28.603Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7139966,
"longitude" : 24.7299656,
"speedoverground" : 6.4,
"courseovergroundtrue" : 200.4,
"windspeedapparent" : 15.6,
"anglespeedapparent" : 44.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442321, "navigation.trip.log": 81136, "navigation.headingTrue": 3.571, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 13.999, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 70.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 2.69, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 34, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:44:28.629Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7124464,
"longitude" : 24.728924,
"speedoverground" : 5.7,
"courseovergroundtrue" : 197.6,
"windspeedapparent" : 12.2,
"anglespeedapparent" : 31.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442506, "navigation.trip.log": 81321, "navigation.headingTrue": 3.571, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 13.999, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.07, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 43.9, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 2.05, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 66, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:45:28.645Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7106379,
"longitude" : 24.728088,
"speedoverground" : 6.7,
"courseovergroundtrue" : 187.8,
"windspeedapparent" : 13.7,
"anglespeedapparent" : 42.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442692, "navigation.trip.log": 81507, "navigation.headingTrue": 3.4313, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 14.029, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 36.1, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 2.78, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14.03, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 69, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:46:28.664Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7087125,
"longitude" : 24.7276837,
"speedoverground" : 7.0,
"courseovergroundtrue" : 185.4,
"windspeedapparent" : 13.5,
"anglespeedapparent" : 51.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442877, "navigation.trip.log": 81692, "navigation.headingTrue": 3.3004, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 22.689, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 25.2, "electrical.batteries.1.voltage": 14.55, "navigation.gnss.antennaAltitude": 3.24, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 22.69, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 45, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:47:28.696Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7245499,
"longitude" : 24.736394999999998,
"speedoverground" : 6.7,
"courseovergroundtrue" : 196.5,
"windspeedapparent" : 14.1,
"anglespeedapparent" : 42.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441025, "navigation.trip.log": 79951, "navigation.headingTrue": 3.475, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 71.039, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 16.4, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 2.32, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 71.04, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 64, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:48:28.712Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7226901,
"longitude" : 24.735683899999998,
"speedoverground" : 6.4,
"courseovergroundtrue" : 187.7,
"windspeedapparent" : 14.9,
"anglespeedapparent" : 44.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441210, "navigation.trip.log": 80136, "navigation.headingTrue": 3.4313, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 33.169000000000004, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 172, "electrical.batteries.1.voltage": 13.35, "navigation.gnss.antennaAltitude": 2.13, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 33.17, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 32, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 5, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 6, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 13, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 1, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 1, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:49:28.726Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7209298,
"longitude" : 24.73472,
"speedoverground" : 6.8,
"courseovergroundtrue" : 190.7,
"windspeedapparent" : 15.4,
"anglespeedapparent" : 38.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441395, "navigation.trip.log": 80340, "navigation.headingTrue": 3.4663, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 30.738999999999997, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.47, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 211.6, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 0.76, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 30.74, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 52, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 9, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:50:28.743Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7190454,
"longitude" : 24.7337548,
"speedoverground" : 7.0,
"courseovergroundtrue" : 194.4,
"windspeedapparent" : 16.3,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441580, "navigation.trip.log": 80544, "navigation.headingTrue": 3.4226, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 23.808999999999997, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.61, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 231.5, "electrical.batteries.1.voltage": 13.02, "navigation.gnss.antennaAltitude": 2.06, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 23.81, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 73, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 1, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 1, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:51:28.762Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7172775,
"longitude" : 24.7325491,
"speedoverground" : 6.5,
"courseovergroundtrue" : 198.3,
"windspeedapparent" : 13.9,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441766, "navigation.trip.log": 80747, "navigation.headingTrue": 3.5797, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 20.948999999999998, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.47, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 192.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 0.39, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 20.95, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 64, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:52:28.783Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7156422,
"longitude" : 24.7312116,
"speedoverground" : 6.4,
"courseovergroundtrue" : 203.3,
"windspeedapparent" : 15.6,
"anglespeedapparent" : 58.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442136, "navigation.trip.log": 80951, "navigation.headingTrue": 3.5361, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 17.529, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 2.93, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 107.6, "electrical.batteries.1.voltage": 14.55, "navigation.gnss.antennaAltitude": 2.49, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 17.53, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 43, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 11, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:53:28.800Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7139716,
"longitude" : 24.7299463,
"speedoverground" : 6.4,
"courseovergroundtrue" : 200.4,
"windspeedapparent" : 14.1,
"anglespeedapparent" : 44.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442321, "navigation.trip.log": 81136, "navigation.headingTrue": 3.571, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 13.999, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 70.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 2.69, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 34, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:54:28.819Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7124464,
"longitude" : 24.728924,
"speedoverground" : 5.7,
"courseovergroundtrue" : 197.6,
"windspeedapparent" : 15.9,
"anglespeedapparent" : 31.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442506, "navigation.trip.log": 81321, "navigation.headingTrue": 3.571, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 13.999, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.07, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 43.9, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 2.05, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 66, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:55:28.837Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7106379,
"longitude" : 24.728088,
"speedoverground" : 6.7,
"courseovergroundtrue" : 187.8,
"windspeedapparent" : 16.9,
"anglespeedapparent" : 42.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442692, "navigation.trip.log": 81507, "navigation.headingTrue": 3.4313, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 14.029, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 36.1, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 2.74, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14.03, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 69, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:56:28.855Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7086808,
"longitude" : 24.7276789,
"speedoverground" : 7.0,
"courseovergroundtrue" : 185.4,
"windspeedapparent" : 13.5,
"anglespeedapparent" : 51.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442877, "navigation.trip.log": 81692, "navigation.headingTrue": 3.3004, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 22.689, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 25.2, "electrical.batteries.1.voltage": 14.55, "navigation.gnss.antennaAltitude": 3.24, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 22.69, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 45, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:57:28.876Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7245198,
"longitude" : 24.7363702,
"speedoverground" : 6.7,
"courseovergroundtrue" : 196.5,
"windspeedapparent" : 14.1,
"anglespeedapparent" : 42.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441025, "navigation.trip.log": 79951, "navigation.headingTrue": 3.475, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 71.039, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 16.4, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 2.32, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 71.04, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 64, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:58:28.899Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7226901,
"longitude" : 24.735683899999998,
"speedoverground" : 6.4,
"courseovergroundtrue" : 187.7,
"windspeedapparent" : 14.9,
"anglespeedapparent" : 44.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441210, "navigation.trip.log": 80136, "navigation.headingTrue": 3.4313, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 33.169000000000004, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 172, "electrical.batteries.1.voltage": 13.35, "navigation.gnss.antennaAltitude": 2.13, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 33.17, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 32, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 5, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 6, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 13, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 1, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 1, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T11:59:28.928Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7208986,
"longitude" : 24.734705599999998,
"speedoverground" : 6.8,
"courseovergroundtrue" : 190.7,
"windspeedapparent" : 15.4,
"anglespeedapparent" : 38.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441395, "navigation.trip.log": 80358, "navigation.headingTrue": 3.5099, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 30.738999999999997, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.47, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 211.6, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 0.78, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 30.74, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 52, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 9, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:00:28.941Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7190454,
"longitude" : 24.7337548,
"speedoverground" : 7.0,
"courseovergroundtrue" : 194.4,
"windspeedapparent" : 17.0,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441580, "navigation.trip.log": 80544, "navigation.headingTrue": 3.4226, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 23.808999999999997, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.61, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 231.5, "electrical.batteries.1.voltage": 13.02, "navigation.gnss.antennaAltitude": 2.06, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 23.81, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 73, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 1, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 1, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:01:28.966Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7172775,
"longitude" : 24.7325491,
"speedoverground" : 6.5,
"courseovergroundtrue" : 198.3,
"windspeedapparent" : 15.1,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441766, "navigation.trip.log": 80747, "navigation.headingTrue": 3.5797, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 20.948999999999998, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.47, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 192.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 0.39, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 20.95, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 64, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:02:28.982Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7156162,
"longitude" : 24.731190599999998,
"speedoverground" : 6.4,
"courseovergroundtrue" : 203.1,
"windspeedapparent" : 15.6,
"anglespeedapparent" : 58.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442136, "navigation.trip.log": 80951, "navigation.headingTrue": 3.5273, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 17.529, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 2.93, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 107.6, "electrical.batteries.1.voltage": 14.55, "navigation.gnss.antennaAltitude": 2.6, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 17.53, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 43, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 11, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:03:28.988Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7139716,
"longitude" : 24.7299463,
"speedoverground" : 6.4,
"courseovergroundtrue" : 200.4,
"windspeedapparent" : 14.1,
"anglespeedapparent" : 44.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442321, "navigation.trip.log": 81136, "navigation.headingTrue": 3.571, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 13.999, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.07, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 70.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 2.69, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 34, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:04:29.008Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7124174,
"longitude" : 24.7289112,
"speedoverground" : 5.7,
"courseovergroundtrue" : 197.6,
"windspeedapparent" : 15.9,
"anglespeedapparent" : 31.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442506, "navigation.trip.log": 81321, "navigation.headingTrue": 3.571, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 13.749, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.07, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 43.9, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 2.05, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 13.75, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 66, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:05:29.025Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7106066,
"longitude" : 24.7280837,
"speedoverground" : 6.7,
"courseovergroundtrue" : 191.2,
"windspeedapparent" : 16.9,
"anglespeedapparent" : 42.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442692, "navigation.trip.log": 81507, "navigation.headingTrue": 3.4313, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 13.539, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 36.1, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 2.74, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 13.54, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 69, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:06:29.043Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7086808,
"longitude" : 24.7276789,
"speedoverground" : 7.0,
"courseovergroundtrue" : 185.4,
"windspeedapparent" : 14.4,
"anglespeedapparent" : 51.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442877, "navigation.trip.log": 81710, "navigation.headingTrue": 3.3004, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 22.689, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 25.2, "electrical.batteries.1.voltage": 14.55, "navigation.gnss.antennaAltitude": 3.24, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 22.69, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 45, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:07:29.071Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7245198,
"longitude" : 24.7363702,
"speedoverground" : 6.7,
"courseovergroundtrue" : 196.5,
"windspeedapparent" : 14.1,
"anglespeedapparent" : 42.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441025, "navigation.trip.log": 79951, "navigation.headingTrue": 3.475, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 71.039, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.19, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 16.4, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 2.51, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 71.04, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 64, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:08:29.081Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7226901,
"longitude" : 24.7356839,
"speedoverground" : 6.4,
"courseovergroundtrue" : 187.7,
"windspeedapparent" : 14.9,
"anglespeedapparent" : 44.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441210, "navigation.trip.log": 80155, "navigation.headingTrue": 3.4313, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 33.719, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.09, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 172, "electrical.batteries.1.voltage": 13.35, "navigation.gnss.antennaAltitude": 2.13, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 33.72, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 72, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 6, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 13, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 1, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 1, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:09:29.094Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7208986,
"longitude" : 24.734705599999998,
"speedoverground" : 6.8,
"courseovergroundtrue" : 191.3,
"windspeedapparent" : 15.4,
"anglespeedapparent" : 38.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441395, "navigation.trip.log": 80358, "navigation.headingTrue": 3.5099, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 31.529, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.47, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 211.6, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 0.78, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 31.53, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 49, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 9, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:10:29.108Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7190454,
"longitude" : 24.7337548,
"speedoverground" : 7.0,
"courseovergroundtrue" : 194.4,
"windspeedapparent" : 17.0,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441580, "navigation.trip.log": 80544, "navigation.headingTrue": 3.4226, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 25.398999999999997, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.47, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 231.5, "electrical.batteries.1.voltage": 13.02, "navigation.gnss.antennaAltitude": 1.8, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 25.4, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 57, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 1, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 1, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:11:29.120Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7172775,
"longitude" : 24.7325491,
"speedoverground" : 6.5,
"courseovergroundtrue" : 198.3,
"windspeedapparent" : 15.1,
"anglespeedapparent" : 41.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17441766, "navigation.trip.log": 80747, "navigation.headingTrue": 3.5797, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 20.948999999999998, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 192.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 0.39, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 20.95, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 81, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:12:29.137Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7156162,
"longitude" : 24.731190599999998,
"speedoverground" : 6.4,
"courseovergroundtrue" : 203.1,
"windspeedapparent" : 15.6,
"anglespeedapparent" : 58.0,
"status" : "sailing",
"metrics" : {"navigation.log": 17442136, "navigation.trip.log": 80951, "navigation.headingTrue": 3.5273, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 17.529, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.21, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 107.6, "electrical.batteries.1.voltage": 14.55, "navigation.gnss.antennaAltitude": 2.6, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 17.53, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 2, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 55, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 3, "network.n2k.ngt-1.130356.ch1.bandwidth": 2, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 11, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:13:29.150Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7139716,
"longitude" : 24.729946299999998,
"speedoverground" : 6.4,
"courseovergroundtrue" : 200.7,
"windspeedapparent" : 14.1,
"anglespeedapparent" : 44.0,
"status" : "anchored",
"metrics" : {"navigation.log": 17442321, "navigation.trip.log": 81136, "navigation.headingTrue": 3.571, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 14.209000000000001, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.07, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 70.4, "electrical.batteries.1.voltage": 14.56, "navigation.gnss.antennaAltitude": 2.38, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 14.21, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 70, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 4, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
},
{
"time" : "2022-07-31T12:14:29.168Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7124174,
"longitude" : 24.7289112,
"speedoverground" : 5.7,
"courseovergroundtrue" : 197.6,
"windspeedapparent" : 15.9,
"anglespeedapparent" : 31.0,
"status" : "ais-sart",
"metrics" : {}
},
{
"time" : "2022-07-31T12:14:29.168Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7124174,
"longitude" : 24.7289112,
"speedoverground" : 55.7,
"courseovergroundtrue" : 197.6,
"windspeedapparent" : 15.9,
"anglespeedapparent" : 31.0,
"status" : "anchored",
"metrics" : {}
},
{
"time" : "2022-07-31T12:14:29.168Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
"latitude" : 59.7124174,
"longitude" : 24.7289112,
"speedoverground" : 5.7,
"courseovergroundtrue" : 197.6,
"windspeedapparent" : 15.9,
"anglespeedapparent" : 31.0,
"status" : "anchored",
"metrics" : {}
}
]}

View File

@@ -0,0 +1,663 @@
{
"metrics": [
{
"time" : "2022-07-30T14:52:28.000Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.077666666666666,
"longitude" : 23.530866666666668,
"speedoverground" : 0.0,
"courseovergroundtrue" : 207.5,
"windspeedapparent" : 14.8,
"anglespeedapparent" : -12.0,
"status" : "moored",
"metrics" : {"environment.wind.speedTrue": 4.44, "navigation.speedThroughWater": 3.0918118943701245, "performance.velocityMadeGood": 2.9323340761912995, "environment.wind.angleTrueWater": -0.3665191430024964, "environment.depth.belowTransducer": 13.1, "navigation.courseOverGroundMagnetic": 3.620685534088946, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T14:52:28.000Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.077666666666666,
"longitude" : 23.530866666666668,
"speedoverground" : 0.0,
"courseovergroundtrue" : 207.5,
"windspeedapparent" : 14.8,
"anglespeedapparent" : -12.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 4.44, "navigation.speedThroughWater": 3.0918118943701245, "performance.velocityMadeGood": 2.9323340761912995, "environment.wind.angleTrueWater": -0.3665191430024964, "environment.depth.belowTransducer": 13.1, "navigation.courseOverGroundMagnetic": 3.620685534088946, "navigation.courseRhumbline.crossTrackError": 0, "propulsion.main.runTime":1776241 }
},
{
"time" : "2022-07-30T14:53:28.000Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.077666666666666,
"longitude" : 23.530866666666668,
"speedoverground" : 5.7,
"courseovergroundtrue" : 207.5,
"windspeedapparent" : 14.8,
"anglespeedapparent" : -12.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 4.44, "navigation.speedThroughWater": 3.0918118943701245, "performance.velocityMadeGood": 2.9323340761912995, "environment.wind.angleTrueWater": -0.3665191430024964, "environment.depth.belowTransducer": 13.1, "navigation.courseOverGroundMagnetic": 3.620685534088946, "navigation.courseRhumbline.crossTrackError": 0 }
},
{
"time" : "2022-07-30T14:54:28.016Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.07065,
"longitude" : 23.52355,
"speedoverground" : 5.6,
"courseovergroundtrue" : 211.5,
"windspeedapparent" : 14.8,
"anglespeedapparent" : -7.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 4.52, "navigation.speedThroughWater": 3.0815230028747167, "performance.velocityMadeGood": 2.9683451964252274, "environment.wind.angleTrueWater": -0.20943951028714078, "environment.depth.belowTransducer": 13.58, "navigation.courseOverGroundMagnetic": 3.6910223029603775, "navigation.courseRhumbline.crossTrackError": 0, "propulsion.main.runTime":1776241 }
},
{
"time" : "2022-07-30T14:55:28.021Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.0637,
"longitude" : 23.515866666666668,
"speedoverground" : 5.6,
"courseovergroundtrue" : 211.3,
"windspeedapparent" : 15.7,
"anglespeedapparent" : 0.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 4.99, "navigation.speedThroughWater": 3.0558007741361966, "performance.velocityMadeGood": 3.00950076240686, "environment.wind.angleTrueWater": 0, "environment.depth.belowTransducer": 16.75, "navigation.courseOverGroundMagnetic": 3.6878807103060707, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T14:56:28.033Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.05671666666667,
"longitude" : 23.507866666666665,
"speedoverground" : 5.9,
"courseovergroundtrue" : 211.1,
"windspeedapparent" : 19.8,
"anglespeedapparent" : -2.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 4.22, "navigation.speedThroughWater": 3.1895563635765014, "performance.velocityMadeGood": 3.1586896890902767, "environment.wind.angleTrueWater": -0.0174532925239284, "environment.depth.belowTransducer": 18.96, "navigation.courseOverGroundMagnetic": 3.683866453025567, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T14:57:28.049Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.04915,
"longitude" : 23.500533333333333,
"speedoverground" : 6.1,
"courseovergroundtrue" : 212.0,
"windspeedapparent" : 14.2,
"anglespeedapparent" : -5.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.9, "navigation.speedThroughWater": 3.2204230380627252, "performance.velocityMadeGood": 3.168978580585685, "environment.wind.angleTrueWater": -0.19198621776321237, "environment.depth.belowTransducer": 21.41, "navigation.courseOverGroundMagnetic": 3.6997489492223417, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T14:58:28.064Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.04163333333333,
"longitude" : 23.493,
"speedoverground" : 6.0,
"courseovergroundtrue" : 204.9,
"windspeedapparent" : 12.0,
"anglespeedapparent" : -26.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 1.93, "navigation.speedThroughWater": 3.2615786040443577, "performance.velocityMadeGood": 0.8025335366418294, "environment.wind.angleTrueWater": -1.30899693929463, "environment.depth.belowTransducer": 21.01, "navigation.courseOverGroundMagnetic": 3.5753069735267324, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T14:59:28.095Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.03398333333333,
"longitude" : 23.485466666666667,
"speedoverground" : 6.1,
"courseovergroundtrue" : 206.0,
"windspeedapparent" : 9.5,
"anglespeedapparent" : -23.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.29, "navigation.speedThroughWater": 3.2461452668012454, "performance.velocityMadeGood": 1.6668004222561073, "environment.wind.angleTrueWater": -0.9948376738639186, "environment.depth.belowTransducer": 27.74, "navigation.courseOverGroundMagnetic": 3.595203727004011, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:00:28.106Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.02621666666667,
"longitude" : 23.479033333333334,
"speedoverground" : 6.0,
"courseovergroundtrue" : 201.6,
"windspeedapparent" : 10.5,
"anglespeedapparent" : -8.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.21, "navigation.speedThroughWater": 3.2410008210535413, "performance.velocityMadeGood": 3.0660896656316043, "environment.wind.angleTrueWater": -0.38397243552642474, "environment.depth.belowTransducer": 31.18, "navigation.courseOverGroundMagnetic": 3.518409239898726, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:01:28.107Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.01835,
"longitude" : 23.47295,
"speedoverground" : 6.0,
"courseovergroundtrue" : 197.4,
"windspeedapparent" : 10.9,
"anglespeedapparent" : -13.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.07, "navigation.speedThroughWater": 3.2461452668012454, "performance.velocityMadeGood": 2.896322955957371, "environment.wind.angleTrueWater": -0.40142572805035315, "environment.depth.belowTransducer": 34.89, "navigation.courseOverGroundMagnetic": 3.445454477148705, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:02:28.118Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.01045,
"longitude" : 23.46745,
"speedoverground" : 6.0,
"courseovergroundtrue" : 201.6,
"windspeedapparent" : 12.8,
"anglespeedapparent" : -26.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 3.98, "navigation.speedThroughWater": 3.2101341465673174, "performance.velocityMadeGood": 2.263556128989775, "environment.wind.angleTrueWater": -0.8203047486246348, "environment.depth.belowTransducer": 23.33, "navigation.courseOverGroundMagnetic": 3.518060174048247, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:03:28.140Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 60.00351666666667,
"longitude" : 23.461033333333333,
"speedoverground" : 5.1,
"courseovergroundtrue" : 210.5,
"windspeedapparent" : 12.0,
"anglespeedapparent" : -8.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.34, "navigation.speedThroughWater": 2.69054512604921, "performance.velocityMadeGood": 2.5207784163749767, "environment.wind.angleTrueWater": -0.3665191430024964, "environment.depth.belowTransducer": 18.21, "navigation.courseOverGroundMagnetic": 3.673219944585971, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:04:28.159Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.99755,
"longitude" : 23.45415,
"speedoverground" : 4.8,
"courseovergroundtrue" : 215.7,
"windspeedapparent" : 14.4,
"anglespeedapparent" : 6.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 4.86, "navigation.speedThroughWater": 2.5670784281043133, "performance.velocityMadeGood": 2.546500645113497, "environment.wind.angleTrueWater": 0.20943951028714078, "environment.depth.belowTransducer": 8.97, "navigation.courseOverGroundMagnetic": 3.765198796187073, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:05:28.169Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.99235,
"longitude" : 23.445683333333335,
"speedoverground" : 4.7,
"courseovergroundtrue" : 225.9,
"windspeedapparent" : 16.1,
"anglespeedapparent" : -5.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 6.075590428038464, "navigation.speedThroughWater": 2.5053450791318648, "performance.velocityMadeGood": 2.4641895131502323, "environment.wind.angleTrueWater": -0.12217304766749879, "environment.depth.belowTransducer": 7.03, "navigation.courseOverGroundMagnetic": 3.9426987811554253, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:06:28.196Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.989266666666666,
"longitude" : 23.438766666666666,
"speedoverground" : 1.9,
"courseovergroundtrue" : 223.2,
"windspeedapparent" : 16.7,
"anglespeedapparent" : 3.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 7.61, "navigation.speedThroughWater": 1.0134558122976947, "performance.velocityMadeGood": 0.9825891378114705, "environment.wind.angleTrueWater": 0.052359877571785195, "environment.depth.belowTransducer": 5.78, "navigation.courseOverGroundMagnetic": 3.89522582549034, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:07:28.205Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.98786666666667,
"longitude" : 23.435116666666666,
"speedoverground" : 1.9,
"courseovergroundtrue" : 229.6,
"windspeedapparent" : 18.1,
"anglespeedapparent" : -5.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 8.02, "navigation.speedThroughWater": 1.0186002580453988, "performance.velocityMadeGood": 0.9620113548206545, "environment.wind.angleTrueWater": -0.19198621776321237, "environment.depth.belowTransducer": 5.33, "navigation.courseOverGroundMagnetic": 4.007799562269678, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:08:28.218Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.986333333333334,
"longitude" : 23.43165,
"speedoverground" : 1.4,
"courseovergroundtrue" : 204.7,
"windspeedapparent" : 14.8,
"anglespeedapparent" : 4.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 6.3, "navigation.speedThroughWater": 0.745944633417085, "performance.velocityMadeGood": 0.7408001876693809, "environment.wind.angleTrueWater": 0, "environment.depth.belowTransducer": 5.49, "navigation.courseOverGroundMagnetic": 3.5732125784238606, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:09:28.241Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.984833333333334,
"longitude" : 23.4292,
"speedoverground" : 1.3,
"courseovergroundtrue" : 225.8,
"windspeedapparent" : 13.4,
"anglespeedapparent" : -20.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 4.48, "navigation.speedThroughWater": 0.745944633417085, "performance.velocityMadeGood": 0.7613779706601971, "environment.wind.angleTrueWater": -0.40142572805035315, "environment.depth.belowTransducer": 7.8, "navigation.courseOverGroundMagnetic": 3.9414770506787504, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:10:28.257Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.9862,
"longitude" : 23.432566666666666,
"speedoverground" : 3.3,
"courseovergroundtrue" : 41.1,
"windspeedapparent" : 16.1,
"anglespeedapparent" : 175.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 9.93, "navigation.speedThroughWater": 1.6205004105267706, "performance.velocityMadeGood": -1.6153559647790667, "environment.wind.angleTrueWater": 3.071779484211398, "environment.depth.belowTransducer": 5.41, "navigation.courseOverGroundMagnetic": 0.7171557898082179, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:11:43.288Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.98726666666666,
"longitude" : 23.43375,
"speedoverground" : 1.7,
"courseovergroundtrue" : 228.8,
"windspeedapparent" : 15.7,
"anglespeedapparent" : 8.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 6.37, "navigation.speedThroughWater": 0.9825891378114705, "performance.velocityMadeGood": 0.8694113313619818, "environment.wind.angleTrueWater": 0.15707963271535558, "environment.depth.belowTransducer": 5.31, "navigation.courseOverGroundMagnetic": 3.9934878624000567, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:12:58.309Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.98615,
"longitude" : 23.431566666666665,
"speedoverground" : 1.0,
"courseovergroundtrue" : 223.5,
"windspeedapparent" : 22.1,
"anglespeedapparent" : -6.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 7.63, "navigation.speedThroughWater": 0.6687779472015245, "performance.velocityMadeGood": 0.61218904397678, "environment.wind.angleTrueWater": -0.12217304766749879, "environment.depth.belowTransducer": 5.71, "navigation.courseOverGroundMagnetic": 3.9011599449484757, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:13:58.309Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.98615,
"longitude" : 23.431566666666665,
"speedoverground" : 1.0,
"courseovergroundtrue" : 223.5,
"windspeedapparent" : 22.1,
"anglespeedapparent" : -6.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 7.63, "navigation.speedThroughWater": 0.6687779472015245, "performance.velocityMadeGood": 0.61218904397678, "environment.wind.angleTrueWater": -0.12217304766749879, "environment.depth.belowTransducer": 5.71, "navigation.courseOverGroundMagnetic": 3.9011599449484757, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:14:28.343Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.98565,
"longitude" : 23.4307,
"speedoverground" : 1.2,
"courseovergroundtrue" : 224.5,
"windspeedapparent" : 19.4,
"anglespeedapparent" : 2.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 5.83, "navigation.speedThroughWater": 0.7613779706601971, "performance.velocityMadeGood": 0.7150779589308607, "environment.wind.angleTrueWater": -0.29670597290678274, "environment.depth.belowTransducer": 6.44, "navigation.courseOverGroundMagnetic": 3.9177405728462076, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:15:28.366Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.98468333333334,
"longitude" : 23.429383333333334,
"speedoverground" : 6.1,
"courseovergroundtrue" : 219.9,
"windspeedapparent" : 13.2,
"anglespeedapparent" : 3.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 3.35, "navigation.speedThroughWater": 3.4622119882048152, "performance.velocityMadeGood": 3.32845639876451, "environment.wind.angleTrueWater": 0, "environment.depth.belowTransducer": 8.07, "navigation.courseOverGroundMagnetic": 3.837455427236137, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:16:28.373Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.978233333333336,
"longitude" : 23.42106666666667,
"speedoverground" : 5.3,
"courseovergroundtrue" : 216.7,
"windspeedapparent" : 15.3,
"anglespeedapparent" : -8.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 4.93, "navigation.speedThroughWater": 2.9632007506775238, "performance.velocityMadeGood": 2.772856258012474, "environment.wind.angleTrueWater": -0.20943951028714078, "environment.depth.belowTransducer": 5.16, "navigation.courseOverGroundMagnetic": 3.7824775557857624, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:17:28.386Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.977716666666666,
"longitude" : 23.431,
"speedoverground" : 2.1,
"courseovergroundtrue" : 151.9,
"windspeedapparent" : 12.8,
"anglespeedapparent" : 32.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.74, "navigation.speedThroughWater": 1.1214891729994796, "performance.velocityMadeGood": 0.2520778416374977, "environment.wind.angleTrueWater": 1.5533430346296275, "environment.depth.belowTransducer": 3.49, "navigation.courseOverGroundMagnetic": 2.6518532660856806, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:18:28.434Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.97688333333333,
"longitude" : 23.432133333333333,
"speedoverground" : 0.0,
"courseovergroundtrue" : 179.3,
"windspeedapparent" : 11.1,
"anglespeedapparent" : 88.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 3.1895563635765014, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 1.8151424224885533, "environment.depth.belowTransducer": 1.67, "navigation.courseOverGroundMagnetic": 3.1290262836898832, "navigation.courseRhumbline.crossTrackError": 0 }
},
{
"time" : "2022-07-30T15:19:28.467Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.97688333333333,
"longitude" : 23.4321,
"speedoverground" : 0.0,
"courseovergroundtrue" : 241.0,
"windspeedapparent" : 4.3,
"anglespeedapparent" : 74.0,
"status" : "moored",
"metrics" : {"environment.wind.speedTrue": 0, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 0.7853981635767779, "environment.depth.belowTransducer": 1.65, "navigation.courseOverGroundMagnetic": 4.206068965341505, "navigation.courseRhumbline.crossTrackError": 0, "propulsion.main.runTime":1776251}
},
{
"time" : "2022-07-30T15:20:28.467Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.97688333333333,
"longitude" : 23.4321,
"speedoverground" : 0.0,
"courseovergroundtrue" : 241.0,
"windspeedapparent" : 4.3,
"anglespeedapparent" : 74.0,
"status" : "moored",
"metrics" : {"environment.wind.speedTrue": 0, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 0.7853981635767779, "environment.depth.belowTransducer": 1.65, "navigation.courseOverGroundMagnetic": 4.206068965341505, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:20:28.467Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.97688333333333,
"longitude" : 23.4321,
"speedoverground" : 0.0,
"courseovergroundtrue" : 241.0,
"windspeedapparent" : 4.3,
"anglespeedapparent" : 74.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 0.7853981635767779, "environment.depth.belowTransducer": 1.65, "navigation.courseOverGroundMagnetic": 4.206068965341505, "navigation.courseRhumbline.crossTrackError": 0, "propulsion.main.runTime":1776251}
},
{
"time" : "2022-07-30T15:21:28.467Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.97688333333333,
"longitude" : 23.4321,
"speedoverground" : 0.0,
"courseovergroundtrue" : 241.0,
"windspeedapparent" : 4.3,
"anglespeedapparent" : 74.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 0.7853981635767779, "environment.depth.belowTransducer": 1.65, "navigation.courseOverGroundMagnetic": 4.206068965341505, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:22:28.479Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.9781,
"longitude" : 23.425533333333334,
"speedoverground" : 5.0,
"courseovergroundtrue" : 258.3,
"windspeedapparent" : 9.1,
"anglespeedapparent" : 11.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.24, "navigation.speedThroughWater": 2.577367319599721, "performance.velocityMadeGood": 2.3561561524484476, "environment.wind.angleTrueWater": 0.41887902057428156, "environment.depth.belowTransducer": 5.05, "navigation.courseOverGroundMagnetic": 4.507661860154987, "navigation.courseRhumbline.crossTrackError": 0, "propulsion.main.runTime": 1776251}
},
{
"time" : "2022-07-30T15:23:28.492Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.9738,
"longitude" : 23.41165,
"speedoverground" : 6.2,
"courseovergroundtrue" : 228.0,
"windspeedapparent" : 11.8,
"anglespeedapparent" : 12.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.92, "navigation.speedThroughWater": 3.194700809324205, "performance.velocityMadeGood": 2.5207784163749767, "environment.wind.angleTrueWater": 0.8028514561007063, "environment.depth.belowTransducer": 5.44, "navigation.courseOverGroundMagnetic": 3.9854593478390496, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:24:28.498Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.967,
"longitude" : 23.40138333333333,
"speedoverground" : 6.1,
"courseovergroundtrue" : 197.7,
"windspeedapparent" : 10.9,
"anglespeedapparent" : 16.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.99, "navigation.speedThroughWater": 3.215278592315021, "performance.velocityMadeGood": 1.2964003284214165, "environment.wind.angleTrueWater": 1.274090354246773, "environment.depth.belowTransducer": 5.74, "navigation.courseOverGroundMagnetic": 3.4508649978311228, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:25:28.540Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.958866666666665,
"longitude" : 23.395816666666665,
"speedoverground" : 6.2,
"courseovergroundtrue" : 203.9,
"windspeedapparent" : 10.5,
"anglespeedapparent" : 38.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 3.44, "navigation.speedThroughWater": 3.215278592315021, "performance.velocityMadeGood": 1.512467049824986, "environment.wind.angleTrueWater": 1.2566370617228446, "environment.depth.belowTransducer": 7.13, "navigation.courseOverGroundMagnetic": 3.5587263456290006, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:26:28.572Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.9508,
"longitude" : 23.390166666666666,
"speedoverground" : 6.2,
"courseovergroundtrue" : 200.6,
"windspeedapparent" : 12.0,
"anglespeedapparent" : 63.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 4.12, "navigation.speedThroughWater": 3.2461452668012454, "performance.velocityMadeGood": -1.3787114603846813, "environment.wind.angleTrueWater": 1.884955592584267, "environment.depth.belowTransducer": 8.6, "navigation.courseOverGroundMagnetic": 3.5004323485990794, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:27:28.599Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.94275,
"longitude" : 23.38365,
"speedoverground" : 6.3,
"courseovergroundtrue" : 206.9,
"windspeedapparent" : 7.8,
"anglespeedapparent" : 22.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 2.13, "navigation.speedThroughWater": 3.2255674838104293, "performance.velocityMadeGood": -1.1317780644948876, "environment.wind.angleTrueWater": 1.9373154701560522, "environment.depth.belowTransducer": 12.98, "navigation.courseOverGroundMagnetic": 3.6112607561260246, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:28:28.606Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.93578333333333,
"longitude" : 23.37245,
"speedoverground" : 6.5,
"courseovergroundtrue" : 227.5,
"windspeedapparent" : 7.6,
"anglespeedapparent" : -10.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 1.12, "navigation.speedThroughWater": 3.2307119295581335, "performance.velocityMadeGood": -2.896322955957371, "environment.wind.angleTrueWater": -2.7750735113046154, "environment.depth.belowTransducer": 11.7, "navigation.courseOverGroundMagnetic": 3.9704495162684714, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:29:28.619Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.93076666666666,
"longitude" : 23.3572,
"speedoverground" : 6.3,
"courseovergroundtrue" : 232.5,
"windspeedapparent" : 6.8,
"anglespeedapparent" : 19.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.92, "navigation.speedThroughWater": 3.2307119295581335, "performance.velocityMadeGood": -1.960033829875237, "environment.wind.angleTrueWater": 2.2165681505389068, "environment.depth.belowTransducer": 14.23, "navigation.courseOverGroundMagnetic": 4.05858864351431, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:30:28.637Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.925133333333335,
"longitude" : 23.3448,
"speedoverground" : 5.9,
"courseovergroundtrue" : 228.4,
"windspeedapparent" : 7.2,
"anglespeedapparent" : 13.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.82, "navigation.speedThroughWater": 3.2358563753058376, "performance.velocityMadeGood": -2.1246560938017662, "environment.wind.angleTrueWater": 2.373647783254262, "environment.depth.belowTransducer": 14.29, "navigation.courseOverGroundMagnetic": 3.986855611240964, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:31:28.668Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.918933333333335,
"longitude" : 23.33415,
"speedoverground" : 6.0,
"courseovergroundtrue" : 211.6,
"windspeedapparent" : 6.4,
"anglespeedapparent" : -15.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.86, "navigation.speedThroughWater": 3.2255674838104293, "performance.velocityMadeGood": -0.38068898533009854, "environment.wind.angleTrueWater": -1.7278759598689115, "environment.depth.belowTransducer": 18.1, "navigation.courseOverGroundMagnetic": 3.6927676322127705, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:32:28.735Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.9114,
"longitude" : 23.32743333333333,
"speedoverground" : 6.2,
"courseovergroundtrue" : 175.6,
"windspeedapparent" : 6.4,
"anglespeedapparent" : -8.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.36, "navigation.speedThroughWater": 3.199845255071909, "performance.velocityMadeGood": -0.3189556363576501, "environment.wind.angleTrueWater": -1.6057029122014126, "environment.depth.belowTransducer": 10.95, "navigation.courseOverGroundMagnetic": 3.064972700127066, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:33:28.757Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.90315,
"longitude" : 23.329966666666667,
"speedoverground" : 6.1,
"courseovergroundtrue" : 144.0,
"windspeedapparent" : 5.8,
"anglespeedapparent" : -3.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.24, "navigation.speedThroughWater": 3.2049897008196133, "performance.velocityMadeGood": -2.4847672961410483, "environment.wind.angleTrueWater": -2.3911010757781903, "environment.depth.belowTransducer": 13.92, "navigation.courseOverGroundMagnetic": 2.512575991744732, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:34:28.787Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.89735,
"longitude" : 23.3428,
"speedoverground" : 6.3,
"courseovergroundtrue" : 129.8,
"windspeedapparent" : 5.2,
"anglespeedapparent" : -2.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 1, "navigation.speedThroughWater": 3.2307119295581335, "performance.velocityMadeGood": -3.1895563635765014, "environment.wind.angleTrueWater": -2.932153144019971, "environment.depth.belowTransducer": 11.32, "navigation.courseOverGroundMagnetic": 2.265960968381624, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:35:28.798Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.88953333333333,
"longitude" : 23.3492,
"speedoverground" : 6.5,
"courseovergroundtrue" : 162.5,
"windspeedapparent" : 7.2,
"anglespeedapparent" : -7.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.95, "navigation.speedThroughWater": 3.199845255071909, "performance.velocityMadeGood": -3.050656328388492, "environment.wind.angleTrueWater": -2.844886681400329, "environment.depth.belowTransducer": 10.04, "navigation.courseOverGroundMagnetic": 2.8354619034374076, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:36:28.812Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.88126666666667,
"longitude" : 23.345833333333335,
"speedoverground" : 6.1,
"courseovergroundtrue" : 196.5,
"windspeedapparent" : 8.7,
"anglespeedapparent" : -15.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.87, "navigation.speedThroughWater": 3.199845255071909, "performance.velocityMadeGood": -1.1832225219719277, "environment.wind.angleTrueWater": -1.972222055203909, "environment.depth.belowTransducer": 22.3, "navigation.courseOverGroundMagnetic": 3.429921046802409, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:37:28.835Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.87278333333333,
"longitude" : 23.344066666666667,
"speedoverground" : 6.2,
"courseovergroundtrue" : 160.5,
"windspeedapparent" : 7.8,
"anglespeedapparent" : -5.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.57, "navigation.speedThroughWater": 3.174123026333389, "performance.velocityMadeGood": 2.870600727218851, "environment.wind.angleTrueWater": -0.261799387858926, "environment.depth.belowTransducer": 24.73, "navigation.courseOverGroundMagnetic": 2.8016025159409867, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:38:28.838Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.866616666666665,
"longitude" : 23.355716666666666,
"speedoverground" : 6.1,
"courseovergroundtrue" : 131.4,
"windspeedapparent" : 35.8,
"anglespeedapparent" : 9.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.51, "navigation.speedThroughWater": 3.2255674838104293, "performance.velocityMadeGood": -1.347844785898457, "environment.wind.angleTrueWater": 1.7453292523928399, "environment.depth.belowTransducer": 9.85, "navigation.courseOverGroundMagnetic": 2.292489973017995, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:39:28.867Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.86,
"longitude" : 23.365766666666666,
"speedoverground" : 5.8,
"courseovergroundtrue" : 122.0,
"windspeedapparent" : 37.2,
"anglespeedapparent" : 10.0,
"status" : "sailing",
"metrics" : {"environment.wind.speedTrue": 0.63, "navigation.speedThroughWater": 3.2255674838104293, "performance.velocityMadeGood": -2.242978345998959, "environment.wind.angleTrueWater": 2.3038346131585485, "environment.depth.belowTransducer": 17.73, "navigation.courseOverGroundMagnetic": 2.129127154994025, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:40:28.867Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.86,
"longitude" : 23.365766666666666,
"speedoverground" : 4.5,
"courseovergroundtrue" : 122.0,
"windspeedapparent" : 7.2,
"anglespeedapparent" : 10.0,
"status" : "anchored",
"metrics" : {"environment.wind.speedTrue": 0.63, "navigation.speedThroughWater": 3.2255674838104293, "performance.velocityMadeGood": -2.242978345998959, "environment.wind.angleTrueWater": 2.3038346131585485, "environment.depth.belowTransducer": 17.73, "navigation.courseOverGroundMagnetic": 2.129127154994025, "navigation.courseRhumbline.crossTrackError": 0, "propulsion.main.runTime":1776262}
},
{
"time" : "2022-07-30T15:41:28.867Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.86,
"longitude" : 23.365766666666666,
"speedoverground" : 0.0,
"courseovergroundtrue" : 122.0,
"windspeedapparent" : 7.2,
"anglespeedapparent" : 10.0,
"status" : "anchored",
"metrics" : {"environment.wind.speedTrue": 0.63, "navigation.speedThroughWater": 3.2255674838104293, "performance.velocityMadeGood": -2.242978345998959, "environment.wind.angleTrueWater": 2.3038346131585485, "environment.depth.belowTransducer": 17.73, "navigation.courseOverGroundMagnetic": 2.129127154994025, "navigation.courseRhumbline.crossTrackError": 0}
},
{
"time" : "2022-07-30T15:41:28.867Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.86,
"longitude" : 23.365766666666666,
"speedoverground" : 0.0,
"courseovergroundtrue" : 122.0,
"windspeedapparent" : 7.2,
"anglespeedapparent" : 10.0,
"status" : "unknown",
"metrics" : {}
},
{
"time" : "2022-07-30T15:41:28.867Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.86,
"longitude" : 23.365766666666666,
"speedoverground" : 60.0,
"courseovergroundtrue" : 122.0,
"windspeedapparent" : 7.2,
"anglespeedapparent" : 10.0,
"status" : "anchored",
"metrics" : {}
},
{
"time" : "2022-07-30T15:41:28.867Z",
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
"latitude" : 59.86,
"longitude" : 23.365766666666666,
"speedoverground" : 0.0,
"courseovergroundtrue" : 122.0,
"windspeedapparent" : 7.2,
"anglespeedapparent" : 10.0,
"status" : "anchored",
"metrics" : {}
}
]}

View File

@@ -0,0 +1,879 @@
{
"metrics": [
{
"time" : "2022-12-13 20:39:04.562",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.3786471,
"longitude" : 2.277667383333333,
"speedoverground" : 4.1,
"courseovergroundtrue" : 17.4,
"windspeedapparent" : 8.1,
"anglespeedapparent" : -32.3,
"status" : "moored",
"metrics" : {"navigation.headingTrue": 0.3036872899163541, "environment.wind.speedTrue": 1.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.3036872899163541, "environment.depth.belowKeel": 9.3, "navigation.speedThroughWater": 2.109222756558654, "environment.water.temperature": 280.75, "environment.depth.belowSurface": 9.3, "environment.wind.directionTrue": 5.223770452411769, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 0, "propulsion.engine_2.revolutions": 0, "environment.depth.belowTransducer": 9.3, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 5.223770452411769, "navigation.gnss.horizontalDilution": 1.8, "navigation.courseOverGroundMagnetic": 0.3036872899163541, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670963929}
},
{
"time" : "2022-12-13 20:40:04.568",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.37983186666666,
"longitude" : 2.2781615333333334,
"speedoverground" : 4.1,
"courseovergroundtrue" : 16.5,
"windspeedapparent" : 6.0,
"anglespeedapparent" : -25.2,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.28797932664481857, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.28797932664481857, "environment.depth.belowKeel": 11.8, "navigation.speedThroughWater": 2.109222756558654, "environment.water.temperature": 280.95, "environment.depth.belowSurface": 11.8, "environment.wind.directionTrue": 5.483824511018303, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 0, "propulsion.engine_2.revolutions": 0, "environment.depth.belowTransducer": 11.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 5.483824511018303, "navigation.gnss.horizontalDilution": 1.7, "navigation.courseOverGroundMagnetic": 0.28797932664481857, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670963985}
},
{
"time" : "2022-12-13 20:41:04.592",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.380872266666664,
"longitude" : 2.2785093333333335,
"speedoverground" : 4.1,
"courseovergroundtrue" : 13.6,
"windspeedapparent" : 7.2,
"anglespeedapparent" : -34.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.2373647783254262, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.2373647783254262, "environment.depth.belowKeel": 10.7, "navigation.speedThroughWater": 2.109222756558654, "environment.water.temperature": 281.15, "environment.depth.belowSurface": 10.7, "environment.wind.directionTrue": 4.974188369319593, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 0, "propulsion.engine_2.revolutions": 0, "environment.depth.belowTransducer": 10.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 4.974188369319593, "navigation.gnss.horizontalDilution": 1.3, "navigation.courseOverGroundMagnetic": 0.2373647783254262, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964045}
},
{
"time" : "2022-12-13 20:42:04.609",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.38208281666667,
"longitude" : 2.2788272,
"speedoverground" : 4.2,
"courseovergroundtrue" : 10.7,
"windspeedapparent" : 10.0,
"anglespeedapparent" : -39.2,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.18675023000603386, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.18675023000603386, "environment.depth.belowKeel": 12.4, "navigation.speedThroughWater": 2.1606672140356946, "environment.water.temperature": 281.15, "environment.depth.belowSurface": 12.4, "environment.wind.directionTrue": 5.223770452411769, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10, "propulsion.engine_2.revolutions": 10, "environment.depth.belowTransducer": 12.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 5.223770452411769, "navigation.gnss.horizontalDilution": 1.6, "navigation.courseOverGroundMagnetic": 0.18675023000603386, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964109}
},
{
"time" : "2022-12-13 20:43:04.645",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.383208116666665,
"longitude" : 2.2791273833333334,
"speedoverground" : 4.1,
"courseovergroundtrue" : 11.2,
"windspeedapparent" : 6.5,
"anglespeedapparent" : -10.8,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.19547687626799803, "environment.wind.speedTrue": 1.2, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.19547687626799803, "environment.depth.belowKeel": 14.4, "navigation.speedThroughWater": 2.109222756558654, "environment.water.temperature": 280.95, "environment.depth.belowSurface": 14.4, "environment.wind.directionTrue": 5.967280713931119, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.403333333333334, "propulsion.engine_2.revolutions": 10.2, "environment.depth.belowTransducer": 14.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 5.967280713931119, "navigation.gnss.horizontalDilution": 1.7, "navigation.courseOverGroundMagnetic": 0.19547687626799803, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964164}
},
{
"time" : "2022-12-13 20:44:04.683",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.38424145,
"longitude" : 2.27946345,
"speedoverground" : 4.2,
"courseovergroundtrue" : 13.6,
"windspeedapparent" : 7.0,
"anglespeedapparent" : -12.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.2373647783254262, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.2373647783254262, "environment.depth.belowKeel": 11.8, "navigation.speedThroughWater": 2.1606672140356946, "environment.water.temperature": 280.95, "environment.depth.belowSurface": 11.8, "environment.wind.directionTrue": 5.967280713931119, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.403333333333334, "propulsion.engine_2.revolutions": 10, "environment.depth.belowTransducer": 11.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 5.967280713931119, "navigation.gnss.horizontalDilution": 1.3, "navigation.courseOverGroundMagnetic": 0.2373647783254262, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964225}
},
{
"time" : "2022-12-13 20:45:04.722",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.385386716666666,
"longitude" : 2.2798840166666667,
"speedoverground" : 4.2,
"courseovergroundtrue" : 15.8,
"windspeedapparent" : 5.2,
"anglespeedapparent" : -34.4,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.2757620218780687, "environment.wind.speedTrue": 1.2, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.2757620218780687, "environment.depth.belowKeel": 15.1, "navigation.speedThroughWater": 2.1606672140356946, "environment.water.temperature": 280.75, "environment.depth.belowSurface": 15.1, "environment.wind.directionTrue": 4.241150083314601, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.2, "propulsion.engine_2.revolutions": 11.04, "environment.depth.belowTransducer": 15.1, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 4.241150083314601, "navigation.gnss.horizontalDilution": 1.2, "navigation.courseOverGroundMagnetic": 0.2757620218780687, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964290}
},
{
"time" : "2022-12-13 20:46:04.743",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.38658911666667,
"longitude" : 2.2802688166666667,
"speedoverground" : 4.6,
"courseovergroundtrue" : 13.6,
"windspeedapparent" : 2.8,
"anglespeedapparent" : 166.2,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.2373647783254262, "environment.wind.speedTrue": 3.1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.2373647783254262, "environment.depth.belowKeel": 10.7, "navigation.speedThroughWater": 2.3664450439438554, "environment.water.temperature": 280.54999999999995, "environment.depth.belowSurface": 10.7, "environment.wind.directionTrue": 3.323106896555967, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.2, "propulsion.engine_2.revolutions": 10.403333333333334, "environment.depth.belowTransducer": 10.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 3.323106896555967, "navigation.gnss.horizontalDilution": 1.5, "navigation.courseOverGroundMagnetic": 0.2373647783254262, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964355}
},
{
"time" : "2022-12-13 20:47:04.759",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.387908966666664,
"longitude" : 2.2806784166666665,
"speedoverground" : 4.6,
"courseovergroundtrue" : 13.6,
"windspeedapparent" : 8.2,
"anglespeedapparent" : 138.2,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.2373647783254262, "environment.wind.speedTrue": 4.4, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.2373647783254262, "environment.depth.belowKeel": 9.7, "navigation.speedThroughWater": 2.3664450439438554, "environment.water.temperature": 280.25, "environment.depth.belowSurface": 9.7, "environment.wind.directionTrue": 3.0141836188824342, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 10.403333333333334, "environment.depth.belowTransducer": 9.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 3.0141836188824342, "navigation.gnss.horizontalDilution": 1.5, "navigation.courseOverGroundMagnetic": 0.2373647783254262, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964414}
},
{
"time" : "2022-12-13 20:48:04.770",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.389180233333335,
"longitude" : 2.28109,
"speedoverground" : 4.7,
"courseovergroundtrue" : 15.0,
"windspeedapparent" : 3.6,
"anglespeedapparent" : 60.8,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.261799387858926, "environment.wind.speedTrue": 2.1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.261799387858926, "environment.depth.belowKeel": 8.8, "navigation.speedThroughWater": 2.417889501420896, "environment.water.temperature": 279.84999999999997, "environment.depth.belowSurface": 8.8, "environment.wind.directionTrue": 2.733185609247187, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.2, "propulsion.engine_2.revolutions": 10.2, "environment.depth.belowTransducer": 8.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 2.733185609247187, "navigation.gnss.horizontalDilution": 1.7, "navigation.courseOverGroundMagnetic": 0.261799387858926, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964474}
},
{
"time" : "2022-12-13 20:49:04.782",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.39048306666667,
"longitude" : 2.28150255,
"speedoverground" : 4.8,
"courseovergroundtrue" : 12.3,
"windspeedapparent" : 5.2,
"anglespeedapparent" : 27.3,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.21467549804431932, "environment.wind.speedTrue": 1.2, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.21467549804431932, "environment.depth.belowKeel": 7.3, "navigation.speedThroughWater": 2.4693339588979364, "environment.water.temperature": 280.04999999999995, "environment.depth.belowSurface": 7.3, "environment.wind.directionTrue": 1.8500490075364102, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.486666666666668, "propulsion.engine_2.revolutions": 10.611666666666668, "environment.depth.belowTransducer": 7.3, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.8500490075364102, "navigation.gnss.horizontalDilution": 2, "navigation.courseOverGroundMagnetic": 0.21467549804431932, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964534}
},
{
"time" : "2022-12-13 20:50:04.811",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.3917979,
"longitude" : 2.28186645,
"speedoverground" : 4.8,
"courseovergroundtrue" : 10.2,
"windspeedapparent" : 8.3,
"anglespeedapparent" : 35.3,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.17802358374406965, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.17802358374406965, "environment.depth.belowKeel": 4.9, "navigation.speedThroughWater": 2.4693339588979364, "environment.water.temperature": 280.54999999999995, "environment.depth.belowSurface": 4.9, "environment.wind.directionTrue": 1.380555438642736, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 10, "environment.depth.belowTransducer": 4.9, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.380555438642736, "navigation.gnss.horizontalDilution": 2.3, "navigation.courseOverGroundMagnetic": 0.17802358374406965, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964595}
},
{
"time" : "2022-12-13 20:51:04.862",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.39308273333333,
"longitude" : 2.2821629833333335,
"speedoverground" : 5.0,
"courseovergroundtrue" : 10.2,
"windspeedapparent" : 8.6,
"anglespeedapparent" : 34.5,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.17802358374406965, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.17802358374406965, "environment.depth.belowKeel": 3.7, "navigation.speedThroughWater": 2.572222873852017, "environment.water.temperature": 280.65, "environment.depth.belowSurface": 3.7, "environment.wind.directionTrue": 1.380555438642736, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.486666666666668, "propulsion.engine_2.revolutions": 10.2, "environment.depth.belowTransducer": 3.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.380555438642736, "navigation.gnss.horizontalDilution": 2.3, "navigation.courseOverGroundMagnetic": 0.17802358374406965, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964654}
},
{
"time" : "2022-12-13 20:52:04.904",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.394408983333335,
"longitude" : 2.2825271833333334,
"speedoverground" : 4.8,
"courseovergroundtrue" : 11.2,
"windspeedapparent" : 9.5,
"anglespeedapparent" : 46.9,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.19547687626799803, "environment.wind.speedTrue": 3.7, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.19547687626799803, "environment.depth.belowKeel": 4.5, "navigation.speedThroughWater": 2.4693339588979364, "environment.water.temperature": 280.34999999999997, "environment.depth.belowSurface": 4.5, "environment.wind.directionTrue": 1.5219271080865564, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 10.403333333333334, "environment.depth.belowTransducer": 4.5, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.5219271080865564, "navigation.gnss.horizontalDilution": 2.3, "navigation.courseOverGroundMagnetic": 0.19547687626799803, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964714}
},
{
"time" : "2022-12-13 20:53:04.922",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.39573106666667,
"longitude" : 2.2829267166666667,
"speedoverground" : 4.9,
"courseovergroundtrue" : 12.3,
"windspeedapparent" : 10.2,
"anglespeedapparent" : 42.4,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.21467549804431932, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.21467549804431932, "environment.depth.belowKeel": 4.5, "navigation.speedThroughWater": 2.5207784163749767, "environment.water.temperature": 280.65, "environment.depth.belowSurface": 4.5, "environment.wind.directionTrue": 1.679006740801912, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 10, "environment.depth.belowTransducer": 4.5, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.679006740801912, "navigation.gnss.horizontalDilution": 3.4, "navigation.courseOverGroundMagnetic": 0.21467549804431932, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964775}
},
{
"time" : "2022-12-13 20:54:04.947",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.39700681666667,
"longitude" : 2.2833958666666665,
"speedoverground" : 4.9,
"courseovergroundtrue" : 16.5,
"windspeedapparent" : 8.0,
"anglespeedapparent" : 35.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.28797932664481857, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.28797932664481857, "environment.depth.belowKeel": 4.9, "navigation.speedThroughWater": 2.5207784163749767, "environment.water.temperature": 280.65, "environment.depth.belowSurface": 4.9, "environment.wind.directionTrue": 1.5219271080865564, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 10, "environment.depth.belowTransducer": 4.9, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.5219271080865564, "navigation.gnss.horizontalDilution": 4.4, "navigation.courseOverGroundMagnetic": 0.28797932664481857, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964835}
},
{
"time" : "2022-12-13 20:55:04.978",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.39837505,
"longitude" : 2.283934583333333,
"speedoverground" : 4.9,
"courseovergroundtrue" : 16.5,
"windspeedapparent" : 9.4,
"anglespeedapparent" : 27.9,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.28797932664481857, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.28797932664481857, "environment.depth.belowKeel": 6, "navigation.speedThroughWater": 2.5207784163749767, "environment.water.temperature": 280.65, "environment.depth.belowSurface": 6, "environment.wind.directionTrue": 1.253146403218059, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 10.2, "environment.depth.belowTransducer": 6, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.253146403218059, "navigation.gnss.horizontalDilution": 4.7, "navigation.courseOverGroundMagnetic": 0.28797932664481857, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964895}
},
{
"time" : "2022-12-13 20:56:04.996",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.39972915,
"longitude" : 2.2844581333333336,
"speedoverground" : 5.2,
"courseovergroundtrue" : 16.5,
"windspeedapparent" : 10.3,
"anglespeedapparent" : 23.8,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.28797932664481857, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.28797932664481857, "environment.depth.belowKeel": 5.4, "navigation.speedThroughWater": 2.675111788806098, "environment.water.temperature": 280.84999999999997, "environment.depth.belowSurface": 5.4, "environment.wind.directionTrue": 1.1362093433077387, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.486666666666668, "propulsion.engine_2.revolutions": 10.2, "environment.depth.belowTransducer": 5.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.1362093433077387, "navigation.gnss.horizontalDilution": 3.8, "navigation.courseOverGroundMagnetic": 0.28797932664481857, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670964954}
},
{
"time" : "2022-12-13 20:57:05.017",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.401043683333334,
"longitude" : 2.284932,
"speedoverground" : 5.3,
"courseovergroundtrue" : 15.0,
"windspeedapparent" : 14.3,
"anglespeedapparent" : 33.6,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.261799387858926, "environment.wind.speedTrue": 5.3, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.261799387858926, "environment.depth.belowKeel": 4.9, "navigation.speedThroughWater": 2.726556246283138, "environment.water.temperature": 280.65, "environment.depth.belowSurface": 4.9, "environment.wind.directionTrue": 1.1362093433077387, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 10.2, "environment.depth.belowTransducer": 4.9, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.1362093433077387, "navigation.gnss.horizontalDilution": 3.8, "navigation.courseOverGroundMagnetic": 0.261799387858926, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965014}
},
{
"time" : "2022-12-13 20:58:05.043",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.40248723333333,
"longitude" : 2.2853501666666665,
"speedoverground" : 5.2,
"courseovergroundtrue" : 11.2,
"windspeedapparent" : 25.2,
"anglespeedapparent" : 36.0,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.19547687626799803, "environment.wind.speedTrue": 7.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.19547687626799803, "environment.depth.belowKeel": 5.4, "navigation.speedThroughWater": 2.675111788806098, "environment.water.temperature": 280.65, "environment.depth.belowSurface": 5.4, "environment.wind.directionTrue": 1.0297442589117756, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.2, "propulsion.engine_2.revolutions": 10.2, "environment.depth.belowTransducer": 5.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.0297442589117756, "navigation.gnss.horizontalDilution": 4.4, "navigation.courseOverGroundMagnetic": 0.19547687626799803, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965075}
},
{
"time" : "2022-12-13 20:59:05.063",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.40396921666667,
"longitude" : 2.28573555,
"speedoverground" : 5.6,
"courseovergroundtrue" : 10.2,
"windspeedapparent" : 14.6,
"anglespeedapparent" : 27.6,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.17802358374406965, "environment.wind.speedTrue": 3.7, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.17802358374406965, "environment.depth.belowKeel": 5.4, "navigation.speedThroughWater": 2.8808896187142587, "environment.water.temperature": 280.65, "environment.depth.belowSurface": 5.4, "environment.wind.directionTrue": 1.0297442589117756, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.825, "propulsion.engine_2.revolutions": 10.611666666666668, "environment.depth.belowTransducer": 5.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.0297442589117756, "navigation.gnss.horizontalDilution": 4.4, "navigation.courseOverGroundMagnetic": 0.17802358374406965, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965135}
},
{
"time" : "2022-12-13 21:00:05.096",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.40549068333333,
"longitude" : 2.2861892666666668,
"speedoverground" : 5.5,
"courseovergroundtrue" : 13.6,
"windspeedapparent" : 15.4,
"anglespeedapparent" : 20.0,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.2373647783254262, "environment.wind.speedTrue": 5.3, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.2373647783254262, "environment.depth.belowKeel": 4.9, "navigation.speedThroughWater": 2.829445161237219, "environment.water.temperature": 280.65, "environment.depth.belowSurface": 4.9, "environment.wind.directionTrue": 0.7696902003052424, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.403333333333334, "propulsion.engine_2.revolutions": 10.825, "environment.depth.belowTransducer": 4.9, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.7696902003052424, "navigation.gnss.horizontalDilution": 4.7, "navigation.courseOverGroundMagnetic": 0.2373647783254262, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965195}
},
{
"time" : "2022-12-13 21:01:05.130",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.406966833333335,
"longitude" : 2.2866105,
"speedoverground" : 5.5,
"courseovergroundtrue" : 10.2,
"windspeedapparent" : 19.4,
"anglespeedapparent" : 24.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.17802358374406965, "environment.wind.speedTrue": 3.7, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.17802358374406965, "environment.depth.belowKeel": 4.5, "navigation.speedThroughWater": 2.829445161237219, "environment.water.temperature": 280.54999999999995, "environment.depth.belowSurface": 4.5, "environment.wind.directionTrue": 0.9337511500301693, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10, "propulsion.engine_2.revolutions": 12.19, "environment.depth.belowTransducer": 4.5, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.9337511500301693, "navigation.gnss.horizontalDilution": 5, "navigation.courseOverGroundMagnetic": 0.17802358374406965, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965255}
},
{
"time" : "2022-12-13 21:02:05.156",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.40843818333333,
"longitude" : 2.2869834833333336,
"speedoverground" : 5.3,
"courseovergroundtrue" : 10.2,
"windspeedapparent" : 14.6,
"anglespeedapparent" : 29.0,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.17802358374406965, "environment.wind.speedTrue": 5.3, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.17802358374406965, "environment.depth.belowKeel": 4.9, "navigation.speedThroughWater": 2.726556246283138, "environment.water.temperature": 280.25, "environment.depth.belowSurface": 4.9, "environment.wind.directionTrue": 0.9337511500301693, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.825, "propulsion.engine_2.revolutions": 11.716666666666667, "environment.depth.belowTransducer": 4.9, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.9337511500301693, "navigation.gnss.horizontalDilution": 3.3, "navigation.courseOverGroundMagnetic": 0.17802358374406965, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965315}
},
{
"time" : "2022-12-13 21:03:05.202",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.40989965,
"longitude" : 2.287289433333333,
"speedoverground" : 5.4,
"courseovergroundtrue" : 9.2,
"windspeedapparent" : 14.4,
"anglespeedapparent" : 26.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.16057029122014124, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.16057029122014124, "environment.depth.belowKeel": 6, "navigation.speedThroughWater": 2.7780007037601786, "environment.water.temperature": 280.25, "environment.depth.belowSurface": 6, "environment.wind.directionTrue": 1.1362093433077387, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.403333333333334, "propulsion.engine_2.revolutions": 11.261666666666667, "environment.depth.belowTransducer": 6, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.1362093433077387, "navigation.gnss.horizontalDilution": 3.8, "navigation.courseOverGroundMagnetic": 0.16057029122014124, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965375}
},
{
"time" : "2022-12-13 21:04:05.224",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.411323816666666,
"longitude" : 2.287555033333333,
"speedoverground" : 5.1,
"courseovergroundtrue" : 7.6,
"windspeedapparent" : 9.3,
"anglespeedapparent" : 22.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.1326450231818558, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.1326450231818558, "environment.depth.belowKeel": 5.4, "navigation.speedThroughWater": 2.6236673313290573, "environment.water.temperature": 279.95, "environment.depth.belowSurface": 5.4, "environment.wind.directionTrue": 0.9337511500301693, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.261666666666667, "propulsion.engine_2.revolutions": 10.825, "environment.depth.belowTransducer": 5.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.9337511500301693, "navigation.gnss.horizontalDilution": 3.3, "navigation.courseOverGroundMagnetic": 0.1326450231818558, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965435}
},
{
"time" : "2022-12-13 21:05:05.253",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.412672,
"longitude" : 2.2878124166666667,
"speedoverground" : 4.7,
"courseovergroundtrue" : 8.4,
"windspeedapparent" : 16.5,
"anglespeedapparent" : 24.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.14660765720099855, "environment.wind.speedTrue": 5.3, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.14660765720099855, "environment.depth.belowKeel": 4.5, "navigation.speedThroughWater": 2.417889501420896, "environment.water.temperature": 279.65, "environment.depth.belowSurface": 4.5, "environment.wind.directionTrue": 0.7696902003052424, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.261666666666667, "propulsion.engine_2.revolutions": 10.825, "environment.depth.belowTransducer": 4.5, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.7696902003052424, "navigation.gnss.horizontalDilution": 5, "navigation.courseOverGroundMagnetic": 0.14660765720099855, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965495}
},
{
"time" : "2022-12-13 21:06:05.281",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.413960216666666,
"longitude" : 2.288052583333333,
"speedoverground" : 4.8,
"courseovergroundtrue" : 7.6,
"windspeedapparent" : 19.1,
"anglespeedapparent" : 24.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.1326450231818558, "environment.wind.speedTrue": 7.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.1326450231818558, "environment.depth.belowKeel": 3.3, "navigation.speedThroughWater": 2.4693339588979364, "environment.water.temperature": 279.65, "environment.depth.belowSurface": 3.3, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.716666666666667, "propulsion.engine_2.revolutions": 11.716666666666667, "environment.depth.belowTransducer": 3.3, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 3.1, "navigation.courseOverGroundMagnetic": 0.1326450231818558, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965555}
},
{
"time" : "2022-12-13 21:07:05.315",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.41527805,
"longitude" : 2.288307083333333,
"speedoverground" : 4.8,
"courseovergroundtrue" : 7.6,
"windspeedapparent" : 22.1,
"anglespeedapparent" : 22.3,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.1326450231818558, "environment.wind.speedTrue": 5.3, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.1326450231818558, "environment.depth.belowKeel": 4, "navigation.speedThroughWater": 2.4693339588979364, "environment.water.temperature": 279.54999999999995, "environment.depth.belowSurface": 4, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.261666666666667, "propulsion.engine_2.revolutions": 12.681666666666667, "environment.depth.belowTransducer": 4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 2.7, "navigation.courseOverGroundMagnetic": 0.1326450231818558, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965615}
},
{
"time" : "2022-12-13 21:08:05.353",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.41658073333333,
"longitude" : 2.2885240166666665,
"speedoverground" : 4.7,
"courseovergroundtrue" : 6.9,
"windspeedapparent" : 16.6,
"anglespeedapparent" : 12.4,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.12042771841510595, "environment.wind.speedTrue": 1.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.12042771841510595, "environment.depth.belowKeel": 4.9, "navigation.speedThroughWater": 2.417889501420896, "environment.water.temperature": 279.45, "environment.depth.belowSurface": 4.9, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.716666666666667, "propulsion.engine_2.revolutions": 14.860000000000001, "environment.depth.belowTransducer": 4.9, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 3.1, "navigation.courseOverGroundMagnetic": 0.12042771841510595, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965675}
},
{
"time" : "2022-12-13 21:09:05.371",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.41785333333333,
"longitude" : 2.2887258333333333,
"speedoverground" : 4.5,
"courseovergroundtrue" : 6.2,
"windspeedapparent" : 8.6,
"anglespeedapparent" : 9.2,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.10821041364835607, "environment.wind.speedTrue": 1.2, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.10821041364835607, "environment.depth.belowKeel": 6, "navigation.speedThroughWater": 2.3150005864668155, "environment.water.temperature": 279.25, "environment.depth.belowSurface": 6, "environment.wind.directionTrue": 0.5742133240372442, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.403333333333334, "propulsion.engine_2.revolutions": 13.195, "environment.depth.belowTransducer": 6, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5742133240372442, "navigation.gnss.horizontalDilution": 2.7, "navigation.courseOverGroundMagnetic": 0.10821041364835607, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965735}
},
{
"time" : "2022-12-13 21:10:05.404",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.41911275,
"longitude" : 2.28890385,
"speedoverground" : 4.6,
"courseovergroundtrue" : 6.9,
"windspeedapparent" : 7.2,
"anglespeedapparent" : 7.8,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.12042771841510595, "environment.wind.speedTrue": 1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.12042771841510595, "environment.depth.belowKeel": 7.3, "navigation.speedThroughWater": 2.3664450439438554, "environment.water.temperature": 279.25, "environment.depth.belowSurface": 7.3, "environment.wind.directionTrue": 0.5742133240372442, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 12.681666666666667, "environment.depth.belowTransducer": 7.3, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5742133240372442, "navigation.gnss.horizontalDilution": 2.7, "navigation.courseOverGroundMagnetic": 0.12042771841510595, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965795}
},
{
"time" : "2022-12-13 21:11:05.414",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.42043938333333,
"longitude" : 2.2891021,
"speedoverground" : 4.8,
"courseovergroundtrue" : 6.9,
"windspeedapparent" : 7.5,
"anglespeedapparent" : 9.6,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.12042771841510595, "environment.wind.speedTrue": 1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.12042771841510595, "environment.depth.belowKeel": 7.3, "navigation.speedThroughWater": 2.4693339588979364, "environment.water.temperature": 279.54999999999995, "environment.depth.belowSurface": 7.3, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 12.681666666666667, "environment.depth.belowTransducer": 7.3, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 4.1, "navigation.courseOverGroundMagnetic": 0.12042771841510595, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965855}
},
{
"time" : "2022-12-13 21:12:05.470",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.42173171666666,
"longitude" : 2.2892923166666668,
"speedoverground" : 4.5,
"courseovergroundtrue" : 5.7,
"windspeedapparent" : 7.3,
"anglespeedapparent" : 14.8,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.09948376738639188, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.09948376738639188, "environment.depth.belowKeel": 7.3, "navigation.speedThroughWater": 2.3150005864668155, "environment.water.temperature": 279.54999999999995, "environment.depth.belowSurface": 7.3, "environment.wind.directionTrue": 0.7696902003052424, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 12.681666666666667, "environment.depth.belowTransducer": 7.3, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.7696902003052424, "navigation.gnss.horizontalDilution": 5, "navigation.courseOverGroundMagnetic": 0.09948376738639188, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965915}
},
{
"time" : "2022-12-13 21:13:05.493",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.42296415,
"longitude" : 2.28942895,
"speedoverground" : 4.4,
"courseovergroundtrue" : 4.2,
"windspeedapparent" : 6.9,
"anglespeedapparent" : 12.4,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.07330382860049928, "environment.wind.speedTrue": 1.2, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.07330382860049928, "environment.depth.belowKeel": 11.8, "navigation.speedThroughWater": 2.263556128989775, "environment.water.temperature": 279.84999999999997, "environment.depth.belowSurface": 11.8, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.2, "propulsion.engine_2.revolutions": 11.716666666666667, "environment.depth.belowTransducer": 11.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 4.1, "navigation.courseOverGroundMagnetic": 0.07330382860049928, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670965975}
},
{
"time" : "2022-12-13 21:14:05.507",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.42416923333333,
"longitude" : 2.2895500166666665,
"speedoverground" : 4.4,
"courseovergroundtrue" : 4.7,
"windspeedapparent" : 11.3,
"anglespeedapparent" : 13.3,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.08203047486246347, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.08203047486246347, "environment.depth.belowKeel": 13, "navigation.speedThroughWater": 2.263556128989775, "environment.water.temperature": 280.04999999999995, "environment.depth.belowSurface": 13, "environment.wind.directionTrue": 0.5201081172130663, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 11.261666666666667, "environment.depth.belowTransducer": 13, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5201081172130663, "navigation.gnss.horizontalDilution": 4.7, "navigation.courseOverGroundMagnetic": 0.08203047486246347, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966035}
},
{
"time" : "2022-12-13 21:15:05.532",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.42538771666667,
"longitude" : 2.2896824166666665,
"speedoverground" : 4.5,
"courseovergroundtrue" : 4.7,
"windspeedapparent" : 9.1,
"anglespeedapparent" : 18.6,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.08203047486246347, "environment.wind.speedTrue": 2.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.08203047486246347, "environment.depth.belowKeel": 14.4, "navigation.speedThroughWater": 2.3150005864668155, "environment.water.temperature": 279.84999999999997, "environment.depth.belowSurface": 14.4, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 12.19, "environment.depth.belowTransducer": 14.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 3.6, "navigation.courseOverGroundMagnetic": 0.08203047486246347, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966095}
},
{
"time" : "2022-12-13 21:16:05.578",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.42659741666667,
"longitude" : 2.2898227,
"speedoverground" : 4.4,
"courseovergroundtrue" : 4.7,
"windspeedapparent" : 11.2,
"anglespeedapparent" : 19.6,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.08203047486246347, "environment.wind.speedTrue": 3.7, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.08203047486246347, "environment.depth.belowKeel": 13, "navigation.speedThroughWater": 2.263556128989775, "environment.water.temperature": 279.45, "environment.depth.belowSurface": 13, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 12.681666666666667, "environment.depth.belowTransducer": 13, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 5, "navigation.courseOverGroundMagnetic": 0.08203047486246347, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966155}
},
{
"time" : "2022-12-13 21:17:05.597",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.42780106666667,
"longitude" : 2.2899329666666666,
"speedoverground" : 4.3,
"courseovergroundtrue" : 4.2,
"windspeedapparent" : 12.5,
"anglespeedapparent" : 15.5,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.07330382860049928, "environment.wind.speedTrue": 1.2, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.07330382860049928, "environment.depth.belowKeel": 14.4, "navigation.speedThroughWater": 2.2121116715127345, "environment.water.temperature": 279.95, "environment.depth.belowSurface": 14.4, "environment.wind.directionTrue": 0.8482300166629202, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 12.19, "environment.depth.belowTransducer": 14.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.8482300166629202, "navigation.gnss.horizontalDilution": 2.7, "navigation.courseOverGroundMagnetic": 0.07330382860049928, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966215}
},
{
"time" : "2022-12-13 21:18:05.608",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.42899953333333,
"longitude" : 2.2900571666666667,
"speedoverground" : 4.3,
"courseovergroundtrue" : 4.2,
"windspeedapparent" : 9.2,
"anglespeedapparent" : 32.1,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.07330382860049928, "environment.wind.speedTrue": 3.1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.07330382860049928, "environment.depth.belowKeel": 15.8, "navigation.speedThroughWater": 2.2121116715127345, "environment.water.temperature": 279.84999999999997, "environment.depth.belowSurface": 15.8, "environment.wind.directionTrue": 1.0297442589117756, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 11.716666666666667, "environment.depth.belowTransducer": 15.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.0297442589117756, "navigation.gnss.horizontalDilution": 3.1, "navigation.courseOverGroundMagnetic": 0.07330382860049928, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966275}
},
{
"time" : "2022-12-13 21:19:05.630",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.43017308333334,
"longitude" : 2.290175666666667,
"speedoverground" : 4.2,
"courseovergroundtrue" : 4.7,
"windspeedapparent" : 12.2,
"anglespeedapparent" : 26.6,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.08203047486246347, "environment.wind.speedTrue": 4.4, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.08203047486246347, "environment.depth.belowKeel": 11.8, "navigation.speedThroughWater": 2.1606672140356946, "environment.water.temperature": 279.95, "environment.depth.belowSurface": 11.8, "environment.wind.directionTrue": 0.7696902003052424, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 13.728333333333333, "environment.depth.belowTransducer": 11.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.7696902003052424, "navigation.gnss.horizontalDilution": 3.6, "navigation.courseOverGroundMagnetic": 0.08203047486246347, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966335}
},
{
"time" : "2022-12-13 21:20:05.651",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.431364,
"longitude" : 2.2902968333333336,
"speedoverground" : 4.2,
"courseovergroundtrue" : 4.7,
"windspeedapparent" : 9.9,
"anglespeedapparent" : 14.2,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.08203047486246347, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.08203047486246347, "environment.depth.belowKeel": 9.7, "navigation.speedThroughWater": 2.1606672140356946, "environment.water.temperature": 279.95, "environment.depth.belowSurface": 9.7, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 13.195, "environment.depth.belowTransducer": 9.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 3.6, "navigation.courseOverGroundMagnetic": 0.08203047486246347, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966395}
},
{
"time" : "2022-12-13 21:21:05.683",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.43257,
"longitude" : 2.2904335666666666,
"speedoverground" : 4.4,
"courseovergroundtrue" : 4.7,
"windspeedapparent" : 8.1,
"anglespeedapparent" : 19.1,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.08203047486246347, "environment.wind.speedTrue": 2.1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.08203047486246347, "environment.depth.belowKeel": 11.8, "navigation.speedThroughWater": 2.263556128989775, "environment.water.temperature": 279.65, "environment.depth.belowSurface": 11.8, "environment.wind.directionTrue": 0.7696902003052424, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.486666666666668, "propulsion.engine_2.revolutions": 12.681666666666667, "environment.depth.belowTransducer": 11.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.7696902003052424, "navigation.gnss.horizontalDilution": 2.4, "navigation.courseOverGroundMagnetic": 0.08203047486246347, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966455}
},
{
"time" : "2022-12-13 21:22:05.702",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.4338153,
"longitude" : 2.2905634333333333,
"speedoverground" : 4.4,
"courseovergroundtrue" : 4.2,
"windspeedapparent" : 7.6,
"anglespeedapparent" : 10.9,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.07330382860049928, "environment.wind.speedTrue": 1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.07330382860049928, "environment.depth.belowKeel": 13, "navigation.speedThroughWater": 2.263556128989775, "environment.water.temperature": 279.45, "environment.depth.belowSurface": 13, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 13.195, "environment.depth.belowTransducer": 13, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 1.8, "navigation.courseOverGroundMagnetic": 0.07330382860049928, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966515}
},
{
"time" : "2022-12-13 21:23:05.714",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.43504795,
"longitude" : 2.2906984,
"speedoverground" : 4.5,
"courseovergroundtrue" : 4.7,
"windspeedapparent" : 8.0,
"anglespeedapparent" : 21.0,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.08203047486246347, "environment.wind.speedTrue": 2.1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.08203047486246347, "environment.depth.belowKeel": 13, "navigation.speedThroughWater": 2.3150005864668155, "environment.water.temperature": 279.65, "environment.depth.belowSurface": 13, "environment.wind.directionTrue": 0.8482300166629202, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.2, "propulsion.engine_2.revolutions": 13.728333333333333, "environment.depth.belowTransducer": 13, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.8482300166629202, "navigation.gnss.horizontalDilution": 1.6, "navigation.courseOverGroundMagnetic": 0.08203047486246347, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966575}
},
{
"time" : "2022-12-13 21:24:05.728",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.436295183333336,
"longitude" : 2.2908305666666666,
"speedoverground" : 4.5,
"courseovergroundtrue" : 4.2,
"windspeedapparent" : 7.9,
"anglespeedapparent" : 20.9,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.07330382860049928, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.07330382860049928, "environment.depth.belowKeel": 11.8, "navigation.speedThroughWater": 2.3150005864668155, "environment.water.temperature": 279.54999999999995, "environment.depth.belowSurface": 11.8, "environment.wind.directionTrue": 1.0297442589117756, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.2, "propulsion.engine_2.revolutions": 14.281666666666666, "environment.depth.belowTransducer": 11.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 1.0297442589117756, "navigation.gnss.horizontalDilution": 1.4, "navigation.courseOverGroundMagnetic": 0.07330382860049928, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966635}
},
{
"time" : "2022-12-13 21:25:05.748",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.437534766666666,
"longitude" : 2.2909366166666665,
"speedoverground" : 4.5,
"courseovergroundtrue" : 3.1,
"windspeedapparent" : 7.9,
"anglespeedapparent" : 14.2,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.054105206824178034, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.054105206824178034, "environment.depth.belowKeel": 11.8, "navigation.speedThroughWater": 2.3150005864668155, "environment.water.temperature": 279.84999999999997, "environment.depth.belowSurface": 11.8, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10, "propulsion.engine_2.revolutions": 13.195, "environment.depth.belowTransducer": 11.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 0.9, "navigation.courseOverGroundMagnetic": 0.054105206824178034, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966695}
},
{
"time" : "2022-12-13 21:26:05.787",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.43881773333333,
"longitude" : 2.2910521833333335,
"speedoverground" : 4.6,
"courseovergroundtrue" : 4.2,
"windspeedapparent" : 8.5,
"anglespeedapparent" : 11.0,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.07330382860049928, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.07330382860049928, "environment.depth.belowKeel": 17.5, "navigation.speedThroughWater": 2.3664450439438554, "environment.water.temperature": 279.45, "environment.depth.belowSurface": 17.5, "environment.wind.directionTrue": 0.5742133240372442, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.825, "propulsion.engine_2.revolutions": 13.728333333333333, "environment.depth.belowTransducer": 17.5, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5742133240372442, "navigation.gnss.horizontalDilution": 1.4, "navigation.courseOverGroundMagnetic": 0.07330382860049928, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966755}
},
{
"time" : "2022-12-13 21:27:05.827",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.4401189,
"longitude" : 2.2911686833333333,
"speedoverground" : 4.7,
"courseovergroundtrue" : 4.2,
"windspeedapparent" : 7.0,
"anglespeedapparent" : 7.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.07330382860049928, "environment.wind.speedTrue": 1.2, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.07330382860049928, "environment.depth.belowKeel": 23.4, "navigation.speedThroughWater": 2.417889501420896, "environment.water.temperature": 279.54999999999995, "environment.depth.belowSurface": 23.4, "environment.wind.directionTrue": 0.47123889814606673, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10, "propulsion.engine_2.revolutions": 14.281666666666666, "environment.depth.belowTransducer": 23.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.47123889814606673, "navigation.gnss.horizontalDilution": 1.2, "navigation.courseOverGroundMagnetic": 0.07330382860049928, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966815}
},
{
"time" : "2022-12-13 21:28:05.867",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.441560683333336,
"longitude" : 2.291303533333333,
"speedoverground" : 4.9,
"courseovergroundtrue" : 4.2,
"windspeedapparent" : 7.5,
"anglespeedapparent" : 11.8,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.07330382860049928, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.07330382860049928, "environment.depth.belowKeel": 28.4, "navigation.speedThroughWater": 2.5207784163749767, "environment.water.temperature": 279.45, "environment.depth.belowSurface": 28.4, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.403333333333334, "propulsion.engine_2.revolutions": 16.085, "environment.depth.belowTransducer": 28.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 1.1, "navigation.courseOverGroundMagnetic": 0.07330382860049928, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966875}
},
{
"time" : "2022-12-13 21:29:05.889",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.44287495,
"longitude" : 2.291422783333333,
"speedoverground" : 4.7,
"courseovergroundtrue" : 3.8,
"windspeedapparent" : 10.4,
"anglespeedapparent" : 12.2,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.0663225115909279, "environment.wind.speedTrue": 1.5, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.0663225115909279, "environment.depth.belowKeel": 28.4, "navigation.speedThroughWater": 2.417889501420896, "environment.water.temperature": 279.54999999999995, "environment.depth.belowSurface": 28.4, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.825, "propulsion.engine_2.revolutions": 18.113333333333333, "environment.depth.belowTransducer": 28.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 1.2, "navigation.courseOverGroundMagnetic": 0.0663225115909279, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966935}
},
{
"time" : "2022-12-13 21:30:05.922",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.44416415,
"longitude" : 2.2915161166666667,
"speedoverground" : 4.7,
"courseovergroundtrue" : 2.9,
"windspeedapparent" : 10.1,
"anglespeedapparent" : 20.9,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.050614548319392355, "environment.wind.speedTrue": 3.1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.050614548319392355, "environment.depth.belowKeel": 23.4, "navigation.speedThroughWater": 2.417889501420896, "environment.water.temperature": 279.45, "environment.depth.belowSurface": 23.4, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.825, "propulsion.engine_2.revolutions": 15.46, "environment.depth.belowTransducer": 23.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 1.4, "navigation.courseOverGroundMagnetic": 0.050614548319392355, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670966995}
},
{
"time" : "2022-12-13 21:31:05.937",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.44544285,
"longitude" : 2.2916172833333333,
"speedoverground" : 4.6,
"courseovergroundtrue" : 3.5,
"windspeedapparent" : 25.5,
"anglespeedapparent" : 23.5,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.06108652383374939, "environment.wind.speedTrue": 9.2, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.06108652383374939, "environment.depth.belowKeel": 19.3, "navigation.speedThroughWater": 2.3664450439438554, "environment.water.temperature": 279.25, "environment.depth.belowSurface": 19.3, "environment.wind.directionTrue": 0.5742133240372442, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.825, "propulsion.engine_2.revolutions": 16.085, "environment.depth.belowTransducer": 19.3, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5742133240372442, "navigation.gnss.horizontalDilution": 1.6, "navigation.courseOverGroundMagnetic": 0.06108652383374939, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967055}
},
{
"time" : "2022-12-13 21:32:05.953",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.44671025,
"longitude" : 2.2917142166666666,
"speedoverground" : 4.6,
"courseovergroundtrue" : 3.1,
"windspeedapparent" : 22.0,
"anglespeedapparent" : 17.4,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.054105206824178034, "environment.wind.speedTrue": 4.4, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.054105206824178034, "environment.depth.belowKeel": 21.2, "navigation.speedThroughWater": 2.3664450439438554, "environment.water.temperature": 279.54999999999995, "environment.depth.belowSurface": 21.2, "environment.wind.directionTrue": 0.5201081172130663, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.825, "propulsion.engine_2.revolutions": 16.735, "environment.depth.belowTransducer": 21.2, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5201081172130663, "navigation.gnss.horizontalDilution": 1.4, "navigation.courseOverGroundMagnetic": 0.054105206824178034, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967114}
},
{
"time" : "2022-12-13 21:33:05.972",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.447963533333336,
"longitude" : 2.2918140833333336,
"speedoverground" : 4.4,
"courseovergroundtrue" : 3.5,
"windspeedapparent" : 22.0,
"anglespeedapparent" : 21.8,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.06108652383374939, "environment.wind.speedTrue": 6.4, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.06108652383374939, "environment.depth.belowKeel": 23.4, "navigation.speedThroughWater": 2.263556128989775, "environment.water.temperature": 279.25, "environment.depth.belowSurface": 23.4, "environment.wind.directionTrue": 0.5742133240372442, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.825, "propulsion.engine_2.revolutions": 16.085, "environment.depth.belowTransducer": 23.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5742133240372442, "navigation.gnss.horizontalDilution": 1.6, "navigation.courseOverGroundMagnetic": 0.06108652383374939, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967175}
},
{
"time" : "2022-12-13 21:34:05.994",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.44916808333333,
"longitude" : 2.2919039666666667,
"speedoverground" : 4.3,
"courseovergroundtrue" : 3.1,
"windspeedapparent" : 21.7,
"anglespeedapparent" : 24.7,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.054105206824178034, "environment.wind.speedTrue": 6.4, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.054105206824178034, "environment.depth.belowKeel": 23.4, "navigation.speedThroughWater": 2.2121116715127345, "environment.water.temperature": 279.04999999999995, "environment.depth.belowSurface": 23.4, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.261666666666667, "propulsion.engine_2.revolutions": 18.845000000000002, "environment.depth.belowTransducer": 23.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 1.6, "navigation.courseOverGroundMagnetic": 0.054105206824178034, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967235}
},
{
"time" : "2022-12-13 21:35:06.024",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.45040101666667,
"longitude" : 2.2919925833333332,
"speedoverground" : 4.5,
"courseovergroundtrue" : 3.5,
"windspeedapparent" : 14.3,
"anglespeedapparent" : 24.1,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.06108652383374939, "environment.wind.speedTrue": 4.4, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.06108652383374939, "environment.depth.belowKeel": 21.2, "navigation.speedThroughWater": 2.3150005864668155, "environment.water.temperature": 278.95, "environment.depth.belowSurface": 21.2, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10, "propulsion.engine_2.revolutions": 20.398333333333333, "environment.depth.belowTransducer": 21.2, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 1.8, "navigation.courseOverGroundMagnetic": 0.06108652383374939, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967295}
},
{
"time" : "2022-12-13 21:36:06.057",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.451662666666664,
"longitude" : 2.2920915833333333,
"speedoverground" : 4.6,
"courseovergroundtrue" : 3.1,
"windspeedapparent" : 29.1,
"anglespeedapparent" : 39.0,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.054105206824178034, "environment.wind.speedTrue": 13.2, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.054105206824178034, "environment.depth.belowKeel": 19.3, "navigation.speedThroughWater": 2.3664450439438554, "environment.water.temperature": 279.04999999999995, "environment.depth.belowSurface": 19.3, "environment.wind.directionTrue": 0.8482300166629202, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 19.60666666666667, "environment.depth.belowTransducer": 19.3, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.8482300166629202, "navigation.gnss.horizontalDilution": 1.8, "navigation.courseOverGroundMagnetic": 0.054105206824178034, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967355}
},
{
"time" : "2022-12-13 21:37:06.080",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.45293165,
"longitude" : 2.2921954833333333,
"speedoverground" : 4.7,
"courseovergroundtrue" : 3.8,
"windspeedapparent" : 44.2,
"anglespeedapparent" : 26.1,
"status" : "sailing",
"metrics" : {"navigation.headingTrue": 0.0663225115909279, "environment.wind.speedTrue": 20.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.0663225115909279, "environment.depth.belowKeel": 17.5, "navigation.speedThroughWater": 2.417889501420896, "environment.water.temperature": 279.45, "environment.depth.belowSurface": 17.5, "environment.wind.directionTrue": 0.5742133240372442, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 22.08, "environment.depth.belowTransducer": 17.5, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5742133240372442, "navigation.gnss.horizontalDilution": 2.1, "navigation.courseOverGroundMagnetic": 0.0663225115909279, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967415}
},
{
"time" : "2022-12-13 21:38:06.100",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.45424523333333,
"longitude" : 2.2922960833333335,
"speedoverground" : 4.8,
"courseovergroundtrue" : 3.1,
"windspeedapparent" : 37.6,
"anglespeedapparent" : 14.2,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.054105206824178034, "environment.wind.speedTrue": 4.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.054105206824178034, "environment.depth.belowKeel": 15.8, "navigation.speedThroughWater": 2.4693339588979364, "environment.water.temperature": 279.45, "environment.depth.belowSurface": 15.8, "environment.wind.directionTrue": 0.42760566683624573, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 23.9, "environment.depth.belowTransducer": 15.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.42760566683624573, "navigation.gnss.horizontalDilution": 1.6, "navigation.courseOverGroundMagnetic": 0.054105206824178034, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967475}
},
{
"time" : "2022-12-13 21:39:06.118",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.4555779,
"longitude" : 2.292398116666667,
"speedoverground" : 4.8,
"courseovergroundtrue" : 3.5,
"windspeedapparent" : 15.7,
"anglespeedapparent" : 10.8,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.06108652383374939, "environment.wind.speedTrue": 3.3, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.06108652383374939, "environment.depth.belowKeel": 15.8, "navigation.speedThroughWater": 2.4693339588979364, "environment.water.temperature": 279.45, "environment.depth.belowSurface": 15.8, "environment.wind.directionTrue": 0.38746309403121043, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.2, "propulsion.engine_2.revolutions": 22.08, "environment.depth.belowTransducer": 15.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.38746309403121043, "navigation.gnss.horizontalDilution": 1.4, "navigation.courseOverGroundMagnetic": 0.06108652383374939, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967535}
},
{
"time" : "2022-12-13 21:40:06.142",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.45691458333334,
"longitude" : 2.2925072833333333,
"speedoverground" : 4.9,
"courseovergroundtrue" : 3.1,
"windspeedapparent" : 15.7,
"anglespeedapparent" : 15.7,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.054105206824178034, "environment.wind.speedTrue": 4.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.054105206824178034, "environment.depth.belowKeel": 15.8, "navigation.speedThroughWater": 2.5207784163749767, "environment.water.temperature": 279.65, "environment.depth.belowSurface": 15.8, "environment.wind.directionTrue": 0.47123889814606673, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 25.87, "environment.depth.belowTransducer": 15.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.47123889814606673, "navigation.gnss.horizontalDilution": 0.8, "navigation.courseOverGroundMagnetic": 0.054105206824178034, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967595}
},
{
"time" : "2022-12-13 21:41:06.155",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.458240233333335,
"longitude" : 2.2926219333333333,
"speedoverground" : 4.7,
"courseovergroundtrue" : 3.8,
"windspeedapparent" : 12.4,
"anglespeedapparent" : 9.2,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.0663225115909279, "environment.wind.speedTrue": 1.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.0663225115909279, "environment.depth.belowKeel": 14.4, "navigation.speedThroughWater": 2.417889501420896, "environment.water.temperature": 279.95, "environment.depth.belowSurface": 14.4, "environment.wind.directionTrue": 0.47123889814606673, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 10.611666666666668, "propulsion.engine_2.revolutions": 26.916666666666668, "environment.depth.belowTransducer": 14.4, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.47123889814606673, "navigation.gnss.horizontalDilution": 0.8, "navigation.courseOverGroundMagnetic": 0.0663225115909279, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967655}
},
{
"time" : "2022-12-13 21:42:06.187",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.45955073333333,
"longitude" : 2.2927578,
"speedoverground" : 4.8,
"courseovergroundtrue" : 4.7,
"windspeedapparent" : 9.8,
"anglespeedapparent" : 12.4,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.08203047486246347, "environment.wind.speedTrue": 1.6, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.08203047486246347, "environment.depth.belowKeel": 13, "navigation.speedThroughWater": 2.4693339588979364, "environment.water.temperature": 279.84999999999997, "environment.depth.belowSurface": 13, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 25.87, "environment.depth.belowTransducer": 13, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 0.6, "navigation.courseOverGroundMagnetic": 0.08203047486246347, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967715}
},
{
"time" : "2022-12-13 21:43:06.208",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.46092023333333,
"longitude" : 2.2929017833333334,
"speedoverground" : 5.0,
"courseovergroundtrue" : 4.2,
"windspeedapparent" : 8.2,
"anglespeedapparent" : 11.7,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.07330382860049928, "environment.wind.speedTrue": 1.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.07330382860049928, "environment.depth.belowKeel": 13, "navigation.speedThroughWater": 2.572222873852017, "environment.water.temperature": 279.84999999999997, "environment.depth.belowSurface": 13, "environment.wind.directionTrue": 0.5742133240372442, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 28.003333333333334, "environment.depth.belowTransducer": 13, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5742133240372442, "navigation.gnss.horizontalDilution": 0.5, "navigation.courseOverGroundMagnetic": 0.07330382860049928, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967775}
},
{
"time" : "2022-12-13 21:44:06.237",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.46235585,
"longitude" : 2.2930669666666668,
"speedoverground" : 5.3,
"courseovergroundtrue" : 5.1,
"windspeedapparent" : 10.6,
"anglespeedapparent" : 13.7,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.08901179187203483, "environment.wind.speedTrue": 1.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.08901179187203483, "environment.depth.belowKeel": 11.8, "navigation.speedThroughWater": 2.726556246283138, "environment.water.temperature": 279.65, "environment.depth.belowSurface": 11.8, "environment.wind.directionTrue": 0.698131700957136, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 25.87, "environment.depth.belowTransducer": 11.8, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.698131700957136, "navigation.gnss.horizontalDilution": 0.6, "navigation.courseOverGroundMagnetic": 0.08901179187203483, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967835}
},
{
"time" : "2022-12-13 21:45:06.284",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.46385398333334,
"longitude" : 2.2932485833333334,
"speedoverground" : 5.4,
"courseovergroundtrue" : 5.7,
"windspeedapparent" : 7.9,
"anglespeedapparent" : 7.3,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.09948376738639188, "environment.wind.speedTrue": 1, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.09948376738639188, "environment.depth.belowKeel": 9.7, "navigation.speedThroughWater": 2.7780007037601786, "environment.water.temperature": 280.04999999999995, "environment.depth.belowSurface": 9.7, "environment.wind.directionTrue": 0.5742133240372442, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 11.04, "propulsion.engine_2.revolutions": 23.9, "environment.depth.belowTransducer": 9.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.5742133240372442, "navigation.gnss.horizontalDilution": 0.5, "navigation.courseOverGroundMagnetic": 0.09948376738639188, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967895}
},
{
"time" : "2022-12-13 21:46:06.327",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.46533328333334,
"longitude" : 2.2934791,
"speedoverground" : 5.5,
"courseovergroundtrue" : 6.9,
"windspeedapparent" : 9.3,
"anglespeedapparent" : 11.3,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.12042771841510595, "environment.wind.speedTrue": 1.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.12042771841510595, "environment.depth.belowKeel": 9.7, "navigation.speedThroughWater": 2.829445161237219, "environment.water.temperature": 280.25, "environment.depth.belowSurface": 9.7, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 12.433333333333334, "propulsion.engine_2.revolutions": 22.971666666666668, "environment.depth.belowTransducer": 9.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 0.6, "navigation.courseOverGroundMagnetic": 0.12042771841510595, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967955}
},
{
"time" : "2022-12-13 21:47:06.327",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.46533328333334,
"longitude" : 2.2934791,
"speedoverground" : 5.5,
"courseovergroundtrue" : 6.9,
"windspeedapparent" : 9.3,
"anglespeedapparent" : 11.3,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.12042771841510595, "environment.wind.speedTrue": 1.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.12042771841510595, "environment.depth.belowKeel": 9.7, "navigation.speedThroughWater": 2.829445161237219, "environment.water.temperature": 280.25, "environment.depth.belowSurface": 9.7, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 12.433333333333334, "propulsion.engine_2.revolutions": 22.971666666666668, "environment.depth.belowTransducer": 9.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 0.6, "navigation.courseOverGroundMagnetic": 0.12042771841510595, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967955}
},
{
"time" : "2022-12-13 21:48:06.327",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.46533328333334,
"longitude" : 2.2934791,
"speedoverground" : 5.5,
"courseovergroundtrue" : 6.9,
"windspeedapparent" : 9.3,
"anglespeedapparent" : 11.3,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.12042771841510595, "environment.wind.speedTrue": 1.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.12042771841510595, "environment.depth.belowKeel": 9.7, "navigation.speedThroughWater": 2.829445161237219, "environment.water.temperature": 280.25, "environment.depth.belowSurface": 9.7, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 12.433333333333334, "propulsion.engine_2.revolutions": 22.971666666666668, "environment.depth.belowTransducer": 9.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 0.6, "navigation.courseOverGroundMagnetic": 0.12042771841510595, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967955}
},
{
"time" : "2022-12-13 21:49:06.327",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.46533328333334,
"longitude" : 2.2934791,
"speedoverground" : 5.5,
"courseovergroundtrue" : 6.9,
"windspeedapparent" : 9.3,
"anglespeedapparent" : 11.3,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.12042771841510595, "environment.wind.speedTrue": 1.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.12042771841510595, "environment.depth.belowKeel": 9.7, "navigation.speedThroughWater": 2.829445161237219, "environment.water.temperature": 280.25, "environment.depth.belowSurface": 9.7, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 12.433333333333334, "propulsion.engine_2.revolutions": 22.971666666666668, "environment.depth.belowTransducer": 9.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 0.6, "navigation.courseOverGroundMagnetic": 0.12042771841510595, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967955}
},
{
"time" : "2022-12-13 21:50:06.327",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.46533328333334,
"longitude" : 2.2934791,
"speedoverground" : 5.5,
"courseovergroundtrue" : 6.9,
"windspeedapparent" : 9.3,
"anglespeedapparent" : 11.3,
"status" : "motoring",
"metrics" : {"navigation.headingTrue": 0.12042771841510595, "environment.wind.speedTrue": 1.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.12042771841510595, "environment.depth.belowKeel": 9.7, "navigation.speedThroughWater": 2.829445161237219, "environment.water.temperature": 280.25, "environment.depth.belowSurface": 9.7, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 12.433333333333334, "propulsion.engine_2.revolutions": 22.971666666666668, "environment.depth.belowTransducer": 9.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 0.6, "navigation.courseOverGroundMagnetic": 0.12042771841510595, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967955}
},
{
"time" : "2022-12-13 21:51:06.327",
"client_id" : "vessels.urn:mrn:imo:mmsi:787654321",
"latitude" : 41.46533328333334,
"longitude" : 2.2934791,
"speedoverground" : 5.5,
"courseovergroundtrue" : 6.9,
"windspeedapparent" : 9.3,
"anglespeedapparent" : 11.3,
"status" : "moored",
"metrics" : {"navigation.headingTrue": 0.12042771841510595, "environment.wind.speedTrue": 1.8, "navigation.gnss.satellites": 4, "navigation.headingMagnetic": 0.12042771841510595, "environment.depth.belowKeel": 9.7, "navigation.speedThroughWater": 2.829445161237219, "environment.water.temperature": 280.25, "environment.depth.belowSurface": 9.7, "environment.wind.directionTrue": 0.6318091893662081, "navigation.gnss.antennaAltitude": 2, "navigation.gnss.differentialAge": 0, "propulsion.engine_1.revolutions": 12.433333333333334, "propulsion.engine_2.revolutions": 22.971666666666668, "environment.depth.belowTransducer": 9.7, "navigation.gnss.geoidalSeparation": 0, "environment.wind.directionMagnetic": 0.6318091893662081, "navigation.gnss.horizontalDilution": 0.6, "navigation.courseOverGroundMagnetic": 0.12042771841510595, "environment.depth.surfaceToTransducer": 0.3, "navigation.gnss.differentialReference": 0, "navigation.magneticVariationAgeOfService": 1670967955}
}
]}

12
tests/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"scripts": {
"tests": "mocha index.js"
},
"dependencies": {
"mocha": "^10.2.0",
"mochawesome": "^7.1.3",
"moment": "^2.29.4",
"should": "^13.2.3",
"supertest": "^6.3.3"
}
}

20
tests/sql/anonymous.sql Normal file
View File

@@ -0,0 +1,20 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
\echo 'Validate anonymous access'
SELECT api.ispublic_fn('kapla', 'public_test');
SELECT api.ispublic_fn('kapla', 'public_logs_list');
SELECT api.ispublic_fn('kapla', 'public_logs', 1);
SELECT api.ispublic_fn('kapla', 'public_logs', 3);
SELECT api.ispublic_fn('kapla', 'public_monitoring');
SELECT api.ispublic_fn('kapla', 'public_timelapse');

View File

@@ -0,0 +1,26 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
Validate anonymous access
-[ RECORD 1 ]--
ispublic_fn | f
-[ RECORD 1 ]--
ispublic_fn | f
-[ RECORD 1 ]--
ispublic_fn | t
-[ RECORD 1 ]--
ispublic_fn | f
-[ RECORD 1 ]--
ispublic_fn | t
-[ RECORD 1 ]--
ispublic_fn | f

85
tests/sql/badges.sql Normal file
View File

@@ -0,0 +1,85 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
SELECT v.vessel_id as "vessel_id" FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud' \gset
--\echo :"vessel_id"
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
\echo 'Insert new api.logbook for badges'
INSERT INTO api.logbook
(id, active, "name", "_from", "_from_lat", "_from_lng", "_to", "_to_lat", "_to_lng", track_geom, track_geog, track_geojson, "_from_time", "_to_time", distance, duration, avg_speed, max_speed, max_wind_speed, notes, vessel_id)
OVERRIDING SYSTEM VALUE VALUES
(nextval('api.logbook_id_seq'), false, 'Tropics Zone', NULL, NULL, NULL, NULL, NULL, NULL, 'SRID=4326;LINESTRING (-63.151124640791096 14.01074681627324, -77.0912026418618 12.870995731013664)'::public.geometry, NULL, NULL, NOW(), NOW(), 123, NULL, NULL, NULL, NULL, NULL, current_setting('vessel.id', false)),
(nextval('api.logbook_id_seq'), false, 'Alaska Zone', NULL, NULL, NULL, NULL, NULL, NULL, 'SRID=4326;LINESTRING (-143.5773697471158 59.4404631255976, -152.35402122385003 56.58243132943173)'::public.geometry, NULL, NULL, NOW(), NOW(), 1234, NULL, NULL, NULL, NULL, NULL, current_setting('vessel.id', false));
\echo 'Set config'
SELECT set_config('user.email', 'demo+kapla@openplotter.cloud', false);
--SELECT set_config('vessel.client_id', 'vessels.urn:mrn:imo:mmsi:123456789', false);
\echo 'Process badge'
SELECT badges_logbook_fn(5,NOW()::TEXT);
SELECT badges_logbook_fn(6,NOW()::TEXT);
SELECT badges_geom_fn(5,NOW()::TEXT);
SELECT badges_geom_fn(6,NOW()::TEXT);
\echo 'Check badges for user'
SELECT jsonb_object_keys ( a.preferences->'badges' ) FROM auth.accounts a;
\echo 'Check details from vessel_id kapla'
--SELECT get_user_settings_from_vesselid_fn('vessels.urn:mrn:imo:mmsi:123456789'::TEXT);
SELECT
json_build_object(
'boat', v.name,
'recipient', a.first,
'email', v.owner_email,
--'settings', a.preferences,
'pushover_key', a.preferences->'pushover_key'
--'badges', a.preferences->'badges'
) as user_settings
FROM auth.accounts a, auth.vessels v, api.metadata m
WHERE m.vessel_id = v.vessel_id
AND m.vessel_id = current_setting('vessel.id', false)
AND lower(a.email) = current_setting('user.email', false);
\echo 'Insert new api.moorages for badges'
INSERT INTO api.moorages
(id,"name",country,stay_code,stay_duration,reference_count,latitude,longitude,geog,home_flag,notes,vessel_id)
OVERRIDING SYSTEM VALUE VALUES
(8,'Badge Mooring Pro',NULL,3,'11 days 00:39:56.418',1,NULL,NULL,NULL,false,'Badge Mooring Pro',current_setting('vessel.id', false)),
(9,'Badge Anchormaster',NULL,2,'26 days 00:49:56.418',1,NULL,NULL,NULL,false,'Badge Anchormaster',current_setting('vessel.id', false));
\echo 'Set config'
SELECT set_config('user.email', 'demo+aava@openplotter.cloud', false);
--SELECT set_config('vessel.client_id', 'vessels.urn:mrn:imo:mmsi:787654321', false);
SELECT v.vessel_id as "vessel_id" FROM auth.vessels v WHERE v.owner_email = 'demo+aava@openplotter.cloud' \gset
--\echo :"vessel_id"
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
\echo 'Process badge'
SELECT badges_moorages_fn();
\echo 'Check details from vessel_id aava'
--SELECT get_user_settings_from_vesselid_fn('vessels.urn:mrn:imo:mmsi:787654321'::TEXT);
SELECT
json_build_object(
'boat', v.name,
'recipient', a.first,
'email', v.owner_email,
--'settings', a.preferences,
'pushover_key', a.preferences->'pushover_key'
--'badges', a.preferences->'badges'
) as user_settings
FROM auth.accounts a, auth.vessels v, api.metadata m
WHERE m.vessel_id = v.vessel_id
AND m.vessel_id = current_setting('vessel.id', false)
AND lower(a.email) = current_setting('user.email', false);

View File

@@ -0,0 +1,82 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
-[ RECORD 1 ]
vessel_id | t
Insert new api.logbook for badges
INSERT 0 2
Set config
-[ RECORD 1 ]----------------------------
set_config | demo+kapla@openplotter.cloud
Process badge
-[ RECORD 1 ]-----+-
badges_logbook_fn |
-[ RECORD 1 ]-----+-
badges_logbook_fn |
-[ RECORD 1 ]--+-
badges_geom_fn |
-[ RECORD 1 ]--+-
badges_geom_fn |
Check badges for user
-[ RECORD 1 ]-----+------------------
jsonb_object_keys | Helmsman
-[ RECORD 2 ]-----+------------------
jsonb_object_keys | Wake Maker
-[ RECORD 3 ]-----+------------------
jsonb_object_keys | Balearic Sea
-[ RECORD 4 ]-----+------------------
jsonb_object_keys | Stormtrooper
-[ RECORD 5 ]-----+------------------
jsonb_object_keys | Gulf of Finland
-[ RECORD 6 ]-----+------------------
jsonb_object_keys | Helmsman
-[ RECORD 7 ]-----+------------------
jsonb_object_keys | Wake Maker
-[ RECORD 8 ]-----+------------------
jsonb_object_keys | Club Alaska
-[ RECORD 9 ]-----+------------------
jsonb_object_keys | Stormtrooper
-[ RECORD 10 ]----+------------------
jsonb_object_keys | Captain Award
-[ RECORD 11 ]----+------------------
jsonb_object_keys | Caribbean Sea
-[ RECORD 12 ]----+------------------
jsonb_object_keys | Gulf of Alaska
-[ RECORD 13 ]----+------------------
jsonb_object_keys | Gulf of Finland
-[ RECORD 14 ]----+------------------
jsonb_object_keys | Navigator Award
-[ RECORD 15 ]----+------------------
jsonb_object_keys | Tropical Traveler
Check details from vessel_id kapla
-[ RECORD 1 ]-+-----------------------------------------------------------------------------------------------------------------
user_settings | {"boat" : "kapla", "recipient" : "First_kapla", "email" : "demo+kapla@openplotter.cloud", "pushover_key" : null}
Insert new api.moorages for badges
INSERT 0 2
Set config
-[ RECORD 1 ]---------------------------
set_config | demo+aava@openplotter.cloud
-[ RECORD 1 ]
vessel_id | t
Process badge
-[ RECORD 1 ]------+-
badges_moorages_fn |
Check details from vessel_id aava
-[ RECORD 1 ]-+--------------------------------------------------------------------------------------------------------------
user_settings | {"boat" : "aava", "recipient" : "first_aava", "email" : "demo+aava@openplotter.cloud", "pushover_key" : null}

View File

@@ -0,0 +1,76 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
-- set user_id
SELECT a.user_id as "user_id" FROM auth.accounts a WHERE a.email = 'demo+kapla@openplotter.cloud' \gset
--\echo :"user_id"
SELECT set_config('user.id', :'user_id', false) IS NOT NULL as user_id;
-- set vessel_id
SELECT v.vessel_id as "vessel_id" FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud' \gset
--\echo :"vessel_id"
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
-- Test logbook for user
\echo 'logbook'
SELECT count(*) FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false);
\echo 'logbook'
SELECT name,_from_time IS NOT NULL AS _from_time,_to_time IS NOT NULL AS _to_time, track_geojson IS NOT NULL AS track_geojson, track_geom, distance,duration,avg_speed,max_speed,max_wind_speed,notes,extra FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false);
-- Test stays for user
\echo 'stays'
SELECT count(*) FROM api.stays WHERE vessel_id = current_setting('vessel.id', false);
\echo 'stays'
SELECT active,name IS NOT NULL AS name,geog,stay_code FROM api.stays WHERE vessel_id = current_setting('vessel.id', false);
-- Test event logs view for user
\echo 'eventlogs_view'
SELECT count(*) from api.eventlogs_view;
-- Test event logs view for user
\echo 'stats_logs_fn'
SELECT api.stats_logs_fn(null, null) INTO stats_jsonb;
SELECT stats_logs_fn->'name' AS name,
stats_logs_fn->'count' AS count,
stats_logs_fn->'max_speed' As max_speed,
stats_logs_fn->'max_distance' AS max_distance,
stats_logs_fn->'max_duration' AS max_duration,
stats_logs_fn->'max_speed_id',
stats_logs_fn->'sum_distance',
stats_logs_fn->'sum_duration',
stats_logs_fn->'max_wind_speed',
stats_logs_fn->'max_distance_id',
stats_logs_fn->'max_duration_id',
stats_logs_fn->'max_wind_speed_id',
stats_logs_fn->'first_date' IS NOT NULL AS first_date,
stats_logs_fn->'last_date' IS NOT NULL AS last_date
FROM stats_jsonb;
DROP TABLE stats_jsonb;
SELECT api.stats_logs_fn('2022-01-01'::text,'2022-06-12'::text);
-- Update logbook observations
\echo 'update_logbook_observations_fn'
SELECT extra FROM api.logbook l WHERE id = 1 AND vessel_id = current_setting('vessel.id', false);
SELECT api.update_logbook_observations_fn(1, '{"observations":{"cloudCoverage":1}}'::TEXT);
SELECT extra FROM api.logbook l WHERE id = 1 AND vessel_id = current_setting('vessel.id', false);
-- Check export
--\echo 'check logbook export fn'
--SELECT api.export_logbook_geojson_fn(1);
--SELECT api.export_logbook_gpx_fn(1);
--SELECT api.export_logbook_kml_fn(1);
-- Check history
--\echo 'monitoring history fn'
--select api.monitoring_history_fn();
--select api.monitoring_history_fn('24');

View File

@@ -0,0 +1,102 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
-[ RECORD 1 ]
user_id | t
-[ RECORD 1 ]
vessel_id | t
logbook
-[ RECORD 1 ]
count | 2
logbook
-[ RECORD 1 ]--+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Pojoviken to Norra hamnen
_from_time | t
_to_time | t
track_geojson | t
track_geom | 0102000020E61000001C000000B0DEBBE0E68737404DA938FBF0094E40B0DEBBE0E68737404DA938FBF0094E4020D26F5F0786374030BB270F0B094E400C6E7ED60F843740AA60545227084E40D60FC48C03823740593CE27D42074E407B39D9F322803740984C158C4A064E4091ED7C3F357E3740898BB63D54054E40A8A1208B477C37404BA3DC9059044E404C5CB4EDA17A3740C4F856115B034E40A9A44E4013793740D8F0F44A59024E40E4839ECDAA773740211FF46C56014E405408D147067637408229F03B73004E40787AA52C43743740F90FE9B7AFFF4D40F8098D4D18723740C217265305FF4D4084E82303537037409A2D464AA0FE4D4022474DCE636F37402912396A72FE4D408351499D806E374088CFB02B40FE4D4076711B0DE06D3740B356C7040FFE4D404EAC66B0BC6E374058A835CD3BFE4D40D7A3703D0A6F3740D3E10EC15EFE4D4087602F277B6E3740A779C7293AFE4D4087602F277B6E3740A779C7293AFE4D402063EE5A426E3740B5A679C729FE4D40381DEE10EC6D37409ECA7C1A0AFE4D40E2C46A06CB6B37400A43F7BF36FD4D4075931804566E3740320BDAD125FD4D409A2D464AA06E37404A5658830AFD4D40029A081B9E6E37404A5658830AFD4D40
distance | 7.6447
duration | PT27M
avg_speed | 3.6357142857142852
max_speed | 6.1
max_wind_speed | 22.1
notes |
extra | {"metrics": {"propulsion.main.runTime": "PT10S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": -1}}
-[ RECORD 2 ]--+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Norra hamnen to Ekenäs
_from_time | t
_to_time | t
track_geojson | t
track_geom | 0102000020E610000015000000029A081B9E6E37404A5658830AFD4D40029A081B9E6E37404A5658830AFD4D404806A6C0EF6C3740DA1B7C6132FD4D40FE65F7E461693740226C787AA5FC4D407DD3E10EC1663740B29DEFA7C6FB4D40898BB63D5465374068479724BCFA4D409A5271F6E1633740B6847CD0B3F94D40431CEBE236623740E9263108ACF84D402C6519E2585F37407E678EBFC7F74D4096218E75715B374027C5B45C23F74D402AA913D044583740968DE1C46AF64D405AF5B9DA8A5537407BEF829B9FF54D407449C2ABD253374086C954C1A8F44D407D1A0AB278543740F2B0506B9AF34D409D11A5BDC15737406688635DDCF24D4061C3D32B655937402CAF6F3ADCF14D408988888888583740B3319C58CDF04D4021FAC8C0145837408C94405DB7EF4D40B8F9593F105B37403DC0804BEDEE4D40DE4C5FE2A25D3740AE47E17A14EE4D40DE4C5FE2A25D3740AE47E17A14EE4D40
distance | 8.8968
duration | PT20M
avg_speed | 5.4523809523809526
max_speed | 6.5
max_wind_speed | 37.2
notes |
extra | {"metrics": {"propulsion.main.runTime": "PT11S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": -1}}
stays
-[ RECORD 1 ]
count | 3
stays
-[ RECORD 1 ]-------------------------------------------------
active | t
name | f
geog |
stay_code | 2
-[ RECORD 2 ]-------------------------------------------------
active | f
name | t
geog | 0101000020E6100000B0DEBBE0E68737404DA938FBF0094E40
stay_code | 2
-[ RECORD 3 ]-------------------------------------------------
active | f
name | t
geog | 0101000020E6100000029A081B9E6E37404A5658830AFD4D40
stay_code | 4
eventlogs_view
-[ RECORD 1 ]
count | 12
stats_logs_fn
SELECT 1
-[ RECORD 1 ]+----------
name | "kapla"
count | 4
max_speed | 7.1
max_distance | 8.8968
max_duration | "PT1H11M"
?column? | 3
?column? | 30.1154
?column? | "PT2H43M"
?column? | 44.2
?column? | 2
?column? | 4
?column? | 4
first_date | t
last_date | t
DROP TABLE
-[ RECORD 1 ]-+-
stats_logs_fn |
update_logbook_observations_fn
-[ RECORD 1 ]---------------------------------------------------------------------------------------------------------------------
extra | {"metrics": {"propulsion.main.runTime": "PT10S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": -1}}
-[ RECORD 1 ]------------------+--
update_logbook_observations_fn | t
-[ RECORD 1 ]--------------------------------------------------------------------------------------------------------------------
extra | {"metrics": {"propulsion.main.runTime": "PT10S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": 1}}

View File

@@ -0,0 +1,25 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
-- Check the number of process pending
\echo 'Check the number of process pending'
-- Should be 22
SELECT count(*) as jobs from public.process_queue pq where pq.processed is null;
--set role scheduler
SELECT public.run_cron_jobs();
-- Check any pending job
SELECT count(*) as any_pending_jobs from public.process_queue pq where pq.processed is null;
-- Check the number of metrics entries
\echo 'Check the number of metrics entries'
SELECT count(*) as metrics_count from api.metrics;

View File

@@ -0,0 +1,21 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
Check the number of process pending
-[ RECORD 1 ]
jobs | 26
-[ RECORD 1 ]-+-
run_cron_jobs |
-[ RECORD 1 ]----+--
any_pending_jobs | 2
Check the number of metrics entries
-[ RECORD 1 ]-+----
metrics_count | 172

79
tests/sql/grafana.sql Normal file
View File

@@ -0,0 +1,79 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
--
-- grafana_auth
SET ROLE grafana_auth;
\echo 'ROLE grafana_auth current_setting'
SELECT current_user, current_setting('user.email', true), current_setting('vessel.client_id', true), current_setting('vessel.id', true);
--SELECT a.pass,v.name,m.client_id FROM auth.accounts a JOIN auth.vessels v ON a.email = 'demo+kapla@openplotter.cloud' AND a.role = 'user_role' AND cast(a.preferences->>'email_valid' as Boolean) = True AND v.owner_email = a.email JOIN api.metadata m ON m.vessel_id = v.vessel_id;
--SELECT a.pass,v.name,m.client_id FROM auth.accounts a JOIN auth.vessels v ON a.email = 'demo+kapla@openplotter.cloud' AND a.role = 'user_role' AND v.owner_email = a.email JOIN api.metadata m ON m.vessel_id = v.vessel_id;
\echo 'link vessel and user based on current_setting'
SELECT v.name,m.client_id FROM auth.accounts a JOIN auth.vessels v ON a.role = 'user_role' AND v.owner_email = a.email JOIN api.metadata m ON m.vessel_id = v.vessel_id;
\echo 'auth.accounts details'
SELECT a.user_id IS NOT NULL AS user_id, a.email, a.first, a.last, a.pass IS NOT NULL AS pass, a.role, a.preferences->'telegram'->'chat' AS telegram, a.preferences->'pushover_user_key' AS pushover_user_key FROM auth.accounts AS a;
\echo 'auth.vessels details'
--SELECT 'SELECT ' || STRING_AGG('v.' || column_name, ', ') || ' FROM auth.vessels AS v' FROM information_schema.columns WHERE table_name = 'vessels' AND table_schema = 'auth' AND column_name NOT IN ('created_at', 'updated_at');
SELECT v.vessel_id IS NOT NULL AS vessel_id, v.owner_email, v.mmsi, v.name, v.role FROM auth.vessels AS v;
\echo 'api.metadata details'
--
SELECT m.id, m.name, m.mmsi, m.client_id, m.length, m.beam, m.height, m.ship_type, m.plugin_version, m.signalk_version, m.time IS NOT NULL AS time, m.active FROM api.metadata AS m;
--
-- grafana
SET ROLE grafana;
\echo 'ROLE grafana current_setting'
\echo 'Set current_setting value'
SET "user.email" = 'demo+kapla@openplotter.cloud';
--SET vessel.client_id = 'vessels.urn:mrn:imo:mmsi:123456789';
--select v.vessel_id FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud';
SELECT v.vessel_id as "vessel_id" FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud' \gset
--\echo :"vessel_id"
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
--SELECT current_user, current_setting('user.email', true), current_setting('vessel.client_id', true), current_setting('vessel.id', true);
SELECT current_user, current_setting('user.email', true), current_setting('vessel.client_id', true);
SELECT v.name AS __text, m.client_id AS __value FROM auth.vessels v JOIN api.metadata m ON v.owner_email = 'demo+kapla@openplotter.cloud' and m.vessel_id = v.vessel_id;
\echo 'auth.vessels details'
--SELECT * FROM auth.vessels v;
SELECT v.vessel_id IS NOT NULL AS vessel_id, v.owner_email, v.mmsi, v.name, v.role FROM auth.vessels AS v;
--SELECT * FROM api.metadata m;
\echo 'api.metadata details'
SELECT m.id, m.name, m.mmsi, m.client_id, m.length, m.beam, m.height, m.ship_type, m.plugin_version, m.signalk_version, m.time IS NOT NULL AS time, m.active FROM api.metadata AS m;
\echo 'api.logs_view'
--SELECT * FROM api.logbook l;
--SELECT * FROM api.logs_view l;
SELECT l.id, l.name, l.from, l.to, l.distance, l.duration, l._from_moorage_id, l._to_moorage_id FROM api.logs_view AS l;
--SELECT * FROM api.log_view l;
\echo 'api.stays'
--SELECT * FROM api.stays s;
SELECT m.id, m.vessel_id IS NOT NULL AS vessel_id, m.moorage_id, m.active, m.name IS NOT NULL AS name, m.latitude, m.longitude, m.geog, m.arrived IS NOT NULL AS arrived, m.departed IS NOT NULL AS departed, m.duration, m.stay_code, m.notes FROM api.stays AS m;
\echo 'stays_view'
--SELECT * FROM api.stays_view s;
SELECT m.id, m.name IS NOT NULL AS name, m.moorage, m.moorage_id, m.duration, m.stayed_at, m.stayed_at_id, m.arrived IS NOT NULL AS arrived, m.departed IS NOT NULL AS departed, m.notes FROM api.stays_view AS m;
\echo 'api.moorages'
--SELECT * FROM api.moorages m;
SELECT m.id, m.vessel_id IS NOT NULL AS vessel_id, m.name, m.country, m.stay_code, m.stay_duration, m.reference_count, m.latitude, m.longitude, m.geog, m.home_flag, m.notes FROM api.moorages AS m;
\echo 'api.moorages_view'
SELECT * FROM api.moorages_view s;

View File

@@ -0,0 +1,271 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
SET
ROLE grafana_auth current_setting
-[ RECORD 1 ]---+-------------
current_user | grafana_auth
current_setting |
current_setting |
current_setting |
link vessel and user based on current_setting
-[ RECORD 1 ]----------------------------------------------------------------
name | aava
client_id | vessels.urn:mrn:imo:mmsi:787654321
-[ RECORD 2 ]----------------------------------------------------------------
name | kapla
client_id | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
auth.accounts details
-[ RECORD 1 ]-----+-----------------------------
user_id | t
email | demo+aava@openplotter.cloud
first | first_aava
last | last_aava
pass | t
role | user_role
telegram |
pushover_user_key |
-[ RECORD 2 ]-----+-----------------------------
user_id | t
email | demo+kapla@openplotter.cloud
first | First_kapla
last | Last_kapla
pass | t
role | user_role
telegram |
pushover_user_key |
auth.vessels details
-[ RECORD 1 ]-----------------------------
vessel_id | t
owner_email | demo+kapla@openplotter.cloud
mmsi |
name | kapla
role | vessel_role
-[ RECORD 2 ]-----------------------------
vessel_id | t
owner_email | demo+aava@openplotter.cloud
mmsi | 787654321
name | aava
role | vessel_role
api.metadata details
-[ RECORD 1 ]---+------------------------------------------------------------------
id | 1
name | kapla
mmsi | 123456789
client_id | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
length | 12
beam | 10
height | 24
ship_type | 36
plugin_version | 0.0.1
signalk_version | signalk_version
time | t
active | t
-[ RECORD 2 ]---+------------------------------------------------------------------
id | 2
name | aava
mmsi | 787654321
client_id | vessels.urn:mrn:imo:mmsi:787654321
length | 12
beam | 10
height | 24
ship_type | 37
plugin_version | 1.0.2
signalk_version | 1.20.0
time | t
active | t
SET
ROLE grafana current_setting
Set current_setting value
SET
-[ RECORD 1 ]
vessel_id | t
-[ RECORD 1 ]---+-----------------------------
current_user | grafana
current_setting | demo+kapla@openplotter.cloud
current_setting |
-[ RECORD 1 ]--------------------------------------------------------------
__text | kapla
__value | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
auth.vessels details
-[ RECORD 1 ]-----------------------------
vessel_id | t
owner_email | demo+kapla@openplotter.cloud
mmsi |
name | kapla
role | vessel_role
api.metadata details
-[ RECORD 1 ]---+------------------------------------------------------------------
id | 1
name | kapla
mmsi | 123456789
client_id | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
length | 12
beam | 10
height | 24
ship_type | 36
plugin_version | 0.0.1
signalk_version | signalk_version
time | t
active | t
api.logs_view
-[ RECORD 1 ]----+-----------------------
id | 2
name | Norra hamnen to Ekenäs
from | Norra hamnen
to | Ekenäs
distance | 8.8968
duration | PT20M
_from_moorage_id | 2
_to_moorage_id | 3
-[ RECORD 2 ]----+-----------------------
id | 1
name | patch log name 3
from | patch moorage name 3
to | Norra hamnen
distance | 7.6447
duration | PT27M
_from_moorage_id | 1
_to_moorage_id | 2
api.stays
-[ RECORD 1 ]--------------------------------------------------
id | 3
vessel_id | t
moorage_id |
active | t
name | f
latitude | 59.86
longitude | 23.365766666666666
geog |
arrived | t
departed | f
duration |
stay_code | 2
notes |
-[ RECORD 2 ]--------------------------------------------------
id | 1
vessel_id | t
moorage_id | 1
active | f
name | t
latitude | 60.077666666666666
longitude | 23.530866666666668
geog | 0101000020E6100000B0DEBBE0E68737404DA938FBF0094E40
arrived | t
departed | t
duration | PT1M
stay_code | 2
notes | new stay note 3
-[ RECORD 3 ]--------------------------------------------------
id | 2
vessel_id | t
moorage_id | 2
active | f
name | t
latitude | 59.97688333333333
longitude | 23.4321
geog | 0101000020E6100000029A081B9E6E37404A5658830AFD4D40
arrived | t
departed | t
duration | PT2M
stay_code | 4
notes |
stays_view
-[ RECORD 1 ]+---------------------
id | 2
name | t
moorage | Norra hamnen
moorage_id | 2
duration | PT2M
stayed_at | Dock
stayed_at_id | 4
arrived | t
departed | t
notes |
-[ RECORD 2 ]+---------------------
id | 1
name | t
moorage | patch moorage name 3
moorage_id | 1
duration | PT1M
stayed_at | Anchor
stayed_at_id | 2
arrived | t
departed | t
notes | new stay note 3
api.moorages
-[ RECORD 1 ]---+---------------------------------------------------
id | 1
vessel_id | t
name | patch moorage name 3
country | fi
stay_code | 2
stay_duration | PT1M
reference_count | 1
latitude | 60.0776666666667
longitude | 23.5308666666667
geog | 0101000020E6100000B9DEBBE0E687374052A938FBF0094E40
home_flag | t
notes | new moorage note 3
-[ RECORD 2 ]---+---------------------------------------------------
id | 2
vessel_id | t
name | Norra hamnen
country | fi
stay_code | 4
stay_duration | PT2M
reference_count | 2
latitude | 59.9768833333333
longitude | 23.4321
geog | 0101000020E6100000029A081B9E6E3740455658830AFD4D40
home_flag | f
notes |
-[ RECORD 3 ]---+---------------------------------------------------
id | 3
vessel_id | t
name | Ekenäs
country | fi
stay_code | 1
stay_duration |
reference_count | 1
latitude | 59.86
longitude | 23.3657666666667
geog | 0101000020E6100000E84C5FE2A25D3740AE47E17A14EE4D40
home_flag | f
notes |
api.moorages_view
-[ RECORD 1 ]-------+---------------------
id | 2
moorage | Norra hamnen
default_stay | Dock
default_stay_id | 4
total_stay | 0
total_duration | PT2M
arrivals_departures | 2
-[ RECORD 2 ]-------+---------------------
id | 1
moorage | patch moorage name 3
default_stay | Anchor
default_stay_id | 2
total_stay | 0
total_duration | PT1M
arrivals_departures | 1

62
tests/sql/monitoring.sql Normal file
View File

@@ -0,0 +1,62 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
\echo 'Set vessel_id and vessel.name'
-- set vessel_id
SELECT v.vessel_id as "vessel_id" FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud' \gset
--\echo :"vessel_id"
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
-- set name
SELECT v.name as "name" FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud' \gset
--\echo :"vessel_id"
SELECT set_config('vessel.name', :'name', false) IS NOT NULL as name;
\echo 'Test monitoring_view for user'
-- Test monitoring for user
--select * from api.monitoring_view;
select count(*) from api.monitoring_view;
\echo 'Test monitoring_view2 for user'
-- Test monitoring for user
--select * from api.monitoring_view2;
select count(*) from api.monitoring_view2;
\echo 'Test monitoring_view3 for user'
-- Test monitoring for user
--select * from api.monitoring_view3;
select count(*) from api.monitoring_view3;
\echo 'Test monitoring_voltage for user'
-- Test monitoring for user
--select * from api.monitoring_voltage;
select count(*) from api.monitoring_voltage;
\echo 'Test monitoring_temperatures for user'
-- Test monitoring for user
--select * from api.monitoring_temperatures;
select count(*) from api.monitoring_temperatures;
\echo 'Test monitoring_humidity for user'
-- Test monitoring for user
--select * from api.monitoring_humidity;
select count(*) from api.monitoring_humidity;
\echo 'Test metersToKnots'
select public.metersToKnots(1);
\echo 'Test radiantToDegrees'
select public.radiantToDegrees(1);
\echo 'Test valToPercent'
select public.valToPercent(1);

View File

@@ -0,0 +1,50 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
Set vessel_id and vessel.name
-[ RECORD 1 ]
vessel_id | t
-[ RECORD 1 ]
name | t
Test monitoring_view for user
-[ RECORD 1 ]
count | 1
Test monitoring_view2 for user
-[ RECORD 1 ]
count | 21
Test monitoring_view3 for user
-[ RECORD 1 ]
count | 3736
Test monitoring_voltage for user
-[ RECORD 1 ]
count | 47
Test monitoring_temperatures for user
-[ RECORD 1 ]
count | 120
Test monitoring_humidity for user
-[ RECORD 1 ]
count | 0
Test metersToKnots
-[ RECORD 1 ]-+-----
meterstoknots | 1.94
Test radiantToDegrees
-[ RECORD 1 ]----+---
radianttodegrees | 57
Test valToPercent
-[ RECORD 1 ]+----
valtopercent | 100

26
tests/sql/otp.sql Normal file
View File

@@ -0,0 +1,26 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
--
--
\echo 'Count auth.accounts'
SELECT count(*) from auth.accounts;
\echo 'Settings auth.accounts'
SELECT preferences->'email_notifications' as email_notifications from auth.accounts;
SELECT preferences->'phone_notifications' as phone_notifications from auth.accounts;
SELECT preferences->'telegram'->'chat'->'id' as telegram from auth.accounts;
--SELECT preferences->'telegram'->'date' - INTERVAL 5 minutes from auth.accounts;
SELECT count(*)
FROM auth.accounts
WHERE preferences->'telegram'->'chat'->'id' is null;

30
tests/sql/otp.sql.output Normal file
View File

@@ -0,0 +1,30 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
Count auth.accounts
-[ RECORD 1 ]
count | 2
Settings auth.accounts
-[ RECORD 1 ]-------+------
email_notifications | false
-[ RECORD 2 ]-------+------
email_notifications | false
-[ RECORD 1 ]-------+------
phone_notifications | false
-[ RECORD 2 ]-------+------
phone_notifications | false
-[ RECORD 1 ]--------
telegram | 1234567890
-[ RECORD 2 ]--------
telegram | 9876543210
-[ RECORD 1 ]
count | 0

94
tests/sql/summary.sql Normal file
View File

@@ -0,0 +1,94 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
-- List PostgreSQL version
--SELECT version();
-- check only version number to remove arch details
SHOW server_version;
-- List Postgis version
SELECT postgis_full_version();
-- List of installed extensions
-- \dx
--SELECT extname,extversion FROM pg_extension;
SELECT e.extname AS "Name", e.extversion AS "Version", n.nspname AS "Schema", c.description AS "Description"
FROM pg_catalog.pg_extension e
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace
LEFT JOIN pg_catalog.pg_description c ON c.objoid = e.oid AND c.classoid = 'pg_catalog.pg_extension'::pg_catalog.regclass
ORDER BY 1;
-- List of installed extensions available for upgrade
SELECT name, default_version, installed_version FROM pg_available_extensions where default_version <> installed_version;
-- List Language
\echo 'List Language'
SELECT * FROM pg_language;
-- List of databases
-- ICU Missing entry in some system?
--\l
SELECT datname,datconnlimit,datcollate,datctype,datallowconn FROM pg_database;
-- List of relations
\echo 'List of relations'
\dtables
-- List tables from schema api
select t.table_name as schema_api
from information_schema.tables t
where t.table_schema = 'api'
and t.table_type = 'BASE TABLE'
order by t.table_name;
-- List tables from schema public
select t.table_name as schema_public
from information_schema.tables t
where t.table_schema = 'public'
and t.table_type = 'BASE TABLE'
order by t.table_name;
-- List tables from schema auth
select t.table_name as schema_auth
from information_schema.tables t
where t.table_schema = 'auth'
and t.table_type = 'BASE TABLE'
order by t.table_name;
-- List tables from schema jwt
select t.table_name as schema_jwt
from information_schema.tables t
where t.table_schema = 'jwt'
and t.table_type = 'BASE TABLE'
order by t.table_name;
-- List Row Security Policies - todo reduce and improve output
\echo 'List Row Security Policies'
select * from pg_policies;
-- Test functions
\echo 'Test nominatim reverse_geocode_py_fn'
SELECT public.reverse_geocode_py_fn('nominatim', 1.4440116666666667, 38.82985166666667);
\echo 'Test geoip reverse_geoip_py_fn'
--SELECT reverse_geoip_py_fn('62.74.13.231');
\echo 'Test opverpass API overpass_py_fn'
SELECT public.overpass_py_fn(2.19917, 41.386873333333334); -- Port Olimpic
SELECT public.overpass_py_fn(1.92574333333, 41.258915); -- Port de la Ginesta
SELECT public.overpass_py_fn(23.4321, 59.9768833333333); -- Norra hamnen
-- List details product versions
SELECT api.versions_fn();
SELECT * FROM api.versions_view;
-- List application settings
--SELECT * IS NOT NULl FROM public.app_settings;

View File

@@ -0,0 +1,656 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
-[ RECORD 1 ]--+-------------------------------
server_version | 16.2 (Debian 16.2-1.pgdg110+2)
-[ RECORD 1 ]--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
postgis_full_version | POSTGIS="3.4.2 c19ce56" [EXTENSION] PGSQL="160" GEOS="3.9.0-CAPI-1.16.2" PROJ="7.2.1 NETWORK_ENABLED=OFF URL_ENDPOINT=https://cdn.proj.org USER_WRITABLE_DIRECTORY=/var/lib/postgresql/.local/share/proj DATABASE_PATH=/usr/share/proj/proj.db" LIBXML="2.9.10" LIBJSON="0.15" LIBPROTOBUF="1.3.3" WAGYU="0.5.0 (Internal)"
-[ RECORD 1 ]--------------------------------------------------------------------------------------
Name | citext
Version | 1.6
Schema | public
Description | data type for case-insensitive character strings
-[ RECORD 2 ]--------------------------------------------------------------------------------------
Name | jsonb_plpython3u
Version | 1.0
Schema | public
Description | transform between jsonb and plpython3u
-[ RECORD 3 ]--------------------------------------------------------------------------------------
Name | moddatetime
Version | 1.0
Schema | public
Description | functions for tracking last modification time
-[ RECORD 4 ]--------------------------------------------------------------------------------------
Name | pg_stat_statements
Version | 1.10
Schema | public
Description | track planning and execution statistics of all SQL statements executed
-[ RECORD 5 ]--------------------------------------------------------------------------------------
Name | pgcrypto
Version | 1.3
Schema | public
Description | cryptographic functions
-[ RECORD 6 ]--------------------------------------------------------------------------------------
Name | plpgsql
Version | 1.0
Schema | pg_catalog
Description | PL/pgSQL procedural language
-[ RECORD 7 ]--------------------------------------------------------------------------------------
Name | plpython3u
Version | 1.0
Schema | pg_catalog
Description | PL/Python3U untrusted procedural language
-[ RECORD 8 ]--------------------------------------------------------------------------------------
Name | postgis
Version | 3.4.2
Schema | public
Description | PostGIS geometry and geography spatial types and functions
-[ RECORD 9 ]--------------------------------------------------------------------------------------
Name | timescaledb
Version | 2.14.2
Schema | public
Description | Enables scalable inserts and complex queries for time-series data (Community Edition)
-[ RECORD 10 ]-------------------------------------------------------------------------------------
Name | uuid-ossp
Version | 1.1
Schema | public
Description | generate universally unique identifiers (UUIDs)
(0 rows)
List Language
-[ RECORD 1 ]-+-----------
oid | 12
lanname | internal
lanowner | 10
lanispl | f
lanpltrusted | f
lanplcallfoid | 0
laninline | 0
lanvalidator | 2246
lanacl |
-[ RECORD 2 ]-+-----------
oid | 13
lanname | c
lanowner | 10
lanispl | f
lanpltrusted | f
lanplcallfoid | 0
laninline | 0
lanvalidator | 2247
lanacl |
-[ RECORD 3 ]-+-----------
oid | 14
lanname | sql
lanowner | 10
lanispl | f
lanpltrusted | t
lanplcallfoid | 0
laninline | 0
lanvalidator | 2248
lanacl |
-[ RECORD 4 ]-+-----------
oid | 13545
lanname | plpgsql
lanowner | 10
lanispl | t
lanpltrusted | t
lanplcallfoid | 13542
laninline | 13543
lanvalidator | 13544
lanacl |
-[ RECORD 5 ]-+-----------
oid | 18175
lanname | plpython3u
lanowner | 10
lanispl | t
lanpltrusted | t
lanplcallfoid | 18172
laninline | 18173
lanvalidator | 18174
lanacl |
-[ RECORD 1 ]+-----------
datname | postgres
datconnlimit | -1
datcollate | en_US.utf8
datctype | en_US.utf8
datallowconn | t
-[ RECORD 2 ]+-----------
datname | template1
datconnlimit | -1
datcollate | en_US.utf8
datctype | en_US.utf8
datallowconn | t
-[ RECORD 3 ]+-----------
datname | template0
datconnlimit | -1
datcollate | en_US.utf8
datctype | en_US.utf8
datallowconn | f
-[ RECORD 4 ]+-----------
datname | signalk
datconnlimit | 100
datcollate | en_US.utf8
datctype | en_US.utf8
datallowconn | t
List of relations
List of relations
-[ RECORD 1 ]---------------------------------
Schema | public
Name | aistypes
Type | table
Owner | username
-[ RECORD 2 ]---------------------------------
Schema | public
Name | app_settings
Type | table
Owner | username
-[ RECORD 3 ]---------------------------------
Schema | public
Name | badges
Type | table
Owner | username
-[ RECORD 4 ]---------------------------------
Schema | public
Name | email_templates
Type | table
Owner | username
-[ RECORD 5 ]---------------------------------
Schema | public
Name | geocoders
Type | table
Owner | username
-[ RECORD 6 ]---------------------------------
Schema | public
Name | iso3166
Type | table
Owner | username
-[ RECORD 7 ]---------------------------------
Schema | public
Name | mid
Type | table
Owner | username
-[ RECORD 8 ]---------------------------------
Schema | public
Name | ne_10m_geography_marine_polys
Type | table
Owner | username
-[ RECORD 9 ]---------------------------------
Schema | public
Name | ne_10m_geography_marine_polys_gid_seq
Type | sequence
Owner | username
-[ RECORD 10 ]--------------------------------
Schema | public
Name | process_queue
Type | table
Owner | username
-[ RECORD 11 ]--------------------------------
Schema | public
Name | process_queue_id_seq
Type | sequence
Owner | username
-[ RECORD 12 ]--------------------------------
Schema | public
Name | spatial_ref_sys
Type | table
Owner | username
-[ RECORD 1 ]--------
schema_api | logbook
-[ RECORD 2 ]--------
schema_api | metadata
-[ RECORD 3 ]--------
schema_api | metrics
-[ RECORD 4 ]--------
schema_api | moorages
-[ RECORD 5 ]--------
schema_api | stays
-[ RECORD 6 ]--------
schema_api | stays_at
-[ RECORD 1 ]-+------------------------------
schema_public | aistypes
-[ RECORD 2 ]-+------------------------------
schema_public | app_settings
-[ RECORD 3 ]-+------------------------------
schema_public | badges
-[ RECORD 4 ]-+------------------------------
schema_public | email_templates
-[ RECORD 5 ]-+------------------------------
schema_public | geocoders
-[ RECORD 6 ]-+------------------------------
schema_public | iso3166
-[ RECORD 7 ]-+------------------------------
schema_public | mid
-[ RECORD 8 ]-+------------------------------
schema_public | ne_10m_geography_marine_polys
-[ RECORD 9 ]-+------------------------------
schema_public | process_queue
-[ RECORD 10 ]+------------------------------
schema_public | spatial_ref_sys
-[ RECORD 1 ]---------
schema_auth | accounts
-[ RECORD 2 ]---------
schema_auth | otp
-[ RECORD 3 ]---------
schema_auth | users
-[ RECORD 4 ]---------
schema_auth | vessels
(0 rows)
List Row Security Policies
-[ RECORD 1 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 2 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 3 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 4 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | api_scheduler_role
permissive | PERMISSIVE
roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 5 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | grafana_role
permissive | PERMISSIVE
roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 6 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | grafana_proxy_role
permissive | PERMISSIVE
roles | {grafana_auth}
cmd | ALL
qual | true
with_check | false
-[ RECORD 7 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 8 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 9 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | vessels
policyname | grafana_proxy_role
permissive | PERMISSIVE
roles | {grafana_auth}
cmd | ALL
qual | true
with_check | false
-[ RECORD 10 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 11 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_scheduler_role
permissive | PERMISSIVE
roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 12 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | grafana_role
permissive | PERMISSIVE
roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 13 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_anonymous_role
permissive | PERMISSIVE
roles | {api_anonymous}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 14 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 15 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 16 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | accounts
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 17 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 18 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | api_scheduler_role
permissive | PERMISSIVE
roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 19 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | grafana_role
permissive | PERMISSIVE
roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 20 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | api_anonymous_role
permissive | PERMISSIVE
roles | {api_anonymous}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 21 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 22 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 23 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 24 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | api_scheduler_role
permissive | PERMISSIVE
roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 25 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | grafana_role
permissive | PERMISSIVE
roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 26 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | api_anonymous_role
permissive | PERMISSIVE
roles | {api_anonymous}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 27 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 28 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 29 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 30 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | api_scheduler_role
permissive | PERMISSIVE
roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 31 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | grafana_role
permissive | PERMISSIVE
roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 32 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | api_anonymous_role
permissive | PERMISSIVE
roles | {api_anonymous}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 33 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | vessels
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 34 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | vessels
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | ((vessel_id = current_setting('vessel.id'::text, true)) AND ((owner_email)::text = current_setting('user.email'::text, true)))
with_check | ((vessel_id = current_setting('vessel.id'::text, true)) AND ((owner_email)::text = current_setting('user.email'::text, true)))
-[ RECORD 35 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | vessels
policyname | grafana_role
permissive | PERMISSIVE
roles | {grafana}
cmd | ALL
qual | ((owner_email)::text = current_setting('user.email'::text, true))
with_check | false
-[ RECORD 36 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | accounts
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | ((email)::text = current_setting('user.email'::text, true))
with_check | ((email)::text = current_setting('user.email'::text, true))
-[ RECORD 37 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | accounts
policyname | api_scheduler_role
permissive | PERMISSIVE
roles | {scheduler}
cmd | ALL
qual | ((email)::text = current_setting('user.email'::text, true))
with_check | ((email)::text = current_setting('user.email'::text, true))
-[ RECORD 38 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | accounts
policyname | grafana_proxy_role
permissive | PERMISSIVE
roles | {grafana_auth}
cmd | ALL
qual | true
with_check | false
-[ RECORD 39 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | public
tablename | process_queue
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 40 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | public
tablename | process_queue
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | ((ref_id = current_setting('user.id'::text, true)) OR (ref_id = current_setting('vessel.id'::text, true)))
with_check | true
-[ RECORD 41 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | public
tablename | process_queue
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | ((ref_id = current_setting('user.id'::text, true)) OR (ref_id = current_setting('vessel.id'::text, true)))
with_check | ((ref_id = current_setting('user.id'::text, true)) OR (ref_id = current_setting('vessel.id'::text, true)))
-[ RECORD 42 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | public
tablename | process_queue
policyname | api_scheduler_role
permissive | PERMISSIVE
roles | {scheduler}
cmd | ALL
qual | true
with_check | false
Test nominatim reverse_geocode_py_fn
-[ RECORD 1 ]---------+----------------------------------------
reverse_geocode_py_fn | {"name": "Spain", "country_code": "es"}
Test geoip reverse_geoip_py_fn
Test opverpass API overpass_py_fn
-[ RECORD 1 ]--+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
overpass_py_fn | {"fee": "yes", "vhf": "09", "name": "Port Olímpic", "phone": "+34 933561016", "leisure": "marina", "website": "https://portolimpic.barcelona/", "wikidata": "Q171204", "wikipedia": "ca:Port Olímpic de Barcelona", "addr:street": "Moll de Xaloc", "power_supply": "yes", "seamark:type": "harbour", "addr:postcode": "08005", "internet_access": "wlan", "wikimedia_commons": "Category:Port Olímpic (Barcelona)", "sanitary_dump_station": "yes", "seamark:harbour:category": "marina"}
-[ RECORD 1 ]--+----------------------------------------------------------------------------------------------------------------------------------------------------------------------
overpass_py_fn | {"name": "Port de la Ginesta", "type": "multipolygon", "leisure": "marina", "name:ca": "Port de la Ginesta", "wikidata": "Q16621038", "wikipedia": "ca:Port Ginesta"}
-[ RECORD 1 ]--+----------------------------------------------
overpass_py_fn | {"name": "Norra hamnen", "leisure": "marina"}
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------------------------------
versions_fn | {"api_version" : "0.7.1", "sys_version" : "PostgreSQL 16.2", "timescaledb" : "2.14.2", "postgis" : "3.4.2", "postgrest" : "PostgREST 12.0.2"}
-[ RECORD 1 ]-----------------
api_version | 0.7.1
sys_version | PostgreSQL 16.2
timescaledb | 2.14.2
postgis | 3.4.2
postgrest | PostgREST 12.0.2

48
tests/sql/telegram.sql Normal file
View File

@@ -0,0 +1,48 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
--
-- telegram
SET ROLE username;
-- Does chat id session exist?
SELECT auth.telegram_session_exists_fn(1234567890);
SELECT auth.telegram_session_exists_fn(9876543210);
SELECT auth.telegram_session_exists_fn(1472583690);
-- Assign vessel_id var
SELECT v.vessel_id as "vessel_id_kapla" FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud' \gset
SELECT v.vessel_id as "vessel_id_aava" FROM auth.vessels v WHERE v.owner_email = 'demo+aava@openplotter.cloud' \gset
SET ROLE api_anonymous;
SELECT api.telegram(1234567890::BIGINT) IS NOT NULL as telegram_session;
SELECT api.telegram(9876543210::BIGINT) IS NOT NULL as telegram_session;
SELECT api.telegram(1472583690::BIGINT) IS NULL as telegram_session;
SET ROLE user_role;
SET "user.email" = 'demo+kapla@openplotter.cloud';
--SET vessel.id = 'f94e995cf4d3';
SELECT set_config('vessel.id', :'vessel_id_kapla', false) IS NOT NULL as vessel_id;
SET vessel.name = 'kapla';
--SET vessel.client_id = 'vessels.urn:mrn:imo:mmsi:123456789';
--SELECT * FROM api.vessels_view v;
SELECT name, mmsi, created_at IS NOT NULL as created_at, last_contact IS NOT NULL as last_contact FROM api.vessels_view v;
SELECT name,geojson->'geometry' as geometry,watertemperature,insidetemperature,outsidetemperature FROM api.monitoring_view m;
SET "user.email" = 'demo+aava@openplotter.cloud';
SELECT set_config('vessel.id', :'vessel_id_aava', false) IS NOT NULL as vessel_id;
--SET vessel.id = '341dcfa30afb';
SET vessel.name = 'aava';
--SET vessel.client_id = 'vessels.urn:mrn:imo:mmsi:787654321';
--SELECT * FROM api.vessels_view v;
SELECT name, mmsi, created_at IS NOT NULL as created_at, last_contact IS NOT NULL as last_contact FROM api.vessels_view v;
SELECT name,geojson->'geometry' as geometry,watertemperature,insidetemperature,outsidetemperature FROM api.monitoring_view m;

View File

@@ -0,0 +1,64 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
SET
-[ RECORD 1 ]--------------+--
telegram_session_exists_fn | f
-[ RECORD 1 ]--------------+--
telegram_session_exists_fn | f
-[ RECORD 1 ]--------------+--
telegram_session_exists_fn | f
SET
-[ RECORD 1 ]----+--
telegram_session | f
-[ RECORD 1 ]----+--
telegram_session | f
-[ RECORD 1 ]----+--
telegram_session | t
SET
SET
-[ RECORD 1 ]
vessel_id | t
SET
-[ RECORD 1 ]+------
name | kapla
mmsi |
created_at | t
last_contact | t
-[ RECORD 1 ]------+--------------------------------------------------------
name | kapla
geometry | {"type": "Point", "coordinates": [23.365766667, 59.86]}
watertemperature |
insidetemperature |
outsidetemperature |
SET
-[ RECORD 1 ]
vessel_id | t
SET
-[ RECORD 1 ]+----------
name | aava
mmsi | 787654321
created_at | t
last_contact | t
-[ RECORD 1 ]------+------------------------------------------------------------
name | aava
geometry | {"type": "Point", "coordinates": [2.2934791, 41.465333283]}
watertemperature | 280.25
insidetemperature |
outsidetemperature |

208
tests/tests.sh Normal file
View File

@@ -0,0 +1,208 @@
# PostgSail Unit test
if [[ -z "${PGSAIL_DB_URI}" ]]; then
echo "PGSAIL_DB_URI is undefined"
exit 1
fi
if [[ -z "${PGSAIL_API_URI}" ]]; then
echo "PGSAIL_API_URI is undefined"
exit 1
fi
# psql
if [[ ! -x "/usr/bin/psql" ]]; then
apt update && apt -y install postgresql-client
fi
# go install
if [[ ! -x "/usr/bin/go" || ! -x "/root/go/bin/mermerd" ]]; then
#wget -q https://go.dev/dl/go1.21.4.linux-arm64.tar.gz && \
#rm -rf /usr/local/go && tar -C /usr/local -xzf go1.21.4.linux-arm64.tar.gz && \
apt update && apt -y install golang && \
go install github.com/KarnerTh/mermerd@latest
fi
# pnpm install
if [[ ! -x "/usr/local/bin/pnpm" ]]; then
npm install -g pnpm
fi
pnpm install || exit 1
# settings
export mymocha="./node_modules/mocha/bin/_mocha"
mkdir -p output/ && rm -rf output/*
$mymocha index.js --reporter ./node_modules/mochawesome --reporter-options reportDir=output/,reportFilename=report1.html
if [ $? -eq 0 ]; then
echo OK
else
echo mocha index.js
exit 1
fi
$mymocha index2.js --reporter ./node_modules/mochawesome --reporter-options reportDir=output/,reportFilename=report2.html
if [ $? -eq 0 ]; then
echo OK
else
echo mocha index2.js
exit 1
fi
# https://www.postgresql.org/docs/current/app-psql.html
# run cron jobs
#psql -U ${POSTGRES_USER} -h 172.30.0.1 signalk < sql/cron_run_jobs.sql > output/cron_run_jobs.sql.output
psql ${PGSAIL_DB_URI} < sql/cron_run_jobs.sql > output/cron_run_jobs.sql.output
diff sql/cron_run_jobs.sql.output output/cron_run_jobs.sql.output > /dev/null
#diff -u sql/cron_run_jobs.sql.output output/cron_run_jobs.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo OK
else
echo SQL cron_run_jobs.sql FAILED
diff -u sql/cron_run_jobs.sql.output output/cron_run_jobs.sql.output
exit 1
fi
# handle post processing
#psql -U ${POSTGRES_USER} -h 172.30.0.1 signalk < sql/cron_post_jobs.sql > output/cron_post_jobs.sql.output
psql ${PGSAIL_DB_URI} < sql/cron_post_jobs.sql > output/cron_post_jobs.sql.output
diff sql/cron_post_jobs.sql.output output/cron_post_jobs.sql.output > /dev/null
#diff -u sql/cron_post_jobs.sql.output output/cron_post_jobs.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo OK
else
echo SQL cron_post_jobs.sql FAILED
diff -u sql/cron_post_jobs.sql.output output/cron_post_jobs.sql.output
exit 1
fi
$mymocha index3.js --reporter ./node_modules/mochawesome --reporter-options reportDir=output/,reportFilename=report3.html
#echo 0
if [ $? -eq 0 ]; then
echo OK
else
echo mocha index3.js
exit 1
fi
# Grafana Auth Proxy and role unit tests
psql ${PGSAIL_DB_URI} < sql/grafana.sql > output/grafana.sql.output
diff sql/grafana.sql.output output/grafana.sql.output > /dev/null
#diff -u sql/grafana.sql.output output/grafana.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo OK
else
echo SQL grafana.sql FAILED
diff -u sql/grafana.sql.output output/grafana.sql.output
exit 1
fi
# Telegram and role unit tests
psql ${PGSAIL_DB_URI} < sql/telegram.sql > output/telegram.sql.output
diff sql/telegram.sql.output output/telegram.sql.output > /dev/null
#diff -u sql/telegram.sql.output output/telegram.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo OK
else
echo SQL telegram.sql FAILED
diff -u sql/telegram.sql.output output/telegram.sql.output
exit 1
fi
# Badges unit tests
psql ${PGSAIL_DB_URI} < sql/badges.sql > output/badges.sql.output
diff sql/badges.sql.output output/badges.sql.output > /dev/null
#diff -u sql/badges.sql.output output/badges.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo OK
else
echo SQL badges.sql FAILED
diff -u sql/badges.sql.output output/badges.sql.output
exit
fi
# Summary unit tests
psql ${PGSAIL_DB_URI} < sql/summary.sql > output/summary.sql.output
diff sql/summary.sql.output output/summary.sql.output > /dev/null
#diff -u sql/summary.sql.output output/summary.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo OK
else
echo SQL summary.sql FAILED
diff -u sql/summary.sql.output output/summary.sql.output
exit 1
fi
# Monitoring API unit tests
$mymocha index4.js --reporter ./node_modules/mochawesome --reporter-options reportDir=output/,reportFilename=report4.html
if [ $? -eq 0 ]; then
echo OK
else
echo mocha index4.js
exit 1
fi
# Monitoring SQL unit tests
psql ${PGSAIL_DB_URI} < sql/monitoring.sql > output/monitoring.sql.output
diff sql/monitoring.sql.output output/monitoring.sql.output > /dev/null
#diff -u sql/monitoring.sql.output output/monitoring.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo SQL monitoring.sql OK
else
echo SQL monitoring.sql FAILED
diff -u sql/monitoring.sql.output output/monitoring.sql.output
exit 1
fi
# Anonymous API unit tests
$mymocha index5.js --reporter ./node_modules/mochawesome --reporter-options reportDir=output/,reportFilename=report5.html
if [ $? -eq 0 ]; then
echo OK
else
echo mocha index5.js
exit 1
fi
# Anonymous SQL unit tests
psql ${PGSAIL_DB_URI} < sql/anonymous.sql > output/anonymous.sql.output
diff sql/anonymous.sql.output output/anonymous.sql.output > /dev/null
#diff -u sql/anonymous.sql.output output/anonymous.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo SQL anonymous.sql OK
else
echo SQL anonymous.sql FAILED
diff -u sql/anonymous.sql.output output/anonymous.sql.output
exit 1
fi
# Download and update openapi documentation
wget ${PGSAIL_API_URI} -O openapi.json
#echo 0
if [ $? -eq 0 ]; then
cp openapi.json ../openapi.json
echo openapi.json OK
else
echo openapi.json FAILED
exit 1
fi
# Generate and update mermaid schema documentation
/root/go/bin/mermerd --runConfig ../docs/ERD/mermerdConfig.yaml
#echo $?
echo 0 # not working in github-actions
if [ $? -eq 0 ]; then
cp postgsail.md ../docs/ERD/postgsail.md
echo postgsail.md OK
else
echo postgsail.md FAILED
exit 1
fi
#npm i -D schemalint && npx schemalint