Bluelink: add CCS api support (#13713)
This commit is contained in:
parent
4109b4bf74
commit
bf584796c0
4 changed files with 310 additions and 94 deletions
|
|
@ -94,7 +94,7 @@ func newBluelinkFromConfig(brand string, other map[string]interface{}, settings
|
|||
|
||||
v := &Bluelink{
|
||||
embed: &cc.embed,
|
||||
Provider: bluelink.NewProvider(api, vehicle.VehicleID, cc.Expiry, cc.Cache),
|
||||
Provider: bluelink.NewProvider(api, vehicle, cc.Expiry, cc.Cache),
|
||||
}
|
||||
|
||||
return v, nil
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
VehiclesURL = "vehicles"
|
||||
StatusURL = "vehicles/%s/status"
|
||||
StatusLatestURL = "vehicles/%s/status/latest"
|
||||
VehiclesURL = "vehicles"
|
||||
StatusURL = "vehicles/%s/status"
|
||||
StatusLatestURL = "vehicles/%s/status/latest"
|
||||
StatusURLCCS2 = "vehicles/%s/ccs2/carstatus"
|
||||
StatusLatestURLCCS2 = "vehicles/%s/ccs2/carstatus/latest"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -51,6 +53,7 @@ func NewAPI(log *util.Logger, baseURI string, decorator func(*http.Request) erro
|
|||
|
||||
type Vehicle struct {
|
||||
VIN, VehicleName, VehicleID string
|
||||
CcuCCS2ProtocolSupport int
|
||||
}
|
||||
|
||||
func (v *API) Vehicles() ([]Vehicle, error) {
|
||||
|
|
@ -63,27 +66,47 @@ func (v *API) Vehicles() ([]Vehicle, error) {
|
|||
}
|
||||
|
||||
// StatusLatest retrieves the latest server-side status
|
||||
func (v *API) StatusLatest(vid string) (StatusLatestResponse, error) {
|
||||
var res StatusLatestResponse
|
||||
func (v *API) StatusLatest(vehicle Vehicle) (BluelinkVehicleStatusLatest, 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
|
||||
}
|
||||
|
||||
var res StatusLatestResponse
|
||||
uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusLatestURL, vid))
|
||||
err := v.GetJSON(uri, &res)
|
||||
if err == nil && res.RetCode != resOK {
|
||||
err = fmt.Errorf("unexpected response: %s", res.RetCode)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// StatusPartial refreshes the status
|
||||
func (v *API) StatusPartial(vid string) (StatusResponse, error) {
|
||||
var res StatusResponse
|
||||
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
|
||||
}
|
||||
|
||||
var res StatusResponse
|
||||
uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusURL, vid))
|
||||
err := v.GetJSON(uri, &res)
|
||||
if err == nil && res.RetCode != resOK {
|
||||
err = fmt.Errorf("unexpected response: %s", res.RetCode)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,50 +12,50 @@ const refreshTimeout = 2 * time.Minute
|
|||
// Provider implements the vehicle api.
|
||||
// Based on https://github.com/Hacksore/bluelinky.
|
||||
type Provider struct {
|
||||
statusG func() (VehicleStatus, error)
|
||||
statusLG func() (StatusLatestResponse, error)
|
||||
refreshG func() (StatusResponse, error)
|
||||
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, vid string, expiry, cache time.Duration) *Provider {
|
||||
func NewProvider(api *API, vehicle Vehicle, expiry, cache time.Duration) *Provider {
|
||||
v := &Provider{
|
||||
refreshG: func() (StatusResponse, error) {
|
||||
return api.StatusPartial(vid)
|
||||
refreshG: func() (BluelinkVehicleStatus, error) {
|
||||
return api.StatusPartial(vehicle)
|
||||
},
|
||||
expiry: expiry,
|
||||
}
|
||||
|
||||
v.statusG = provider.Cached(func() (VehicleStatus, error) {
|
||||
v.statusG = provider.Cached(func() (BluelinkVehicleStatus, error) {
|
||||
return v.status(
|
||||
func() (StatusLatestResponse, error) { return api.StatusLatest(vid) },
|
||||
func() (BluelinkVehicleStatus, error) { return api.StatusPartial(vehicle) },
|
||||
)
|
||||
}, cache)
|
||||
|
||||
v.statusLG = provider.Cached(func() (StatusLatestResponse, error) {
|
||||
return api.StatusLatest(vid)
|
||||
v.statusLG = provider.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() (StatusLatestResponse, error)) (VehicleStatus, error) {
|
||||
func (v *Provider) status(statusG func() (BluelinkVehicleStatus, error)) (BluelinkVehicleStatus, error) {
|
||||
res, err := statusG()
|
||||
|
||||
var ts time.Time
|
||||
if err == nil {
|
||||
ts, err = res.ResMsg.VehicleStatusInfo.VehicleStatus.Updated()
|
||||
ts, err = res.Updated()
|
||||
if err != nil {
|
||||
return res.ResMsg.VehicleStatusInfo.VehicleStatus, err
|
||||
return res, err
|
||||
}
|
||||
|
||||
// return the current value
|
||||
if time.Since(ts) <= v.expiry {
|
||||
v.refreshTime = time.Time{}
|
||||
return res.ResMsg.VehicleStatusInfo.VehicleStatus, nil
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,15 +66,15 @@ func (v *Provider) status(statusG func() (StatusLatestResponse, error)) (Vehicle
|
|||
// TODO async refresh
|
||||
res, err := v.refreshG()
|
||||
if err == nil {
|
||||
if ts, err = res.ResMsg.Updated(); err == nil && time.Since(ts) <= v.expiry {
|
||||
if ts, err = res.Updated(); err == nil && time.Since(ts) <= v.expiry {
|
||||
v.refreshTime = time.Time{}
|
||||
return res.ResMsg, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
err = api.ErrMustRetry
|
||||
}
|
||||
|
||||
return VehicleStatus{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// refresh finally expired
|
||||
|
|
@ -88,7 +88,7 @@ func (v *Provider) status(statusG func() (StatusLatestResponse, error)) (Vehicle
|
|||
err = api.ErrMustRetry
|
||||
}
|
||||
|
||||
return VehicleStatus{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Provider)(nil)
|
||||
|
|
@ -99,12 +99,7 @@ func (v *Provider) Soc() (float64, error) {
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if res.EvStatus != nil {
|
||||
return res.EvStatus.BatteryStatus, nil
|
||||
}
|
||||
|
||||
return 0, api.ErrNotAvailable
|
||||
return res.SoC()
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Provider)(nil)
|
||||
|
|
@ -112,25 +107,11 @@ 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
|
||||
}
|
||||
|
||||
if res.EvStatus != nil {
|
||||
status = api.StatusA
|
||||
if res.EvStatus.BatteryPlugin > 0 || res.EvStatus.ChargePortDoorOpenStatus == 1 {
|
||||
status = api.StatusB
|
||||
}
|
||||
if res.EvStatus.BatteryCharge {
|
||||
status = api.StatusC
|
||||
}
|
||||
} else {
|
||||
err = api.ErrNotAvailable
|
||||
}
|
||||
|
||||
return status, err
|
||||
return res.Status()
|
||||
}
|
||||
|
||||
var _ api.VehicleFinishTimer = (*Provider)(nil)
|
||||
|
|
@ -141,19 +122,7 @@ func (v *Provider) FinishTime() (time.Time, error) {
|
|||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
if res.EvStatus != nil {
|
||||
remaining := res.EvStatus.RemainTime2.Atc.Value
|
||||
|
||||
if remaining == 0 {
|
||||
return time.Time{}, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
ts, err := res.Updated()
|
||||
return ts.Add(time.Duration(remaining) * time.Minute), err
|
||||
}
|
||||
|
||||
return time.Time{}, api.ErrNotAvailable
|
||||
return res.FinishTime()
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Provider)(nil)
|
||||
|
|
@ -164,14 +133,7 @@ func (v *Provider) Range() (int64, error) {
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if res.EvStatus != nil {
|
||||
if dist := res.EvStatus.DrvDistance; len(dist) == 1 {
|
||||
return int64(dist[0].RangeByFuel.EvModeRange.Value), nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, api.ErrNotAvailable
|
||||
return res.Range()
|
||||
}
|
||||
|
||||
var _ api.VehicleOdometer = (*Provider)(nil)
|
||||
|
|
@ -182,12 +144,7 @@ func (v *Provider) Odometer() (float64, error) {
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if res.ResMsg.VehicleStatusInfo.Odometer != nil {
|
||||
return res.ResMsg.VehicleStatusInfo.Odometer.Value, err
|
||||
}
|
||||
|
||||
return 0, api.ErrNotAvailable
|
||||
return res.Odometer()
|
||||
}
|
||||
|
||||
var _ api.SocLimiter = (*Provider)(nil)
|
||||
|
|
@ -198,16 +155,7 @@ func (v *Provider) GetLimitSoc() (int64, error) {
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if res.EvStatus != nil {
|
||||
for _, targetSOC := range res.EvStatus.ReservChargeInfos.TargetSocList {
|
||||
if targetSOC.PlugType == plugTypeAC {
|
||||
return int64(targetSOC.TargetSocLevel), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, api.ErrNotAvailable
|
||||
return res.GetLimitSoc()
|
||||
}
|
||||
|
||||
var _ api.VehiclePosition = (*Provider)(nil)
|
||||
|
|
@ -218,13 +166,7 @@ func (v *Provider) Position() (float64, float64, error) {
|
|||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if res.ResMsg.VehicleStatusInfo.VehicleLocation != nil {
|
||||
coord := res.ResMsg.VehicleStatusInfo.VehicleLocation.Coord
|
||||
return coord.Lat, coord.Lon, err
|
||||
}
|
||||
|
||||
return 0, 0, api.ErrNotAvailable
|
||||
return res.Position()
|
||||
}
|
||||
|
||||
var _ api.Resurrector = (*Provider)(nil)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,25 @@
|
|||
package bluelink
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
)
|
||||
|
||||
type BluelinkVehicleStatus interface {
|
||||
Updated() (time.Time, error)
|
||||
SoC() (float64, error)
|
||||
Status() (api.ChargeStatus, error)
|
||||
FinishTime() (time.Time, error)
|
||||
Range() (int64, error)
|
||||
GetLimitSoc() (int64, error)
|
||||
}
|
||||
|
||||
type BluelinkVehicleStatusLatest interface {
|
||||
Odometer() (float64, error)
|
||||
Position() (float64, float64, error)
|
||||
}
|
||||
|
||||
type VehiclesResponse struct {
|
||||
RetCode string
|
||||
|
|
@ -84,3 +103,235 @@ type TargetSoc struct {
|
|||
TargetSocLevel int
|
||||
PlugType int
|
||||
}
|
||||
|
||||
func (d StatusResponse) Updated() (time.Time, error) {
|
||||
return time.Parse(timeFormat, d.ResMsg.Time+timeOffset)
|
||||
}
|
||||
|
||||
func (d StatusResponse) SoC() (float64, error) {
|
||||
if d.ResMsg.EvStatus != nil {
|
||||
return d.ResMsg.EvStatus.BatteryStatus, nil
|
||||
}
|
||||
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusResponse) Status() (api.ChargeStatus, error) {
|
||||
if d.ResMsg.EvStatus != nil {
|
||||
status := api.StatusA
|
||||
if d.ResMsg.EvStatus.BatteryPlugin > 0 || d.ResMsg.EvStatus.ChargePortDoorOpenStatus == 1 {
|
||||
status = api.StatusB
|
||||
}
|
||||
if d.ResMsg.EvStatus.BatteryCharge {
|
||||
status = api.StatusC
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
return api.StatusNone, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusResponse) FinishTime() (time.Time, error) {
|
||||
if d.ResMsg.EvStatus != nil {
|
||||
remaining := d.ResMsg.EvStatus.RemainTime2.Atc.Value
|
||||
|
||||
if remaining != 0 {
|
||||
ts, err := d.ResMsg.Updated()
|
||||
return ts.Add(time.Duration(remaining) * time.Minute), err
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusResponse) Range() (int64, error) {
|
||||
if d.ResMsg.EvStatus != nil {
|
||||
if dist := d.ResMsg.EvStatus.DrvDistance; len(dist) == 1 {
|
||||
return int64(dist[0].RangeByFuel.EvModeRange.Value), nil
|
||||
}
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusResponse) GetLimitSoc() (int64, error) {
|
||||
if d.ResMsg.EvStatus != nil {
|
||||
for _, targetSOC := range d.ResMsg.EvStatus.ReservChargeInfos.TargetSocList {
|
||||
if targetSOC.PlugType == plugTypeAC {
|
||||
return int64(targetSOC.TargetSocLevel), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusLatestResponse) Odometer() (float64, error) {
|
||||
if d.ResMsg.VehicleStatusInfo.Odometer != nil {
|
||||
return d.ResMsg.VehicleStatusInfo.Odometer.Value, nil
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusLatestResponse) Position() (float64, float64, error) {
|
||||
if d.ResMsg.VehicleStatusInfo.VehicleLocation != nil {
|
||||
pos := d.ResMsg.VehicleStatusInfo.VehicleLocation.Coord
|
||||
return pos.Lat, pos.Lon, nil
|
||||
}
|
||||
return 0, 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
type StatusLatestResponseCCS struct {
|
||||
RetCode string
|
||||
ResCode string
|
||||
ResMsg struct {
|
||||
State struct {
|
||||
Vehicle VehicleStatusCCS
|
||||
}
|
||||
LastUpdateTime string
|
||||
}
|
||||
}
|
||||
|
||||
type VehicleStatusCCS struct {
|
||||
Location *struct {
|
||||
GeoCoord struct {
|
||||
Latitude, Longitude, Altitude float64
|
||||
Type int
|
||||
Date string
|
||||
}
|
||||
}
|
||||
Green *struct {
|
||||
BatteryManagement struct {
|
||||
BatteryRemain struct {
|
||||
Ratio float64
|
||||
Value float64
|
||||
}
|
||||
BatteryCapacity struct {
|
||||
Value float64
|
||||
}
|
||||
SoH struct {
|
||||
Ratio float64
|
||||
}
|
||||
}
|
||||
ChargingInformation struct {
|
||||
ConnectorFastening struct {
|
||||
// 1 connected
|
||||
State int
|
||||
}
|
||||
Charging struct {
|
||||
RemainTime float64
|
||||
RemainTimeUnit int
|
||||
}
|
||||
EstimatedTime struct {
|
||||
Standard float64
|
||||
ICCB float64
|
||||
Quick float64
|
||||
Unit int
|
||||
}
|
||||
ExpectedTime struct {
|
||||
StartDay int
|
||||
StartHour int
|
||||
StartMin int
|
||||
EndDay int
|
||||
EndHour int
|
||||
EndMin int
|
||||
}
|
||||
TargetSoC struct {
|
||||
Standard int64
|
||||
Quick int64
|
||||
}
|
||||
DTE struct {
|
||||
TargetSoC struct {
|
||||
// in Drivetrain.FuelSystem.DTE.Unit
|
||||
Standard float64
|
||||
Quick float64
|
||||
}
|
||||
}
|
||||
}
|
||||
ChargingDoor struct {
|
||||
// 0, 2 closed, 1 open
|
||||
State int
|
||||
}
|
||||
Electric struct {
|
||||
SmartGrid struct {
|
||||
VehicleToLoad struct {
|
||||
DischargeLimitation struct {
|
||||
SoC float64
|
||||
RemainTime float64
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Drivetrain struct {
|
||||
Odometer float64
|
||||
FuelSystem struct {
|
||||
DTE struct {
|
||||
Total int64
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d StatusLatestResponseCCS) Updated() (time.Time, error) {
|
||||
epoch, err := strconv.ParseInt(d.ResMsg.LastUpdateTime, 10, 64)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
return time.UnixMilli(epoch), nil
|
||||
}
|
||||
|
||||
func (d StatusLatestResponseCCS) SoC() (float64, error) {
|
||||
if d.ResMsg.State.Vehicle.Green != nil {
|
||||
return d.ResMsg.State.Vehicle.Green.BatteryManagement.BatteryRemain.Ratio, nil
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusLatestResponseCCS) Status() (api.ChargeStatus, error) {
|
||||
if d.ResMsg.State.Vehicle.Green != nil {
|
||||
if d.ResMsg.State.Vehicle.Green.ChargingInformation.ConnectorFastening.State == 1 {
|
||||
return api.StatusB, nil
|
||||
}
|
||||
if d.ResMsg.State.Vehicle.Green.ChargingInformation.Charging.RemainTime > 0 {
|
||||
return api.StatusC, nil
|
||||
}
|
||||
return api.StatusA, nil
|
||||
}
|
||||
return api.StatusNone, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusLatestResponseCCS) FinishTime() (time.Time, error) {
|
||||
if d.ResMsg.State.Vehicle.Green != nil {
|
||||
remaining := d.ResMsg.State.Vehicle.Green.ChargingInformation.Charging.RemainTime
|
||||
|
||||
if remaining == 0 {
|
||||
return time.Time{}, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
ts, err := d.Updated()
|
||||
return ts.Add(time.Duration(remaining) * time.Minute), err
|
||||
}
|
||||
|
||||
return time.Now(), api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusLatestResponseCCS) Range() (int64, error) {
|
||||
return d.ResMsg.State.Vehicle.Drivetrain.FuelSystem.DTE.Total, nil
|
||||
}
|
||||
|
||||
func (d StatusLatestResponseCCS) GetLimitSoc() (int64, error) {
|
||||
if d.ResMsg.State.Vehicle.Green != nil {
|
||||
return d.ResMsg.State.Vehicle.Green.ChargingInformation.TargetSoC.Standard, nil
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (d StatusLatestResponseCCS) Odometer() (float64, error) {
|
||||
return d.ResMsg.State.Vehicle.Drivetrain.Odometer, nil
|
||||
}
|
||||
|
||||
func (d StatusLatestResponseCCS) Position() (float64, float64, error) {
|
||||
if d.ResMsg.State.Vehicle.Location != nil {
|
||||
return d.ResMsg.State.Vehicle.Location.GeoCoord.Latitude, d.ResMsg.State.Vehicle.Location.GeoCoord.Longitude, nil
|
||||
}
|
||||
return 0, 0, api.ErrNotAvailable
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue