Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mybmw] Improve authentication #18235

Merged
merged 5 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions bundles/org.openhab.binding.mybmw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Please note **this isn't a real-time binding**.
If a door is opened the state isn't transmitted and changed immediately.
It's not a flaw in the binding itself because the state in BMW's own MyBMW App is also updated with some delay.

This binding does not support the region: China.

## Supported Things

### Bridge
Expand Down Expand Up @@ -82,26 +84,35 @@ Properties will be attached to predefined vehicles if the VIN is matching.
|-----------------|---------|--------------------------------------------------------------------------------------------------------|
| userName | text | MyBMW Username |
| password | text | MyBMW Password |
| hcaptchatoken | text | HCaptcha-Token for initial login (see https://bimmer-connected.readthedocs.io/en/latest/captcha.html) |
| region | text | Select region in order to connect to the appropriate BMW server. |

The region Configuration has 3 different options
The region Configuration has 2 different options

- _NORTH_AMERICA_
- _CHINA_
- _ROW_ (Rest of World)

At first initialization, follow the online instructions for login into the BMW API.

#### Advanced Configuration

| Parameter | Type | Description |
|-----------------|---------|---------------------------------------------------------|
| language | text | Channel data can be returned in the desired language |
| Parameter | Type | Description |
|-----------------|---------|--------------------------------------------------------------------------------------------------------|
| language | text | Channel data can be returned in the desired language |
| hcaptchatoken | text | HCaptcha-Token for initial login (see https://bimmer-connected.readthedocs.io/en/stable/captcha.html) |
| callbackIP | text | IP address for openHAB callback URL, defaults to IP of openHAB host |
| callbackPort | integer | Port Number for openHAB callback URL, default 8090 |

Language is predefined as _AUTODETECT_.
Some textual descriptions, date and times are delivered based on your local language.
You can overwrite this setting with lowercase 2-letter [language code reagrding ISO 639](https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html)
So if want your UI in english language place _en_ as desired language.

The initial login to the BMW API requires a Captcha Token.
At first configuration, you can set the Captcha Token as a configuration parameter manually.

To set the Captcha Token online, a webpage is presented and a callback to the bridge is created temporarily on the hosts IP address and a default port.
If the port is already in use, or you have a complex network setup, you may have to override the defaults provided.

### Thing Configuration

Same configuration is needed for all things
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*
* @author Bernd Weymann - Initial contribution
* @author Martin Grassl - renamed and added hcaptchastring
* @author Mark Herwege - added authorisation servlet
*/
@NonNullByDefault
public class MyBMWBridgeConfiguration {
Expand Down Expand Up @@ -49,6 +50,16 @@ public class MyBMWBridgeConfiguration {
*/
private String hcaptchatoken = Constants.EMPTY;

/**
* the callback IP address for the authorisation servlet
*/
private String callbackIP = Constants.EMPTY;

/**
* the callback port for the authorisation servlet
*/
private int callbackPort = 8090;

public String getRegion() {
return region;
}
Expand Down Expand Up @@ -81,17 +92,34 @@ public void setLanguage(String language) {
this.language = language;
}

public String getHcaptchatoken() {
public String getHCaptchaToken() {
return hcaptchatoken;
}

public void setHcaptchatoken(String hcaptchatoken) {
public void setHCaptchaToken(String hcaptchatoken) {
this.hcaptchatoken = hcaptchatoken;
}

public String getCallbackIP() {
return Constants.EMPTY.equals(callbackIP) ? "" : callbackIP;
}

public void setCallbackIP(String callbackIP) {
this.callbackIP = callbackIP;
}

public int getCallbackPort() {
return callbackPort;
}

public void setCallbackPort(int callbackPort) {
this.callbackPort = callbackPort;
}

@Override
public String toString() {
return "MyBMWBridgeConfiguration [region=" + region + ", userName=" + userName + ", password=" + password
+ ", language=" + language + ", hcaptchatoken=" + hcaptchatoken + "]";
+ ", language=" + language + ", hcaptchatoken=" + hcaptchatoken + ", callbackAddress=" + callbackIP
+ ":" + callbackPort + "]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
package org.openhab.binding.mybmw.internal;

import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand Down Expand Up @@ -60,6 +61,23 @@ public interface MyBMWConstants {

static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 60;

// Captcha servlet constants
static final String LOCAL_OPENHAB_BASE_PATH = "/" + BINDING_ID + "/";
static final String CAPTCHA_URL_ROOT = "captcha/";
static final String NORTH_AMERICA = "NORTH_AMERICA";
static final String ROW = "ROW";
static final Map<String, String> CAPTCHA_HTML = Map.of(NORTH_AMERICA, CAPTCHA_URL_ROOT + "north_america_form.html",
ROW, CAPTCHA_URL_ROOT + "rest_of_world_form.html");

// Thing status detail messages
static final String STATUS_AUTH_NEEDED = "@text/mybmw.status.authorization-needed";
static final String STATUS_USER_DETAILS_MISSING = "@text/mybmw.status.user-details-missing";
static final String STATUS_REGION_MISSING = "@text/mybmw.status.region-missing";
static final String STATUS_IP_MISSING = "@text/mybmw.status.ip-missing";
static final String STATUS_VEHICLE_RETRIEVAL_ERROR = "@text/mybmw.status.vehicle-retrieval-error";
static final String STATUS_NETWORK_ERROR = "@text/mybmw.status.network-error";
static final String STATUS_QUOTA_ERROR = "@text/mybmw.status.quota-error";

// See constants from bimmer-connected
// https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/vehicle.py
enum VehicleType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@
*/
package org.openhab.binding.mybmw.internal;

import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUPPORTED_THING_SET;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.THING_TYPE_CONNECTED_DRIVE_ACCOUNT;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
lsiepel marked this conversation as resolved.
Show resolved Hide resolved

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
import org.openhab.binding.mybmw.internal.handler.MyBMWCommandOptionProvider;
import org.openhab.binding.mybmw.internal.handler.VehicleHandler;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
Expand All @@ -33,6 +34,7 @@
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;

/**
* The {@link MyBMWHandlerFactory} is responsible for creating things and thing
Expand All @@ -45,18 +47,26 @@
@Component(configurationPid = "binding.mybmw", service = ThingHandlerFactory.class)
public class MyBMWHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory;
private final OAuthFactory oAuthFactory;
private final MyBMWCommandOptionProvider commandOptionProvider;
private final NetworkAddressService networkAddressService;
private final HttpService httpService;
private final LocationProvider locationProvider;
private final TimeZoneProvider timeZoneProvider;
private final LocaleProvider localeProvider;

@Activate
public MyBMWHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference OAuthFactory oAuthFactory,
final @Reference MyBMWCommandOptionProvider commandOptionProvider,
final @Reference NetworkAddressService networkAddressService, final @Reference HttpService httpService,
lsiepel marked this conversation as resolved.
Show resolved Hide resolved
final @Reference LocaleProvider localeProvider, final @Reference LocationProvider locationProvider,
final @Reference TimeZoneProvider timeZoneProvider) {
this.httpClientFactory = httpClientFactory;
this.oAuthFactory = oAuthFactory;
this.commandOptionProvider = commandOptionProvider;
this.networkAddressService = networkAddressService;
this.httpService = httpService;
this.locationProvider = locationProvider;
this.timeZoneProvider = timeZoneProvider;
this.localeProvider = localeProvider;
Expand All @@ -71,7 +81,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(thingTypeUID)) {
return new MyBMWBridgeHandler((Bridge) thing, httpClientFactory, localeProvider);
return new MyBMWBridgeHandler((Bridge) thing, httpClientFactory, oAuthFactory, httpService,
networkAddressService, localeProvider);
} else if (SUPPORTED_THING_SET.contains(thingTypeUID)) {
return new VehicleHandler(thing, commandOptionProvider, locationProvider, timeZoneProvider,
thingTypeUID.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
*/
package org.openhab.binding.mybmw.internal.console;

import static org.openhab.binding.mybmw.internal.MyBMWConstants.BINDING_ID;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.THING_TYPE_CONNECTED_DRIVE_ACCOUNT;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
lsiepel marked this conversation as resolved.
Show resolved Hide resolved

import java.io.File;
import java.io.FileOutputStream;
Expand Down Expand Up @@ -253,7 +252,7 @@ private void writeJsonToFile(String pathString, String filename, String json) th

// ensure full path exists
File file = new File(path);
file.getParentFile().mkdirs();
Objects.requireNonNull(file.getParentFile()).mkdirs();

final byte[] contents = json.getBytes(StandardCharsets.UTF_8);
Files.write(file.toPath(), contents);
Expand Down Expand Up @@ -313,8 +312,7 @@ public boolean complete(String[] args, int cursorArgumentIndex, int cursorPositi
.filter(t -> THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(t.getThingTypeUID())
&& args[1].equals(t.getConfiguration().get("userName")))
.map(t -> t.getHandler()).findAny().get();
List<VehicleBase> vehicles = handler != null ? handler.getMyBmwProxy().get().requestVehiclesBase()
: List.of();
List<VehicleBase> vehicles = handler.getMyBmwProxy().get().requestVehiclesBase();
return new StringsCompleter(
vehicles.stream().map(v -> v.getVin()).filter(Objects::nonNull).collect(Collectors.toList()),
false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,29 +83,33 @@ public void discoverVehicles() {
try {
return prox.requestVehicles();
} catch (NetworkException e) {
throw new IllegalStateException("vehicles could not be discovered: " + e.getMessage(), e);
throw new IllegalStateException(e);
}
});
vehicleList.ifPresentOrElse(vehicles -> {
if (vehicles.size() > 0) {
if (!vehicles.isEmpty()) {
thingHandler.vehicleDiscoverySuccess();
processVehicles(vehicles);
} else {
logger.warn("no vehicle found, maybe because of network error");
thingHandler.vehicleDiscoveryError();
thingHandler.vehicleDiscoveryError(MyBMWConstants.STATUS_NETWORK_ERROR);
}
}, () -> thingHandler.vehicleDiscoveryError());
}, () -> thingHandler.vehicleDiscoveryError(Constants.EMPTY));
} catch (IllegalStateException ex) {
thingHandler.vehicleDiscoveryError();
NetworkException ne = (NetworkException) ex.getCause();
if (ne != null && (ne.getStatus() == 403 || ne.getStatus() == 429)) {
thingHandler.vehicleQuotaDiscoveryError(myBMWProxy.get().getNextQuota());
} else {
thingHandler.vehicleDiscoveryError(MyBMWConstants.STATUS_NETWORK_ERROR);
}
}
}

/**
* this method is called by the bridgeHandler if the list of vehicles was retrieved successfully
*
*
* it iterates through the list of existing things and checks if the vehicles found via the API
* call are already known to OH. If not, it creates a new thing and puts it into the inbox
*
*
* @param vehicleList
*/
private void processVehicles(List<Vehicle> vehicleList) {
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading