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

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

View File

@@ -38,13 +38,13 @@ func pressureHPAFromPayload(raw map[string]any) (float64, bool) {
); ok {
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
}
if v, ok := findFloat(raw, "pressure_kpa"); ok {
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 0, false

View File

@@ -16,8 +16,10 @@ const (
type Latest struct {
mu sync.RWMutex
lastTS time.Time
last *WS90Payload
lastTS time.Time
last *WS90Payload
baroTS time.Time
baroHPA *float64
// Rain tracking
mode rainMode
@@ -144,6 +146,7 @@ type Snapshot struct {
RainLastHourMM float64
DailyRainMM float64
PressureHPA *float64
}
func (l *Latest) Snapshot() (Snapshot, bool) {
@@ -162,10 +165,25 @@ func (l *Latest) Snapshot() (Snapshot, bool) {
daySum += rp.mm
}
var pressure *float64
if l.baroHPA != nil {
v := *l.baroHPA
pressure = &v
}
return Snapshot{
TS: l.lastTS,
P: *l.last,
RainLastHourMM: hourSum,
DailyRainMM: daySum,
PressureHPA: pressure,
}, 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
DailyRainMM float64
PressureHPA *float64
DateUTC string // "now" recommended
}
@@ -63,6 +64,11 @@ func (c *WundergroundClient) Upload(ctx context.Context, u WUUpload) (string, er
// UV index
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.
// WU expects solarradiation in W/m^2, so we omit it unless you add a conversion/actual sensor field.
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 msToMph(ms float64) float64 { return ms * 2.2369362920544 }
func mmToIn(mm float64) float64 { return mm / 25.4 }
func hPaToInHg(hpa float64) float64 {
return hpa * 0.0295299830714
}