add support for barometric pressure
This commit is contained in:
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -12,12 +13,13 @@ type Config struct {
|
||||
LogLevel string `yaml:"log_level"`
|
||||
|
||||
MQTT struct {
|
||||
Broker string `yaml:"broker"`
|
||||
ClientID string `yaml:"client_id"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Topic string `yaml:"topic"`
|
||||
QoS byte `yaml:"qos"`
|
||||
Broker string `yaml:"broker"`
|
||||
ClientID string `yaml:"client_id"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Topic string `yaml:"topic"`
|
||||
QoS byte `yaml:"qos"`
|
||||
Topics []MQTTTopic `yaml:"topics"`
|
||||
} `yaml:"mqtt"`
|
||||
|
||||
DB struct {
|
||||
@@ -52,6 +54,13 @@ type Config struct {
|
||||
} `yaml:"wunderground"`
|
||||
}
|
||||
|
||||
type MQTTTopic struct {
|
||||
Name string `yaml:"name"`
|
||||
Topic string `yaml:"topic"`
|
||||
Type string `yaml:"type"`
|
||||
QoS *byte `yaml:"qos"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -64,8 +73,31 @@ func Load(path string) (*Config, error) {
|
||||
}
|
||||
|
||||
// Minimal validation
|
||||
if c.MQTT.Broker == "" || c.MQTT.Topic == "" {
|
||||
return nil, errors.New("mqtt broker and topic are required")
|
||||
if c.MQTT.Broker == "" {
|
||||
return nil, errors.New("mqtt broker is required")
|
||||
}
|
||||
if len(c.MQTT.Topics) == 0 && c.MQTT.Topic != "" {
|
||||
qos := c.MQTT.QoS
|
||||
c.MQTT.Topics = []MQTTTopic{{
|
||||
Name: "ws90",
|
||||
Topic: c.MQTT.Topic,
|
||||
Type: "ws90",
|
||||
QoS: &qos,
|
||||
}}
|
||||
}
|
||||
if len(c.MQTT.Topics) == 0 {
|
||||
return nil, errors.New("mqtt topic(s) are required")
|
||||
}
|
||||
for i := range c.MQTT.Topics {
|
||||
t := c.MQTT.Topics[i]
|
||||
if t.Topic == "" {
|
||||
return nil, errors.New("mqtt topics must include topic")
|
||||
}
|
||||
if t.Type == "" {
|
||||
t.Type = "ws90"
|
||||
}
|
||||
t.Type = strings.ToLower(t.Type)
|
||||
c.MQTT.Topics[i] = t
|
||||
}
|
||||
if c.DB.ConnString == "" {
|
||||
return nil, errors.New("db conn_string is required")
|
||||
|
||||
@@ -122,3 +122,27 @@ func (d *DB) UpsertOpenMeteoHourly(ctx context.Context, p InsertOpenMeteoHourlyP
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type InsertBarometerParams struct {
|
||||
TS time.Time
|
||||
Site string
|
||||
Source string
|
||||
PressureHPA float64
|
||||
Payload map[string]any
|
||||
}
|
||||
|
||||
func (d *DB) InsertBarometer(ctx context.Context, p InsertBarometerParams) error {
|
||||
b, _ := json.Marshal(p.Payload)
|
||||
|
||||
payloadJSON := json.RawMessage(b)
|
||||
|
||||
_, err := d.Pool.Exec(ctx, `
|
||||
INSERT INTO observations_baro (
|
||||
ts, site, source, pressure_hpa, payload_json
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5
|
||||
)
|
||||
`, p.TS, p.Site, p.Source, p.PressureHPA, payloadJSON)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
105
internal/mqttingest/barometer.go
Normal file
105
internal/mqttingest/barometer.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package mqttingest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type BarometerPayload struct {
|
||||
PressureHPA float64
|
||||
}
|
||||
|
||||
func ParseBarometer(b []byte) (*BarometerPayload, map[string]any, error) {
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pressure, ok := pressureHPAFromPayload(raw)
|
||||
if !ok {
|
||||
return nil, raw, fmt.Errorf("barometer payload missing pressure field")
|
||||
}
|
||||
|
||||
return &BarometerPayload{
|
||||
PressureHPA: pressure,
|
||||
}, raw, nil
|
||||
}
|
||||
|
||||
func pressureHPAFromPayload(raw map[string]any) (float64, bool) {
|
||||
if v, ok := findFloat(raw,
|
||||
"pressure_hpa",
|
||||
"pressure_mb",
|
||||
"pressure_mbar",
|
||||
"barometer_hpa",
|
||||
"baro_hpa",
|
||||
"pressure",
|
||||
); ok {
|
||||
return v, true
|
||||
}
|
||||
if v, ok := findFloat(raw, "pressure_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 {
|
||||
return v * 33.8638866667, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func findFloat(raw map[string]any, keys ...string) (float64, bool) {
|
||||
for _, key := range keys {
|
||||
v, ok := raw[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if f, ok := asFloat(v); ok {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func asFloat(v any) (float64, bool) {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
return t, true
|
||||
case float32:
|
||||
return float64(t), true
|
||||
case int:
|
||||
return float64(t), true
|
||||
case int8:
|
||||
return float64(t), true
|
||||
case int16:
|
||||
return float64(t), true
|
||||
case int32:
|
||||
return float64(t), true
|
||||
case int64:
|
||||
return float64(t), true
|
||||
case uint:
|
||||
return float64(t), true
|
||||
case uint8:
|
||||
return float64(t), true
|
||||
case uint16:
|
||||
return float64(t), true
|
||||
case uint32:
|
||||
return float64(t), true
|
||||
case uint64:
|
||||
return float64(t), true
|
||||
case json.Number:
|
||||
f, err := t.Float64()
|
||||
return f, err == nil
|
||||
case string:
|
||||
s := strings.TrimSpace(t)
|
||||
if s == "" {
|
||||
return 0, false
|
||||
}
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
return f, err == nil
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,12 @@ type MQTTConfig struct {
|
||||
Password string
|
||||
Topic string
|
||||
QoS byte
|
||||
Topics []Subscription
|
||||
}
|
||||
|
||||
type Subscription struct {
|
||||
Topic string
|
||||
QoS byte
|
||||
}
|
||||
|
||||
type Handler func(ctx context.Context, topic string, payload []byte) error
|
||||
@@ -38,7 +44,22 @@ func RunSubscriber(ctx context.Context, cfg MQTTConfig, h Handler) error {
|
||||
}
|
||||
|
||||
// Subscribe
|
||||
if tok := client.Subscribe(cfg.Topic, cfg.QoS, func(_ mqtt.Client, msg mqtt.Message) {
|
||||
subs := map[string]byte{}
|
||||
if len(cfg.Topics) > 0 {
|
||||
for _, sub := range cfg.Topics {
|
||||
if sub.Topic == "" {
|
||||
continue
|
||||
}
|
||||
subs[sub.Topic] = sub.QoS
|
||||
}
|
||||
} else if cfg.Topic != "" {
|
||||
subs[cfg.Topic] = cfg.QoS
|
||||
}
|
||||
if len(subs) == 0 {
|
||||
return fmt.Errorf("mqtt subscribe: no topics configured")
|
||||
}
|
||||
|
||||
if tok := client.SubscribeMultiple(subs, func(_ mqtt.Client, msg mqtt.Message) {
|
||||
// Keep callback short; do work with context
|
||||
_ = h(ctx, msg.Topic(), msg.Payload())
|
||||
}); tok.Wait() && tok.Error() != nil {
|
||||
|
||||
36
internal/mqttingest/topic_match.go
Normal file
36
internal/mqttingest/topic_match.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package mqttingest
|
||||
|
||||
import "strings"
|
||||
|
||||
// TopicMatches reports whether a topic filter (with + or # wildcards) matches a topic name.
|
||||
// It follows the MQTT v3.1.1 wildcard rules and supports shared subscriptions ($share).
|
||||
func TopicMatches(filter, topic string) bool {
|
||||
return matchTopic(routeSplit(filter), strings.Split(topic, "/"))
|
||||
}
|
||||
|
||||
func matchTopic(route []string, topic []string) bool {
|
||||
if len(route) == 0 {
|
||||
return len(topic) == 0
|
||||
}
|
||||
if len(topic) == 0 {
|
||||
return route[0] == "#"
|
||||
}
|
||||
if route[0] == "#" {
|
||||
return true
|
||||
}
|
||||
if route[0] == "+" || route[0] == topic[0] {
|
||||
return matchTopic(route[1:], topic[1:])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// routeSplit removes $share/group/ when matching shared subscription filters.
|
||||
func routeSplit(route string) []string {
|
||||
if strings.HasPrefix(route, "$share/") {
|
||||
parts := strings.Split(route, "/")
|
||||
if len(parts) > 2 {
|
||||
return parts[2:]
|
||||
}
|
||||
}
|
||||
return strings.Split(route, "/")
|
||||
}
|
||||
Reference in New Issue
Block a user