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

Configuration reference

Every key in wxalerts.yml, covering counties, delivery limits, the rule language, and the message templates.


One YAML file, read from /config/wxalerts.yml (override with the WXALERTS_CONFIG environment variable or -c). It is validated in full at startup rather than lazily, because the first use of a broken rule is a tornado warning at three in the morning.

There is no database and no admin UI. The configuration is a list of counties and a severity threshold that changes about twice a year; what you actually want to watch day to day, meaning what went out and what it cost in airtime, is MeshMonitor’s own message log.

The whole file

mqtt:
  url: wss://mqtt.wxalerts.org/mqtt
  username: wxalerts
  password: wxalerts          # public on purpose; the broker denies publish

meshmonitor:
  url: http://meshmonitor:8080
  token: ${MESHMONITOR_TOKEN}
  source_id: default          # optional; for a multi-source MeshMonitor

timezone: America/Chicago

counties:
  - same: "012113"
    label: Santa Rosa         # what appears in the message

offices: [KMOB]               # optional; marine zones have no county

delivery:
  max_bytes: 200
  warmup_seconds: 20
  max_per_hour: 12
  burst: 4
  queue_max_age: 15m
  send_all_clear: true
  office_alerts: unzoned
  dry_run: false

rules:
  - name: life-threatening
    phenomena: [TO, FF, SQ, HU]
    significance: [W]
    channel: WX
    priority: 100
    template: "{emoji}{event} {counties} til {ends} {instruction}"

${VAR} and ${VAR:-default} are expanded from the environment anywhere in the file, so the token never has to live on disk. A ${VAR} with no default and no environment variable set is a config error, not an empty string.

Durations (warmup_seconds, queue_max_age, min_interval) accept a bare number of seconds or a suffixed string: 90, "90s", "15m", "2h".

Top level

Key Default
timezone UTC IANA zone. Every time in a message is rendered in it, so set it to the mesh’s local zone
topic_root wxalerts/nws/v1 Only change this against a private broker
log_level INFO (or $LOG_LEVEL) DEBUG logs every alert that matched no rule, with its codes
http_port 8092 Health, stats and metrics
emoji see below Per-phenomenon glyph overrides
abbreviations see below Event-name shortenings used when the full name will not fit

mqtt

Defaults point at the public WxAlerts feed and are usually left alone. The credential is published deliberately, because the broker enforces that this account cannot publish, so the read-only feed needs no signup.

Key
url ws, wss, mqtt, mqtts or tcp. The port and TLS follow the scheme; a websocket URL keeps its path, and a wrong path hangs the upgrade in a way that looks exactly like a firewall
username / password both default to wxalerts
client_id defaults to wxmesh-<hostname>. Two clients sharing an id kick each other off in a reconnect loop that looks like a flapping network, so set it explicitly if you run two bridges on one host

meshmonitor

Key
url Required, with scheme. Inside a container this is the compose service name, not localhost
token Required. Must start with mm_, which is checked at load so a pasted-wrong value fails at startup rather than at the first warning
source_id Optional. Omitted or default picks the source marked primary, else the first one. Otherwise matched against a source id or name
timeout_seconds 15

counties and offices

counties is the main subscription. Each entry is a six-digit SAME code and a label:

counties:
  - same: "012113"
    label: Santa Rosa

Quote the code. YAML eats the leading zero on an unquoted 012113. The loader re-pads it rather than rejecting it, because the mistake is near universal, but quoting says what you meant. The label is what appears in the message; if you omit it, the raw code does, which helps nobody. Run init --lat <lat> --lon <lon> to look a code up.

offices subscribes to a WFO’s whole output. The reason to do it is marine and offshore zones: those carry no SAME code at all, so they never appear on the county tree and the forecast office is the only way to receive them. See delivery.office_alerts for what gets kept.

delivery

Key Default
max_bytes 200 Must be 40–237. Lowered automatically if the source caps lower; raising it above what the source allows does nothing
warmup_seconds 20 Quiet window after connecting, while the retained backlog is absorbed into state
max_per_hour 12 The airtime budget, refilled continuously rather than in an hourly step
burst 4 How many can go out back to back
queue_max_age 15m Anything still queued past this is discarded rather than delivered late
send_all_clear true Announce hazards ending
office_alerts unzoned unzoned keeps only alerts with no county behind them, meaning marine and offshore. all keeps everything the office issues, for every county in its area
dry_run false Render and log, transmit nothing

Rules

Rules are evaluated top to bottom and the first match wins, so they read like a firewall: specific rules above general ones. An alert matching no rule is never sent. The config is an allowlist, which means a new phenomenon code appearing upstream cannot start transmitting on its own.

