KIA/Hyundai Bluelink: remove refresh logic (#25670)

This commit is contained in:
Menno Luiten 2025-12-14 11:37:14 +01:00 • committed by GitHub
parent 893d610891
commit 6e367f379c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 87 additions and 117 deletions

View file

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

View file

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

View file

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

View file

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