diff --git a/vehicle/carwings.go b/vehicle/carwings.go index 2d7779495..e4c3b044d 100644 --- a/vehicle/carwings.go +++ b/vehicle/carwings.go @@ -1,6 +1,7 @@ package vehicle import ( + "errors" "time" "github.com/andig/evcc/api" @@ -10,14 +11,20 @@ import ( "github.com/joeshaw/carwings" ) +const ( + carwingsStatusExpiry = 5 * time.Minute // if returned status value is older, evcc will init refresh + carwingsRefreshTimeout = 2 * time.Minute // timeout to get status after refresh +) + // CarWings is an api.Vehicle implementation for CarWings cars type CarWings struct { *embed + log *util.Logger user, password string - region string session *carwings.Session - chargeStateG func() (float64, error) - hvacG func() (interface{}, error) + statusG func() (interface{}, error) + refreshKey string + refreshTime time.Time } func init() { @@ -39,70 +46,166 @@ func NewCarWingsFromConfig(other map[string]interface{}) (api.Vehicle, error) { return nil, err } - v := &CarWings{ - embed: &cc.embed, - user: cc.User, - password: cc.Password, - region: cc.Region, + if cc.User == "" || cc.Password == "" { + return nil, errors.New("missing credentials") } - v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter() - v.hvacG = provider.NewCached(v.hvacAPI, cc.Cache).InterfaceGetter() + log := util.NewLogger("carwin") + + v := &CarWings{ + embed: &cc.embed, + log: log, + user: cc.User, + password: cc.Password, + session: &carwings.Session{Region: cc.Region}, + } + + v.statusG = provider.NewCached(func() (interface{}, error) { + return nil, v.status() + }, cc.Cache).InterfaceGetter() return v, nil } -// Init new carwings session -func (v *CarWings) initSession() error { - v.session = &carwings.Session{ - Region: v.region, - } - - return v.session.Connect(v.user, v.password) -} - -// chargeState implements the api.Vehicle interface -func (v *CarWings) chargeState() (float64, error) { - if v.session == nil { - if err := v.initSession(); err != nil { - return 0, api.ErrNotAvailable +// 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" { + if err = v.session.Connect(v.user, v.password); err == nil { + err = api.ErrMustRetry } } + return err +} +func (v *CarWings) status() error { bs, err := v.session.BatteryStatus() - return float64(bs.StateOfCharge), err -} + if err == nil { + if elapsed := time.Since(bs.Timestamp); elapsed > carwingsStatusExpiry { + // api result is stale + if v.refreshKey != "" { + return v.refreshResult() + } -// hvacAPI provides hvac-status api response -func (v *CarWings) hvacAPI() (interface{}, error) { - if v.session == nil { - if err := v.initSession(); err != nil { - return 0, api.ErrNotAvailable + if err = v.refreshRequest(); err != nil { + return err + } + + err = api.ErrMustRetry + } else { + // reset if elapsed < carwingsStatusExpiry, + // otherwise next check after soc timeout does not trigger update because refreshResult succeeds on old key + v.refreshKey = "" } + } else { + err = v.connectIfRequired(err) } - return v.session.ClimateControlStatus() + return err +} + +// refreshResult triggers an update if not already in progress, otherwise gets result +func (v *CarWings) refreshResult() error { + finished, err := v.session.CheckUpdate(v.refreshKey) + + // update successful and completed + if err == nil && finished { + v.refreshKey = "" + return nil + } + + // update still in progress, keep retrying + if time.Since(v.refreshTime) < carwingsRefreshTimeout { + return api.ErrMustRetry + } + + // give up + v.refreshKey = "" + if err == nil { + err = api.ErrTimeout + } + + return err +} + +// refreshRequest requests status refresh tracked by refreshKey +func (v *CarWings) refreshRequest() (err error) { + if v.refreshKey, err = v.session.UpdateStatus(); err == nil { + v.refreshTime = time.Now() + if v.refreshKey == "" { + err = errors.New("refresh failed") + } + } else { + err = v.connectIfRequired(err) + } + + return err } // SoC implements the api.Vehicle interface -func (v *CarWings) SoC() (float64, error) { - return v.chargeStateG() +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) + } + } + + return soc, 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 + + 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 + } + } + } + + return status, err +} + +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 + } + } + + return rng, err } var _ api.VehicleClimater = (*CarWings)(nil) -// Climater implements the api.Vehicle.Climater interface +// Climater implements the api.VehicleClimater interface func (v *CarWings) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) { - res, err := v.hvacG() - if res, ok := res.(carwings.ClimateStatus); err == nil && ok { - active = res.Running + 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 + } - targetTemp = float64(res.Temperature) - - outsideTemp = 0 //fixed value - - return active, outsideTemp, targetTemp, nil + return active, outsideTemp, targetTemp, err } - return false, 0, 0, api.ErrNotAvailable + return false, 0, 0, err }