diff --git a/go.mod b/go.mod index 2190723f9..78f17650e 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/itchyny/gojq v0.12.4 github.com/jeremywohl/flatten v1.0.1 github.com/jinzhu/copier v0.3.2 - github.com/joeshaw/carwings v0.0.0-20210208214325-dacfdd3d7acc + github.com/joeshaw/carwings v0.0.0-20210616193757-7fbc6f7c3c73 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/compress v1.13.1 // indirect github.com/koron/go-ssdp v0.0.2 diff --git a/go.sum b/go.sum index 39c5dbd1e..580e9123e 100644 --- a/go.sum +++ b/go.sum @@ -383,8 +383,8 @@ github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w= github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= github.com/joeshaw/carwings v0.0.0-20191118152321-61b46581307a/go.mod h1:tB0OlpicmRVTL1Vksc5XRiYo+wkK2kl/GI7eGMIl6Rs= -github.com/joeshaw/carwings v0.0.0-20210208214325-dacfdd3d7acc h1:lvs05O5riVMhA70SOK53fthEZ5lMDHHy51ZKDldr0aE= -github.com/joeshaw/carwings v0.0.0-20210208214325-dacfdd3d7acc/go.mod h1:tB0OlpicmRVTL1Vksc5XRiYo+wkK2kl/GI7eGMIl6Rs= +github.com/joeshaw/carwings v0.0.0-20210616193757-7fbc6f7c3c73 h1:0GC7kKnW8vFHZB1txMdeiaVx1X65V9x8sWi5xoxUCfc= +github.com/joeshaw/carwings v0.0.0-20210616193757-7fbc6f7c3c73/go.mod h1:tB0OlpicmRVTL1Vksc5XRiYo+wkK2kl/GI7eGMIl6Rs= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jsgoecke/tesla v0.0.0-20200530171421-e02ebd220e5a/go.mod h1:GlLJU/C6KA3FEAIZTFDvIDlNGE1+YlKGHHXdNVkt5ow= diff --git a/vehicle/carwings.go b/vehicle/carwings.go index e4c3b044d..13295a301 100644 --- a/vehicle/carwings.go +++ b/vehicle/carwings.go @@ -2,11 +2,15 @@ package vehicle import ( "errors" + "net" + "net/http" + "sync" "time" "github.com/andig/evcc/api" "github.com/andig/evcc/provider" "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" "github.com/joeshaw/carwings" ) @@ -19,10 +23,11 @@ const ( // CarWings is an api.Vehicle implementation for CarWings cars type CarWings struct { *embed - log *util.Logger + wg sync.WaitGroup user, password string session *carwings.Session statusG func() (interface{}, error) + climateG func() (interface{}, error) refreshKey string refreshTime time.Time } @@ -34,9 +39,9 @@ func init() { // NewCarWingsFromConfig creates a new vehicle func NewCarWingsFromConfig(other map[string]interface{}) (api.Vehicle, error) { cc := struct { - embed `mapstructure:",squash"` - User, Password, Region string - Cache time.Duration + embed `mapstructure:",squash"` + User, Password, Region, VIN string + Cache time.Duration }{ Region: carwings.RegionEurope, Cache: interval, @@ -50,18 +55,54 @@ func NewCarWingsFromConfig(other map[string]interface{}) (api.Vehicle, error) { return nil, errors.New("missing credentials") } - log := util.NewLogger("carwin") + // http client with high dial/handshake timeout + const timeout = 90 * time.Second + log := util.NewLogger("carwings") + + transport := request.NewTripper(log, &http.Transport{ + Proxy: http.ProxyFromEnvironment, // default + DialContext: (&net.Dialer{ + Timeout: timeout, + KeepAlive: 30 * time.Second, // default + }).DialContext, + TLSHandshakeTimeout: timeout, + ForceAttemptHTTP2: true, // default + MaxIdleConns: 100, // default + IdleConnTimeout: 90 * time.Second, // default + ExpectContinueTimeout: 1 * time.Second, // default + }) + + carwings.Client = &http.Client{ + Timeout: timeout, + Transport: transport, + } v := &CarWings{ embed: &cc.embed, - log: log, user: cc.User, password: cc.Password, - session: &carwings.Session{Region: cc.Region}, + session: &carwings.Session{ + Region: cc.Region, + VIN: cc.VIN, + }, } + // initial connect + v.wg.Add(1) + go func() { + if err := v.session.Connect(v.user, v.password); err != nil { + log.ERROR.Println("login failed:", err) + } + v.wg.Done() + }() + v.statusG = provider.NewCached(func() (interface{}, error) { - return nil, v.status() + return v.status() + }, cc.Cache).InterfaceGetter() + + v.climateG = provider.NewCached(func() (interface{}, error) { + v.wg.Wait() // initial connect + return v.session.ClimateControlStatus() }, cc.Cache).InterfaceGetter() return v, nil @@ -70,6 +111,7 @@ func NewCarWingsFromConfig(other map[string]interface{}) (api.Vehicle, error) { // connectIfRequired will return ErrMustRetry if ErrNotLoggedIn error could be resolved func (v *CarWings) connectIfRequired(err error) error { if err == carwings.ErrNotLoggedIn || err.Error() == "received status code 404" { + v.wg.Wait() // initial connect if err = v.session.Connect(v.user, v.password); err == nil { err = api.ErrMustRetry } @@ -77,17 +119,22 @@ func (v *CarWings) connectIfRequired(err error) error { return err } -func (v *CarWings) status() error { +func (v *CarWings) status() (interface{}, error) { + v.wg.Wait() // initial connect + + // api result is stale + if v.refreshKey != "" { + if err := v.refreshResult(); err != nil { + return nil, err + } + } + bs, err := v.session.BatteryStatus() + if err == nil { if elapsed := time.Since(bs.Timestamp); elapsed > carwingsStatusExpiry { - // api result is stale - if v.refreshKey != "" { - return v.refreshResult() - } - if err = v.refreshRequest(); err != nil { - return err + return nil, err } err = api.ErrMustRetry @@ -100,7 +147,7 @@ func (v *CarWings) status() error { err = v.connectIfRequired(err) } - return err + return bs, err } // refreshResult triggers an update if not already in progress, otherwise gets result @@ -142,34 +189,28 @@ func (v *CarWings) refreshRequest() (err error) { } // SoC implements the api.Vehicle interface -func (v *CarWings) SoC() (soc float64, err error) { - soc = 0 - - if _, err = v.statusG(); err == nil { - var bs carwings.BatteryStatus - if bs, err = v.session.BatteryStatus(); err == nil { - soc = float64(bs.StateOfCharge) - } +func (v *CarWings) SoC() (float64, error) { + res, err := v.statusG() + if res, ok := res.(carwings.BatteryStatus); err == nil && ok { + return float64(res.StateOfCharge), nil } - return soc, err + return 0, err } var _ api.ChargeState = (*CarWings)(nil) // Status implements the api.ChargeState interface -func (v *CarWings) Status() (status api.ChargeStatus, err error) { - status = api.StatusA // disconnected +func (v *CarWings) Status() (api.ChargeStatus, error) { + status := api.StatusA // disconnected - if _, err = v.statusG(); err == nil { - var bs carwings.BatteryStatus - if bs, err = v.session.BatteryStatus(); err == nil { - if bs.PluginState == carwings.Connected { - status = api.StatusB // connected, not charging - } - if bs.ChargingStatus == carwings.NormalCharging { - status = api.StatusC // charging - } + res, err := v.statusG() + if res, ok := res.(carwings.BatteryStatus); err == nil && ok { + if res.PluginState == carwings.Connected { + status = api.StatusB // connected, not charging + } + if res.ChargingStatus == carwings.NormalCharging { + status = api.StatusC // charging } } @@ -179,30 +220,25 @@ func (v *CarWings) Status() (status api.ChargeStatus, err error) { var _ api.VehicleRange = (*CarWings)(nil) // Range implements the api.VehicleRange interface -func (v *CarWings) Range() (rng int64, err error) { - rng = 0 - - if _, err = v.statusG(); err == nil { - var bs carwings.BatteryStatus - if bs, err = v.session.BatteryStatus(); err == nil { - rng = int64(bs.CruisingRangeACOn) / 1000 - } +func (v *CarWings) Range() (int64, error) { + res, err := v.statusG() + if res, ok := res.(carwings.BatteryStatus); err == nil && ok { + return int64(res.CruisingRangeACOn) / 1000, nil } - return rng, err + return 0, err } var _ api.VehicleClimater = (*CarWings)(nil) // Climater implements the api.VehicleClimater interface func (v *CarWings) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) { - if _, err = v.statusG(); err == nil { - var ccs carwings.ClimateStatus - if ccs, err = v.session.ClimateControlStatus(); err == nil { - active = ccs.Running - targetTemp = float64(ccs.Temperature) - outsideTemp = targetTemp - } + res, err := v.climateG() + + if res, ok := res.(carwings.ClimateStatus); err == nil && ok { + active = res.Running + targetTemp = float64(res.Temperature) + outsideTemp = targetTemp return active, outsideTemp, targetTemp, err } diff --git a/vehicle/nissan.go b/vehicle/nissan.go index 6a85e1ece..803fb29f7 100644 --- a/vehicle/nissan.go +++ b/vehicle/nissan.go @@ -32,12 +32,11 @@ func init() { // NewNissanFromConfig creates a new vehicle func NewNissanFromConfig(other map[string]interface{}) (api.Vehicle, error) { cc := struct { - embed `mapstructure:",squash"` - User, Password, Region, VIN string - Cache time.Duration + embed `mapstructure:",squash"` + User, Password, VIN string + Cache time.Duration }{ - Region: "de_DE", - Cache: interval, + Cache: interval, } if err := util.DecodeOther(other, &cc); err != nil {