Nordlys

Guide

Build a station from scratch

This is the exact setup behind weather.al199.no: an Ecowitt gateway polled over the LAN, weewx running in Docker, and Nordlys rendering the dashboard that nginx serves. It ends with the skin install.

What you build

piecechoice
SensorsEcowitt WS90 7-in-1 array
GatewayEcowitt GW2000A, polled over the LAN API
weewx5.4.0, pip-installed into a venv
Drivergjr80 Ecowitt Gateway driver (GW1000)
DatabaseSQLite, the weewx default
Archive interval300 s (5 minutes)
Unitsmetricwx
RuntimeDocker Compose, two containers
Web servernginx, serving a shared volume
DashboardNordlys, at the site root

Nothing here is required by Nordlys. It is a skin for weewx, so it works with any station weewx supports - this is simply a setup that is known to work end to end, written down so it can be copied. If weewx is already running, skip to the last section.

The hardware

The WS90 is a single mast-mounted unit carrying temperature, humidity, wind, light, UV and a haptic piezo rain sensor. No moving parts, so nothing to seize up over a winter, and one thing to mount rather than five. It talks over 868 MHz to the GW2000A gateway indoors, which puts the readings on your LAN.

The gateway can push to a collector, but this setup polls it instead, over the local API on port 45000. That means weewx makes the connection outbound and nothing needs to reach the machine, so there are no ports to publish and nothing to expose.

The weewx image

weewx is installed with pip into a virtual environment on a slim Python base. Pinning the version matters: this is a thing that runs unattended for years, and a surprise upgrade at rebuild time is not what you want on the morning a storm rolls through.

Dockerfile

FROM python:3.12-slim-bookworm

ARG WEEWX_VERSION="5.4.0"
ENV WEEWX_HOME="/home/weewx-data"

RUN apt-get update && \
    apt-get install -q -y --no-install-recommends wget ca-certificates tzdata && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

# weewx timestamps everything in local time; set the zone explicitly.
RUN ln -sf /usr/share/zoneinfo/Europe/Oslo /etc/localtime

WORKDIR ${WEEWX_HOME}

RUN python3 -m venv ${WEEWX_HOME}/weewx-venv && \
    . ${WEEWX_HOME}/weewx-venv/bin/activate && \
    python3 -m pip install --no-cache-dir weewx==${WEEWX_VERSION} six==1.17.0

Station identity

weectl station create writes the initial weewx.conf. It is created with the simulator driver and switched to the real one afterwards, because the gateway driver runs an interactive discovery step that has no place in a build.

Dockerfile

RUN . ${WEEWX_HOME}/weewx-venv/bin/activate && \
    weectl station create "${WEEWX_HOME}" --no-prompt \
        --driver=weewx.drivers.simulator \
        --altitude="20,meter" \
        --latitude=66.365413 \
        --longitude=13.034191 \
        --location="Aldersundet, Lurøy, Norway" \
        --register="y" \
        --station-url="https://weather.example.com/" \
        --units="metricwx"
  • Altitude is the sensor height above sea level, not the mast height. weewx uses it to reduce station pressure to sea level, so a wrong figure makes barometer readings wrong.
  • Units. metricwx gives °C, mm, m/s and hPa - the sensible metric mix. Remember that every threshold you later set in the skin is read in these units.
  • Registration is optional. --register=y lists the station on weewx.com and needs a reachable --station-url.

The archive interval is left at the weewx default of 300 seconds. Nordlys reads it and times its own auto-refresh to match, so pages update shortly after each new record.

weewx extensions

weewx is deliberately small; extensions add the rest. These are installed from source archives, with the unmaintained ones pinned to a specific commit rather than a branch.

extensionwhat it adds
weewx-gw1000The Ecowitt gateway driver. Required.
weewx-forecastForecast data. Nordlys computes Zambretti itself, so this is optional.
weewx-cmonHost metrics: CPU, memory, disk, network.
weewx-xaggsExtra aggregation types for templates.
weewx-xcumulativeCumulative aggregates.
weewx-GTSGrowing degree days and related agricultural sums.

Dockerfile

# gjr80's account is gone; the community fork is pinned at a known commit.
ARG GW1000_REF="a01a4b47ce3b39fab39d4e364f1f9ac1324cb89c"

RUN wget -nv -O weewx-gw1000.zip \
      "https://github.com/weewx-contrib/weewx-gw1000/archive/${GW1000_REF}.zip"

RUN . ${WEEWX_HOME}/weewx-venv/bin/activate && \
    weectl extension install -y --config "${WEEWX_HOME}/weewx.conf" /tmp/weewx-gw1000.zip

Point weewx at the gateway

Three changes to weewx.conf, applied with configobj so they are order-independent and do not trigger the driver’s interactive discovery.

Dockerfile

RUN . ${WEEWX_HOME}/weewx-venv/bin/activate && python3 - <<'PY'
import configobj
c = configobj.ConfigObj("/home/weewx-data/weewx.conf", encoding="utf-8")

