diff --git a/bundles/org.openhab.binding.mercedesme/README.md b/bundles/org.openhab.binding.mercedesme/README.md index 964ac2c3d02..5096675ece2 100644 --- a/bundles/org.openhab.binding.mercedesme/README.md +++ b/bundles/org.openhab.binding.mercedesme/README.md @@ -7,12 +7,11 @@ This binding provides access to your Mercedes Benz vehicle like _Mercedes Me_ Sm First time users shall follow the following sequence 1. Setup and configure [Bridge](#bridge-configuration) -2. Follow the [Bridge Authorization](#bridge-authorization) process -3. [Discovery](#discovery) shall find now vehicles associated to your account -4. Add your vehicle from discovery and [configure](#thing-configuration) it with correct VIN -5. Connect your desired items in UI or [text-configuration](#full-example) -6. Optional: you can [Discover your Vehicle](#discover-your-vehicle) more deeply -7. In case of problems check [Troubleshooting](#troubleshooting) section +2. [Discovery](#discovery) shall find now vehicles associated to your account +3. Add your vehicle from discovery and [configure](#thing-configuration) it with correct VIN +4. Connect your desired items in UI or [text-configuration](#full-example) +5. Optional: you can [Discover your Vehicle](#discover-your-vehicle) more deeply +6. In case of problems check [Troubleshooting](#troubleshooting) section ## Supported Things @@ -35,14 +34,20 @@ There's no manual discovery! Bridge needs configuration in order to connect properly to your Mercedes Me account. -| Name | Type | Description | Default | Required | Advanced | -|-----------------|---------|-----------------------------------------|-------------|----------|----------| -| email | text | Mercedes Me registered email Address | N/A | yes | no | -| pin | text | Mercedes Me Smartphone App PIN | N/A | no | no | -| region | text | Your region | EU | yes | no | -| refreshInterval | integer | API refresh interval | 15 | yes | no | -| callbackIP | text | IP Address of openHAB Device | N/A | yes | yes | -| callbackPort | integer | Port Number of openHAB Device | N/A | yes | yes | +| Name | Type | Description | Default | Required | +|-------------------|---------|---------------------------------------------|---------------------------|----------| +| email | text | Mercedes Me registered email Address | N/A | yes | +| refreshToken | text | Refresh Token from MB Token Requester app | takeover previous token | yes | +| pin | text | Mercedes Me Smartphone App PIN | N/A | no | +| region | text | Your region | EU | yes | +| refreshInterval | integer | API refresh interval | 15 | yes | + +`refreshToken` is needed to get access to your Mercedes Me account. +Users already running this binding can stay on default value `takeover previous token`. +New users need to generate `refreshToken` with [MB Token Requester app]( https://github.com/ReneNulschDE/mbapi2020/wiki/How%E2%80%90to:-create-the-access-and-refresh-token ). +It simulates the Mercedes Me application *only for authorization process* on your computer, **not your openHAB system!** +The generated *refresh token* has to be pasted into the bridge configuration. +The generated *token* can be ignored! Set `region` to your location @@ -63,46 +68,6 @@ Commands protected by PIN - Open / Ventilate Windows - Open / Lift Sunroof -IP `callbackIP` and port `callbackPort` will be auto-detected. -If you're running on server with more than one network interface please select manually. - -### Bridge Authorization - -Authorization is needed to activate the Bridge which is connected to your Mercedes Me Account. -The Bridge will indicate in the status headline if authorization is needed including the URL which needs to be opened in your browser. - -Three steps are needed - -1. Open the mentioned URL like 192.168.x.x:8090/mb-auth -Opening this URL will request a PIN which will be send to your configured email. -Check your Mail Account if you received the PIN. -Click on _Continue_ to proceed with Step 2. - -2. Enter your PIN in the shown field. -Leave GUID as identifier as it is. -Click on _Submit_ button. - -3. Confirmation shall be shown that authorization was successful. - -In case of non successful authorization check your log for errors. -Below screenshots are illustrating the authorization flow. - -### After Bridge Setup - - - -### Authorization Step 1 - - - -### Authorization Step 2 - - - -### Authorization Step 3 - - - ## Thing Configuration | Name | Type | Description | Default | Required | Advanced | @@ -814,7 +779,7 @@ Keep these 3 channels disconnected during normal operation. ### Things file ```java -Bridge mercedesme:account:4711 "Mercedes Me John Doe" [ email="YOUR_MAIL_ADDRESS", region="EU", pin=9876, refreshInterval=15] { +Bridge mercedesme:account:4711 "Mercedes Me John Doe" [ email="YOUR_MAIL_ADDRESS", region="EU", pin=9876, refreshToken="abc", refreshInterval=15] { Thing bev eqa "Mercedes EQA" [ vin="VEHICLE_VIN", batteryCapacity=66.5] } ``` diff --git a/bundles/org.openhab.binding.mercedesme/doc/OH-Step0.png b/bundles/org.openhab.binding.mercedesme/doc/OH-Step0.png deleted file mode 100644 index a5b6c179b3c..00000000000 Binary files a/bundles/org.openhab.binding.mercedesme/doc/OH-Step0.png and /dev/null differ diff --git a/bundles/org.openhab.binding.mercedesme/doc/OH-Step1.png b/bundles/org.openhab.binding.mercedesme/doc/OH-Step1.png deleted file mode 100644 index 9bc764f8ff2..00000000000 Binary files a/bundles/org.openhab.binding.mercedesme/doc/OH-Step1.png and /dev/null differ diff --git a/bundles/org.openhab.binding.mercedesme/doc/OH-Step2.png b/bundles/org.openhab.binding.mercedesme/doc/OH-Step2.png deleted file mode 100644 index 8924125cfd0..00000000000 Binary files a/bundles/org.openhab.binding.mercedesme/doc/OH-Step2.png and /dev/null differ diff --git a/bundles/org.openhab.binding.mercedesme/doc/OH-Step3.png b/bundles/org.openhab.binding.mercedesme/doc/OH-Step3.png deleted file mode 100644 index 1d1d9d986a7..00000000000 Binary files a/bundles/org.openhab.binding.mercedesme/doc/OH-Step3.png and /dev/null differ diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java index 575a767cc1a..148bb8c7f1e 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java @@ -268,7 +268,6 @@ public class Constants { public static final String OH_CHANNEL_CONSTANT = "constant"; public static final String OH_CHANNEL_BONUS_RANGE = "bonus"; - public static final String CALLBACK_ENDPOINT = "/mb-auth"; // https://developer.mercedes-benz.com/content-page/api_migration_guide public static final String IMAGE_BASE_URL = "https://api.mercedes-benz.com/vehicle_images/v2"; public static final String IMAGE_EXTERIOR_RESOURCE_URL = IMAGE_BASE_URL + "/vehicles/%s"; @@ -278,9 +277,7 @@ public class Constants { public static final String STATUS_EMAIL_MISSING = ".status.email-missing"; public static final String STATUS_REGION_MISSING = ".status.region-missing"; public static final String STATUS_REFRESH_INVALID = ".status.refresh-invalid"; - public static final String STATUS_IP_MISSING = ".status.ip-missing"; - public static final String STATUS_PORT_MISSING = ".status.port-missing"; - public static final String STATUS_SERVER_RESTART = ".status.server-restart"; + public static final String STATUS_REFRESH_TOKEN_MISSING = ".status.refresh-token-missing"; public static final String STATUS_BRIDGE_MISSING = ".status.bridge-missing"; public static final String SPACE = " "; @@ -346,7 +343,6 @@ public class Constants { public static final String MAX_SOC_KEY = "maxsoc"; public static final String AUTO_UNLOCK_KEY = "autolock"; - public static final String JUNIT_SERVER_ADDR = "http://999.999.999.999:99999/mb-auth"; public static final String JUNIT_TOKEN = "junitTestToken"; public static final String JUNIT_REFRESH_TOKEN = "junitRefreshToken"; } diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeHandlerFactory.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeHandlerFactory.java index 30d98a3a087..ed0524ced00 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeHandlerFactory.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/MercedesMeHandlerFactory.java @@ -31,7 +31,6 @@ import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.UnitProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.items.MetadataRegistry; -import org.openhab.core.net.NetworkAddressService; import org.openhab.core.storage.StorageService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; @@ -65,7 +64,6 @@ public class MercedesMeHandlerFactory extends BaseThingHandlerFactory { private final MercedesMeDiscoveryService discoveryService; private final MercedesMeCommandOptionProvider mmcop; private final MercedesMeStateOptionProvider mmsop; - private final NetworkAddressService networkService; private @Nullable ServiceRegistration discoveryServiceReg; private @Nullable MercedesMeMetadataAdjuster mdAdjuster; @@ -76,10 +74,8 @@ public class MercedesMeHandlerFactory extends BaseThingHandlerFactory { final @Reference LocaleProvider lp, final @Reference LocationProvider locationP, final @Reference TimeZoneProvider tzp, final @Reference MercedesMeCommandOptionProvider cop, final @Reference MercedesMeStateOptionProvider sop, final @Reference UnitProvider up, - final @Reference MetadataRegistry mdr, final @Reference ItemChannelLinkRegistry iclr, - final @Reference NetworkAddressService nas) { + final @Reference MetadataRegistry mdr, final @Reference ItemChannelLinkRegistry iclr) { this.storageService = storageService; - networkService = nas; localeProvider = lp; locationProvider = locationP; mmcop = cop; @@ -112,8 +108,7 @@ public class MercedesMeHandlerFactory extends BaseThingHandlerFactory { discoveryServiceReg = bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, null); } - return new AccountHandler((Bridge) thing, discoveryService, httpClient, localeProvider, storageService, - networkService); + return new AccountHandler((Bridge) thing, discoveryService, httpClient, localeProvider, storageService); } else if (THING_TYPE_BEV.equals(thingTypeUID) || THING_TYPE_COMB.equals(thingTypeUID) || THING_TYPE_HYBRID.equals(thingTypeUID)) { return new VehicleHandler(thing, locationProvider, mmcop, mmsop); diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java index f6aee49314a..71f5b65a8dc 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java @@ -26,9 +26,7 @@ public class AccountConfiguration { public String email = NOT_SET; public String region = NOT_SET; + public String refreshToken = "takeover previous token"; public String pin = NOT_SET; public int refreshInterval = 15; - - public String callbackIP = NOT_SET; - public int callbackPort = -1; } diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/dto/TokenResponse.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/dto/TokenResponse.java index 385c20dde7e..32bc9a8fa2b 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/dto/TokenResponse.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/dto/TokenResponse.java @@ -33,6 +33,6 @@ public class TokenResponse { @SerializedName("token_type") public String tokenType = Constants.NOT_SET; @SerializedName("expires_in") - public int expiresIn; - public String createdOn = Instant.now().toString(); + public int expiresIn = 0; + public String createdOn = Instant.MIN.toString(); } diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java index 8e7e0f12312..51f7df0f85a 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java @@ -38,15 +38,12 @@ import org.json.JSONObject; import org.openhab.binding.mercedesme.internal.Constants; import org.openhab.binding.mercedesme.internal.config.AccountConfiguration; import org.openhab.binding.mercedesme.internal.discovery.MercedesMeDiscoveryService; -import org.openhab.binding.mercedesme.internal.server.AuthServer; import org.openhab.binding.mercedesme.internal.server.AuthService; import org.openhab.binding.mercedesme.internal.server.MBWebsocket; import org.openhab.binding.mercedesme.internal.utils.Utils; import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener; import org.openhab.core.auth.client.oauth2.AccessTokenResponse; -import org.openhab.core.config.core.Configuration; import org.openhab.core.i18n.LocaleProvider; -import org.openhab.core.net.NetworkAddressService; import org.openhab.core.storage.Storage; import org.openhab.core.storage.StorageService; import org.openhab.core.thing.Bridge; @@ -80,7 +77,6 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr private static final String COMMAND_APPENDIX = "-commands"; private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); - private final NetworkAddressService networkService; private final MercedesMeDiscoveryService discoveryService; private final HttpClient httpClient; private final LocaleProvider localeProvider; @@ -89,8 +85,6 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr private final Map vepUpdateMap = new HashMap<>(); private final Map> capabilitiesMap = new HashMap<>(); - private Optional server = Optional.empty(); - private Optional authService = Optional.empty(); private Optional> refreshScheduler = Optional.empty(); private List eventQueue = new ArrayList<>(); private boolean updateRunning = false; @@ -99,16 +93,16 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr private String commandCapabilitiesEndpoint = "/v1/vehicle/%s/capabilities/commands"; private String poiEndpoint = "/v1/vehicle/%s/route"; + Optional authService = Optional.empty(); final MBWebsocket ws; - Optional config = Optional.empty(); + AccountConfiguration config = new AccountConfiguration(); @Nullable ClientMessage message; public AccountHandler(Bridge bridge, MercedesMeDiscoveryService mmds, HttpClient hc, LocaleProvider lp, - StorageService store, NetworkAddressService nas) { + StorageService store) { super(bridge); discoveryService = mmds; - networkService = nas; ws = new MBWebsocket(this); httpClient = hc; localeProvider = lp; @@ -122,83 +116,39 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr @Override public void initialize() { updateStatus(ThingStatus.UNKNOWN); - config = Optional.of(getConfigAs(AccountConfiguration.class)); - autodetectCallback(); + config = getConfigAs(AccountConfiguration.class); String configValidReason = configValid(); if (!configValidReason.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configValidReason); } else { - String callbackUrl = Utils.getCallbackAddress(config.get().callbackIP, config.get().callbackPort); - thing.setProperty("callbackUrl", callbackUrl); - server = Optional.of(new AuthServer(httpClient, config.get(), callbackUrl)); - authService = Optional - .of(new AuthService(this, httpClient, config.get(), localeProvider.getLocale(), storage)); - if (!server.get().start()) { - String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId() - + Constants.STATUS_SERVER_RESTART; - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, - textKey + " [\"" + thing.getProperties().get("callbackUrl") + "\"]"); - } else { - refreshScheduler = Optional.of(scheduler.scheduleWithFixedDelay(this::refresh, 0, - config.get().refreshInterval, TimeUnit.MINUTES)); - } + authService = Optional.of(new AuthService(this, httpClient, config, localeProvider.getLocale(), storage, + config.refreshToken)); + refreshScheduler = Optional + .of(scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshInterval, TimeUnit.MINUTES)); } } public void refresh() { - if (server.isPresent()) { - if (!Constants.NOT_SET.equals(authService.get().getToken())) { - ws.run(); - } else { - // all failed - start manual authorization - String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId() - + Constants.STATUS_AUTH_NEEDED; - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - textKey + " [\"" + thing.getProperties().get("callbackUrl") + "\"]"); - } + if (!Constants.NOT_SET.equals(authService.get().getToken())) { + ws.run(); } else { - // server not running - fix first + // all failed - start manual authorization String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId() - + Constants.STATUS_SERVER_RESTART; - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, textKey); + + Constants.STATUS_AUTH_NEEDED; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, textKey); } } - private void autodetectCallback() { - // if Callback IP and Callback Port are not set => autodetect these values - config = Optional.of(getConfigAs(AccountConfiguration.class)); - Configuration updateConfig = super.editConfiguration(); - if (!updateConfig.containsKey("callbackPort")) { - updateConfig.put("callbackPort", Utils.getFreePort()); - } else { - Utils.addPort(config.get().callbackPort); - } - if (!updateConfig.containsKey("callbackIP")) { - String ip = networkService.getPrimaryIpv4HostAddress(); - if (ip != null) { - updateConfig.put("callbackIP", ip); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, - "@text/mercedesme.account.status.ip-autodetect-failure"); - } - } - super.updateConfiguration(updateConfig); - // get new config after update - config = Optional.of(getConfigAs(AccountConfiguration.class)); - } - private String configValid() { - config = Optional.of(getConfigAs(AccountConfiguration.class)); + config = getConfigAs(AccountConfiguration.class); String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId(); - if (Constants.NOT_SET.equals(config.get().callbackIP)) { - return textKey + Constants.STATUS_IP_MISSING; - } else if (config.get().callbackPort == -1) { - return textKey + Constants.STATUS_PORT_MISSING; - } else if (Constants.NOT_SET.equals(config.get().email)) { + if (Constants.NOT_SET.equals(config.refreshToken)) { + return textKey + Constants.STATUS_REFRESH_TOKEN_MISSING; + } else if (Constants.NOT_SET.equals(config.email)) { return textKey + Constants.STATUS_EMAIL_MISSING; - } else if (Constants.NOT_SET.equals(config.get().region)) { + } else if (Constants.NOT_SET.equals(config.region)) { return textKey + Constants.STATUS_REGION_MISSING; - } else if (config.get().refreshInterval <= 01) { + } else if (config.refreshInterval < 5) { return textKey + Constants.STATUS_REFRESH_INVALID; } else { return Constants.EMPTY; @@ -207,13 +157,6 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr @Override public void dispose() { - if (server.isPresent()) { - AuthServer authServer = server.get(); - authServer.stop(); - authServer.dispose(); - server = Optional.empty(); - Utils.removePort(config.get().callbackPort); - } refreshScheduler.ifPresent(schedule -> { if (!schedule.isCancelled()) { schedule.cancel(true); @@ -223,6 +166,13 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr eventQueue.clear(); } + @Override + public void handleRemoval() { + storage.remove(config.email); + authService = Optional.empty(); + super.handleRemoval(); + } + /** * https://next.openhab.org/javadoc/latest/org/openhab/core/auth/client/oauth2/package-summary.html */ @@ -230,27 +180,16 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr public void onAccessTokenResponse(AccessTokenResponse tokenResponse) { if (!Constants.NOT_SET.equals(tokenResponse.getAccessToken())) { scheduler.schedule(this::refresh, 2, TimeUnit.SECONDS); - } else if (server.isEmpty()) { - // server not running - fix first - String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId() - + Constants.STATUS_SERVER_RESTART; - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, textKey); } else { // all failed - start manual authorization String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId() + Constants.STATUS_AUTH_NEEDED; - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - textKey + " [\"" + thing.getProperties().get("callbackUrl") + "\"]"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, textKey); } } - @Override - public String toString() { - return Integer.toString(config.get().callbackPort); - } - public String getWSUri() { - return Utils.getWebsocketServer(config.get().region); + return Utils.getWebsocketServer(config.region); } public ClientUpgradeRequest getClientUpgradeRequest() { @@ -260,12 +199,12 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr request.setHeader("X-TrackingId", UUID.randomUUID().toString()); request.setHeader("Ris-Os-Name", Constants.RIS_OS_NAME); request.setHeader("Ris-Os-Version", Constants.RIS_OS_VERSION); - request.setHeader("Ris-Sdk-Version", Utils.getRisSDKVersion(config.get().region)); + request.setHeader("Ris-Sdk-Version", Utils.getRisSDKVersion(config.region)); request.setHeader("X-Locale", localeProvider.getLocale().getLanguage() + "-" + localeProvider.getLocale().getCountry()); // de-DE - request.setHeader("User-Agent", Utils.getApplication(config.get().region)); - request.setHeader("X-Applicationname", Utils.getUserAgent(config.get().region)); - request.setHeader("Ris-Application-Version", Utils.getRisApplicationVersion(config.get().region)); + request.setHeader("User-Agent", Utils.getApplication(config.region)); + request.setHeader("X-Applicationname", Utils.getUserAgent(config.region)); + request.setHeader("Ris-Application-Version", Utils.getRisApplicationVersion(config.region)); return request; } @@ -452,8 +391,7 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr Map featureMap = new HashMap<>(); try { // add vehicle capabilities - String capabilitiesUrl = Utils.getRestAPIServer(config.get().region) - + String.format(capabilitiesEndpoint, vin); + String capabilitiesUrl = Utils.getRestAPIServer(config.region) + String.format(capabilitiesEndpoint, vin); Request capabilitiesRequest = httpClient.newRequest(capabilitiesUrl); authService.get().addBasicHeaders(capabilitiesRequest); capabilitiesRequest.header("X-SessionId", UUID.randomUUID().toString()); @@ -489,7 +427,7 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr } // add command capabilities - String commandCapabilitiesUrl = Utils.getRestAPIServer(config.get().region) + String commandCapabilitiesUrl = Utils.getRestAPIServer(config.region) + String.format(commandCapabilitiesEndpoint, vin); Request commandCapabilitiesRequest = httpClient.newRequest(commandCapabilitiesUrl); authService.get().addBasicHeaders(commandCapabilitiesRequest); @@ -557,7 +495,7 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr */ public void sendPoi(String vin, JSONObject poi) { - String poiUrl = Utils.getRestAPIServer(config.get().region) + String.format(poiEndpoint, vin); + String poiUrl = Utils.getRestAPIServer(config.region) + String.format(poiEndpoint, vin); Request poiRequest = httpClient.POST(poiUrl); authService.get().addBasicHeaders(poiRequest); poiRequest.header("X-SessionId", UUID.randomUUID().toString()); diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java index 586d2600f4c..12e3a533987 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java @@ -216,7 +216,7 @@ public class VehicleHandler extends BaseThingHandler { var crBuilder = CommandRequest.newBuilder().setVin(config.get().vin).setRequestId(UUID.randomUUID().toString()); String group = channelUID.getGroupId(); String channel = channelUID.getIdWithoutGroup(); - String pin = accountHandler.get().config.get().pin; + String pin = accountHandler.get().config.pin; if (group == null) { logger.trace("No command {} found for {}", command, channel); return; diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthServer.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthServer.java deleted file mode 100644 index 5cac0ee07d9..00000000000 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthServer.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2010-2025 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mercedesme.internal.server; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletHandler; -import org.openhab.binding.mercedesme.internal.Constants; -import org.openhab.binding.mercedesme.internal.config.AccountConfiguration; -import org.openhab.core.auth.client.oauth2.AccessTokenResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link AuthServer} provides HTTP Server to show servlet content of the authentication process - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class AuthServer { - private static final Logger LOGGER = LoggerFactory.getLogger(AuthServer.class); - private static final Map SERVER_MAP = new HashMap<>(); - private static final AccessTokenResponse INVALID_ACCESS_TOKEN = new AccessTokenResponse(); - - private final HttpClient httpClient; - - private Optional server = Optional.empty(); - private AccountConfiguration config; - public String callbackUrl; - - public AuthServer(HttpClient hc, AccountConfiguration config, String callbackUrl) { - httpClient = hc; - SERVER_MAP.put(Integer.valueOf(config.callbackPort), this); - this.config = config; - this.callbackUrl = callbackUrl; - INVALID_ACCESS_TOKEN.setAccessToken(Constants.EMPTY); - } - - public void dispose() { - SERVER_MAP.remove(Integer.valueOf(config.callbackPort)); - } - - public boolean start() { - // avoid real server start for unit tests - if (server.isPresent() || Constants.JUNIT_SERVER_ADDR.equals(callbackUrl)) { - return true; - } - server = Optional.of(new Server()); - ServerConnector connector = new ServerConnector(server.get()); - connector.setPort(config.callbackPort); - server.get().setConnectors(new Connector[] { connector }); - ServletHandler servletHandler = new ServletHandler(); - server.get().setHandler(servletHandler); - servletHandler.addServletWithMapping(AuthServlet.class, Constants.CALLBACK_ENDPOINT); - try { - server.get().start(); - return true; - } catch (Exception e) { - LOGGER.trace("Cannot start Callback Server for port {}, Error {}", config.callbackPort, e.getMessage()); - server = Optional.empty(); - return false; - } - } - - public void stop() { - try { - if (server.isPresent()) { - server.get().stop(); - server = Optional.empty(); - } - } catch (Exception e) { - LOGGER.trace("Cannot start Callback Server for port {}, Error {}", config.callbackPort, e.getMessage()); - } - } - - @Nullable - public static AuthServer getServer(int port) { - return SERVER_MAP.get(port); - } - - public HttpClient getHttpClient() { - return httpClient; - } - - public String getRegion() { - return config.region; - } -} diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthService.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthService.java index c660545c958..585f9a1cc35 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthService.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthService.java @@ -16,9 +16,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Instant; -import java.util.HashMap; import java.util.Locale; -import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -33,7 +31,6 @@ import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.openhab.binding.mercedesme.internal.Constants; import org.openhab.binding.mercedesme.internal.config.AccountConfiguration; -import org.openhab.binding.mercedesme.internal.dto.PINRequest; import org.openhab.binding.mercedesme.internal.dto.TokenResponse; import org.openhab.binding.mercedesme.internal.utils.Utils; import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener; @@ -42,6 +39,8 @@ import org.openhab.core.storage.Storage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonSyntaxException; + /** * {@link AuthService} helpers for token management * @@ -49,23 +48,19 @@ import org.slf4j.LoggerFactory; */ @NonNullByDefault public class AuthService { - public static final AccessTokenResponse INVALID_TOKEN = new AccessTokenResponse(); private static final int EXPIRATION_BUFFER = 5; - private static final Map AUTH_MAP = new HashMap<>(); private final Logger logger = LoggerFactory.getLogger(AuthService.class); - AccessTokenRefreshListener listener; + private AccessTokenRefreshListener listener; + private AccountConfiguration config; + private AccessTokenResponse token = Utils.INVALID_TOKEN; + private Storage storage; private HttpClient httpClient; private String identifier; - private AccountConfiguration config; private Locale locale; - private Storage storage; - private AccessTokenResponse token; public AuthService(AccessTokenRefreshListener atrl, HttpClient hc, AccountConfiguration ac, Locale l, - Storage store) { - INVALID_TOKEN.setAccessToken(Constants.NOT_SET); - INVALID_TOKEN.setRefreshToken(Constants.NOT_SET); + Storage store, String refreshToken) { listener = atrl; httpClient = hc; config = ac; @@ -73,129 +68,53 @@ public class AuthService { locale = l; storage = store; - // restore token - String storedObject = storage.get(identifier); - if (storedObject == null) { - token = INVALID_TOKEN; - listener.onAccessTokenResponse(token); - } else { - token = Utils.fromString(storedObject); - if (token.isExpired(Instant.now(), EXPIRATION_BUFFER)) { - if (!Constants.NOT_SET.equals(token.getRefreshToken())) { - refreshToken(); - listener.onAccessTokenResponse(token); - } else { - token = INVALID_TOKEN; - listener.onAccessTokenResponse(token); + // restore token from persistence if available + String storedToken = storage.get(identifier); + if (storedToken != null) { + // returns INVALID_TOKEN in case of an error + logger.trace("MB-Auth {} Restore token from persistence", prefix()); + try { + logger.trace("MB-Auth {} storedToken {}", prefix(), storedToken); + TokenResponse tokenResponseJson = Utils.GSON.fromJson(storedToken, TokenResponse.class); + token = decodeToken(tokenResponseJson); + if (!tokenIsValid()) { + token = Utils.INVALID_TOKEN; + storage.remove(identifier); + logger.trace("MB-Auth {} invalid storedToken {}", prefix(), storedToken); } - } else { - listener.onAccessTokenResponse(token); + } catch (JsonSyntaxException jse) { + // fallback of non human readable base64 token persistence + logger.debug("MB-Auth {} Fallback token decoding", prefix()); + token = Utils.fromString(storedToken); } + } else { + // initialize token with refresh token from configuration and expiration 0 + // this will trigger an immediately refresh of the token + logger.trace("MB-Auth {} Create token from config", prefix()); + token = new AccessTokenResponse(); + token.setAccessToken(refreshToken); + token.setRefreshToken(refreshToken); + token.setExpiresIn(0); } - AUTH_MAP.put(config.callbackPort, this); } - @Nullable - public static AuthService getAuthService(Integer key) { - return AUTH_MAP.get(key); + public synchronized String getToken() { + if (token.isExpired(Instant.now(), EXPIRATION_BUFFER)) { + if (tokenIsValid()) { + refreshToken(); + } + } + return token.getAccessToken(); } - /** - * - * @return guid from request to create token in next step - */ - public String requestPin() { - String configUrl = Utils.getAuthConfigURL(config.region); - String sessionId = UUID.randomUUID().toString(); - Request configRequest = httpClient.newRequest(configUrl); - addBasicHeaders(configRequest); - configRequest.header("X-Trackingid", UUID.randomUUID().toString()); - configRequest.header("X-Sessionid", sessionId); - try { - ContentResponse cr = configRequest.timeout(Constants.REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(); - if (cr.getStatus() == 200) { - logger.trace("{} Config Request PIN fine {} {}", prefix(), cr.getStatus(), cr.getContentAsString()); - } else { - logger.trace("{} Failed to request config for pin {} {}", prefix(), cr.getStatus(), - cr.getContentAsString()); - return Constants.NOT_SET; - } - } catch (InterruptedException | TimeoutException | ExecutionException e) { - logger.trace("{} Failed to request config for pin {}", prefix(), e.getMessage()); - return Constants.NOT_SET; - } - - String url = Utils.getAuthURL(config.region); - Request req = httpClient.POST(url); - addBasicHeaders(req); - req.header("X-Trackingid", UUID.randomUUID().toString()); - req.header("X-Sessionid", sessionId); - - PINRequest pr = new PINRequest(config.email, locale.getCountry()); - req.header(HttpHeader.CONTENT_TYPE, "application/json"); - logger.trace("{} payload {}", url, Utils.GSON.toJson(pr)); - req.content(new StringContentProvider(Utils.GSON.toJson(pr), "utf-8")); - - try { - ContentResponse cr = req.timeout(Constants.REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(); - if (cr.getStatus() == 200) { - logger.trace("{} Request PIN fine {} {}", prefix(), cr.getStatus(), cr.getContentAsString()); - return pr.nonce; - } else { - logger.trace("{} Failed to request pin {} {}", prefix(), cr.getStatus(), cr.getContentAsString()); - } - } catch (InterruptedException | TimeoutException | ExecutionException e) { - logger.trace("{} Failed to request pin {}", prefix(), e.getMessage()); - } - return Constants.NOT_SET; - } - - public boolean requestToken(String password) { - try { - // Request + headers - String url = Utils.getTokenUrl(config.region); - Request req = httpClient.POST(url); - addBasicHeaders(req); - req.header("Stage", "prod"); - req.header("X-Device-Id", UUID.randomUUID().toString()); - req.header("X-Request-Id", UUID.randomUUID().toString()); - - // Content URL form - String clientId = "client_id=" - + URLEncoder.encode(Utils.getLoginAppId(config.region), StandardCharsets.UTF_8.toString()); - String grantAttribute = "grant_type=password"; - String userAttribute = "username=" + URLEncoder.encode(config.email, StandardCharsets.UTF_8.toString()); - String passwordAttribute = "password=" + URLEncoder.encode(password, StandardCharsets.UTF_8.toString()); - String scopeAttribute = "scope=" + URLEncoder.encode(Constants.SCOPE, StandardCharsets.UTF_8.toString()); - String content = clientId + "&" + grantAttribute + "&" + userAttribute + "&" + passwordAttribute + "&" - + scopeAttribute; - req.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded"); - req.content(new StringContentProvider(content)); - - // Send - ContentResponse cr = req.timeout(Constants.REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(); - if (cr.getStatus() == 200) { - String responseString = cr.getContentAsString(); - saveTokenResponse(responseString); - listener.onAccessTokenResponse(token); - return true; - } else { - logger.trace("{} Failed to get token {} {}", prefix(), cr.getStatus(), cr.getContentAsString()); - } - } catch (InterruptedException | TimeoutException | ExecutionException | UnsupportedEncodingException e) { - logger.trace("{} Failed to get token {}", prefix(), e.getMessage()); - } - return false; - } - - public void refreshToken() { + private void refreshToken() { + logger.trace("MB-Auth {} refreshToken", prefix()); try { String url = Utils.getTokenUrl(config.region); Request req = httpClient.POST(url); req.header("X-Device-Id", UUID.randomUUID().toString()); req.header("X-Request-Id", UUID.randomUUID().toString()); - // Content URL form String grantAttribute = "grant_type=refresh_token"; String refreshTokenAttribute = "refresh_token=" + URLEncoder.encode(token.getRefreshToken(), StandardCharsets.UTF_8.toString()); @@ -203,35 +122,79 @@ public class AuthService { req.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded"); req.content(new StringContentProvider(content)); - // Send ContentResponse cr = req.timeout(Constants.REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(); - if (cr.getStatus() == 200) { - saveTokenResponse(cr.getContentAsString()); - listener.onAccessTokenResponse(token); + int tokenResponseStatus = cr.getStatus(); + String tokenResponse = cr.getContentAsString(); + if (tokenResponseStatus == 200) { + TokenResponse tokenResponseJson = Utils.GSON.fromJson(tokenResponse, TokenResponse.class); + if (tokenResponseJson != null) { + // response doesn't contain creation date time so set it manually + tokenResponseJson.createdOn = Instant.now().toString(); + // a new refresh token is delivered optional + // if not set in response take old one + if (Constants.NOT_SET.equals(tokenResponseJson.refreshToken)) { + tokenResponseJson.refreshToken = token.getRefreshToken(); + } + token = decodeToken(tokenResponseJson); + if (tokenIsValid()) { + String tokenStore = Utils.GSON.toJson(tokenResponseJson); + logger.debug("MB-Auth {} refreshToken result {}", prefix(), token.toString()); + storage.put(identifier, tokenStore); + } else { + token = Utils.INVALID_TOKEN; + storage.remove(identifier); + logger.warn("MB-Auth {} Refresh token delivered invalid result {} {}", prefix(), + tokenResponseStatus, tokenResponse); + } + } else { + logger.debug("MB-Auth {} token refersh delivered not parsable result {}", prefix(), tokenResponse); + token = Utils.INVALID_TOKEN; + } } else { - logger.trace("{} Failed to refresh token {} {}", prefix(), cr.getStatus(), cr.getContentAsString()); + token = Utils.INVALID_TOKEN; + /** + * 1) remove token from storage + * 2) listener will be informed about INVALID_TOKEN and bridge will go OFFLINE + * 3) user needs to update refreshToken configuration parameter + */ + storage.remove(identifier); + logger.warn("MB-Auth {} Failed to refresh token {} {}", prefix(), tokenResponseStatus, tokenResponse); } - } catch (InterruptedException | TimeoutException | ExecutionException | UnsupportedEncodingException e) { - logger.trace("{} Failed to refresh token {}", prefix(), e.getMessage()); + listener.onAccessTokenResponse(token); + } catch (InterruptedException | TimeoutException | ExecutionException | UnsupportedEncodingException + | JsonSyntaxException e) { + logger.info("{} Failed to refresh token {}", prefix(), e.getMessage()); } } - public String getToken() { - if (token.isExpired(Instant.now(), EXPIRATION_BUFFER)) { - if (!Constants.NOT_SET.equals(token.getRefreshToken())) { - refreshToken(); - // token shall be updated now - retry expired check - if (token.isExpired(Instant.now(), EXPIRATION_BUFFER)) { - token = INVALID_TOKEN; - listener.onAccessTokenResponse(token); - return Constants.NOT_SET; - } + private AccessTokenResponse decodeToken(@Nullable TokenResponse tokenJson) { + if (tokenJson != null) { + AccessTokenResponse atr = new AccessTokenResponse(); + atr.setCreatedOn(Instant.parse(tokenJson.createdOn)); + atr.setExpiresIn(tokenJson.expiresIn); + atr.setAccessToken(tokenJson.accessToken); + if (!Constants.NOT_SET.equals(tokenJson.refreshToken)) { + atr.setRefreshToken(tokenJson.refreshToken); } else { - token = INVALID_TOKEN; - logger.trace("{} Refresh token empty", prefix()); + // Preserve refresh token if available + if (!Constants.NOT_SET.equals(token.getRefreshToken())) { + atr.setRefreshToken(token.getRefreshToken()); + } else { + logger.debug("MB-Auth {} Neither new nor old refresh token available", prefix()); + return Utils.INVALID_TOKEN; + } } + atr.setTokenType("Bearer"); + atr.setScope(Constants.SCOPE); + return atr; + } else { + logger.debug("MB-Auth {} Neither Token Response is null", prefix()); } - return token.getAccessToken(); + return Utils.INVALID_TOKEN; + } + + private boolean tokenIsValid() { + return !Constants.NOT_SET.equals(token.getAccessToken()) && !Constants.NOT_SET.equals(token.getRefreshToken()); } public void addBasicHeaders(Request req) { @@ -244,30 +207,6 @@ public class AuthService { req.header("Ris-Application-Version", Utils.getRisApplicationVersion(config.region)); } - private void saveTokenResponse(String response) { - TokenResponse tr = Utils.GSON.fromJson(response, TokenResponse.class); - AccessTokenResponse atr = new AccessTokenResponse(); - if (tr != null) { - atr.setAccessToken(tr.accessToken); - atr.setCreatedOn(Instant.now()); - atr.setExpiresIn(tr.expiresIn); - // Preserve refresh token if available - if (Constants.NOT_SET.equals(tr.refreshToken) && !Constants.NOT_SET.equals(token.getRefreshToken())) { - atr.setRefreshToken(token.getRefreshToken()); - } else if (!Constants.NOT_SET.equals(tr.refreshToken)) { - atr.setRefreshToken(tr.refreshToken); - } else { - logger.trace("{} Neither new nor old refresh token available", prefix()); - } - atr.setTokenType("Bearer"); - atr.setScope(Constants.SCOPE); - storage.put(identifier, Utils.toString(atr)); - token = atr; - } else { - logger.trace("{} Token Response is null", prefix()); - } - } - private String prefix() { return "[" + config.email + "] "; } diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthServlet.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthServlet.java deleted file mode 100644 index 95bfb4ec147..00000000000 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/AuthServlet.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2010-2025 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mercedesme.internal.server; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.mercedesme.internal.Constants; - -/** - * {@link AuthServlet} provides simple HTML pages for authorization workflow - * - * @author Bernd Weymann - Initial contribution - */ -@SuppressWarnings("serial") -@NonNullByDefault -public class AuthServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - AuthService myAuthService = AuthService.getAuthService(request.getLocalPort()); - String guid = request.getParameter(Constants.GUID); - String pin = request.getParameter(Constants.PIN); - if (guid == null && pin == null && myAuthService != null) { - // request PIN - String requestVal = myAuthService.requestPin(); - if (!Constants.NOT_SET.equals(requestVal)) { - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println(""); - response.getWriter().println(""); - response.getWriter().println("

Step 1 - PIN Requested

"); - response.getWriter().println("
"); - response.getWriter().println("PIN was requested and should be present in your EMail Inbox
"); - response.getWriter() - .println("Check first if you received the PIN and then continue with the below Link
"); - response.getWriter().println("Click here to continue with Step 2"); - response.getWriter().println(""); - response.getWriter().println(""); - } else { - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println(""); - response.getWriter().println(""); - response.getWriter().println("Something went wrong
"); - response.getWriter().println(""); - response.getWriter().println(""); - } - - } else if (guid != null && pin == null && myAuthService != null) { - // show insert PIN input field - - response.setContentType("text/html"); - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println(""); - response.getWriter().println(""); - response.getWriter().println("

Step 2 - Enter PIN

"); - response.getWriter().println("
"); - response.getWriter().println("Enter PIN in second input field - leave guid as it is!
"); - response.getWriter().println("
"); - response.getWriter().println("
"); - response.getWriter().println(""); - response.getWriter().println(""); - response.getWriter().println("
"); - response.getWriter().println(""); - response.getWriter().println(""); - response.getWriter().println("
"); - response.getWriter().println(""); - response.getWriter().println("
"); - response.getWriter().println(""); - response.getWriter().println(""); - } else if (guid != null && pin != null && myAuthService != null) { - // call getToken and show result - boolean result = myAuthService.requestToken(guid + ":" + pin); - response.setContentType("text/html"); - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println(""); - response.getWriter().println(""); - response.getWriter().println("

Step 3 - Save Token

"); - response.getWriter().println("
"); - if (result) { - response.getWriter().println("Success - everything done!
"); - } else { - response.getWriter().println("Failure - Please check logs for further analysis!
"); - } - response.getWriter().println(""); - response.getWriter().println(""); - } - } -} diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/MBWebsocket.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/MBWebsocket.java index 26c7ed05a93..04db1b4e034 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/MBWebsocket.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/MBWebsocket.java @@ -93,11 +93,11 @@ public class MBWebsocket { client.setStopTimeout(CONNECT_TIMEOUT_MS); ClientUpgradeRequest request = accountHandler.getClientUpgradeRequest(); String websocketURL = accountHandler.getWSUri(); - logger.trace("Websocket start {}", websocketURL); if (Constants.JUNIT_TOKEN.equals(request.getHeader("Authorization"))) { // avoid unit test requesting real websocket - simply return return; } + logger.trace("Websocket start {}", websocketURL); client.start(); client.connect(this, new URI(websocketURL), request); while (keepAlive || Instant.now().isBefore(runTill)) { @@ -177,7 +177,8 @@ public class MBWebsocket { } } else { if (!b) { - // after keep alive is finished add 5 minutes to cover e.g. door events after trip is finished + // after keep alive is finished add 5 minutes to cover e.g. door events after + // trip is finished runTill = Instant.now().plusMillis(KEEP_ALIVE_ADDON); logger.trace("Websocket - keep alive stop - run till {}", runTill.toString()); } @@ -199,8 +200,10 @@ public class MBWebsocket { * https://community.openhab.org/t/mercedes-me/136866/12 * Release Websocket thread as early as possible to avoid execeptions * - * 1. Websocket thread responsible for reading stream in bytes and enqueue for AccountHandler. - * 2. AccountHamdler thread responsible for encoding proto message. In case of update enqueue proto message + * 1. Websocket thread responsible for reading stream in bytes and enqueue for + * AccountHandler. + * 2. AccountHamdler thread responsible for encoding proto message. In case of + * update enqueue proto message * at VehicleHandöer * 3. VehicleHandler responsible to update channels */ @@ -225,6 +228,7 @@ public class MBWebsocket { @OnWebSocketError public void onError(Throwable t) { + logger.debug("Error during web socket connection - {}", t.getMessage()); accountHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/mercedesme.account.status.websocket-failure [\"" + t.getMessage() + "\"]"); } diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Utils.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Utils.java index 0954469607e..b64c5a76398 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Utils.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/utils/Utils.java @@ -13,10 +13,8 @@ package org.openhab.binding.mercedesme.internal.utils; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; @@ -37,7 +35,6 @@ import org.json.JSONArray; import org.json.JSONObject; import org.openhab.binding.mercedesme.internal.Constants; import org.openhab.binding.mercedesme.internal.MercedesMeHandlerFactory; -import org.openhab.binding.mercedesme.internal.server.AuthService; import org.openhab.core.auth.client.oauth2.AccessTokenResponse; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TimeZoneProvider; @@ -78,14 +75,13 @@ public class Utils { private static final int R = 6371; // Radius of the earth private static int port = 8090; - private static TimeZoneProvider timeZoneProvider = new TimeZoneProvider() { + public static TimeZoneProvider timeZoneProvider = new TimeZoneProvider() { @Override public ZoneId getTimeZone() { return ZoneId.systemDefault(); } }; - private static LocaleProvider localeProvider = new LocaleProvider() { - + public static LocaleProvider localeProvider = new LocaleProvider() { @Override public Locale getLocale() { return Locale.getDefault(); @@ -94,10 +90,13 @@ public class Utils { public static final Gson GSON = new Gson(); public static final Map ZONE_HASHMAP = new HashMap<>(); public static final Map PROGRAM_HASHMAP = new HashMap<>(); + public static final AccessTokenResponse INVALID_TOKEN = new AccessTokenResponse(); public static void initialize(TimeZoneProvider tzp, LocaleProvider lp) { timeZoneProvider = tzp; localeProvider = lp; + INVALID_TOKEN.setAccessToken(Constants.NOT_SET); + INVALID_TOKEN.setRefreshToken(Constants.NOT_SET); } /** @@ -138,27 +137,6 @@ public class Utils { return port; } - /** - * Register port for an AccountHandler - */ - public static synchronized void addPort(int portNr) { - if (PORTS.contains(portNr) && portNr != 99999) { - LOGGER.warn("Port {} already occupied", portNr); - } - PORTS.add(portNr); - } - - /** - * Unregister port for an AccountHandler - */ - public static synchronized void removePort(int portNr) { - PORTS.remove(Integer.valueOf(portNr)); - } - - public static String getCallbackAddress(String callbackIP, int callbackPort) { - return "http://" + callbackIP + Constants.COLON + callbackPort + Constants.CALLBACK_ENDPOINT; - } - /** * Calculate REST API server address according to region * @@ -286,41 +264,6 @@ public class Utils { } } - /** - * Calculate authorization config URL as pre-configuration prior to authorization call - * - * @param region - configured region - * @return authorization config URL as String - */ - public static String getAuthConfigURL(String region) { - return getRestAPIServer(region) + "/v1/config"; - } - - /** - * Calculate login app id according to region - * - * @param region - configured region - * @return login app id as String - */ - public static String getLoginAppId(String region) { - switch (region) { - case Constants.REGION_CHINA: - return Constants.LOGIN_APP_ID_CN; - default: - return Constants.LOGIN_APP_ID; - } - } - - /** - * Calculate authorization URL for authorization call - * - * @param region - configured region - * @return authorization URL as String - */ - public static String getAuthURL(String region) { - return getRestAPIServer(region) + "/v1/login"; - } - /** * Calculate token URL for getting token * @@ -337,6 +280,7 @@ public class Utils { * @param token - Base64 String from storage * @return AccessTokenResponse decoded from String, invalid token otherwise */ + @Deprecated public static AccessTokenResponse fromString(String token) { try { byte[] data = Base64.getDecoder().decode(token); @@ -347,25 +291,7 @@ public class Utils { } catch (IOException | ClassNotFoundException e) { LOGGER.warn("Error converting string to token {}", e.getMessage()); } - return AuthService.INVALID_TOKEN; - } - - /** - * Encode AccessTokenResponse as Base64 String for storage - * - * @param token - AccessTokenResponse to convert - */ - public static String toString(AccessTokenResponse token) { - try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos); - oos.writeObject(token); - oos.close(); - return Base64.getEncoder().encodeToString(baos.toByteArray()); - } catch (IOException e) { - LOGGER.warn("Error converting token to string {}", e.getMessage()); - } - return Constants.NOT_SET; + return INVALID_TOKEN; } /** diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml index c73b7c5b011..d61b45b6097 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/config/bridge-config.xml @@ -19,6 +19,11 @@ + + + Refresh Token from MB Token Requester app + "takeover previous token" + PIN for commands @@ -29,15 +34,5 @@ Refresh Interval in Minutes 15 - - - IP address for openHAB callback URL - true - - - - Port Number for openHAB callback URL - true - diff --git a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties index a18833ea11b..461ccb4a12a 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties +++ b/bundles/org.openhab.binding.mercedesme/src/main/resources/OH-INF/i18n/mercedesme.properties @@ -19,16 +19,14 @@ thing-type.mercedesme.hybrid.description = Conventional Fuel Vehicle with suppor thing-type.config.mercedesme.bev.batteryCapacity.label = Battery Capacity thing-type.config.mercedesme.bev.batteryCapacity.description = Battery capacity in kWh of vehicle thing-type.config.mercedesme.bev.vin.label = Vehicle Identification Number -thing-type.config.mercedesme.bridge.callbackIP.label = Callback IP Address -thing-type.config.mercedesme.bridge.callbackIP.description = IP address for openHAB callback URL -thing-type.config.mercedesme.bridge.callbackPort.label = Callback Port Number -thing-type.config.mercedesme.bridge.callbackPort.description = Port Number for openHAB callback URL thing-type.config.mercedesme.bridge.email.label = MercedesMe EMail thing-type.config.mercedesme.bridge.email.description = EMail address for MercedesMe account thing-type.config.mercedesme.bridge.pin.label = PIN thing-type.config.mercedesme.bridge.pin.description = PIN for commands thing-type.config.mercedesme.bridge.refreshInterval.label = Refresh Interval thing-type.config.mercedesme.bridge.refreshInterval.description = Refresh Interval in Minutes +thing-type.config.mercedesme.bridge.refreshToken.label = Refresh Token +thing-type.config.mercedesme.bridge.refreshToken.description = Refresh Token from MB Token Requester app thing-type.config.mercedesme.bridge.region.label = Region thing-type.config.mercedesme.bridge.region.option.EU = Europe thing-type.config.mercedesme.bridge.region.option.NA = North America @@ -374,13 +372,10 @@ longitudeDescription = Longitude of the location # thing status types -mercedesme.account.status.authorization-needed = Manual Authorization needed at {0} +mercedesme.account.status.authorization-needed = Generate new refresh token +mercedesme.account.status.refresh-token-missing = Refresh token missing mercedesme.account.status.email-missing = EMail missing mercedesme.account.status.region-missing = Region missing mercedesme.account.status.refresh-invalid = Refresh Interval Invalid -mercedesme.account.status.ip-missing = Callback IP missing -mercedesme.account.status.port-missing = Callback Port missing -mercedesme.account.status.server-restart = Disable and enable Bridge to restart Authorization Server mercedesme.vehicle.status.bridge-missing = Bridge not set -mercedesme.account.status.ip-autodetect-failure = Callback IP cannot be detected mercedesme.account.status.websocket-failure = Websocket Exception: Reason: {0} diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/StatusTests.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/StatusTests.java index 7506d8f5c8a..37d4639ff06 100644 --- a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/StatusTests.java +++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/StatusTests.java @@ -13,12 +13,18 @@ package org.openhab.binding.mercedesme; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; -import java.time.Instant; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; import org.junit.jupiter.api.Test; import org.openhab.binding.mercedesme.internal.Constants; import org.openhab.binding.mercedesme.internal.handler.AccountHandlerMock; @@ -33,7 +39,7 @@ import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.internal.BridgeImpl; /** - * {@link StatusTests} sequencess for testing ThingStatus + * {@link StatusTests} sequences for testing ThingStatus * * @author Bernd Weymann - Initial contribution */ @@ -41,23 +47,41 @@ import org.openhab.core.thing.internal.BridgeImpl; class StatusTests { public static void tearDown(AccountHandlerMock ahm) { - // ahm.setCallback(null); - ahm.dispose(); try { Thread.sleep(250); } catch (InterruptedException e) { fail(); } + ahm.dispose(); + } + + public static HttpClient getHttpClient(int tokenResponseCode) { + Utils.initialize(Utils.timeZoneProvider, Utils.localeProvider); + HttpClient httpClient = mock(HttpClient.class); + try { + Request clientRequest = mock(Request.class); + when(httpClient.POST(anyString())).thenReturn(clientRequest); + when(clientRequest.header(anyString(), anyString())).thenReturn(clientRequest); + when(clientRequest.content(any())).thenReturn(clientRequest); + when(clientRequest.timeout(anyLong(), any())).thenReturn(clientRequest); + ContentResponse response = mock(ContentResponse.class); + when(response.getStatus()).thenReturn(tokenResponseCode); + String tokenResponse = FileReader.readFileInString("src/test/resources/json/TokenResponse.json"); + when(response.getContentAsString()).thenReturn(tokenResponse); + when(clientRequest.send()).thenReturn(response); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + fail(e.getMessage()); + } + return httpClient; } @Test void testInvalidConfig() { BridgeImpl bi = new BridgeImpl(new ThingTypeUID("test", "account"), "MB"); Map config = new HashMap<>(); - config.put("callbackIP", "999.999.999.999"); - config.put("callbackPort", "99999"); + config.put("refreshToken", Constants.JUNIT_REFRESH_TOKEN); bi.setConfiguration(new Configuration(config)); - AccountHandlerMock ahm = new AccountHandlerMock(bi, null); + AccountHandlerMock ahm = new AccountHandlerMock(bi, null, getHttpClient(404)); ThingCallbackListener tcl = new ThingCallbackListener(); ahm.setCallback(tcl); ahm.initialize(); @@ -83,6 +107,7 @@ class StatusTests { tcl = new ThingCallbackListener(); ahm.setCallback(tcl); ahm.initialize(); + ahm.refreshToken(); tsi = tcl.getThingStatus(); assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Auth offline"); assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, tsi.getStatusDetail(), "Auth detail"); @@ -107,13 +132,13 @@ class StatusTests { config.put("refreshInterval", Integer.MAX_VALUE); config.put("region", "row"); config.put("email", "a@b.c"); - config.put("callbackIP", "999.999.999.999"); - config.put("callbackPort", "99999"); + config.put("refreshToken", "abc"); bi.setConfiguration(new Configuration(config)); - AccountHandlerMock ahm = new AccountHandlerMock(bi, null); + AccountHandlerMock ahm = new AccountHandlerMock(bi, null, getHttpClient(404)); ThingCallbackListener tcl = new ThingCallbackListener(); ahm.setCallback(tcl); ahm.initialize(); + ahm.refreshToken(); ThingStatusInfo tsi = tcl.getThingStatus(); assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Auth Offline"); assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, tsi.getStatusDetail(), "Auth details"); @@ -140,17 +165,10 @@ class StatusTests { config.put("refreshInterval", Integer.MAX_VALUE); config.put("region", "row"); config.put("email", "a@b.c"); - config.put("callbackIP", "999.999.999.999"); - config.put("callbackPort", "99999"); + config.put("refreshToken", "abc"); bi.setConfiguration(new Configuration(config)); - AccessTokenResponse token = new AccessTokenResponse(); - token.setExpiresIn(3000); - token.setAccessToken(Constants.JUNIT_TOKEN); - token.setRefreshToken(Constants.JUNIT_REFRESH_TOKEN); - token.setCreatedOn(Instant.now()); - token.setTokenType("Bearer"); - token.setScope(Constants.SCOPE); - AccountHandlerMock ahm = new AccountHandlerMock(bi, Utils.toString(token)); + String tokenResponse = FileReader.readFileInString("src/test/resources/json/TokenResponse.json"); + AccountHandlerMock ahm = new AccountHandlerMock(bi, tokenResponse, getHttpClient(200)); ThingCallbackListener tcl = new ThingCallbackListener(); ahm.setCallback(tcl); ahm.initialize(); diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/AccountHandlerMock.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/AccountHandlerMock.java index b80f81190aa..48f6939ef27 100644 --- a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/AccountHandlerMock.java +++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/AccountHandlerMock.java @@ -15,7 +15,6 @@ package org.openhab.binding.mercedesme.internal.handler; import static org.mockito.Mockito.mock; import java.util.Locale; -import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -26,7 +25,6 @@ import org.openhab.binding.mercedesme.internal.Constants; import org.openhab.binding.mercedesme.internal.config.AccountConfiguration; import org.openhab.binding.mercedesme.internal.discovery.MercedesMeDiscoveryService; import org.openhab.core.i18n.LocaleProvider; -import org.openhab.core.net.NetworkAddressService; import org.openhab.core.storage.Storage; import org.openhab.core.storage.StorageService; import org.openhab.core.test.storage.VolatileStorageService; @@ -54,18 +52,17 @@ public class AccountHandlerMock extends AccountHandler { public AccountHandlerMock() { super(mock(Bridge.class), mock(MercedesMeDiscoveryService.class), mock(HttpClient.class), - mock(LocaleProvider.class), mock(StorageService.class), mock(NetworkAddressService.class)); - config = Optional.of(new AccountConfiguration()); + mock(LocaleProvider.class), mock(StorageService.class)); + config = new AccountConfiguration(); } - public AccountHandlerMock(Bridge b, @Nullable String storedObject) { - super(b, mock(MercedesMeDiscoveryService.class), mock(HttpClient.class), localeProvider, storageService, - mock(NetworkAddressService.class)); + public AccountHandlerMock(Bridge b, @Nullable String storedObject, HttpClient httpClient) { + super(b, mock(MercedesMeDiscoveryService.class), httpClient, localeProvider, storageService); if (storedObject != null) { Storage storage = storageService.getStorage(Constants.BINDING_ID); storage.put("a@b.c", storedObject); } - config = Optional.of(new AccountConfiguration()); + config = new AccountConfiguration(); } @Override @@ -94,4 +91,8 @@ public class AccountHandlerMock extends AccountHandler { public void connect() { super.ws.onConnect(mock(Session.class)); } + + public void refreshToken() { + authService.get().getToken(); + } } diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandlerTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandlerTest.java index aa44f22512f..0fb80814bc6 100644 --- a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandlerTest.java +++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandlerTest.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.openhab.binding.mercedesme.FileReader; import org.openhab.binding.mercedesme.internal.Constants; @@ -61,6 +62,11 @@ class VehicleHandlerTest { private static final int EVENT_STORAGE_COUNT = HVAC_UPDATE_COUNT + POSITIONING_UPDATE_COUNT + ECOSCORE_UPDATE_COUNT + 76; + @BeforeAll + public static void init() { + Utils.initialize(Utils.timeZoneProvider, Utils.localeProvider); + } + public static Map createBEV() { Thing thingMock = mock(Thing.class); when(thingMock.getThingTypeUID()).thenReturn(Constants.THING_TYPE_BEV); diff --git a/bundles/org.openhab.binding.mercedesme/src/test/resources/json/TokenResponse.json b/bundles/org.openhab.binding.mercedesme/src/test/resources/json/TokenResponse.json index 70e4aa0ab88..ad13e808925 100644 --- a/bundles/org.openhab.binding.mercedesme/src/test/resources/json/TokenResponse.json +++ b/bundles/org.openhab.binding.mercedesme/src/test/resources/json/TokenResponse.json @@ -1,9 +1,9 @@ { - "accessToken": "Tkn", - "tokenType": "Bearer", - "expiresIn": 7199, - "refreshToken": "RfrshTkn", + "access_token": "junitTestToken", + "token_type": "Bearer", + "expires_in": 7199, + "refresh_token": "RfrshTkn", "scope": "openid email phone profile offline_access ciam-uid", "state": null, - "createdOn": "2023-10-04T01:47:08.007038393Z" + "created_on": "2023-10-04T01:47:08.007038393Z" } \ No newline at end of file