Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
ae53a1d5fd | |||
cd04beeec6 | |||
3d3492a283 | |||
ce0ef7d291 | |||
f2f160b112 |
2
go.mod
2
go.mod
@ -3,7 +3,7 @@ module gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter
|
|||||||
go 1.23.4
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
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/go-resty/resty/v2 v2.16.5
|
||||||
github.com/gorilla/schema v1.4.1
|
github.com/gorilla/schema v1.4.1
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
|
2
go.sum
2
go.sum
@ -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.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 h1:LQxVLXEHruY32GaMsS5K/tMdjS5kvw6reUh25gshn40=
|
||||||
gitea.libretechconsulting.com/rmcguire/go-app v0.4.2/go.mod h1:9c71S+sJb2NqvOwt3CFsW5WjE895goiRlMTdLimgwHs=
|
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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||||
|
3
main.go
3
main.go
@ -10,6 +10,7 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"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"
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/ambienthttp"
|
||||||
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config"
|
"gitea.libretechconsulting.com/rmcguire/ambient-weather-local-exporter/pkg/ambient/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,6 +47,8 @@ func main() {
|
|||||||
HandlerFunc: aw.GetAWNHandlerFunc(ctx),
|
HandlerFunc: aw.GetAWNHandlerFunc(ctx),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CustomListener: ambienthttp.NewAWNMutatingListener(ctx,
|
||||||
|
awConfig.HTTP.Listen), // Necessary to fix certain bad AWN firmware
|
||||||
HealthChecks: []srv.HealthCheckFunc{
|
HealthChecks: []srv.HealthCheckFunc{
|
||||||
// TODO: Implement
|
// TODO: Implement
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
|
112
pkg/ambient/ambienthttp/ambienthttp.go
Normal file
112
pkg/ambient/ambienthttp/ambienthttp.go
Normal 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).Trace().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)
|
||||||
|
}
|
@ -51,7 +51,7 @@ func MapWUUpdate(wuUpdate *WundergroundUpdate) *weather.WeatherUpdate {
|
|||||||
StationType: wuUpdate.SoftwareType,
|
StationType: wuUpdate.SoftwareType,
|
||||||
TempOutdoorF: wuUpdate.Tempf,
|
TempOutdoorF: wuUpdate.Tempf,
|
||||||
HumidityOudoor: wuUpdate.Humidity,
|
HumidityOudoor: wuUpdate.Humidity,
|
||||||
WindSpeedMPH: wuUpdate.WindGustMPH,
|
WindSpeedMPH: wuUpdate.WindSpeedMPH,
|
||||||
WindGustMPH: wuUpdate.WindGustMPH,
|
WindGustMPH: wuUpdate.WindGustMPH,
|
||||||
WindDir: wuUpdate.WindDir,
|
WindDir: wuUpdate.WindDir,
|
||||||
UV: wuUpdate.UV,
|
UV: wuUpdate.UV,
|
||||||
|
@ -15,11 +15,27 @@ func (u *WeatherUpdate) Enrich(weatherStations ...*config.WeatherStation) {
|
|||||||
return
|
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 {
|
if u.WindChillF == nil && u.TempOutdoorF != nil && u.WindSpeedMPH != nil {
|
||||||
wc := CalculateWindChill(*u.TempOutdoorF, *u.WindSpeedMPH)
|
wc := CalculateWindChill(*u.TempOutdoorF, *u.WindSpeedMPH)
|
||||||
u.WindChillF = &wc
|
u.WindChillF = &wc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate Dew Point
|
||||||
if u.DewPointF == nil && (u.TempOutdoorF != nil && u.HumidityOudoor != nil) {
|
if u.DewPointF == nil && (u.TempOutdoorF != nil && u.HumidityOudoor != nil) {
|
||||||
if *u.TempOutdoorF != 0 || *u.HumidityOudoor != 0 {
|
if *u.TempOutdoorF != 0 || *u.HumidityOudoor != 0 {
|
||||||
dp := CalculateDewPoint(*u.TempOutdoorF, float64(*u.HumidityOudoor))
|
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 {
|
if u.BaromAbsoluteIn == nil && u.BaromRelativeIn != nil {
|
||||||
u.BaromAbsoluteIn = u.BaromRelativeIn
|
u.BaromAbsoluteIn = u.BaromRelativeIn
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user