package bluelink import ( "strconv" "time" "github.com/evcc-io/evcc/api" ) type BluelinkVehicleStatusLatest interface { Updated() (time.Time, error) SoC() (float64, error) Status() (api.ChargeStatus, error) FinishTime() (time.Time, error) Range() (int64, error) Climater() (bool, error) GetLimitSoc() (int64, error) Odometer() (float64, error) Position() (float64, float64, error) Capacity() (float64, error) } type VehiclesResponse struct { RetCode string ResMsg struct { Vehicles []Vehicle } } type StatusResponse struct { RetCode string ResCode string // ResMsg is intentionally omitted - structure varies between old/CCS2 models } type StatusLatestResponse struct { RetCode string ResCode string ResMsg struct { VehicleStatusInfo struct { VehicleStatus VehicleStatus VehicleLocation *VehicleLocation Odometer *Odometer } } } type VehicleStatus struct { Time string AirCtrlOn bool Defrost bool EvStatus *struct { BatteryCharge bool BatteryStatus float64 BatteryPlugin int RemainTime2 struct { Atc struct { Value, Unit int } } ChargePortDoorOpenStatus int DrvDistance []DrivingDistance ReservChargeInfos ReservChargeInfo } Vehicles []Vehicle } type VehicleLocation struct { Coord struct { Lat, Lon, Alt float64 } Time string // TODO convert to timestamp } type Odometer struct { Value float64 Unit int } const ( timeFormat = "20060102150405 -0700" // Note: must add timeOffset timeOffset = " +0100" plugTypeAC = 1 ) type DrivingDistance struct { RangeByFuel struct { EvModeRange struct { Value int } } } type ReservChargeInfo struct { TargetSocList []TargetSoc } type TargetSoc struct { TargetSocLevel int PlugType int } func (d VehicleStatus) Updated() (time.Time, error) { return time.Parse(timeFormat, d.Time+timeOffset) } func (d VehicleStatus) SoC() (float64, error) { if d.EvStatus != nil { return d.EvStatus.BatteryStatus, nil } return 0, api.ErrNotAvailable } func (d VehicleStatus) Status() (api.ChargeStatus, error) { if d.EvStatus != nil { status := api.StatusA if d.EvStatus.BatteryPlugin > 0 || d.EvStatus.ChargePortDoorOpenStatus == 1 { status = api.StatusB } if d.EvStatus.BatteryCharge { status = api.StatusC } return status, nil } return api.StatusNone, api.ErrNotAvailable } func (d VehicleStatus) FinishTime() (time.Time, error) { if d.EvStatus != nil { remaining := d.EvStatus.RemainTime2.Atc.Value if remaining != 0 { ts, err := d.Updated() return ts.Add(time.Duration(remaining) * time.Minute), err } } return time.Time{}, api.ErrNotAvailable } func (d VehicleStatus) Range() (int64, error) { if d.EvStatus != nil { if dist := d.EvStatus.DrvDistance; len(dist) == 1 { return int64(dist[0].RangeByFuel.EvModeRange.Value), nil } } return 0, api.ErrNotAvailable } func (d VehicleStatus) Climater() (bool, error) { return d.AirCtrlOn || d.Defrost, nil } func (d VehicleStatus) GetLimitSoc() (int64, error) { if d.EvStatus != nil { for _, targetSOC := range d.EvStatus.ReservChargeInfos.TargetSocList { if targetSOC.PlugType == plugTypeAC { return int64(targetSOC.TargetSocLevel), nil } } } return 0, api.ErrNotAvailable } 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) { 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 } func (d StatusLatestResponse) Capacity() (float64, error) { return 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) Climater() (bool, error) { return false, api.ErrNotAvailable } 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 } 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 }