Skip to content

Commit

Permalink
authentication
Browse files Browse the repository at this point in the history
Signed-off-by: Mark Herwege <[email protected]>
  • Loading branch information
mherwege committed Feb 7, 2025
1 parent ea978fe commit b14e30f
Show file tree
Hide file tree
Showing 31 changed files with 988 additions and 1,075 deletions.
18 changes: 14 additions & 4 deletions bundles/org.openhab.binding.mybmw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ 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
Expand All @@ -91,17 +90,28 @@ The region Configuration has 3 different options
- _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,22 @@ public interface MyBMWConstants {

static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 60;

static final String BASE_PATH = "/" + BINDING_ID + "/";

static final String HTML_SOURCE = "captcha/";
static final String NORTH_AMERICA = "NORTH_AMERICA";
static final String ROW = "ROW";
static final Map<String, String> CAPTCHA_HTML = Map.of(NORTH_AMERICA, HTML_SOURCE + "north_america_form.html", ROW,
HTML_SOURCE + "rest_of_world_form.html");

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.*;

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,
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.*;

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
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@

import java.util.List;

import org.openhab.binding.mybmw.internal.utils.Constants;

/**
* The {@link AuthQueryResponse} Data Transfer Object
* The {@link OAuthSettingsQueryResponse} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
* @author Martin Grassl - add toString for debugging
*/
public class AuthQueryResponse {
public class OAuthSettingsQueryResponse {
public String clientName;// ": "mybmwapp",
public String clientSecret;// ": "c0e3393d-70a2-4f6f-9d3c-8530af64d552",
public String clientId;// ": "31c357a0-7a1d-4590-aa99-33b97244d048",
Expand All @@ -31,7 +33,7 @@ public class AuthQueryResponse {
public String country;// ": "US",
public String authorizationEndpoint;// ": "https://customer.bmwgroup.com/oneid/login",
public String tokenEndpoint;// ": "https://customer.bmwgroup.com/gcdm/oauth/token",
public List<String> scopes;// ;": [
private List<String> scopes;// ;": [
// "openid",
// "profile",
// "email",
Expand All @@ -50,10 +52,14 @@ public class AuthQueryResponse {
public List<String> promptValues; // ": ["login"]
/*
* (non-Javadoc)
*
*
* @see java.lang.Object#toString()
*/

public String scopes() {
return String.join(Constants.SPACE, scopes);
}

@Override
public String toString() {
return "AuthQueryResponse [clientName=" + clientName + ", clientSecret=" + clientSecret + ", clientId="
Expand Down
Loading

0 comments on commit b14e30f

Please sign in to comment.