BMW: add wakeup (by locking door) (#10903)

This commit is contained in:
andig 2023-12-01 17:23:33 +05:00 • committed by GitHub
parent 82ecb9f312
commit c6e0c640d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 49 deletions

View file

@ -63,10 +63,15 @@ func NewBMWMiniFromConfig(brand string, other map[string]interface{}) (api.Vehic
api := bmw.NewAPI(log, brand, cc.Region, ts)
cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
vehicle, err := ensureVehicleEx(
cc.VIN, api.Vehicles,
func(v bmw.Vehicle) string {
return v.VIN
},
)
if err == nil {
v.Provider = bmw.NewProvider(api, cc.VIN, cc.Cache)
v.Provider = bmw.NewProvider(api, vehicle.VIN, cc.Cache)
}
return v, err

View file

@ -8,6 +8,7 @@ import (
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
"golang.org/x/oauth2"
)
@ -17,50 +18,36 @@ import (
// API is an api.Vehicle implementation for BMW cars
type API struct {
*request.Helper
xUserAgent string
region string
region string
}
// NewAPI creates a new vehicle
func NewAPI(log *util.Logger, brand, region string, identity oauth2.TokenSource) *API {
v := &API{
Helper: request.NewHelper(log),
xUserAgent: fmt.Sprintf("android(SP1A.210812.016.C1);%s;99.0.0(99999);row", brand),
region: strings.ToUpper(region),
Helper: request.NewHelper(log),
region: strings.ToUpper(region),
}
// replace client transport with authenticated transport
v.Client.Transport = &oauth2.Transport{
Source: identity,
Base: v.Client.Transport,
v.Client.Transport = &transport.Decorator{
Base: &oauth2.Transport{
Source: identity,
Base: v.Client.Transport,
},
Decorator: transport.DecorateHeaders(map[string]string{
"X-User-Agent": fmt.Sprintf("android(SP1A.210812.016.C1);%s;99.0.0(99999);row", brand),
}),
}
return v
}
// Vehicles implements returns the /user/vehicles api
func (v *API) Vehicles() ([]string, error) {
func (v *API) Vehicles() ([]Vehicle, error) {
var res []Vehicle
uri := fmt.Sprintf("%s/eadrax-vcs/v4/vehicles?apptimezone=120&appDateTime=%d", regions[v.region].CocoApiURI, time.Now().UnixMilli())
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
"Content-Type": request.JSONContent,
"X-User-Agent": v.xUserAgent,
})
if err != nil {
return nil, err
}
if err := v.DoJSON(req, &res); err != nil {
return nil, err
}
var vehicles []string
for _, v := range res {
vehicles = append(vehicles, v.VIN)
}
return vehicles, err
err := v.GetJSON(uri, &res)
return res, err
}
// Status implements the /user/vehicles/<vin>/status api
@ -69,9 +56,7 @@ func (v *API) Status(vin string) (VehicleStatus, error) {
uri := fmt.Sprintf("%s/eadrax-vcs/v4/vehicles/state?apptimezone=120&appDateTime=%d", regions[v.region].CocoApiURI, time.Now().UnixMilli())
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
"Content-Type": request.JSONContent,
"X-User-Agent": v.xUserAgent,
"bmw-vin": vin,
"bmw-vin": vin,
})
if err == nil {
err = v.DoJSON(req, &res)
@ -79,3 +64,44 @@ func (v *API) Status(vin string) (VehicleStatus, error) {
return res, err
}
const (
CHARGE_START = "start-charging"
CHARGE_STOP = "stop-charging"
DOOR_LOCK = "door-lock"
LIGHT_FLASH = "light-flash"
REMOTE_SERVICE_BASE_URL = "eadrax-vrccs/v3/presentation/remote-commands"
VEHICLE_CHARGING_BASE_URL = "eadrax-crccs/v1/vehicles"
)
var serviceUrls = map[string]string{
CHARGE_START: VEHICLE_CHARGING_BASE_URL,
CHARGE_STOP: VEHICLE_CHARGING_BASE_URL,
}
type Event struct {
EventID string
CreationTime time.Time
}
// Action implements the /remote-commands/<vin>/<service> api
func (v *API) Action(vin, action string) (Event, error) {
var res Event
path, ok := serviceUrls[action]
if !ok {
path = REMOTE_SERVICE_BASE_URL
}
uri := fmt.Sprintf("%s/%s/%s/%s", regions[v.region].CocoApiURI, path, vin, action)
req, err := request.New(http.MethodPost, uri, nil, map[string]string{
"Accept": request.JSONContent,
})
if err == nil {
err = v.DoJSON(req, &res)
}
return res, err
}

View file

@ -10,6 +10,7 @@ import (
// Provider implements the vehicle api
type Provider struct {
statusG func() (VehicleStatus, error)
actionS func(action string) error
}
// NewProvider creates a vehicle api provider
@ -18,6 +19,10 @@ func NewProvider(api *API, vin string, cache time.Duration) *Provider {
statusG: provider.Cached(func() (VehicleStatus, error) {
return api.Status(vin)
}, cache),
actionS: func(action string) error {
_, err := api.Action(vin, action)
return err
},
}
return impl
}
@ -90,3 +95,21 @@ func (v *Provider) Odometer() (float64, error) {
return float64(res.State.CurrentMileage), nil
}
var _ api.Resurrector = (*Provider)(nil)
func (v *Provider) WakeUp() error {
return v.actionS(DOOR_LOCK)
}
var _ api.VehicleChargeController = (*Provider)(nil)
// StartCharge implements the api.VehicleChargeController interface
func (v *Provider) StartCharge() error {
return v.actionS(CHARGE_START)
}
// StopCharge implements the api.VehicleChargeController interface
func (v *Provider) StopCharge() error {
return v.actionS(CHARGE_STOP)
}

View file

@ -15,16 +15,19 @@ func ensureVehicle(vin string, list func() ([]string, error)) (string, error) {
}
// ensureVehicleEx extracts vehicle with matching VIN from list of vehicles
func ensureVehicleEx[Vehicle any](
func ensureVehicleEx[T any](
vin string,
list func() ([]Vehicle, error),
extract func(Vehicle) string,
) (Vehicle, error) {
list func() ([]T, error),
extract func(T) string,
) (T, error) {
var zero T
vehicles, err := list()
if err != nil {
return *new(Vehicle), fmt.Errorf("cannot get vehicles: %w", err)
return zero, fmt.Errorf("cannot get vehicles: %w", err)
}
// vin defined
if vin = strings.ToUpper(vin); vin != "" {
for _, vehicle := range vehicles {
if vin == extract(vehicle) {
@ -33,17 +36,15 @@ func ensureVehicleEx[Vehicle any](
}
// vin defined but doesn't exist
err = fmt.Errorf("cannot find vehicle: %s", vin)
} else {
// vin empty
if len(vehicles) == 1 {
return vehicles[0], nil
}
err = fmt.Errorf("cannot find vehicle, got: %v", lo.Map(vehicles, func(v Vehicle, _ int) string {
return extract(v)
}))
return zero, fmt.Errorf("cannot find vehicle: %s", vin)
}
return *new(Vehicle), err
// vin empty
if len(vehicles) == 1 {
return vehicles[0], nil
}
return zero, fmt.Errorf("cannot find vehicle, got: %v", lo.Map(vehicles, func(v T, _ int) string {
return extract(v)
}))
}