From c6e0c640d9bd1b9495067e851282bdb711ad8f55 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 1 Dec 2023 17:23:33 +0500 Subject: [PATCH] BMW: add wakeup (by locking door) (#10903) --- vehicle/bmw.go | 9 ++++- vehicle/bmw/api.go | 88 ++++++++++++++++++++++++++--------------- vehicle/bmw/provider.go | 23 +++++++++++ vehicle/helper.go | 33 ++++++++-------- 4 files changed, 104 insertions(+), 49 deletions(-) diff --git a/vehicle/bmw.go b/vehicle/bmw.go index c051971a0..f43f55f45 100644 --- a/vehicle/bmw.go +++ b/vehicle/bmw.go @@ -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 diff --git a/vehicle/bmw/api.go b/vehicle/bmw/api.go index 0da2a03b7..ea7b0ca09 100644 --- a/vehicle/bmw/api.go +++ b/vehicle/bmw/api.go @@ -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//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// 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 +} diff --git a/vehicle/bmw/provider.go b/vehicle/bmw/provider.go index fc838db5a..2a5e5b18c 100644 --- a/vehicle/bmw/provider.go +++ b/vehicle/bmw/provider.go @@ -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) +} diff --git a/vehicle/helper.go b/vehicle/helper.go index a330bff3e..87a096659 100644 --- a/vehicle/helper.go +++ b/vehicle/helper.go @@ -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) + })) }