https://github.com/tlugger/wtwlt
What's the weather, what's the weather, what's the weather like today? Is it sunny? Is it rainy? Is it windy out today?
https://github.com/tlugger/wtwlt
esp32 iot mqtt weather-station
Last synced: 6 days ago
JSON representation
What's the weather, what's the weather, what's the weather like today? Is it sunny? Is it rainy? Is it windy out today?
- Host: GitHub
- URL: https://github.com/tlugger/wtwlt
- Owner: tlugger
- License: mit
- Created: 2026-06-17T01:28:31.000Z (13 days ago)
- Default Branch: main
- Last Pushed: 2026-06-17T03:08:11.000Z (12 days ago)
- Last Synced: 2026-06-17T04:11:37.903Z (12 days ago)
- Topics: esp32, iot, mqtt, weather-station
- Language: C++
- Homepage:
- Size: 62.5 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# wtwlt — What's The Weather Like Today
A self-hosted home weather station, built as a **monorepo**. An outdoor,
solar-powered ESP32 sensor node publishes readings over MQTT to a Raspberry Pi,
which logs them to SQLite and serves an earth-toned dashboard + JSON API.
- **`firmware/`** — ESP32 firmware for the sensor node (PlatformIO). Samples the
sensor suite at 1 Hz, aggregates over a 60-second window, and publishes
metric/SI readings to an MQTT broker. Lightning strikes are published as they
happen. See [`firmware/README.md`](firmware/README.md).
- **`server/`** — the Raspberry Pi backend: a single **Go service** that ingests
the station's MQTT messages into SQLite and serves the dashboard + JSON API from
the same binary. Ships with a local Mosquitto config and a Python mock publisher
for exercising the pipeline without hardware. See [`server/README.md`](server/README.md).
## Architecture
```
┌─────────────────────────┐ MQTT/WiFi ┌──────────────────────────────┐
│ Weather Station Node │ ───────────────────────▶ │ Raspberry Pi │
│ ESP32 + SparkFun │ (Mosquitto broker on │ │
│ MicroMod Weather │ the Pi) │ • Mosquitto broker │
│ Carrier Board │ │ • Go service: MQTT ingest → │
│ │ │ SQLite → dashboard / API │
│ Solar + battery, always │ │ (public exposure via │
│ awake, publishes 1×/min │ │ port-forward + DDNS + │
│ │ │ reverse proxy) │
└─────────────────────────┘ └──────────────────────────────┘
```
**Data flow:** sensors → ESP32 samples @1 Hz → aggregates over 60 s → publishes
one JSON message per minute (plus event-driven lightning) to MQTT → the Go
service on the Pi subscribes, persists to SQLite (downsampling old data into
hourly/daily rollups), and serves the dashboard + API that read it.
## Hardware
An **ESP32** (MicroMod form factor) on a **SparkFun MicroMod Weather Carrier**:
- **BME280** (I²C) — temperature, humidity, barometric pressure
- **VEML6075** (I²C) — UV index
- **AS3935** (SPI) — lightning detection (strike distance + energy)
- **SparkFun Weather Meter Kit** — anemometer, wind vane, tipping-bucket rain
gauge (driven by the `SFEWeatherMeterKit` library)
- **Analog soil moisture** probe on the carrier's terminal
- **Power:** solar panel + LiPo, continuously awake (battery voltage reported in
diagnostics)
**Pin map** (ESP32 MicroMod Processor on the Weather Carrier):
| Signal | Pin | Bus / notes |
|--------|-----|-------------|
| Wind direction (vane) | GPIO 35 (A1) | ADC1 — WiFi-safe analog |
| Wind speed (anemometer) | GPIO 23 (D0) | digital, interrupt |
| Rain gauge | GPIO 27 (D1) | digital, interrupt |
| Soil moisture | GPIO 39 (A0) | ADC1 analog terminal |
| AS3935 lightning | CS = GPIO 12 | SPI; INT pin to verify on the bench |
| BME280 / VEML6075 | I²C | 0x77 / 0x10 |
Pins, cadence, and calibration constants live in
[`firmware/include/config.h`](firmware/include/config.h).
## Dashboard
An earth-toned dashboard whose palette shifts with the time of day and live
conditions (clear / dusk / night / rain / storm):

