signalk-dmi
v0.6.0
Published
Signal K plugin that ingests DMI (Danish Meteorological Institute) free open data — HARMONIE forecasts, met observations, WAM waves, DKSS ocean (sea level, current, water temperature) — into Signal K paths and the v2 Weather API. Authoritative Danish met/
Maintainers
Readme
signalk-dmi
A Signal K server plugin that ingests DMI (Danish Meteorological Institute) free open data — authoritative Danish met/ocean data — into Signal K, richer than the generic OpenWeather provider for Danish, Baltic and North Sea waters.
It uses DMI's Open Data API (host
opendataapi.dmi.dk, no API key required, licensed CC BY 4.0).
Screenshots
Plugin configuration — with in-UI InfluxDB history guidance:

Live DMI data flowing into Signal K environment.* paths:

Requirements
- Signal K server — the Node.js
signalk-server. Theenvironment.*deltas work on any recent version; the v2 Weather API provider feature needs a v2-capable server (Signal K server ≥ 2.x). - Node.js 18 or newer (whatever your Signal K server runs on).
- Platforms — anything that runs Signal K server: Linux (incl. Raspberry Pi ARM), macOS, Windows, and Docker. No native/compiled dependencies (this plugin has zero runtime dependencies), so no build tools are needed.
- Outbound internet to
https://opendataapi.dmi.dk. - No DMI API key, no account, no registration. DMI's Open Data API is free and key-free (DMI Open Data — Authentication).
- A vessel position — either live
navigation.position(GPS) or a configured fixed latitude/longitude (dock/shore install or testing). - Coverage — Danish, North Sea and Baltic waters (DMI's model domains). Outside those areas the plugin simply publishes nothing.
No database is required (see History and trends below). Data licence: DMI data is CC BY 4.0 (attribution required — see Attribution / licence). Not for navigation — informational only (see the Disclaimer).
Install steps for a fresh server are in INSTALL.md.
History and trends (optional)
The DMI paths publish live into Signal K with no extra setup — no database, no InfluxDB, nothing else to install. This plugin does not require InfluxDB.
If you want historical data or trend graphs (sparklines, forecast-vs-actual),
install the separate
signalk-to-influxdb2 plugin,
point it at an InfluxDB 2.x bucket, and allow-list the DMI paths in its
filteringRules (keep the deny-all .* last):
"filteringRules": [
{ "allow": true, "path": "environment\\.outside\\." },
{ "allow": true, "path": "environment\\.water\\." },
{ "allow": true, "path": "environment\\.tide\\." },
{ "allow": true, "path": "environment\\.current\\." },
{ "allow": false, "path": ".*" }
]These are exactly the environment.* groups signalk-dmi publishes; the plugin
shows this same list (and detects whether it's already applied) in its own config
UI — see History & trends status there. Then verify with the Signal K v2
History API
(/signalk/v2/api/history/values?paths=environment.water.temperature&from=…&to=…).
Full step-by-step in INSTALL.md → history & trends.
This is entirely optional and a feature of that plugin, not a requirement of
signalk-dmi.
What it publishes
The plugin feeds Signal K on two surfaces:
1. Signal K v2 Weather API provider
Registers DMI as a weather provider under /signalk/v2/api/weather:
- Forecasts (
/forecasts/point,/forecasts/daily) from HARMONIE (harmonie_dini_sf), merged with WAM waves and DKSS ocean into each forecast step'swater.*block. - Observations (
/observations) from the nearest metObs land/coastal station, plus nearest oceanObs sea level / water temperature. - Warnings (
/warnings) — official DMI weather/marine warnings sourced from the EUMETNET/MeteoAlarm CAP feed (the DMI Open Data API has no warnings collection). Also raised asnotifications.environment.warnings.dmi.*.
2. environment.* deltas
Current conditions + marine, so any consumer reading environment.outside.wind.*
(a drop-in for the OpenWeather provider) gets live data:
| Path | Source | Unit |
|---|---|---|
| environment.outside.temperature | metObs | K |
| environment.outside.dewPointTemperature | metObs | K |
| environment.outside.pressure | metObs | Pa |
| environment.outside.humidity | metObs | ratio |
| environment.outside.wind.speed | metObs | m/s |
| environment.outside.wind.gust | metObs | m/s |
| environment.outside.wind.direction | metObs | rad |
| environment.outside.horizontalVisibility | metObs | m |
| environment.outside.cloudCover | metObs | ratio |
| environment.outside.precipitationRate | metObs | m/s |
| environment.water.temperature | oceanObs → DKSS | K |
| environment.water.level / environment.tide.heightNow | oceanObs → DKSS | m |
| environment.water.waves.significantHeight / .period / .direction | WAM | m / s / rad |
| environment.water.swell.height / .period / .direction | WAM | m / s / rad |
| environment.current.drift / environment.current.setTrue | DKSS → Open-Meteo | m/s / rad |
| environment.water.waves.nearbyDistance / .nearbyBearing | WAM (surrounding-area marker) | m / rad |
| environment.current.nearbyDistance / environment.current.nearbyBearing | DKSS (surrounding-area marker) | m / rad |
| environment.water.waves.source / environment.current.source / environment.water.temperature.source | provenance marker | string |
All values are converted to Signal K SI units (K, Pa, m/s, rad, ratio); ocean current is derived from the model's u/v components (set = direction flowing toward).
Marine surrounding-area sourcing (grid gaps)
DMI's marine models (WAM waves/swell, DKSS ocean/current) are gridded, and the boat's exact position is frequently a gap — a coastal or land cell, an interior hole, or a spot just outside the model domain — so a point query returns nothing even though the water a mile away is fully modelled. To avoid empty wave/swell/current fields in that common case, the fetch layer samples the surrounding area:
- query the exact point first (preferred whenever it has data);
- if a WAM or DKSS collection has no usable numeric data at the point, probe a small capped set of nearby offset points (2 rings, ≤ 8 points — the cardinals at ~0.05° / ~3 nm and the diagonals at ~0.10° / ~6 nm, all within the ~15 nm reach), nearest first, spaced by a short delay, and use the first grid point with numeric data as-is (no interpolation, no fabrication) — stopping immediately on that hit;
- only report empty when the whole capped area has no data.
The probe is deliberately gentle on DMI, which HTTP-429 rate-limits the marine EDR:
- A 429 is backoff, not data. If any call returns 429, the plugin stops probing for that poll and relies on the next one — a 429 is never mistaken for an empty cell that would fuel more probing (the failure mode that made an earlier build amplify the 429).
- Cooldown (bounded). When a collection's capped probe comes back with nothing — genuinely empty or 429-aborted — the plugin remembers that for ~2 poll cycles and, meanwhile, only tries the single exact point (no ring), so it does not re-run the whole probe every 30-minute poll and re-trigger the limit. A numeric hit clears the cooldown at once; otherwise it expires and the area is probed again. The cooldown is capped at 2 h and, crucially, a bare 429 on the single exact-point call does not open or extend it — only a real ring probe does — so a sustained 429 storm can never keep re-extending the window and wedge the ring dark (see Self-recovery below).
Null-masked cells (why this keys on numeric presence, not row count). A masked DMI grid cell — land, out of the wet domain, or otherwise no-data — does not return an empty response; it returns a full-length time series whose parameter values are all
null(common at coastal/inner-bay points). "Has data" is therefore judged by the presence of a numeric value for a requested parameter, never by the row count, or a null-masked cell would be mistaken for a valid reading and the ring would never fire. A cell with a real number in any requested parameter (e.g. DKSSwater-temperaturepresent whilecurrent-u/vare null) counts as present, so working ocean temp/level/tide are never ringed away.
Current gets its own focused probe. Because a mixed DKSS cell (temp/level present, current
null) counts as "has data" above, the surrounding-area ring would not fire for the missing current vector — soenvironment.current.drift/setTruestayed absent wherever the boat's exact cell is masked for current but modelled for temp/level (the common coastal case). When the primary DKSS result yields no current, the plugin now runs a focused current probe that rings specifically for the current vector — requiring bothcurrent-uandcurrent-v— on its own cooldown key, and publishes the nearest wet current cell with its ownenvironment.current.nearbyDistance/nearbyBearingmarker. It never disturbs the temp/level path.
When a value comes from an offset point, the plugin publishes a small honest-display
marker — …nearbyDistance (m) and …nearbyBearing (rad) — so a display can label the
reading "nearby ~X nm" rather than presenting it as measured at the boat. The marker is
cleared (null) as soon as the exact point returns data again. This is a fetch-layer
resilience feature only: the requested parameters, the SI mapping, and the marine value
paths are unchanged.
Reliable marine fallback (Open-Meteo) — waves never vanish
Even with the surrounding-area probe, DMI can leave marine data empty: the boat sits in a wide coastal grid gap, the WAM/DKSS domain doesn't reach, or a 429 storm blocks the fetch. Waves and swell then disappear for hours or days. To stop that, an optional fallback fills any empty marine path from the Open-Meteo Marine API — free, no API key, generous rate limits, global; source models GWAM / EWAM / MFWAM.
- DMI stays primary. Open-Meteo is only queried when DMI's exact point and its surrounding-area ring already came up empty for a group (waves/swell, current, or SST). Where DMI has data, it is used unchanged — authoritative for Danish waters.
- Honest provenance. Each filled group publishes a
…sourcestring:environment.water.waves.source,environment.current.source,environment.water.temperature.source=DMI-WAM/DMI-DKSS/DMIwhen DMI supplied it,Open-Meteowhen the fallback filled it, orDMI-WAM + Open-Meteofor a mixed wave block. You always know which source each value came from. - Same paths, same units. Open-Meteo values land on the identical
environment.water.waves.*/.swell.*/environment.current.*/environment.water.temperaturepaths, converted to SI (km/h→m/s, °→rad, °C→K). - Robust + non-blocking. Per-request
AbortControllertimeout, fully error-caught (never crashes the plugin), and integrated into the same self-recovery poll loop. - Toggle: Reliable marine fallback (Open-Meteo) (
openMeteoFallback, default ON).
Self-recovery (the poll loop never wedges)
DMI's marine EDR is aggressively 429 rate-limited, and a sustained busy period must never leave the plugin permanently dark. The poll loop is self-healing:
- Guaranteed re-arm. Each poll self-schedules the next one in a
finally, so a thrown error, a rejected promise, a 429 storm, or a hung fetch can never stop future polls. (A slow poll can't overlap the next either — the next is armed only after the current completes.) Position not in the model yet at boot re-arms in 30 s rather than a full interval. - Bounded backoff. The ring-probe cooldown is capped at 2 h and is not re-extended by bare exact-point 429s, so the surrounding-area probe is always retried within the cap.
- No frozen-forever data. A last-good value keeps re-publishing across a brief source outage, but past ~4 poll cycles (~2 h at the default) a still-silent source is dropped and the path goes absent — an honest "no data" instead of a stale value wearing a fresh timestamp. Earlier builds could re-publish frozen marine values indefinitely, so a dead feed looked alive until a manual Signal K restart; that can no longer happen.
Install
From the Signal K Appstore (recommended)
- Open your Signal K server admin UI → Appstore → Available.
- Search for signalk-dmi (category Weather) and click Install.
- Restart the server when prompted.
The Appstore lists this plugin because it is published to npm with the
signalk-node-server-plugin keyword; installs and updates are handled for you
from the admin UI.
From npm (manual)
npm install signalk-dmirun in your Signal K config directory (~/.signalk), then restart the server.
From a local tarball (development / offline)
Install the registered-tarball way so it shows in the admin UI and survives
node_modules rebuilds — see INSTALL.md for the full standard.
In short: npm pack → copy the .tgz into the server config dir →
npm install ./signalk-dmi-<version>.tgz there → restart. Do not hand-copy
files into node_modules.
Enable
Enable DMI (Danish Meteorological Institute) free data in Server → Plugin Config, and (optionally) select it as the Weather API provider.
Configuration
Every field has in-UI help text (its description). Full reference:
| Field (title) | Key | Type | Default | What it does |
|---|---|---|---|---|
| Position source | positionSource | enum: Vessel GPS / Fixed | vessel | Where DMI is queried. Vessel GPS follows navigation.position (use underway); Fixed uses the lat/lon below (dock/shore install). |
| Fixed latitude | fixedLatitude | number (deg) | — | Decimal degrees WGS84, N+. Used only when Position source = Fixed. |
| Fixed longitude | fixedLongitude | number (deg) | — | Decimal degrees WGS84, E+. Used only when Position source = Fixed. |
| Poll interval | pollIntervalMinutes | number (min, ≥5) | 30 | Refresh cadence for observations + marine deltas. Lower = fresher, more API calls. Does not affect the on-demand forecast. |
| Forecast window | forecastHours | number (h, 1–120) | 48 | Look-ahead horizon of the v2 Weather API forecast. Length only, not refresh rate. |
| Register as Weather API provider | weatherProvider | boolean | true | ON = serve /signalk/v2/api/weather. Set OFF to run additively beside OpenWeather (deltas still publish). |
| Publish current conditions | observations | boolean | true | Nearest-station metObs → environment.outside.* (temp, pressure, humidity, dew point, wind/gust/dir). |
| Publish extended conditions | tier1 | boolean | true | Adds environment.outside.horizontalVisibility, cloudCover, precipitationRate (HARMONIE surface). |
| Fetch weather warnings | warnings | boolean | true | Official warnings from the EUMETNET/MeteoAlarm CAP feed → Weather API getWarnings. |
| Raise warnings as notifications | warningNotifications | boolean | true | Active warnings → notifications.environment.warnings.dmi.*. No effect unless Fetch warnings is ON. |
| Warning countries | warningCountries | multi-select enum (38 MeteoAlarm countries) | [denmark] | Which national MeteoAlarm feeds to pull. Add neighbours when crossing borders. Only used when Fetch warnings is ON. |
| Publish wave data | waves | boolean | true | WAM model → environment.water.waves.* + .swell.* (height, period, direction). |
| Publish ocean data | ocean | boolean | true | DKSS model → environment.water.temperature, water.level, tide.heightNow, current.drift/setTrue. |
| Reliable marine fallback (Open-Meteo) | openMeteoFallback | boolean | true | Fill waves/swell/current/SST from the free Open-Meteo Marine API when DMI has a grid gap or 429s, so marine data never vanishes. Each group tagged with a …source marker. |
| HARMONIE forecast model | harmonieCollection | enum | harmonie_dini_sf | Surface (SF) model for atmospheric data. DINI = DK/NL/IE/IS (keep for Danish waters); IG = Iceland/Greenland. |
| WAM wave model | wamCollection | enum | wam_dw | Wave domain: Danish Waters (best near DK), North Sea & Baltic (wider), North Atlantic (offshore). |
| DKSS ocean model | dkssCollection | enum | dkss_nsbs | Ocean domain: North Sea–Baltic (widest), Inner Danish Waters, Wadden Sea, Limfjord, Little Belt, Roskilde/Isefjord. Smallest domain covering your position = best resolution. |
MeteoAlarm country values (warningCountries, lowercase feed slugs):
austria, belgium, bosnia-herzegovina, bulgaria, croatia, cyprus,
czechia, denmark, estonia, finland, france, germany, greece,
hungary, iceland, ireland, israel, italy, latvia, lithuania,
luxembourg, malta, moldova, montenegro, netherlands, norway,
poland, portugal, republic-of-north-macedonia, romania, serbia,
slovakia, slovenia, spain, sweden, switzerland, ukraine,
united-kingdom.
Collection ids are DMI Open Data forecastedr collection identifiers
(source of truth: https://opendataapi.dmi.dk/v1/forecastedr/collections). Only
the values in the enums above are valid for this plugin.
Coverage
- HARMONIE DINI: Denmark + surrounding NE Europe.
- WAM
wam_dw: Danish waters (7–16°E, 53–60°N);wam_nsb/wam_natlantextend to North Sea/Baltic / North Atlantic. - DKSS
dkss_nsbs: entire North Sea + Baltic (−4.1–30.3°E, 48.5–65.9°N).
Outside these boxes DMI returns no data and the plugin simply publishes nothing.
Development
npm test # unit tests for unit conversions + mapping (node:test, no deps)Attribution / licence
Weather and ocean data © DMI (Danish Meteorological Institute), licensed under Creative Commons CC BY 4.0. When using this data you must credit DMI, link the licence, and indicate changes.
Warnings are sourced from MeteoAlarm (EUMETNET), which republishes DMI's official warnings under CC BY 4.0 with additional redistribution terms; the plugin labels the warning source "DMI / MeteoAlarm".
The optional marine fallback uses Open-Meteo (Marine
Weather API, models GWAM/EWAM/MFWAM), whose data is licensed
CC BY 4.0. Filled values are labelled
Open-Meteo in their …source marker.
Plugin code: MIT — see LICENSE.
Disclaimer — informational only, NOT for navigation
This plugin and the data it publishes are provided for general information only and are NOT a substitute for official nautical charts, notices to mariners, or an official marine forecast. Do not rely on it for navigation or for safety-of-life decisions. Weather, wave, ocean, and warning data may be delayed, incomplete, wrong, or unavailable, and the plugin may fail silently.
The software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied (see LICENSE). Neither the author nor DMI nor MeteoAlarm is liable for any loss or damage arising from its use. You are responsible for verifying all data against authoritative sources before acting on it.
