add barometric pressure

This commit is contained in:
2026-02-02 16:10:29 +11:00
parent 6f9eee5dc0
commit 08bd117eb8
7 changed files with 146 additions and 62 deletions

View File

@@ -165,6 +165,8 @@ func main() {
source = binding.Topic source = binding.Topic
} }
latest.UpdateBarometer(ts, p.PressureHPA)
if err := d.InsertBarometer(ctx, db.InsertBarometerParams{ if err := d.InsertBarometer(ctx, db.InsertBarometerParams{
TS: ts, TS: ts,
Site: cfg.Site.Name, Site: cfg.Site.Name,
@@ -250,6 +252,7 @@ func runWundergroundUploader(ctx context.Context, latest *mqttingest.Latest, sta
UVI: snap.P.UVI, UVI: snap.P.UVI,
RainLastHourMM: snap.RainLastHourMM, RainLastHourMM: snap.RainLastHourMM,
DailyRainMM: snap.DailyRainMM, DailyRainMM: snap.DailyRainMM,
PressureHPA: snap.PressureHPA,
DateUTC: "now", DateUTC: "now",
} }

View File

@@ -14,6 +14,7 @@ const colors = {
forecast: "#f4b942", forecast: "#f4b942",
gust: "#ff7d6b", gust: "#ff7d6b",
humidity: "#7ee081", humidity: "#7ee081",
pressure: "#8fb8de",
uvi: "#f4d35e", uvi: "#f4d35e",
light: "#b8f2e6", light: "#b8f2e6",
precip: "#4ea8de", precip: "#4ea8de",
@@ -483,6 +484,18 @@ function renderDashboard(data) {
}; };
upsertChart("chart-rh", rhChart); upsertChart("chart-rh", rhChart);
const pressureChart = {
type: "line",
data: {
datasets: [
{ label: "obs pressure hPa", data: series(obsFiltered, "pressure_hpa"), borderColor: colors.pressure },
{ label: "forecast msl hPa", data: series(forecast, "pressure_msl_hpa"), borderColor: colors.forecast },
],
},
options: baseOptions(range),
};
upsertChart("chart-pressure", pressureChart);
const lightOptions = baseOptions(range); const lightOptions = baseOptions(range);
lightOptions.scales.y.ticks.color = colors.uvi; lightOptions.scales.y.ticks.color = colors.uvi;
lightOptions.scales.y.title = { display: true, text: "UVI", color: colors.uvi }; lightOptions.scales.y.title = { display: true, text: "UVI", color: colors.uvi };

View File

@@ -128,6 +128,15 @@
<canvas id="chart-rh"></canvas> <canvas id="chart-rh"></canvas>
</div> </div>
</div> </div>
<div class="chart-card" data-chart="chart-pressure">
<div class="chart-header">
<div class="chart-title">Pressure (obs vs forecast)</div>
<button class="chart-link" data-chart="chart-pressure" title="Copy chart link">Share</button>
</div>
<div class="chart-canvas">
<canvas id="chart-pressure"></canvas>
</div>
</div>
<div class="chart-card" data-chart="chart-light"> <div class="chart-card" data-chart="chart-light">
<div class="chart-header"> <div class="chart-header">
<div class="chart-title">UV Index and Light</div> <div class="chart-title">UV Index and Light</div>

View File

@@ -11,18 +11,19 @@ import (
) )
type ObservationPoint struct { type ObservationPoint struct {
TS time.Time `json:"ts"` TS time.Time `json:"ts"`
TempC *float64 `json:"temp_c,omitempty"` TempC *float64 `json:"temp_c,omitempty"`
RH *float64 `json:"rh,omitempty"` RH *float64 `json:"rh,omitempty"`
WindMS *float64 `json:"wind_m_s,omitempty"` PressureHPA *float64 `json:"pressure_hpa,omitempty"`
WindGustMS *float64 `json:"wind_gust_m_s,omitempty"` WindMS *float64 `json:"wind_m_s,omitempty"`
WindDirDeg *float64 `json:"wind_dir_deg,omitempty"` WindGustMS *float64 `json:"wind_gust_m_s,omitempty"`
UVI *float64 `json:"uvi,omitempty"` WindDirDeg *float64 `json:"wind_dir_deg,omitempty"`
LightLux *float64 `json:"light_lux,omitempty"` UVI *float64 `json:"uvi,omitempty"`
BatteryMV *float64 `json:"battery_mv,omitempty"` LightLux *float64 `json:"light_lux,omitempty"`
SupercapV *float64 `json:"supercap_v,omitempty"` BatteryMV *float64 `json:"battery_mv,omitempty"`
RainMM *float64 `json:"rain_mm,omitempty"` SupercapV *float64 `json:"supercap_v,omitempty"`
RainStart *int64 `json:"rain_start,omitempty"` RainMM *float64 `json:"rain_mm,omitempty"`
RainStart *int64 `json:"rain_start,omitempty"`
} }
type ForecastPoint struct { type ForecastPoint struct {
@@ -59,26 +60,54 @@ func (d *DB) ObservationSeries(ctx context.Context, site, bucket string, start,
} }
query := fmt.Sprintf(` query := fmt.Sprintf(`
WITH ws AS (
SELECT
time_bucket(INTERVAL '%s', ts) AS bucket,
avg(temperature_c) AS temp_c_avg,
avg(humidity) AS rh_avg,
avg(wind_avg_m_s) AS wind_avg_ms_avg,
max(wind_max_m_s) AS wind_gust_ms_max,
avg(wind_dir_deg) AS wind_dir_deg_avg,
max(uvi) AS uvi_max,
max(light_lux) AS light_lux_max,
avg(battery_mv) AS battery_mv_avg,
avg(supercap_v) AS supercap_v_avg,
avg(rain_mm) AS rain_mm_avg,
max(rain_start) AS rain_start_max
FROM observations_ws90
WHERE site = $1
AND ts >= $2
AND ts <= $3
GROUP BY bucket
),
baro AS (
SELECT
time_bucket(INTERVAL '%s', ts) AS bucket,
avg(pressure_hpa) AS pressure_hpa_avg
FROM observations_baro
WHERE site = $1
AND ts >= $2
AND ts <= $3
GROUP BY bucket
)
SELECT SELECT
time_bucket(INTERVAL '%s', ts) AS bucket, COALESCE(ws.bucket, baro.bucket) AS bucket,
avg(temperature_c) AS temp_c_avg, ws.temp_c_avg,
avg(humidity) AS rh_avg, ws.rh_avg,
avg(wind_avg_m_s) AS wind_avg_ms_avg, baro.pressure_hpa_avg,
max(wind_max_m_s) AS wind_gust_ms_max, ws.wind_avg_ms_avg,
avg(wind_dir_deg) AS wind_dir_deg_avg, ws.wind_gust_ms_max,
max(uvi) AS uvi_max, ws.wind_dir_deg_avg,
max(light_lux) AS light_lux_max, ws.uvi_max,
avg(battery_mv) AS battery_mv_avg, ws.light_lux_max,
avg(supercap_v) AS supercap_v_avg, ws.battery_mv_avg,
avg(rain_mm) AS rain_mm_avg, ws.supercap_v_avg,
max(rain_start) AS rain_start_max ws.rain_mm_avg,
FROM observations_ws90 ws.rain_start_max
WHERE site = $1 FROM ws
AND ts >= $2 FULL OUTER JOIN baro ON ws.bucket = baro.bucket
AND ts <= $3
GROUP BY bucket
ORDER BY bucket ASC ORDER BY bucket ASC
`, interval) `, interval, interval)
rows, err := d.Pool.Query(ctx, query, site, start, end) rows, err := d.Pool.Query(ctx, query, site, start, end)
if err != nil { if err != nil {
@@ -90,27 +119,28 @@ func (d *DB) ObservationSeries(ctx context.Context, site, bucket string, start,
for rows.Next() { for rows.Next() {
var ( var (
ts time.Time ts time.Time
temp, rh, wind, gust sql.NullFloat64 temp, rh, pressure, wind, gust sql.NullFloat64
dir, uvi, light, battery, supercap sql.NullFloat64 dir, uvi, light, battery, supercap sql.NullFloat64
rainMM sql.NullFloat64 rainMM sql.NullFloat64
rainStart sql.NullInt64 rainStart sql.NullInt64
) )
if err := rows.Scan(&ts, &temp, &rh, &wind, &gust, &dir, &uvi, &light, &battery, &supercap, &rainMM, &rainStart); err != nil { if err := rows.Scan(&ts, &temp, &rh, &pressure, &wind, &gust, &dir, &uvi, &light, &battery, &supercap, &rainMM, &rainStart); err != nil {
return nil, err return nil, err
} }
points = append(points, ObservationPoint{ points = append(points, ObservationPoint{
TS: ts, TS: ts,
TempC: nullFloatPtr(temp), TempC: nullFloatPtr(temp),
RH: nullFloatPtr(rh), RH: nullFloatPtr(rh),
WindMS: nullFloatPtr(wind), PressureHPA: nullFloatPtr(pressure),
WindGustMS: nullFloatPtr(gust), WindMS: nullFloatPtr(wind),
WindDirDeg: nullFloatPtr(dir), WindGustMS: nullFloatPtr(gust),
UVI: nullFloatPtr(uvi), WindDirDeg: nullFloatPtr(dir),
LightLux: nullFloatPtr(light), UVI: nullFloatPtr(uvi),
BatteryMV: nullFloatPtr(battery), LightLux: nullFloatPtr(light),
SupercapV: nullFloatPtr(supercap), BatteryMV: nullFloatPtr(battery),
RainMM: nullFloatPtr(rainMM), SupercapV: nullFloatPtr(supercap),
RainStart: nullIntPtr(rainStart), RainMM: nullFloatPtr(rainMM),
RainStart: nullIntPtr(rainStart),
}) })
} }
if rows.Err() != nil { if rows.Err() != nil {
@@ -126,6 +156,7 @@ func (d *DB) LatestObservation(ctx context.Context, site string) (*ObservationPo
ts, ts,
temperature_c, temperature_c,
humidity, humidity,
(SELECT pressure_hpa FROM observations_baro WHERE site = $1 ORDER BY ts DESC LIMIT 1) AS pressure_hpa,
wind_avg_m_s, wind_avg_m_s,
wind_max_m_s, wind_max_m_s,
wind_dir_deg, wind_dir_deg,
@@ -143,12 +174,12 @@ func (d *DB) LatestObservation(ctx context.Context, site string) (*ObservationPo
var ( var (
ts time.Time ts time.Time
temp, rh, wind, gust sql.NullFloat64 temp, rh, pressure, wind, gust sql.NullFloat64
dir, uvi, light, battery, supercap sql.NullFloat64 dir, uvi, light, battery, supercap sql.NullFloat64
rainMM sql.NullFloat64 rainMM sql.NullFloat64
rainStart sql.NullInt64 rainStart sql.NullInt64
) )
err := d.Pool.QueryRow(ctx, query, site).Scan(&ts, &temp, &rh, &wind, &gust, &dir, &uvi, &light, &battery, &supercap, &rainMM, &rainStart) err := d.Pool.QueryRow(ctx, query, site).Scan(&ts, &temp, &rh, &pressure, &wind, &gust, &dir, &uvi, &light, &battery, &supercap, &rainMM, &rainStart)
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return nil, nil return nil, nil
@@ -157,18 +188,19 @@ func (d *DB) LatestObservation(ctx context.Context, site string) (*ObservationPo
} }
return &ObservationPoint{ return &ObservationPoint{
TS: ts, TS: ts,
TempC: nullFloatPtr(temp), TempC: nullFloatPtr(temp),
RH: nullFloatPtr(rh), RH: nullFloatPtr(rh),
WindMS: nullFloatPtr(wind), PressureHPA: nullFloatPtr(pressure),
WindGustMS: nullFloatPtr(gust), WindMS: nullFloatPtr(wind),
WindDirDeg: nullFloatPtr(dir), WindGustMS: nullFloatPtr(gust),
UVI: nullFloatPtr(uvi), WindDirDeg: nullFloatPtr(dir),
LightLux: nullFloatPtr(light), UVI: nullFloatPtr(uvi),
BatteryMV: nullFloatPtr(battery), LightLux: nullFloatPtr(light),
SupercapV: nullFloatPtr(supercap), BatteryMV: nullFloatPtr(battery),
RainMM: nullFloatPtr(rainMM), SupercapV: nullFloatPtr(supercap),
RainStart: nullIntPtr(rainStart), RainMM: nullFloatPtr(rainMM),
RainStart: nullIntPtr(rainStart),
}, nil }, nil
} }

View File

@@ -38,13 +38,13 @@ func pressureHPAFromPayload(raw map[string]any) (float64, bool) {
); ok { ); ok {
return v, true return v, true
} }
if v, ok := findFloat(raw, "pressure_pa"); ok { if v, ok := findFloat(raw, "pressure_pa", "pa"); ok {
return v / 100.0, true return v / 100.0, true
} }
if v, ok := findFloat(raw, "pressure_kpa"); ok { if v, ok := findFloat(raw, "pressure_kpa"); ok {
return v * 10.0, true return v * 10.0, true
} }
if v, ok := findFloat(raw, "pressure_inhg", "barometer_inhg"); ok { if v, ok := findFloat(raw, "pressure_inhg", "barometer_inhg", "altim"); ok {
return v * 33.8638866667, true return v * 33.8638866667, true
} }
return 0, false return 0, false

View File

@@ -16,8 +16,10 @@ const (
type Latest struct { type Latest struct {
mu sync.RWMutex mu sync.RWMutex
lastTS time.Time lastTS time.Time
last *WS90Payload last *WS90Payload
baroTS time.Time
baroHPA *float64
// Rain tracking // Rain tracking
mode rainMode mode rainMode
@@ -144,6 +146,7 @@ type Snapshot struct {
RainLastHourMM float64 RainLastHourMM float64
DailyRainMM float64 DailyRainMM float64
PressureHPA *float64
} }
func (l *Latest) Snapshot() (Snapshot, bool) { func (l *Latest) Snapshot() (Snapshot, bool) {
@@ -162,10 +165,25 @@ func (l *Latest) Snapshot() (Snapshot, bool) {
daySum += rp.mm daySum += rp.mm
} }
var pressure *float64
if l.baroHPA != nil {
v := *l.baroHPA
pressure = &v
}
return Snapshot{ return Snapshot{
TS: l.lastTS, TS: l.lastTS,
P: *l.last, P: *l.last,
RainLastHourMM: hourSum, RainLastHourMM: hourSum,
DailyRainMM: daySum, DailyRainMM: daySum,
PressureHPA: pressure,
}, true }, true
} }
func (l *Latest) UpdateBarometer(ts time.Time, pressureHPA float64) {
l.mu.Lock()
defer l.mu.Unlock()
l.baroTS = ts
l.baroHPA = &pressureHPA
}

View File

@@ -33,6 +33,7 @@ type WUUpload struct {
RainLastHourMM float64 RainLastHourMM float64
DailyRainMM float64 DailyRainMM float64
PressureHPA *float64
DateUTC string // "now" recommended DateUTC string // "now" recommended
} }
@@ -63,6 +64,11 @@ func (c *WundergroundClient) Upload(ctx context.Context, u WUUpload) (string, er
// UV index // UV index
q.Set("UV", fmt.Sprintf("%.2f", u.UVI)) q.Set("UV", fmt.Sprintf("%.2f", u.UVI))
// Barometric pressure (inHg)
if u.PressureHPA != nil {
q.Set("baromin", fmt.Sprintf("%.4f", hPaToInHg(*u.PressureHPA)))
}
// NOTE: your WS90 payload provides light in lux, not W/m^2. // NOTE: your WS90 payload provides light in lux, not W/m^2.
// WU expects solarradiation in W/m^2, so we omit it unless you add a conversion/actual sensor field. // WU expects solarradiation in W/m^2, so we omit it unless you add a conversion/actual sensor field.
if u.SolarWm2 != nil { if u.SolarWm2 != nil {
@@ -110,3 +116,6 @@ func (c *WundergroundClient) Upload(ctx context.Context, u WUUpload) (string, er
func cToF(c float64) float64 { return (c * 9.0 / 5.0) + 32.0 } func cToF(c float64) float64 { return (c * 9.0 / 5.0) + 32.0 }
func msToMph(ms float64) float64 { return ms * 2.2369362920544 } func msToMph(ms float64) float64 { return ms * 2.2369362920544 }
func mmToIn(mm float64) float64 { return mm / 25.4 } func mmToIn(mm float64) float64 { return mm / 25.4 }
func hPaToInHg(hpa float64) float64 {
return hpa * 0.0295299830714
}