# 1. Use the gateway driver, polling the LAN API.
c["Station"]["station_type"] = "GW1000"
gw = c.setdefault("GW1000", {})
gw["driver"] = "user.gw1000"
gw["ip_address"] = "192.168.1.50"   # your gateway
gw["poll_interval"] = "20"

# 2. Take rain from the WS90 piezo sensor rather than the absent tipping bucket.
fme = gw.setdefault("field_map_extensions", {})
fme["rain"] = "p_rain"
fme["rainRate"] = "p_rainrate"

# 3. Log to stdout so 'docker logs' shows something.
log = c.setdefault("Logging", {})
log.setdefault("handlers", {})["console"] = {
    "level": "INFO", "formatter": "standard",
    "class": "logging.StreamHandler", "stream": "ext://sys.stdout",
}
log.setdefault("root", {})["handlers"] = ["console"]

c.write()
PY

The piezo rain mapping

This is the step that costs people an afternoon. The WS90 senses rain acoustically rather than with a tipping bucket, and the gateway reports it in separate fields - p_rain and p_rainrate. Without the field map extension, weewx reads the traditional rain field, finds nothing, and records a permanent zero. Every rain tile and every rain-day count then quietly reads zero while everything else looks fine.

Logging to stdout

weewx logs to syslog by default, which in a container means the logs go nowhere you will look. docker logs weewx showing nothing is not a great place to start debugging a driver that will not connect.

Install Nordlys

The release ships pre-built assets, so there is no Node build step. Pin the version and bump it deliberately.

Dockerfile

ARG NORDLYS_VERSION="0.4.2"
RUN . ${WEEWX_HOME}/weewx-venv/bin/activate && \
    wget -nv -O /tmp/weewx-nordlys.zip \
      "https://github.com/TheEskil/weewx-nordlys/releases/download/v${NORDLYS_VERSION}/weewx-nordlys-${NORDLYS_VERSION}.zip" && \
    weectl extension install -y --config "${WEEWX_HOME}/weewx.conf" /tmp/weewx-nordlys.zip

The installer registers a NordlysReport and writes into a nordlys/ subdirectory. To make it the site root instead, point HTML_ROOT at public_html and set the SEO base URL so og:url, og:image and the sitemap resolve against the right origin.

weewx.conf

[StdReport]
    [[NordlysReport]]
        skin = Nordlys
        HTML_ROOT = public_html
        [[[Nordlys]]]
            [[[[seo]]]]
                base_url = https://weather.example.com

Then disable the skins you are replacing, so weewx is not rendering a second dashboard nobody looks at on every cycle:

weewx.conf

[StdReport]
    [[SeasonsReport]]
        enable = false

Run it

Two containers and two named volumes. weewx writes the archive database to one and the generated HTML to the other; nginx mounts the HTML volume read-only.

docker-compose.yml

services:
  weewx:
    build: .
    image: weewx:local
    # No published ports: the driver polls outbound, nothing pushes in.
    volumes:
      - weewx-db:/home/weewx-data/archive
      - weewx-html:/home/weewx-data/public_html
    restart: unless-stopped

  weewx-web:
    image: nginx:1.27.5
    ports:
      - 8181:80
    volumes:
      - weewx-html:/usr/share/nginx/html:ro
    restart: unless-stopped

volumes:
  weewx-db:
  weewx-html:

The entrypoint activates the venv and runs weewx in the foreground, so the container lives and dies with the process and Docker’s restart policy does what you expect.

start.sh

#!/bin/bash
set -e
. "${WEEWX_HOME}/weewx-venv/bin/activate"
exec weewxd --config "${WEEWX_HOME}/weewx.conf"

Serve the HTML

Nordlys generates a complete static site - real HTML files, a stylesheet, a script bundle, a service worker and a manifest. It needs nothing but a static file server, which is why the stock nginx image with no configuration is enough.

Putting a reverse proxy in front for a hostname and a certificate is the usual next step, and entirely up to you. Whatever you use, set the resulting public URL as base_url under [[seo]] so social cards and the sitemap point at the right place.

Verify

weewx writes a record every archive interval, but you do not have to wait for one to see the dashboard:

docker compose logs -f weewx        # driver connected, records archived?
docker compose exec weewx \
  /home/weewx-data/weewx-venv/bin/weectl report run \
  --config /home/weewx-data/weewx.conf
  • A page with data but no rain, ever, on a WS90 means the piezo field mapping did not take.
  • Empty tiles for sensors you do not own are correct: absent observations hide themselves.
  • A barometer reading that looks off is usually the altitude.
  • “Today” boundaries in the wrong place means the container timezone is still UTC.

Already running weewx?

Then none of the above applies and the whole install is two commands. Download the release, install it as an extension, and render once:

weectl extension install weewx-nordlys-0.4.2.zip
weectl report run

The report lands in HTML_ROOT/nordlys/ and regenerates on each archive interval. Everything after that is configuration.