add barometric pressure
This commit is contained in:
@@ -165,6 +165,8 @@ func main() {
|
||||
source = binding.Topic
|
||||
}
|
||||
|
||||
latest.UpdateBarometer(ts, p.PressureHPA)
|
||||
|
||||
if err := d.InsertBarometer(ctx, db.InsertBarometerParams{
|
||||
TS: ts,
|
||||
Site: cfg.Site.Name,
|
||||
@@ -250,6 +252,7 @@ func runWundergroundUploader(ctx context.Context, latest *mqttingest.Latest, sta
|
||||
UVI: snap.P.UVI,
|
||||
RainLastHourMM: snap.RainLastHourMM,
|
||||
DailyRainMM: snap.DailyRainMM,
|
||||
PressureHPA: snap.PressureHPA,
|
||||
DateUTC: "now",
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const colors = {
|
||||
forecast: "#f4b942",
|
||||
gust: "#ff7d6b",
|
||||
humidity: "#7ee081",
|
||||
pressure: "#8fb8de",
|
||||
uvi: "#f4d35e",
|
||||
light: "#b8f2e6",
|
||||
precip: "#4ea8de",
|
||||
@@ -483,6 +484,18 @@ function renderDashboard(data) {
|
||||
};
|
||||
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);
|
||||
lightOptions.scales.y.ticks.color = colors.uvi;
|
||||
lightOptions.scales.y.title = { display: true, text: "UVI", color: colors.uvi };
|
||||
|
||||
@@ -128,6 +128,15 @@
|
||||
<canvas id="chart-rh"></canvas>
|
||||
</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-header">
|
||||
<div class="chart-title">UV Index and Light</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user