diff --git a/vehicle/bluelink.go b/vehicle/bluelink.go index 91f1c4703..22c55f05e 100644 --- a/vehicle/bluelink.go +++ b/vehicle/bluelink.go @@ -96,8 +96,23 @@ func newBluelinkFromConfig(brand string, other map[string]any, settings bluelink v := &Bluelink{ embed: &cc.embed, - Provider: bluelink.NewProvider(api, vehicle, cc.Expiry, cc.Cache), + Provider: bluelink.NewProvider(api, vehicle, cc.Cache), } + // Try to fetch battery capacity from vehicle if the user didn't provide one in the configuration + var capacity float64 = 0 + if v.Capacity_ == 0 { + capacity = fetchVehicleCapacity(api, vehicle, capacity) + } + + v.fromVehicle(vehicle.VehicleName, capacity) + return v, nil } + +func fetchVehicleCapacity(api *bluelink.API, vehicle bluelink.Vehicle, capacity float64) float64 { + if res, err := api.StatusLatest(vehicle); err == nil { + capacity, _ = res.Capacity() + } + return capacity +} diff --git a/vehicle/bluelink/api.go b/vehicle/bluelink/api.go index dd3560133..bf73deaa2 100644 --- a/vehicle/bluelink/api.go +++ b/vehicle/bluelink/api.go @@ -14,10 +14,9 @@ import ( const ( VehiclesURL = "vehicles" - StatusURL = "vehicles/%s/status" - StatusLatestURL = "vehicles/%s/status/latest" - StatusURLCCS2 = "vehicles/%s/ccs2/carstatus" - StatusLatestURLCCS2 = "vehicles/%s/ccs2/carstatus/latest" + StatusURL = "vehicles/%s/status" // Triggers refresh from vehicle (older API) + StatusLatestURL = "vehicles/%s/status/latest" // Cached data with location/odometer (older API) + StatusLatestURLCCS2 = "vehicles/%s/ccs2/carstatus/latest" // Newer API (2024+ vehicles with ccOS) ) const ( @@ -69,7 +68,7 @@ func (v *API) Vehicles() ([]Vehicle, error) { return res.ResMsg.Vehicles, err } -// StatusLatest retrieves the latest server-side status +// StatusLatest retrieves vehicle status (triggers refresh for older API, then returns cached data) func (v *API) StatusLatest(vehicle Vehicle) (BluelinkVehicleStatusLatest, error) { vid := vehicle.VehicleID @@ -83,6 +82,9 @@ func (v *API) StatusLatest(vehicle Vehicle) (BluelinkVehicleStatusLatest, error) return res, err } + // For older API: first trigger refresh, then get latest cached data + _ = v.Refresh(vehicle) // Ignore error, will retry with /status/latest + var res StatusLatestResponse uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusLatestURL, vid)) err := v.GetJSON(uri, &res) @@ -92,25 +94,13 @@ func (v *API) StatusLatest(vehicle Vehicle) (BluelinkVehicleStatusLatest, error) return res, err } -// StatusPartial refreshes the status -func (v *API) StatusPartial(vehicle Vehicle) (BluelinkVehicleStatus, error) { - vid := vehicle.VehicleID - - if vehicle.CcuCCS2ProtocolSupport != 0 { - var res StatusLatestResponseCCS - uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusLatestURLCCS2, vid)) - err := v.GetJSON(uri, &res) - if err == nil && res.RetCode != resOK { - err = fmt.Errorf("unexpected response: %s", res.RetCode) - } - return res, err - } - +// Refresh triggers a status update from the vehicle +func (v *API) Refresh(vehicle Vehicle) error { var res StatusResponse - uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusURL, vid)) + uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusURL, vehicle.VehicleID)) err := v.GetJSON(uri, &res) if err == nil && res.RetCode != resOK { err = fmt.Errorf("unexpected response: %s", res.RetCode) } - return res.ResMsg, err + return err } diff --git a/vehicle/bluelink/provider.go b/vehicle/bluelink/provider.go index 81b916b28..3009f4b30 100644 --- a/vehicle/bluelink/provider.go +++ b/vehicle/bluelink/provider.go @@ -7,88 +7,22 @@ import ( "github.com/evcc-io/evcc/util" ) -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 + statusG func() (BluelinkVehicleStatusLatest, error) + refresh func() error } -// 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, +// NewProvider creates a new BlueLink API +func NewProvider(api *API, vehicle Vehicle, cache time.Duration) *Provider { + impl := &Provider{ + statusG: util.Cached(func() (BluelinkVehicleStatusLatest, error) { + return api.StatusLatest(vehicle) + }, cache), + refresh: func() error { return api.Refresh(vehicle) }, } - - v.statusG = util.Cached(func() (BluelinkVehicleStatus, error) { - return v.status( - func() (BluelinkVehicleStatusLatest, error) { return api.StatusLatest(vehicle) }, - ) - }, cache) - - v.statusLG = util.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 + return impl } var _ api.Battery = (*Provider)(nil) @@ -104,7 +38,7 @@ func (v *Provider) Soc() (float64, error) { var _ api.ChargeState = (*Provider)(nil) -// Status implements the api.Battery interface +// Status implements the api.ChargeState interface func (v *Provider) Status() (api.ChargeStatus, error) { status := api.StatusNone res, err := v.statusG() @@ -138,9 +72,9 @@ func (v *Provider) Range() (int64, error) { var _ api.VehicleOdometer = (*Provider)(nil) -// Range implements the api.VehicleRange interface +// Odometer implements the api.VehicleOdometer interface func (v *Provider) Odometer() (float64, error) { - res, err := v.statusLG() + res, err := v.statusG() if err != nil { return 0, err } @@ -173,7 +107,7 @@ var _ api.VehiclePosition = (*Provider)(nil) // Position implements the api.VehiclePosition interface func (v *Provider) Position() (float64, float64, error) { - res, err := v.statusLG() + res, err := v.statusG() if err != nil { return 0, 0, err } @@ -184,7 +118,6 @@ 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 + // Triggers refresh from vehicle + return v.refresh() } diff --git a/vehicle/bluelink/types.go b/vehicle/bluelink/types.go index ada9edb57..4153cb240 100644 --- a/vehicle/bluelink/types.go +++ b/vehicle/bluelink/types.go @@ -7,7 +7,7 @@ import ( "github.com/evcc-io/evcc/api" ) -type BluelinkVehicleStatus interface { +type BluelinkVehicleStatusLatest interface { Updated() (time.Time, error) SoC() (float64, error) Status() (api.ChargeStatus, error) @@ -15,12 +15,9 @@ type BluelinkVehicleStatus interface { Range() (int64, error) Climater() (bool, error) GetLimitSoc() (int64, error) -} - -type BluelinkVehicleStatusLatest interface { - BluelinkVehicleStatus() BluelinkVehicleStatus Odometer() (float64, error) Position() (float64, float64, error) + Capacity() (float64, error) } type VehiclesResponse struct { @@ -33,7 +30,7 @@ type VehiclesResponse struct { type StatusResponse struct { RetCode string ResCode string - ResMsg VehicleStatus + // ResMsg is intentionally omitted - structure varies between old/CCS2 models } type StatusLatestResponse struct { @@ -168,8 +165,32 @@ func (d VehicleStatus) GetLimitSoc() (int64, error) { return 0, api.ErrNotAvailable } -func (d StatusLatestResponse) BluelinkVehicleStatus() BluelinkVehicleStatus { - return d.ResMsg.VehicleStatusInfo.VehicleStatus +func (d StatusLatestResponse) Updated() (time.Time, error) { + return d.ResMsg.VehicleStatusInfo.VehicleStatus.Updated() +} + +func (d StatusLatestResponse) SoC() (float64, error) { + return d.ResMsg.VehicleStatusInfo.VehicleStatus.SoC() +} + +func (d StatusLatestResponse) Status() (api.ChargeStatus, error) { + return d.ResMsg.VehicleStatusInfo.VehicleStatus.Status() +} + +func (d StatusLatestResponse) FinishTime() (time.Time, error) { + return d.ResMsg.VehicleStatusInfo.VehicleStatus.FinishTime() +} + +func (d StatusLatestResponse) Range() (int64, error) { + return d.ResMsg.VehicleStatusInfo.VehicleStatus.Range() +} + +func (d StatusLatestResponse) Climater() (bool, error) { + return d.ResMsg.VehicleStatusInfo.VehicleStatus.Climater() +} + +func (d StatusLatestResponse) GetLimitSoc() (int64, error) { + return d.ResMsg.VehicleStatusInfo.VehicleStatus.GetLimitSoc() } func (d StatusLatestResponse) Odometer() (float64, error) { @@ -187,6 +208,10 @@ func (d StatusLatestResponse) Position() (float64, float64, error) { return 0, 0, api.ErrNotAvailable } +func (d StatusLatestResponse) Capacity() (float64, error) { + return 0, api.ErrNotAvailable +} + type StatusLatestResponseCCS struct { RetCode string ResCode string @@ -279,10 +304,6 @@ type VehicleStatusCCS struct { } } -func (d StatusLatestResponseCCS) BluelinkVehicleStatus() BluelinkVehicleStatus { - return d -} - func (d StatusLatestResponseCCS) Updated() (time.Time, error) { epoch, err := strconv.ParseInt(d.ResMsg.LastUpdateTime, 10, 64) if err != nil { @@ -351,3 +372,14 @@ func (d StatusLatestResponseCCS) Position() (float64, float64, error) { } return 0, 0, api.ErrNotAvailable } + +func (d StatusLatestResponseCCS) Capacity() (float64, error) { + if d.ResMsg.State.Vehicle.Green != nil { + // Convert from Kilo Joules to kWh: 1 kWh = 3,600kJ + capacityKWh := d.ResMsg.State.Vehicle.Green.BatteryManagement.BatteryCapacity.Value / 3600 + if capacityKWh > 0 { + return capacityKWh, nil + } + } + return 0, api.ErrNotAvailable +}