more documentation and last 24h chart
This commit is contained in:
78
README.md
78
README.md
@@ -1,22 +1,78 @@
|
|||||||
# go-weatherstation
|
# go-weatherstation
|
||||||
|
|
||||||
Starter go-weatherstationrology data pipeline:
|
Starter weather-station data pipeline:
|
||||||
- MQTT ingest of WS90 payloads -> TimescaleDB
|
- MQTT ingest of WS90 payloads -> TimescaleDB
|
||||||
- Baseline forecast polling (Open-Meteo) -> TimescaleDB
|
- ECMWF (Open-Meteo) forecast polling -> TimescaleDB
|
||||||
|
- Web UI with live metrics, comparisons, and charts
|
||||||
|
|
||||||
## Run
|
## Quick start
|
||||||
1) Start services:
|
1) Start services:
|
||||||
docker compose up -d
|
`docker compose up -d`
|
||||||
|
|
||||||
2) Copy config:
|
2) Configure:
|
||||||
cp config.example.yaml config.yaml
|
edit `config.yaml` (or `test.yaml`) with your MQTT broker, topic, and site coordinates.
|
||||||
# edit mqtt topic/broker + site lat/lon
|
|
||||||
|
|
||||||
3) Run:
|
3) Run the ingest service locally:
|
||||||
go run ./cmd/ingestd -config config.yaml
|
`go run ./cmd/ingestd -config config.yaml`
|
||||||
|
|
||||||
4) Web UI:
|
4) Open the UI:
|
||||||
http://localhost:8080
|
`http://localhost:8080`
|
||||||
|
|
||||||
|
## Configuration reference
|
||||||
|
`config.yaml` is the single config file for ingest, forecast polling, and the web UI.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mqtt:
|
||||||
|
broker: "tcp://mosquitto:1883" # MQTT broker URL
|
||||||
|
client_id: "go-weatherstation-ingestd" # Client ID
|
||||||
|
username: "" # Optional username
|
||||||
|
password: "" # Optional password
|
||||||
|
topic: "ecowitt/ws90" # Topic to subscribe to
|
||||||
|
qos: 1 # MQTT QoS (0, 1, or 2)
|
||||||
|
|
||||||
|
db:
|
||||||
|
conn_string: "postgres://postgres:postgres@timescaledb:5432/micrometeo?sslmode=disable"
|
||||||
|
|
||||||
|
site:
|
||||||
|
name: "home"
|
||||||
|
latitude: -33.8688 # WGS84 latitude
|
||||||
|
longitude: 151.2093 # WGS84 longitude
|
||||||
|
elevation_m: 50 # Currently informational (not used by Open-Meteo ECMWF endpoint)
|
||||||
|
|
||||||
|
pollers:
|
||||||
|
open_meteo:
|
||||||
|
enabled: true
|
||||||
|
interval: "30m" # How often to fetch forecast data
|
||||||
|
model: "ecmwf" # Stored as metadata; endpoint is fixed to ECMWF
|
||||||
|
|
||||||
|
web:
|
||||||
|
enabled: true
|
||||||
|
listen: ":8080" # Web UI listen address
|
||||||
|
|
||||||
|
wunderground:
|
||||||
|
enabled: false
|
||||||
|
station_id: "YOUR_STATION_ID"
|
||||||
|
station_key: "YOUR_STATION_KEY"
|
||||||
|
interval: "60s" # Upload cadence
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- The Open-Meteo ECMWF endpoint is queried by the poller only. The UI reads forecasts from TimescaleDB.
|
||||||
|
- Web UI supports Local/UTC toggle and date-aligned ranges (6h, 24h, 72h, 7d).
|
||||||
|
|
||||||
|
## Schema & tables
|
||||||
|
TimescaleDB schema is initialized from `db/init/001_schema.sql` and includes:
|
||||||
|
|
||||||
|
- `observations_ws90` (hypertable): raw WS90 observations with payload metadata, plus the full JSON payload (`payload_json`).
|
||||||
|
- `forecast_openmeteo_hourly` (hypertable): hourly forecast points keyed by `(site, model, retrieved_at, ts)`.
|
||||||
|
- Continuous aggregates:
|
||||||
|
- `cagg_ws90_1m`: 1‑minute rollups (avg/min/max for temp, humidity, wind, uvi, light, rain).
|
||||||
|
- `cagg_ws90_5m`: 5‑minute rollups (same metrics as `cagg_ws90_1m`).
|
||||||
|
|
||||||
|
Retention/compression:
|
||||||
|
- `observations_ws90` has a 90‑day retention policy and compression after 7 days.
|
||||||
|
|
||||||
## Publish a test WS90 payload
|
## Publish a test WS90 payload
|
||||||
|
```sh
|
||||||
mosquitto_pub -h localhost -t ecowitt/ws90 -m '{"model":"Fineoffset-WS90","id":70618,"battery_ok":1,"battery_mV":3180,"temperature_C":24.2,"humidity":60,"wind_dir_deg":129,"wind_avg_m_s":0,"wind_max_m_s":0,"uvi":0,"light_lux":0,"flags":130,"rain_mm":0,"rain_start":0,"supercap_V":0.5,"firmware":160,"data":"3fff000000------0000ff7ff70000","mic":"CRC","protocol":"Fine Offset Electronics WS90 weather station","rssi":-44,"duration":32996}'
|
mosquitto_pub -h localhost -t ecowitt/ws90 -m '{"model":"Fineoffset-WS90","id":70618,"battery_ok":1,"battery_mV":3180,"temperature_C":24.2,"humidity":60,"wind_dir_deg":129,"wind_avg_m_s":0,"wind_max_m_s":0,"uvi":0,"light_lux":0,"flags":130,"rain_mm":0,"rain_start":0,"supercap_V":0.5,"firmware":160,"data":"3fff000000------0000ff7ff70000","mic":"CRC","protocol":"Fine Offset Electronics WS90 weather station","rssi":-44,"duration":32996}'
|
||||||
|
```
|
||||||
|
|||||||
@@ -109,6 +109,13 @@ function computeRange(range, tz) {
|
|||||||
axisEnd = end;
|
axisEnd = end;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "24h-last": {
|
||||||
|
end = now;
|
||||||
|
start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
axisStart = start;
|
||||||
|
axisEnd = end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "72h": {
|
case "72h": {
|
||||||
end = startOfDay(now, tz);
|
end = startOfDay(now, tz);
|
||||||
start = new Date(end.getTime() - 72 * 60 * 60 * 1000);
|
start = new Date(end.getTime() - 72 * 60 * 60 * 1000);
|
||||||
@@ -249,16 +256,6 @@ function renderDashboard(data) {
|
|||||||
updateText("live-supercap", "--");
|
updateText("live-supercap", "--");
|
||||||
}
|
}
|
||||||
|
|
||||||
const forecastUrl = document.getElementById("forecast-url");
|
|
||||||
if (forecastUrl) {
|
|
||||||
if (data.open_meteo_url) {
|
|
||||||
forecastUrl.href = data.open_meteo_url;
|
|
||||||
forecastUrl.textContent = data.open_meteo_url;
|
|
||||||
} else {
|
|
||||||
forecastUrl.textContent = "--";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const obs = data.observations || [];
|
const obs = data.observations || [];
|
||||||
const forecastAll = (data.forecast && data.forecast.points) || [];
|
const forecastAll = (data.forecast && data.forecast.points) || [];
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="segmented" role="group" aria-label="time range">
|
<div class="segmented" role="group" aria-label="time range">
|
||||||
<button class="btn" data-range="6h">6h</button>
|
<button class="btn" data-range="6h">6h</button>
|
||||||
|
<button class="btn" data-range="24h-last">Last 24h</button>
|
||||||
<button class="btn active" data-range="24h">24h</button>
|
<button class="btn active" data-range="24h">24h</button>
|
||||||
<button class="btn" data-range="72h">72h</button>
|
<button class="btn" data-range="72h">72h</button>
|
||||||
<button class="btn" data-range="7d">7d</button>
|
<button class="btn" data-range="7d">7d</button>
|
||||||
@@ -88,12 +89,6 @@
|
|||||||
<div class="callout-title">Forecast Summary</div>
|
<div class="callout-title">Forecast Summary</div>
|
||||||
<div class="callout-body" id="forecast-summary">--</div>
|
<div class="callout-body" id="forecast-summary">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="callout">
|
|
||||||
<div class="callout-title">Forecast Request</div>
|
|
||||||
<div class="callout-body">
|
|
||||||
<a id="forecast-url" class="mono" href="#" target="_blank" rel="noreferrer">--</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user