Carwings: async soc refresh and additional apis (#1057)

This commit is contained in:
maku1604 2021-05-30 13:55:08 +02:00 • committed by GitHub
parent 6d58cf255d
commit e34877ff75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

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