add support for barometric pressure

This commit is contained in:
2026-01-29 14:04:18 +11:00
parent 7a0081b2ed
commit 5d07c5d54b
9 changed files with 401 additions and 51 deletions

View 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
}
}

View File

@@ -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 {

View 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, "/")
}