## MQTT data contract
The node publishes metric/SI values to the Mosquitto broker on the Pi (QoS 1;
retained LWT for status). `` defaults to `wtwlt-01`.
| Topic | Purpose | Cadence |
|-------|---------|---------|
| `wtwlt/station//readings` | aggregated sensor readings | every 60 s |
| `wtwlt/station//lightning` | lightning strike events | on event |
| `wtwlt/station//status` | online/offline + identity (retained, LWT) | on connect/disconnect |
**`readings`** — absent sensors are emitted as `null`:
```json
{
"station_id": "wtwlt-01",
"ts": "2026-06-16T12:00:00Z",
"interval_s": 60,
"temp_c": 21.4,
"humidity_pct": 58.2,
"pressure_hpa": 1013.2,
"uv_index": 3.1,
"wind": { "avg_mps": 2.4, "gust_mps": 5.1, "dir_deg": 270, "dir_cardinal": "W" },
"rain_mm": 0.5,
"soil_moisture_pct": 42.0,
"diagnostics": { "battery_v": 3.92, "rssi_dbm": -67, "uptime_s": 38211, "fw_version": "1.0.0" }
}
```
**`lightning`** — `event` ∈ `strike` | `disturber` | `noise` (only `strike` by default):
```json
{ "station_id": "wtwlt-01", "ts": "2026-06-16T12:00:03Z", "event": "strike", "distance_km": 12, "energy": 158473 }
```
**`status`** — retained; the LWT flips `online` to `false` on disconnect:
```json
{ "station_id": "wtwlt-01", "online": true, "fw_version": "1.0.0", "ip": "192.168.1.42", "boot_ts": "2026-06-16T01:23:45Z" }
```
Timestamps are UTC (the ESP32 syncs via SNTP; the server stamps arrival time if
the node's clock isn't set). Everything is stored metric; the API converts to
imperial on request via a `units=metric|imperial` query param.
## Build & flash the firmware
This repo uses [`just`](https://github.com/casey/just) as a task runner. Firmware
recipes are namespaced under `firmware`:
```bash
just firmware secrets # create firmware/include/secrets.h from the template, then edit it
just firmware test # host unit tests (no board needed)
just firmware build # compile for the ESP32
just firmware flash # flash over USB (append /dev/cu.usbserial-XXXX for a specific port)
just firmware dev # flash, then open the serial monitor
```
Run `just` with no arguments to list recipes and modules. Full instructions and
verification steps are in [`firmware/README.md`](firmware/README.md).
## Run the server pipeline (no hardware)
Exercise MQTT → Go ingest → SQLite → API end-to-end (requires `brew install mosquitto`):
```bash
just server broker # terminal 1: start Mosquitto
just server run # terminal 2: start the Go service (ingests -> SQLite, serves API)
just server setup # terminal 3: one-time, create the mock venv
just server mock # terminal 3: publish mock readings/lightning/status
```
Then open the dashboard at , or `curl localhost:8080/api/current`.
Details in [`server/README.md`](server/README.md).
## Deploy to the Raspberry Pi
Cut a release from the GitHub **Actions → release** workflow (it cross-compiles
the server for `linux/arm64·amd64·arm` and publishes a GitHub Release). Then, on
the Pi:
```bash
curl -fsSL https://raw.githubusercontent.com/tlugger/wtwlt/main/install.sh | sudo bash
```
The installer picks the right binary for the Pi's architecture, installs a
`systemd` service, and starts it. Re-running it upgrades in place. Config lives in
`/home/pi/wtwlt/.env`. Details in [`server/README.md`](server/README.md).