evcc-io/vehicle/bluelink/provider.go

190 lines
4.3 KiB
Go

package bluelink
import (
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/plugin"
)
const refreshTimeout = 2 * time.Minute
// Provider implements the vehicle api.
// Based on https://github.com/Hacksore/bluelinky.
type Provider struct {
statusG func() (BluelinkVehicleStatus, error)
statusLG func() (BluelinkVehicleStatusLatest, error)
refreshG func() (BluelinkVehicleStatus, error)
expiry time.Duration
refreshTime time.Time
}
// New creates a new BlueLink API
func NewProvider(api *API, vehicle Vehicle, expiry, cache time.Duration) *Provider {
v := &Provider{
refreshG: func() (BluelinkVehicleStatus, error) {
return api.StatusPartial(vehicle)
},
expiry: expiry,
}
v.statusG = plugin.Cached(func() (BluelinkVehicleStatus, error) {
return v.status(
func() (BluelinkVehicleStatusLatest, error) { return api.StatusLatest(vehicle) },
)
}, cache)
v.statusLG = plugin.Cached(func() (BluelinkVehicleStatusLatest, error) {
return api.StatusLatest(vehicle)
}, cache)
return v
}
// status wraps the api status call and adds status refresh
func (v *Provider) status(statusG func() (BluelinkVehicleStatusLatest, error)) (BluelinkVehicleStatus, error) {
res, err := statusG()
var ts time.Time
if err == nil {
ts, err = res.BluelinkVehicleStatus().Updated()
if err != nil {
return res.BluelinkVehicleStatus(), err
}
// return the current value
if time.Since(ts) <= v.expiry {
v.refreshTime = time.Time{}
return res.BluelinkVehicleStatus(), nil
}
}
// request a refresh, irrespective of a previous error
if v.refreshTime.IsZero() {
v.refreshTime = time.Now()
// TODO async refresh
res, err := v.refreshG()
if err == nil {
if ts, err = res.Updated(); err == nil && time.Since(ts) <= v.expiry {
v.refreshTime = time.Time{}
return res, nil
}
err = api.ErrMustRetry
}
return nil, err
}
// refresh finally expired
if time.Since(v.refreshTime) > refreshTimeout {
v.refreshTime = time.Time{}
if err == nil {
err = api.ErrTimeout
}
} else {
// wait for refresh, irrespective of a previous error
err = api.ErrMustRetry
}
return nil, err
}
var _ api.Battery = (*Provider)(nil)
// Soc implements the api.Battery interface
func (v *Provider) Soc() (float64, error) {
res, err := v.statusG()
if err != nil {
return 0, err
}
return res.SoC()
}
var _ api.ChargeState = (*Provider)(nil)
// Status implements the api.Battery interface
func (v *Provider) Status() (api.ChargeStatus, error) {
status := api.StatusNone
res, err := v.statusG()
if err != nil {
return status, err
}
return res.Status()
}
var _ api.VehicleFinishTimer = (*Provider)(nil)
// FinishTime implements the api.VehicleFinishTimer interface
func (v *Provider) FinishTime() (time.Time, error) {
res, err := v.statusG()
if err != nil {
return time.Time{}, err
}
return res.FinishTime()
}
var _ api.VehicleRange = (*Provider)(nil)
// Range implements the api.VehicleRange interface
func (v *Provider) Range() (int64, error) {
res, err := v.statusG()
if err != nil {
return 0, err
}
return res.Range()
}
var _ api.VehicleOdometer = (*Provider)(nil)
// Range implements the api.VehicleRange interface
func (v *Provider) Odometer() (float64, error) {
res, err := v.statusLG()
if err != nil {
return 0, err
}
return res.Odometer()
}
var _ api.VehicleClimater = (*Provider)(nil)
// Climater implements the api.VehicleClimater interface
func (v *Provider) Climater() (bool, error) {
res, err := v.statusG()
if err != nil {
return false, err
}
return res.Climater()
}
var _ api.SocLimiter = (*Provider)(nil)
// GetLimitSoc implements the api.SocLimiter interface
func (v *Provider) GetLimitSoc() (int64, error) {
res, err := v.statusG()
if err != nil {
return 0, err
}
return res.GetLimitSoc()
}
var _ api.VehiclePosition = (*Provider)(nil)
// Position implements the api.VehiclePosition interface
func (v *Provider) Position() (float64, float64, error) {
res, err := v.statusLG()
if err != nil {
return 0, 0, err
}
return res.Position()
}
var _ api.Resurrector = (*Provider)(nil)
// WakeUp implements the api.Resurrector interface
func (v *Provider) WakeUp() error {
// forcing an update will usually make the car start charging even if the (first) resulting status still says it does not charge...
_, err := v.refreshG()
return err
}