Community. Driven. Weather. Data. | Always Ad-Free | Developer FriendlyChecking statusDiscord
WxAlerts.org, Community Driven Weather Data
Support us
Sign in
MeshMonitor

MeshMonitor

Bridge NWS watches and warnings onto a Meshtastic or MeshCore mesh through a MeshMonitor instance you already run.


wxalerts-meshmonitor is a one-container bridge. It subscribes to the counties you name on the public WxAlerts MQTT feed, decides which hazards are worth airtime, compresses each one to a single line that fits a radio packet, and hands it to MeshMonitor to transmit.

wss://mqtt.wxalerts.org/mqtt ──▶ wxalerts-meshmonitor ──▶ POST /api/messages/send ──▶ LoRa
  retained, county-routed         filter, squeeze, ration    MeshMonitor

What arrives on the mesh:

🌪 TORNADO WARNING Santa Rosa, Baldwin til 3:45p TAKE COVER NOW!

What you need

  • A running MeshMonitor with at least one source attached, whether Meshtastic or MeshCore both work.
  • An API token from that MeshMonitor, belonging to a user with write permission on whichever channel you intend to transmit on.
  • A host that can reach both mqtt.wxalerts.org and MeshMonitor. A Pi is plenty, and it does not have to be the node itself.

Published images are linux/amd64 and linux/arm64. There is deliberately no 32-bit ARM build, so a Pi running the older 32-bit Raspberry Pi OS cannot pull the image, so reimage to 64-bit or run from source.

1. Get a MeshMonitor API token

In MeshMonitor: User Settings → API Token → Generate. It looks like mm_v1_… and is shown once. The token inherits its user’s permissions, so that user needs write on the channel your rules target.

2. Generate a config

Nobody knows their county’s six-digit SAME code offhand, so the init subcommand resolves it from a latitude and longitude:

docker run --rm ghcr.io/wxalerts/wxalerts-meshmonitor \
  init --lat 30.6103 --lon -87.0547 > wxalerts.yml

That asks api.weather.gov which county zone contains the point, converts the UGC zone to its SAME code, fills in the county name, timezone and local forecast office, and writes a config with the phenomenon codes explained in comments. It only covers the United States and its territories.

The -o <file> flag writes to a file instead of stdout, and refuses to overwrite one that already exists.

3. Run it

# docker-compose.yml
services:
  wxalerts:
    image: ghcr.io/wxalerts/wxalerts-meshmonitor:latest
    restart: unless-stopped
    environment:
      MESHMONITOR_TOKEN: ${MESHMONITOR_TOKEN}
    volumes:
      - ./wxalerts.yml:/config/wxalerts.yml:ro
    ports:
      # /healthz, /stats, /live and Prometheus /metrics. Only needed if you
      # scrape it or are debugging.
      - "8092:8092"

The config mounts read-only because the bridge holds no state on disk. The retained MQTT feed is the state store, and it is re-read on every connect.

If MeshMonitor runs in the same compose project, meshmonitor.url is its service name, http://meshmonitor:8080, not localhost. The repo ships a docker-compose.example.yml with both services side by side.

4. Leave dry_run on for a day

A freshly generated config has delivery.dry_run: true. In that mode every message is selected, rendered and logged exactly as it would be sent, and nothing is transmitted:

2026-08-17 14:22:03 INFO wxmesh.bridge: queued [warnings p80 ch2 87B] ⛈ SEVERE THUNDERSTORM WARNING Santa Rosa til 3:15p Move indoors.
2026-08-17 14:22:03 INFO wxmesh.bridge: [dry-run] ch2: ⛈ SEVERE THUNDERSTORM WARNING Santa Rosa til 3:15p Move indoors.

Watch docker logs -f through one round of real weather. That is how you find out whether your rules are too loud, before your neighbours do. Then set dry_run: false and restart.

Startup, in order

The first three lines of the log say which of the three common mistakes you made, because all of them otherwise look identical to a quiet weather day:

  1. The config is validated in full before anything connects. A bad SAME code, an unknown timezone or a rule with no channel exits immediately.
  2. MeshMonitor reachability, via GET /api/health, which needs no auth. A failure here is usually the URL.
  3. The token, via GET /api/v1/sources, which does. A 401 or 403 here means regenerate it.

Failing 2 or 3 is not fatal: the bridge keeps running and retries, because a token fixed in the MeshMonitor UI should take effect without a restart. A rule naming a channel that MeshMonitor genuinely does not have is fatal: it exits with the list of channels that do exist, because that will never fix itself and crash-looping is the fastest way to get a typo noticed.

Where to go next