KIA/Hyundai Bluelink: remove refresh logic (#25670)
This commit is contained in:
parent
893d610891
commit
6e367f379c
4 changed files with 87 additions and 117 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue