From 89615f399fa31a357abe301e6baabca0ba4ec827 Mon Sep 17 00:00:00 2001 From: Kyrill Poole Date: Tue, 16 Jan 2024 16:28:10 +0000 Subject: [PATCH 1/4] Add better error logging/handling Rather than just returnin an error, wrap where the error has come from so we know what's borked a bit better. --- cmd/cmd.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 551654e..ae958c2 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -87,13 +87,13 @@ func (c *Client) Connect() error { log.Info().Msgf("connecting to %s", dialURL.String()) conn, _, err := dialer.Dial(dialURL.String(), nil) if err != nil { - return err + return fmt.Errorf("dial: %w", err) } // Read the initial message var initMsg map[string]any if err := conn.ReadJSON(&initMsg); err != nil { - return err + return fmt.Errorf("initial message: %w", err) } // Send the authentication message @@ -101,13 +101,13 @@ func (c *Client) Connect() error { "type": "auth", "access_token": viper.GetString("api_key"), }); err != nil { - return err + return fmt.Errorf("auth message: %w", err) } // Read the authentication response var authResp map[string]any if err := conn.ReadJSON(&authResp); err != nil { - return err + return fmt.Errorf("auth response: %w", err) } if authResp["type"] != "auth_ok" { return fmt.Errorf("authentication failed: %v", authResp["message"]) From fbea839eabfc6d368e9e8d627a7cf10ec43d48af Mon Sep 17 00:00:00 2001 From: Kyrill Poole Date: Tue, 16 Jan 2024 16:28:57 +0000 Subject: [PATCH 2/4] Update vendor dependencies --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 95f84bb..50c36c0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/gorilla/websocket v1.5.0 + github.com/olekukonko/tablewriter v0.0.5 github.com/rs/zerolog v1.30.0 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 @@ -21,7 +22,6 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/olekukonko/tablewriter v0.0.5 github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect From 65043d629e26e4554aa8e6dbf24aa66189f5f795 Mon Sep 17 00:00:00 2001 From: Kyrill Poole Date: Tue, 16 Jan 2024 17:01:56 +0000 Subject: [PATCH 3/4] Ensure sensor_id is set If we don't have this configured, the app has no way to know what sensor to read. Previously the value was hard-coded which was stupid, since not everyone is me and don't have all the same sensor values. --- cmd/cmd.go | 15 ++++++++++++--- cmd/root.go | 4 +++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index ae958c2..63996e6 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -25,7 +25,7 @@ type APIResponse struct { Type string `json:"type"` // Type is the type of the response. Success bool `json:"success"` // Success indicates whether the response was successful or not. Result struct { - SmartMeterElectricityImport []struct { + ImportedElectricity []struct { Start int `json:"start"` End int `json:"end"` Change float64 `json:"change"` // Change is the amount of electricity imported. @@ -89,6 +89,7 @@ func (c *Client) Connect() error { if err != nil { return fmt.Errorf("dial: %w", err) } + log.Info().Msg("connected") // Read the initial message var initMsg map[string]any @@ -112,6 +113,7 @@ func (c *Client) Connect() error { if authResp["type"] != "auth_ok" { return fmt.Errorf("authentication failed: %v", authResp["message"]) } + log.Info().Msg("authenticated") c.Conn = conn return nil @@ -240,12 +242,18 @@ func getResults(c *Client) ([][]float64, error) { offset := time.Duration((i+1)*24) * time.Hour start := time.Now().Add(-offset).Truncate(24 * time.Hour).Format("2006-01-02T15:04:05.000Z") + + sensorID := viper.GetString("sensor_id") + if sensorID == "" { + return nil, fmt.Errorf("sensor_id is required") + } + msg := map[string]interface{}{ "id": c.MessageID, "type": "recorder/statistics_during_period", "start_time": start, "end_time": time.Now().Truncate(24 * time.Hour).Format("2006-01-02T15:04:05.000Z"), - "statistic_ids": []string{"sensor.smart_meter_electricity_import_2"}, + "statistic_ids": []string{sensorID}, "period": "hour", "types": []string{"change"}, "units": map[string]string{ @@ -267,8 +275,9 @@ func getResults(c *Client) ([][]float64, error) { return nil, fmt.Errorf("api response error: %v", data.Error) } changeSlice := make([]float64, hoursInADay) + log.Debug().Msgf("got %d results", len(data.Result.ImportedElectricity)) for j := range changeSlice { - changeSlice[j] = data.Result.SmartMeterElectricityImport[j].Change + changeSlice[j] = data.Result.ImportedElectricity[j].Change } results[i] = changeSlice } diff --git a/cmd/root.go b/cmd/root.go index e3141b9..7c731b5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -94,16 +94,18 @@ func initConfig() { func promtUserConfig() error { urlPrompt := prompter.Prompt("Home Assistant URL - e.g. http://localhost:8123", "") token := prompter.Password("Home Assistant Long-Lived Access Token") + sensor := prompter.Prompt("Power sensor entity ID - e.g. sensor.power", "") haURL, err := url.Parse(urlPrompt) if haURL.Scheme == "" { haURL.Scheme = "http" } if err != nil { - return err + return fmt.Errorf("parsing URL: %w", err) } viper.Set("api_key", token) viper.Set("url", haURL.String()) + viper.Set("sensor", sensor) return nil } From de376a68fe2144de2ba82ab8e90c862dd68e08ba Mon Sep 17 00:00:00 2001 From: Kyrill Poole Date: Tue, 16 Jan 2024 17:05:10 +0000 Subject: [PATCH 4/4] Handle missing results gracefully Don't just panic if we don't get the expected number of results from the API. Instead, return a sensible error so the user doesn't see a horrible stacktrace. --- cmd/cmd.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/cmd.go b/cmd/cmd.go index 63996e6..ff12080 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -274,6 +274,11 @@ func getResults(c *Client) ([][]float64, error) { if !data.Success { return nil, fmt.Errorf("api response error: %v", data.Error) } + + if len(data.Result.ImportedElectricity) != hoursInADay { + return nil, fmt.Errorf("expected %d sets of results, got %d", hoursInADay, len(data.Result.ImportedElectricity)) + } + changeSlice := make([]float64, hoursInADay) log.Debug().Msgf("got %d results", len(data.Result.ImportedElectricity)) for j := range changeSlice {