diff --git a/api/http.go b/api/http.go new file mode 100644 index 0000000000..0f70d6761c --- /dev/null +++ b/api/http.go @@ -0,0 +1,94 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +type HTTPHelper struct { + Log *Logger + Client *http.Client +} + +// NewHTTPHelper creates http helper for simplified PUT GET logic +func NewHTTPHelper(log *Logger) *HTTPHelper { + r := &HTTPHelper{ + Log: log, + Client: &http.Client{}, + } + return r +} + +// Response codes other than HTTP 200 or 204 are raised as error +func (r *HTTPHelper) readBody(resp *http.Response, err error) ([]byte, error) { + if err != nil { + return []byte{}, err + } + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []byte{}, err + } + + if r.Log != nil { + r.Log.TRACE.Printf("%s\n%s", resp.Request.URL.String(), string(b)) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + return b, fmt.Errorf("unexpected response %d: %s", resp.StatusCode, string(b)) + } + + return b, nil +} + +func (r *HTTPHelper) decodeJSON(resp *http.Response, err error, res interface{}) ([]byte, error) { + b, err := r.readBody(resp, err) + if err == nil { + err = json.Unmarshal(b, &res) + } + + return b, err +} + +// Get executes HTTP GET request returns the response body +func (r *HTTPHelper) Get(url string) ([]byte, error) { + resp, err := r.Client.Get(url) + return r.readBody(resp, err) +} + +// Put executes HTTP PUT request returns the response body +func (r *HTTPHelper) Put(url string, body []byte) ([]byte, error) { + req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(body)) + if err != nil { + return []byte{}, err + } + + resp, err := r.Client.Do(req) + return r.readBody(resp, err) +} + +// RequestJSON executes HTTP request and decodes JSON response +func (r *HTTPHelper) RequestJSON(req *http.Request, res interface{}) ([]byte, error) { + resp, err := r.Client.Do(req) + return r.decodeJSON(resp, err, res) +} + +// GetJSON executes HTTP GET request and decodes JSON response +func (r *HTTPHelper) GetJSON(url string, res interface{}) ([]byte, error) { + resp, err := r.Client.Get(url) + return r.decodeJSON(resp, err, res) +} + +// PutJSON executes HTTP PUT request and returns the response body +func (r *HTTPHelper) PutJSON(url string, data interface{}) ([]byte, error) { + body, err := json.Marshal(data) + if err != nil { + return []byte{}, err + } + + return r.Put(url, body) +} diff --git a/charger/config.go b/charger/config.go index 50d72c2c37..63f3c52dac 100644 --- a/charger/config.go +++ b/charger/config.go @@ -1,11 +1,6 @@ package charger import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" "strings" "github.com/andig/evcc/api" @@ -36,45 +31,3 @@ func NewFromConfig(log *api.Logger, typ string, other map[string]interface{}) ap return c } - -func getJSON(url string, result interface{}) (*http.Response, []byte, error) { - resp, err := http.Get(url) - if err != nil { - return resp, []byte{}, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return resp, body, err - } - - if resp.StatusCode == http.StatusOK { - err = json.Unmarshal(body, &result) - return resp, body, err - } - - return resp, body, fmt.Errorf("unexpected status %d", resp.StatusCode) -} - -func putJSON(url string, request interface{}) (*http.Response, []byte, error) { - data, err := json.Marshal(request) - if err != nil { - return nil, []byte{}, err - } - - client := &http.Client{} - req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(data)) - if err != nil { - return nil, []byte{}, err - } - - resp, err := client.Do(req) - if err != nil { - return resp, []byte{}, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - return resp, body, err -} diff --git a/charger/go-e.go b/charger/go-e.go index 26509d3b84..00c0d61653 100644 --- a/charger/go-e.go +++ b/charger/go-e.go @@ -26,7 +26,7 @@ type goeStatusResponse struct { // GoE charger implementation type GoE struct { - log *api.Logger + *api.HTTPHelper URI string } @@ -41,11 +41,11 @@ func NewGoEFromConfig(log *api.Logger, other map[string]interface{}) api.Charger // NewGoE creates GoE charger func NewGoE(URI string) *GoE { c := &GoE{ - URI: URI, - log: api.NewLogger("go-e"), + HTTPHelper: api.NewHTTPHelper(api.NewLogger("go-e")), + URI: URI, } - c.log.WARN.Println("-- experimental --") + c.HTTPHelper.Log.WARN.Println("-- experimental --") return c } @@ -55,51 +55,55 @@ func (c *GoE) apiURL(api apiFunction) string { } func (c *GoE) getJSON(url string, result interface{}) error { - resp, body, err := getJSON(url, result) - c.log.TRACE.Printf("GET %s: %s", url, string(body)) - - if err != nil && len(body) == 0 { - return err + b, err := c.GetJSON(url, result) + if err != nil && len(b) > 0 { + var error goeStatusResponse + if err := json.Unmarshal(b, &error); err != nil { + return err + } + + return fmt.Errorf("response code: %d", error.Err) } - var error goeStatusResponse - _ = json.Unmarshal(body, &error) - - return fmt.Errorf("api %d: %d", resp.StatusCode, error.Err) + return err } // Status implements the Charger.Status interface func (c *GoE) Status() (api.ChargeStatus, error) { var status goeStatusResponse - err := c.getJSON(c.apiURL(goeStatus), status) + if err := c.getJSON(c.apiURL(goeStatus), status); err != nil { + return api.StatusNone, err + } switch status.Car { case 1: - return api.StatusA, err + return api.StatusA, nil case 2: - return api.StatusC, err + return api.StatusC, nil case 3: - return api.StatusB, err + return api.StatusB, nil case 4: - return api.StatusB, err + return api.StatusB, nil + default: + return api.StatusNone, fmt.Errorf("unknown result %d", status.Car) } - - return api.StatusNone, fmt.Errorf("unknown result %d", status.Car) } // Enabled implements the Charger.Enabled interface func (c *GoE) Enabled() (bool, error) { var status goeStatusResponse - err := c.getJSON(c.apiURL(goeStatus), status) + if err := c.getJSON(c.apiURL(goeStatus), status); err != nil { + return false, err + } switch status.Alw { case 0: - return false, err + return false, nil case 1: - return true, err + return true, nil + default: + return false, fmt.Errorf("unknown result %d", status.Alw) } - - return false, fmt.Errorf("unknown result %d", status.Alw) } // Enable implements the Charger.Enable interface diff --git a/charger/nrgkick.go b/charger/nrgkick.go index 155046c916..56d6106cd3 100644 --- a/charger/nrgkick.go +++ b/charger/nrgkick.go @@ -3,7 +3,6 @@ package charger import ( "encoding/json" "fmt" - "net/http" "github.com/andig/evcc/api" ) @@ -24,7 +23,7 @@ type NRGMeasurements struct { ChargingPower float64 } -// NRGSettings is the /api/setings request/response +// NRGSettings is the /api/settings request/response type NRGSettings struct { Info NRGInfo `json:"omitempty"` Values NRGValues @@ -59,7 +58,7 @@ type NRGDeviceMetadata struct { // NRGKick charger implementation type NRGKick struct { - log *api.Logger + *api.HTTPHelper IP string MacAddress string Password string @@ -76,13 +75,13 @@ func NewNRGKickFromConfig(log *api.Logger, other map[string]interface{}) api.Cha // NewNRGKick creates NRGKick charger func NewNRGKick(IP, MacAddress, Password string) *NRGKick { nrg := &NRGKick{ + HTTPHelper: api.NewHTTPHelper(api.NewLogger("kick")), IP: IP, MacAddress: MacAddress, Password: Password, - log: api.NewLogger("kick"), } - nrg.log.WARN.Println("-- experimental --") + nrg.HTTPHelper.Log.WARN.Println("-- experimental --") return nrg } @@ -92,35 +91,31 @@ func (nrg *NRGKick) apiURL(api apiFunction) string { } func (nrg *NRGKick) getJSON(url string, result interface{}) error { - resp, body, err := getJSON(url, result) - nrg.log.TRACE.Printf("GET %s: %s", url, string(body)) - - if err != nil && len(body) == 0 { - return err + b, err := nrg.GetJSON(url, result) + if err != nil && len(b) > 0 { + var error NRGResponse + if err := json.Unmarshal(b, &error); err != nil { + return err + } + + return fmt.Errorf("response: %s", error.Message) } - var error NRGResponse - _ = json.Unmarshal(body, &error) - - return fmt.Errorf("api %d: %s", resp.StatusCode, error.Message) + return err } func (nrg *NRGKick) putJSON(url string, request interface{}) error { - resp, body, err := putJSON(url, request) - nrg.log.TRACE.Printf("PUT %v: %s", resp, string(body)) - - if err != nil && len(body) == 0 { + b, err := nrg.PutJSON(url, request) + if err != nil && len(b) == 0 { return err } - if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent { - return nil - } - var error NRGResponse - _ = json.Unmarshal(body, &error) + if err := json.Unmarshal(b, &error); err != nil { + return err + } - return fmt.Errorf("api %d: %s", resp.StatusCode, error.Message) + return fmt.Errorf("response: %s", error.Message) } // Status implements the Charger.Status interface diff --git a/vehicle/audi.go b/vehicle/audi.go index 6de4b31525..9149f08292 100644 --- a/vehicle/audi.go +++ b/vehicle/audi.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "net/http" "net/url" "strings" @@ -20,15 +19,6 @@ const ( audiAuthPrefix = "AudiAuth 1" ) -// Audi is an api.Vehicle implementation for Audi cars -type Audi struct { - *embed - user, password, vin string - token string - tokenValid time.Time - chargeStateG provider.FloatGetter -} - type audiTokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` @@ -60,6 +50,16 @@ type audiBrStateOfCharge struct { Content int } +// Audi is an api.Vehicle implementation for Audi cars +type Audi struct { + *embed + *api.HTTPHelper + user, password, vin string + token string + tokenValid time.Time + chargeStateG provider.FloatGetter +} + // NewAudiFromConfig creates a new vehicle func NewAudiFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicle { cc := struct { @@ -71,10 +71,11 @@ func NewAudiFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicl api.DecodeOther(log, other, &cc) v := &Audi{ - embed: &embed{cc.Title, cc.Capacity}, - user: cc.User, - password: cc.Password, - vin: cc.VIN, + embed: &embed{cc.Title, cc.Capacity}, + HTTPHelper: api.NewHTTPHelper(api.NewLogger("audi")), + user: cc.User, + password: cc.Password, + vin: cc.VIN, } v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter() @@ -122,28 +123,14 @@ func (v *Audi) login(user, password string) error { req.Header.Set("Authorization", audiAuthPrefix) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - var er audiErrorResponse - if err = json.Unmarshal(b, &er); err == nil { - return errors.New(er.Description) - } - return fmt.Errorf("unexpected response %d: %s", resp.StatusCode, string(b)) - } - var tr audiTokenResponse - if err = json.Unmarshal(b, &tr); err != nil { + if b, err := v.RequestJSON(req, &tr); err != nil { + if len(b) > 0 { + var er audiErrorResponse + if err = json.Unmarshal(b, &er); err == nil { + return errors.New(er.Description) + } + } return err } @@ -180,20 +167,8 @@ func (v *Audi) chargeState() (float64, error) { return 0, err } - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return 0, err - } - defer resp.Body.Close() - - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return 0, err - } - var br audiBatteryResponse - err = json.Unmarshal(b, &br) + _, err = v.RequestJSON(req, &br) return float64(br.Charger.Status.BatteryStatusData.StateOfCharge.Content), err } diff --git a/vehicle/bmw.go b/vehicle/bmw.go index 37c0d5f2cc..f4d4747b9e 100644 --- a/vehicle/bmw.go +++ b/vehicle/bmw.go @@ -2,9 +2,7 @@ package vehicle import ( "encoding/base64" - "encoding/json" "fmt" - "io/ioutil" "net/http" "net/url" "strings" @@ -40,6 +38,7 @@ type bmwVehicleStatus struct { // BMW is an api.Vehicle implementation for BMW cars type BMW struct { *embed + *api.HTTPHelper user, password, vin string token, refreshToken string tokenValid time.Time @@ -57,10 +56,11 @@ func NewBMWFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicle api.DecodeOther(log, other, &cc) v := &BMW{ - embed: &embed{cc.Title, cc.Capacity}, - user: cc.User, - password: cc.Password, - vin: cc.VIN, + embed: &embed{cc.Title, cc.Capacity}, + HTTPHelper: api.NewHTTPHelper(api.NewLogger("bmwi")), + user: cc.User, + password: cc.Password, + vin: cc.VIN, } v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter() @@ -95,24 +95,8 @@ func (v *BMW) login(user, password string) error { req.Header.Set("Authorization", v.authHeader()) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected response %d: %s", resp.StatusCode, string(b)) - } - var tr bmwTokenResponse - if err = json.Unmarshal(b, &tr); err != nil { + if _, err = v.RequestJSON(req, &tr); err != nil { return err } @@ -150,20 +134,8 @@ func (v *BMW) chargeState() (float64, error) { return 0, err } - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return 0, err - } - defer resp.Body.Close() - - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return 0, err - } - var br bmwStatusResponse - err = json.Unmarshal(b, &br) + _, err = v.RequestJSON(req, &br) return float64(br.VehicleStatus.ChargingLevelHv), err }