add barometric pressure
This commit is contained in:
@@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user