Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

carwings async requests #1057

Merged
merged 16 commits into from
May 30, 2021
161 changes: 127 additions & 34 deletions vehicle/carwings.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package vehicle

import (
"errors"
"time"

"github.com/andig/evcc/api"
Expand All @@ -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() (bool, error)
refreshKey string
refreshTime time.Time
}

func init() {
Expand All @@ -39,70 +46,156 @@ func NewCarWingsFromConfig(other map[string]interface{}) (api.Vehicle, error) {
return nil, err
}

if cc.User == "" || cc.Password == "" {
return nil, errors.New("missing credentials")
}

log := util.NewLogger("carwin")

v := &CarWings{
embed: &cc.embed,
log: log,
user: cc.User,
password: cc.Password,
region: cc.Region,
session: &carwings.Session{Region: cc.Region},
}

v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
v.hvacG = provider.NewCached(v.hvacAPI, cc.Cache).InterfaceGetter()
v.statusG = provider.NewCached(func() (bool, error) {
return true, v.status()
}, cc.Cache).BoolGetter()

return v, nil
}

// Init new carwings session
func (v *CarWings) initSession() error {
v.session = &carwings.Session{
Region: v.region,
func (v *CarWings) status() error {
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
}

err = api.ErrMustRetry
}
} else if err == carwings.ErrNotLoggedIn {
if err = v.session.Connect(v.user, v.password); err == nil {
err = api.ErrMustRetry
}
}

return v.session.Connect(v.user, v.password)
return err
}

// 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
}
// 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
}

bs, err := v.session.BatteryStatus()
return float64(bs.StateOfCharge), err
// 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
}

// 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
// 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 if err == carwings.ErrNotLoggedIn {
if err = v.session.Connect(v.user, v.password); err == nil {
err = api.ErrMustRetry
}
}

return v.session.ClimateControlStatus()
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.VehicleClimater = (*CarWings)(nil)

// Climater implements the api.Vehicle.Climater 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
}

return active, outsideTemp, targetTemp, err
}

targetTemp = float64(res.Temperature)
return false, 0, 0, err
}

outsideTemp = 0 //fixed value
var _ api.VehicleRange = (*CarWings)(nil)

return active, outsideTemp, targetTemp, nil
// Range implements the api.VehicleRange interface
func (v *CarWings) Range() (Range int64, err error) {
Range = 0

if _, err = v.statusG(); err == nil {
var bs carwings.BatteryStatus
if bs, err = v.session.BatteryStatus(); err == nil {
Range = int64(bs.CruisingRangeACOn) / 1000
}
}

return Range, 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 false, 0, 0, api.ErrNotAvailable
return status, err
}