Compare commits

..

6 Commits

Author SHA1 Message Date
ea93beb6b2 Reclassify malformed header log
All checks were successful
Build and Publish / release (push) Successful in 3m7s
2025-01-29 19:46:00 -05:00
ae53a1d5fd Invalid AWN Request Fixer
All checks were successful
Build and Publish / release (push) Successful in 4m31s
2025-01-29 19:37:23 -05:00
cd04beeec6 Invalid AWN Request Fixer 2025-01-29 18:06:50 -05:00
3d3492a283 Ignore invalid temp 2025-01-28 19:41:57 -05:00
ce0ef7d291 Clear invalid measurements
All checks were successful
Build and Publish / release (push) Successful in 3m34s
2025-01-28 19:26:40 -05:00
f2f160b112 Fix wunderground wind speed mapping
All checks were successful
Build and Publish / release (push) Successful in 2m40s
2025-01-28 17:05:25 -05:00
6 changed files with 136 additions and 2 deletions

2
go.mod
View File

@ -3,7 +3,7 @@ module gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter
go 1.23.4
require (
gitea.libretechconsulting.com/rmcguire/go-app v0.4.2
gitea.libretechconsulting.com/rmcguire/go-app v0.5.0
github.com/go-resty/resty/v2 v2.16.5
github.com/gorilla/schema v1.4.1
github.com/rs/zerolog v1.33.0

2
go.sum
View File

@ -4,6 +4,8 @@ gitea.libretechconsulting.com/rmcguire/go-app v0.4.1 h1:gjDg2M/j1AdMCtkXqQnLCo6j
gitea.libretechconsulting.com/rmcguire/go-app v0.4.1/go.mod h1:9c71S+sJb2NqvOwt3CFsW5WjE895goiRlMTdLimgwHs=
gitea.libretechconsulting.com/rmcguire/go-app v0.4.2 h1:LQxVLXEHruY32GaMsS5K/tMdjS5kvw6reUh25gshn40=
gitea.libretechconsulting.com/rmcguire/go-app v0.4.2/go.mod h1:9c71S+sJb2NqvOwt3CFsW5WjE895goiRlMTdLimgwHs=
gitea.libretechconsulting.com/rmcguire/go-app v0.5.0 h1:5yYyaXXN5KcxMIPBYLZKztvKGMlYol3+oqzUnkvHBaQ=
gitea.libretechconsulting.com/rmcguire/go-app v0.5.0/go.mod h1:9c71S+sJb2NqvOwt3CFsW5WjE895goiRlMTdLimgwHs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=

View File

@ -10,6 +10,7 @@ import (
"golang.org/x/sys/unix"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/ambienthttp"
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config"
)
@ -46,6 +47,8 @@ func main() {
HandlerFunc: aw.GetAWNHandlerFunc(ctx),
},
},
CustomListener: ambienthttp.NewAWNMutatingListener(ctx,
awConfig.HTTP.Listen), // Necessary to fix certain bad AWN firmware
HealthChecks: []srv.HealthCheckFunc{
// TODO: Implement
func(ctx context.Context) error {

View File

@ -0,0 +1,112 @@
// This package exists purely to override the net.Listener used
// by the application's http server. This is necessary for certain versions
// of firmware which errantly put an 0x0a (LF) following PASSKEY for
// AmbientWeather type http reporting.
//
// This needs to be fixed upstream by Ambient Weather and is a complete
// hack that should never be necessary. Without this, the http server
// will silently crank back an HTTP:400
package ambienthttp
import (
"bufio"
"bytes"
"context"
"io"
"net"
"regexp"
"github.com/rs/zerolog"
)
// Invalid Request Pattern
var badReqURI = regexp.MustCompile(`PASSKEY=[^\n&]{16,}$`)
// Listener encapsulates LFStrippingConn to perform
// infuriating strip of newline character present after PASSKEY
// sent errantly by specific versions of firmware sending updates
// in AmbientWeather protocol
type LFStrippingListener struct {
ctx context.Context
net.Listener
}
type LFStrippingConn struct {
ctx context.Context
reader io.Reader
net.Conn
}
func (l *LFStrippingListener) WrapConn(conn net.Conn) net.Conn {
buf := new(bytes.Buffer)
reader := io.TeeReader(conn, buf)
scanner := bufio.NewScanner(reader)
var newData []byte
for scanner.Scan() {
line := scanner.Bytes()
newData = append(newData, line...)
// Only restore newline if not a bad request
if !badReqURI.Match(line) {
newData = append(newData, '\n')
} else {
zerolog.Ctx(l.ctx).Warn().Bytes("line", line).
Msg("malformed request found, stripped 0x0a")
}
if len(line) == 0 {
break
}
}
if scanner.Err() != nil {
zerolog.Ctx(l.ctx).Err(scanner.Err()).Send()
}
zerolog.Ctx(l.ctx).Trace().
Int("numBytes", len(newData)).
Bytes("request", newData).
Msg("stripping conn complete")
// Use a multi-reader to prepend the modified request
finalReader := io.MultiReader(bytes.NewReader(newData), conn)
return &LFStrippingConn{
Conn: conn,
ctx: l.ctx,
reader: finalReader,
}
}
func NewAWNMutatingListener(ctx context.Context, listen string) net.Listener {
rawListener, err := net.Listen("tcp", listen)
if err != nil {
panic(err)
}
// Encapsulate the raw listener with ours
return &LFStrippingListener{
Listener: rawListener,
ctx: ctx,
}
}
func (l *LFStrippingListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
return l.WrapConn(conn), nil
}
func (l *LFStrippingListener) Close() error {
return l.Listener.Close()
}
func (l *LFStrippingListener) Addr() net.Addr {
return l.Listener.Addr()
}
func (c *LFStrippingConn) Read(b []byte) (int, error) {
return c.reader.Read(b)
}

View File

@ -51,7 +51,7 @@ func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate {
StationType: wuUpdate.SoftwareType,
TempOutdoorF: wuUpdate.Tempf,
HumidityOudoor: wuUpdate.Humidity,
WindSpeedMPH: wuUpdate.WindGustMPH,
WindSpeedMPH: wuUpdate.WindSpeedMPH,
WindGustMPH: wuUpdate.WindGustMPH,
WindDir: wuUpdate.WindDir,
UV: wuUpdate.UV,

View File

@ -15,11 +15,27 @@ func (u *WeatherUpdate) Enrich(weatherStations ...*config.WeatherStation) {
return
}
// Clear invalid measurements, would be better if these weren't
// sent when sensor was out of range.
// TODO: This should probably just be done for all fields where
// the value is -9999
if u.BaromAbsoluteIn != nil && *u.BaromAbsoluteIn < 20 {
u.BaromAbsoluteIn = nil
}
if u.BaromRelativeIn != nil && *u.BaromRelativeIn < 20 {
u.BaromRelativeIn = nil
}
if u.TempIndoorF != nil && *u.TempIndoorF < -1000 {
u.TempIndoorF = nil
}
// Calculate Wind Chill
if u.WindChillF == nil && u.TempOutdoorF != nil && u.WindSpeedMPH != nil {
wc := CalculateWindChill(*u.TempOutdoorF, *u.WindSpeedMPH)
u.WindChillF = &wc
}
// Calculate Dew Point
if u.DewPointF == nil && (u.TempOutdoorF != nil && u.HumidityOudoor != nil) {
if *u.TempOutdoorF != 0 || *u.HumidityOudoor != 0 {
dp := CalculateDewPoint(*u.TempOutdoorF, float64(*u.HumidityOudoor))
@ -27,6 +43,7 @@ func (u *WeatherUpdate) Enrich(weatherStations ...*config.WeatherStation) {
}
}
// Use relative pressure if absolute isn't provided
if u.BaromAbsoluteIn == nil && u.BaromRelativeIn != nil {
u.BaromAbsoluteIn = u.BaromRelativeIn
}