Key Default
name rule<N> Appears in logs and in /stats per-rule counters
phenomena any VTEC codes: TO tornado, SV severe thunderstorm, FF flash flood, FL flood, WS winter storm, BZ blizzard, HW high wind, XH extreme heat, FW fire weather, HU hurricane, TR tropical storm, SQ snow squall
significance any W warning, A watch, Y advisory, S statement
severity any Extreme, Severe, Moderate, Minor
channel 0 A channel name resolved against MeshMonitor at startup, or a slot number 0–7
priority 50 Who wins the queue when airtime is tight
template see below The message
min_interval 0 Floor between two sends from this rule
all_clear true Announce when a hazard this rule sent ends
destination none A node id (!ad8c9eff) to send a direct message instead of broadcasting

Every axis you name must match; an axis you omit matches anything. Codes are upper-cased, so to and TO are the same.

Naming the channel

Prefer the name over the slot number. The slot depends on the order channels happened to be added to the device, and a wrong number transmits on the wrong channel instead of failing, which is the worst available outcome. A name is resolved at startup and a name MeshMonitor does not have stops the bridge with a list of the ones it does.

Names are matched ignoring case and a leading hash, because the same channel is written three ways in practice: WX in a config, #wx on a MeshCore device, and wx in someone’s head. WX and WEATHER, however, are two different channels.

min_interval versus escalation

These do different jobs and both are needed. Escalation suppression asks “is this the same thing I already said” and is always on; min_interval asks “am I talking too much about this category” and is off unless you set it. A tornado warning that upgrades from a watch is news and should not be blocked by a category interval, so warning rules normally run with no interval at all, and advisory rules run with a long one.

Templates

The default:

{emoji}{event} {counties} til {ends} {instruction}

A field the alert does not carry renders as empty and the surrounding whitespace collapses, so a template can name fields that only some alerts have.

Field
{emoji} Phenomenon glyph plus a space, or nothing
{event} Event name, upper-cased, abbreviated when the budget is tight
{event_raw} Same, without the upper-casing
{counties} Where it applies; see below
{office} Issuing WFO
{ends} Local wall-clock end, e.g. 3:45p. Falls back to the alert’s expiry
{onset} / {issued} Same format
{severity} {urgency} {certainty} CAP fields
{action} VTEC action: NEW, CON, EXT, UPG, CAN, EXP
{phenomena} {significance} {vtec} {etn} VTEC parts
{headline} The NWS headline, whitespace-collapsed
{instruction} Lead sentence of the call to action
{description} Lead sentence of the description. Dropped first under pressure

Times are deliberately terse: 3:45p, not 3:45 PM CDT. The zone is the reader’s own and at 200 bytes the colon and the suffix are a word of instruction.

How a message shrinks

An NWS warning is several kilobytes; a Meshtastic text packet holds 237 bytes. Rather than chopping the rendered string, which leaves a fragment ending mid-word, the message is re-rendered at successively cheaper tiers until it fits:

Tier Event name Instruction Description Place names
0 full lead sentence, ≤110 chars lead sentence, ≤100 chars full
1 abbreviated ≤60 chars dropped qualifiers dropped
2 abbreviated dropped dropped qualifiers dropped

Only if tier 2 still overflows, from a long literal in the template or a dozen county names, does it fall back to a hard trim on a word boundary, with an ellipsis so the truncation is visible.

The order is the reverse of how the product is written, on purpose. The instruction is the only part that changes what a reader does, so it is dropped last. What survives at the bottom is hazard, place and time, which is what a weather radio tone alone conveys.

Budgets are counted in bytes, not characters, because every emoji is four of them and most degree and quote characters are two or three.

What {counties} resolves to

In order of preference:

  1. The labels you gave your own counties. A warning routinely spans counties you do not care about and the payload lists all of them, so the set is intersected with yours first. More than two collapse to Santa Rosa, Baldwin +3.
  2. The NWS name of the alert’s UGC zones, fetched once from api.weather.gov and cached for the life of the process. This is what marine and offshore alerts get, since they can never reach step 1.
  3. The issuing office, only as a last resort. It answers “who wrote this”, which is the least useful thing that could occupy the place slot.

Zone names are written for a forecast product, so they shed their qualifiers as the budget tightens, so “Pensacola Bay Area including Santa Rosa Sound” becomes “Pensacola Bay Area” before the instruction is touched:

⛵ MARINE WARN Pensacola Bay Area, Pensacola FL to Pascagoula MS til 9:15p Move to safe harbor immediately.

All-clears

The all-clear template is fixed:

{emoji}ALL CLEAR: {event} {counties} ended

It goes out at ten below the rule’s own priority, worth sending but never worth displacing a live hazard from a constrained queue, and only for hazards this bridge actually announced. Telling a mesh that a warning it never heard about is over is pure noise, and on a busy evening it would roughly double the traffic.

Emoji and abbreviations

Both tables are built in and merged with anything you add:

emoji:
  TO: "🌪"
  FW: "🔥"

abbreviations:
  Severe Thunderstorm: SVR TSTM
  Excessive Heat: EXT HEAT

Abbreviations are applied longest-match-first, so Severe Thunderstorm wins over Thunderstorm. They are only used from tier 1 down, because a message that fits keeps the full event name.