Skip to content

Commit

Permalink
fixed API retries on failure, refactored logic (#8)
Browse files Browse the repository at this point in the history
Co-authored-by: sepiroth887 <sepiroth887+git@gmail.com>
  • Loading branch information
sepiroth887 and sepiroth887 committed Jan 3, 2024
1 parent d1553cf commit 8f8985d
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 80 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ require (
github.com/alecthomas/assert/v2 v2.1.0
github.com/alecthomas/kong v0.8.1
github.com/eclipse/paho.mqtt.golang v1.4.3
github.com/rs/zerolog v1.31.0
)

require (
github.com/alecthomas/repr v0.1.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.12.0 // indirect
)
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,28 @@ github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY
github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
172 changes: 92 additions & 80 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

"github.com/alecthomas/kong"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

type CommandError struct {
Expand All @@ -37,52 +39,118 @@ type Start struct {
UseTestData bool `help:"Use sample ./response.json instead of real API (for testing)" short:"d" env:"ACCU_MQTT_TEST_DATA" default:"false"`
}

var cli struct {
Debug bool `help:"enable debug" short:"v"`
Start Start `cmd:"" help:"start the provider"`
type cli struct {
Debug bool `help:"enable debug" short:"v"`
Start Start `cmd:"" help:"start the provider"`
mqttClient mqtt.Client
httpClient http.Client
cast MinuteCast
}

var mqttClient mqtt.Client

func main() {
ctx := kong.Parse(&cli)
if err := ctx.Run(cli.Debug); err != nil {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
c := cli{}
ctx := kong.Parse(&c)
if err := ctx.Run(c); err != nil {
fmt.Printf("failed to run command: %v\n", err)
os.Exit(err.(CommandError).GetExitCode())
}
}

func (c *cli) RefreshCast(apiKey, loc string) {
if c.Start.UseTestData {
return
}
for range time.NewTicker(time.Minute * 80).C {
cast, err := queryAPI(c.httpClient, apiKey, loc)
if err != nil {
log.Warn().Err(err)
}
c.cast = cast
}
}

func (s *Start) Run(debug bool) error {
func queryAPI(httpClient http.Client, apiKey, loc string) (MinuteCast, error) {
res, err := httpClient.Get(fmt.Sprintf("https://dataservice.accuweather.com/forecasts/v1/minute?q=%s&apikey=%s", loc, apiKey))
if err != nil {
log.Warn().Err(err)
return MinuteCast{}, err
}

if res.StatusCode < 199 || res.StatusCode > 299 {
data, _ := io.ReadAll(res.Body)
return MinuteCast{}, fmt.Errorf("failed to request cast from live api: status [%d]: %s", res.StatusCode, string(data))
}

defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
return MinuteCast{}, err
}

var cast MinuteCast
err = json.Unmarshal(data, &cast)
if err != nil {
return MinuteCast{}, err
}
cast.UpdateTime = time.Now()
log.Debug().Msgf("Received live cast: %s", string(data))

data, _ = json.Marshal(&cast)
os.WriteFile("./last_update.json", data, 0777)

return cast, err
}

func (s *Start) Run(c cli) error {
opts := mqtt.NewClientOptions()
opts.AddBroker(s.BrokerURL)
opts.SetClientID("accu-mqtt")
opts.SetCleanSession(true)
opts.SetStore(mqtt.NewMemoryStore())

zerolog.SetGlobalLevel(zerolog.InfoLevel)
if c.Debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
mqttClient = mqtt.NewClient(opts)
c.mqttClient = mqttClient
c.httpClient = http.Client{
Timeout: 10 * time.Second,
}

if t := mqttClient.Connect(); t.Wait() && t.Error() != nil {
return NewCommandError(t.Error(), 1)
}
defer mqttClient.Disconnect(100)

err := registerSensors(debug)
err := c.registerSensors()
if err != nil {
return NewCommandError(err, 2)
}

loc := fmt.Sprintf("%.3f,%.3f", s.Latitude, s.Longitude)
apiKey := s.AccuAPIToken
cast, err := loadCast(s.UseTestData, loc, apiKey)

cast, err := c.loadCast()
if cast.UpdateTime.Before(time.Now().Add(time.Minute * 80)) {
cast, err = queryAPI(c.httpClient, apiKey, loc)
if err != nil {
log.Warn().Err(err)
}
}
go c.RefreshCast(apiKey, loc)

if err != nil {
fmt.Println("Failed to retrieve MinuteCast: ", err)
}
if debug {
if c.Debug {
fmt.Printf("Retrieved cast with data:\n%v\n", cast)
}

if debug {
if c.Debug {
fmt.Println("Sending online status payload on accu-mqtt/available")
}
if t := mqttClient.Publish("accu-mqtt/available", 0, true, "online"); t.Wait() && t.Error() != nil {
Expand All @@ -91,26 +159,20 @@ func (s *Start) Run(debug bool) error {

var data []byte
go func() {
for range time.NewTicker(time.Second * 10).C {
for range time.NewTicker(time.Second * 30).C {
state := getStateFromCast(cast)
data, _ = json.Marshal(&state)
if debug {
fmt.Printf("Sending state update:\n%v\n", state)
}
log.Debug().Msgf("Sending state update:\n%v\n", state)

if t := mqttClient.Publish("accu-mqtt/attributes", 0, false, data); t.Wait() && t.Error() != nil {
fmt.Println("failed to publish state: ", t.Error())
log.Warn().Msgf("failed to publish state: %v", t.Error())
}
if t := mqttClient.Publish("accu-mqtt/state", 0, false, data); t.Wait() && t.Error() != nil {
fmt.Println("failed to publish state: ", t.Error())
log.Warn().Msgf("failed to publish state: %v", t.Error())
}
if time.Now().After(cast.UpdateTime.Add(time.Hour * 1)) {
if debug {
fmt.Println("updating cast")
}
cast, err = loadCast(s.UseTestData, loc, apiKey)
if err != nil {
fmt.Println("Failed to retrieve MinuteCast: ", err)
}

if t := mqttClient.Publish("accu-mqtt/available", 0, true, "online"); t.Wait() && t.Error() != nil {
log.Warn().Msgf("failed to publish state: %v", t.Error())
}
}
}()
Expand All @@ -127,7 +189,7 @@ func (s *Start) Run(debug bool) error {
return nil
}

func registerSensors(debug bool) error {
func (c *cli) registerSensors() error {
registerRain := Registration{
Name: "Rain Indicator",
UniqueID: "a63ca366-9eda-4301-9428-93b173d15b9a_accu",
Expand All @@ -147,9 +209,7 @@ func registerSensors(debug bool) error {
},
}
data, _ := json.Marshal(&registerRain)
if debug {
fmt.Printf("Sending registration payload:\n%v\n", registerRain)
}
log.Debug().Msgf("Sending registration payload:\n%v\n", registerRain)

if t := mqttClient.Publish("homeassistant/sensor/accu-mqtt/rain/config", 0, false, data); t.Wait() && t.Error() != nil {
return t.Error()
Expand All @@ -174,9 +234,8 @@ func registerSensors(debug bool) error {
},
}
data, _ = json.Marshal(&registerStartSensor)
if debug {
fmt.Printf("Sending registration payload:\n%v\n", registerStartSensor)
}

log.Debug().Msgf("Sending registration payload:\n%v\n", registerStartSensor)

if t := mqttClient.Publish("homeassistant/sensor/accu-mqtt/rainstart/config", 0, false, data); t.Wait() && t.Error() != nil {
return t.Error()
Expand All @@ -201,9 +260,7 @@ func registerSensors(debug bool) error {
},
}
data, _ = json.Marshal(&registerEndSensor)
if debug {
fmt.Printf("Sending registration payload:\n%v\n", registerEndSensor)
}
log.Debug().Msgf("Sending registration payload:\n%v\n", registerEndSensor)

if t := mqttClient.Publish("homeassistant/sensor/accu-mqtt/rainend/config", 0, false, data); t.Wait() && t.Error() != nil {
return t.Error()
Expand All @@ -212,54 +269,9 @@ func registerSensors(debug bool) error {
return nil
}

func loadCast(useTestData bool, loc string, apiKey string) (cast MinuteCast, err error) {
hClient := http.Client{}
hClient.Timeout = time.Second * 15

// try to load existing data and check if valid to avoid another query. (e.g. on restarts)
func (c *cli) loadCast() (cast MinuteCast, err error) {
data, _ := os.ReadFile("./last_update.json")
err = json.Unmarshal(data, &cast)
if err == nil && time.Since(cast.UpdateTime) < 1*time.Hour {
fmt.Println("using existing cast data from file")
return cast, err
}

if !useTestData {
res, err := hClient.Get(fmt.Sprintf("https://dataservice.accuweather.com/forecasts/v1/minute?q=%s&apikey=%s", loc, apiKey))
if err != nil {
return cast, err
}

if res.StatusCode < 199 || res.StatusCode > 299 {
data, _ := io.ReadAll(res.Body)
return cast, fmt.Errorf("failed to request cast from live api: status [%d]: %s", res.StatusCode, string(data))
}

defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
return cast, err
}

err = json.Unmarshal(data, &cast)
if err != nil {
return cast, err
}
cast.UpdateTime = time.Now()
if cli.Debug {
fmt.Println("Received live cast: ", string(data))
}

data, _ = json.Marshal(&cast)
os.WriteFile("./last_update.json", data, 0777)
}

data, _ = os.ReadFile("./last_update.json")
err = json.Unmarshal(data, &cast)
if err != nil {
return cast, err
}

return
}

Expand Down

0 comments on commit 8f8985d

Please sign in to comment.