[mercedesme] New authorization process (#18342)

* change auth process

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
main
Bernd Weymann 2025-03-04 22:08:38 +01:00 committed by GitHub
parent a0bae2fd31
commit 79a3c1b242
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 249 additions and 683 deletions

View File

@ -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 First time users shall follow the following sequence
1. Setup and configure [Bridge](#bridge-configuration) 1. Setup and configure [Bridge](#bridge-configuration)
2. Follow the [Bridge Authorization](#bridge-authorization) process 2. [Discovery](#discovery) shall find now vehicles associated to your account
3. [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. 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. Connect your desired items in UI or [text-configuration](#full-example) 5. Optional: you can [Discover your Vehicle](#discover-your-vehicle) more deeply
6. Optional: you can [Discover your Vehicle](#discover-your-vehicle) more deeply 6. In case of problems check [Troubleshooting](#troubleshooting) section
7. In case of problems check [Troubleshooting](#troubleshooting) section
## Supported Things ## Supported Things
@ -35,14 +34,20 @@ There's no manual discovery!
Bridge needs configuration in order to connect properly to your Mercedes Me account. Bridge needs configuration in order to connect properly to your Mercedes Me account.
| Name | Type | Description | Default | Required | Advanced | | Name | Type | Description | Default | Required |
|-----------------|---------|-----------------------------------------|-------------|----------|----------| |-------------------|---------|---------------------------------------------|---------------------------|----------|
| email | text | Mercedes Me registered email Address | N/A | yes | no | | email | text | Mercedes Me registered email Address | N/A | yes |
| pin | text | Mercedes Me Smartphone App PIN | N/A | no | no | | refreshToken | text | Refresh Token from MB Token Requester app | takeover previous token | yes |
| region | text | Your region | EU | yes | no | | pin | text | Mercedes Me Smartphone App PIN | N/A | no |
| refreshInterval | integer | API refresh interval | 15 | yes | no | | region | text | Your region | EU | yes |
| callbackIP | text | IP Address of openHAB Device | N/A | yes | yes | | refreshInterval | integer | API refresh interval | 15 | yes |
| callbackPort | integer | Port Number of openHAB Device | N/A | yes | 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 Set `region` to your location
@ -63,46 +68,6 @@ Commands protected by PIN
- Open / Ventilate Windows - Open / Ventilate Windows
- Open / Lift Sunroof - 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
<img src="./doc/OH-Step0.png" width="500" height="240"/>
### Authorization Step 1
<img src="./doc/OH-Step1.png" width="500" height="200"/>
### Authorization Step 2
<img src="./doc/OH-Step2.png" width="500" height="200"/>
### Authorization Step 3
<img src="./doc/OH-Step3.png" width="400" height="130"/>
## Thing Configuration ## Thing Configuration
| Name | Type | Description | Default | Required | Advanced | | Name | Type | Description | Default | Required | Advanced |
@ -814,7 +779,7 @@ Keep these 3 channels disconnected during normal operation.
### Things file ### Things file
```java ```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] Thing bev eqa "Mercedes EQA" [ vin="VEHICLE_VIN", batteryCapacity=66.5]
} }
``` ```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -268,7 +268,6 @@ public class Constants {
public static final String OH_CHANNEL_CONSTANT = "constant"; public static final String OH_CHANNEL_CONSTANT = "constant";
public static final String OH_CHANNEL_BONUS_RANGE = "bonus"; 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 // 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_BASE_URL = "https://api.mercedes-benz.com/vehicle_images/v2";
public static final String IMAGE_EXTERIOR_RESOURCE_URL = IMAGE_BASE_URL + "/vehicles/%s"; 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_EMAIL_MISSING = ".status.email-missing";
public static final String STATUS_REGION_MISSING = ".status.region-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_REFRESH_INVALID = ".status.refresh-invalid";
public static final String STATUS_IP_MISSING = ".status.ip-missing"; public static final String STATUS_REFRESH_TOKEN_MISSING = ".status.refresh-token-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_BRIDGE_MISSING = ".status.bridge-missing"; public static final String STATUS_BRIDGE_MISSING = ".status.bridge-missing";
public static final String SPACE = " "; public static final String SPACE = " ";
@ -346,7 +343,6 @@ public class Constants {
public static final String MAX_SOC_KEY = "maxsoc"; public static final String MAX_SOC_KEY = "maxsoc";
public static final String AUTO_UNLOCK_KEY = "autolock"; 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_TOKEN = "junitTestToken";
public static final String JUNIT_REFRESH_TOKEN = "junitRefreshToken"; public static final String JUNIT_REFRESH_TOKEN = "junitRefreshToken";
} }

View File

@ -31,7 +31,6 @@ import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.UnitProvider; import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.items.MetadataRegistry; import org.openhab.core.items.MetadataRegistry;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.storage.StorageService; import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
@ -65,7 +64,6 @@ public class MercedesMeHandlerFactory extends BaseThingHandlerFactory {
private final MercedesMeDiscoveryService discoveryService; private final MercedesMeDiscoveryService discoveryService;
private final MercedesMeCommandOptionProvider mmcop; private final MercedesMeCommandOptionProvider mmcop;
private final MercedesMeStateOptionProvider mmsop; private final MercedesMeStateOptionProvider mmsop;
private final NetworkAddressService networkService;
private @Nullable ServiceRegistration<?> discoveryServiceReg; private @Nullable ServiceRegistration<?> discoveryServiceReg;
private @Nullable MercedesMeMetadataAdjuster mdAdjuster; private @Nullable MercedesMeMetadataAdjuster mdAdjuster;
@ -76,10 +74,8 @@ public class MercedesMeHandlerFactory extends BaseThingHandlerFactory {
final @Reference LocaleProvider lp, final @Reference LocationProvider locationP, final @Reference LocaleProvider lp, final @Reference LocationProvider locationP,
final @Reference TimeZoneProvider tzp, final @Reference MercedesMeCommandOptionProvider cop, final @Reference TimeZoneProvider tzp, final @Reference MercedesMeCommandOptionProvider cop,
final @Reference MercedesMeStateOptionProvider sop, final @Reference UnitProvider up, final @Reference MercedesMeStateOptionProvider sop, final @Reference UnitProvider up,
final @Reference MetadataRegistry mdr, final @Reference ItemChannelLinkRegistry iclr, final @Reference MetadataRegistry mdr, final @Reference ItemChannelLinkRegistry iclr) {
final @Reference NetworkAddressService nas) {
this.storageService = storageService; this.storageService = storageService;
networkService = nas;
localeProvider = lp; localeProvider = lp;
locationProvider = locationP; locationProvider = locationP;
mmcop = cop; mmcop = cop;
@ -112,8 +108,7 @@ public class MercedesMeHandlerFactory extends BaseThingHandlerFactory {
discoveryServiceReg = bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, discoveryServiceReg = bundleContext.registerService(DiscoveryService.class.getName(), discoveryService,
null); null);
} }
return new AccountHandler((Bridge) thing, discoveryService, httpClient, localeProvider, storageService, return new AccountHandler((Bridge) thing, discoveryService, httpClient, localeProvider, storageService);
networkService);
} else if (THING_TYPE_BEV.equals(thingTypeUID) || THING_TYPE_COMB.equals(thingTypeUID) } else if (THING_TYPE_BEV.equals(thingTypeUID) || THING_TYPE_COMB.equals(thingTypeUID)
|| THING_TYPE_HYBRID.equals(thingTypeUID)) { || THING_TYPE_HYBRID.equals(thingTypeUID)) {
return new VehicleHandler(thing, locationProvider, mmcop, mmsop); return new VehicleHandler(thing, locationProvider, mmcop, mmsop);

View File

@ -26,9 +26,7 @@ public class AccountConfiguration {
public String email = NOT_SET; public String email = NOT_SET;
public String region = NOT_SET; public String region = NOT_SET;
public String refreshToken = "takeover previous token";
public String pin = NOT_SET; public String pin = NOT_SET;
public int refreshInterval = 15; public int refreshInterval = 15;
public String callbackIP = NOT_SET;
public int callbackPort = -1;
} }

View File

@ -33,6 +33,6 @@ public class TokenResponse {
@SerializedName("token_type") @SerializedName("token_type")
public String tokenType = Constants.NOT_SET; public String tokenType = Constants.NOT_SET;
@SerializedName("expires_in") @SerializedName("expires_in")
public int expiresIn; public int expiresIn = 0;
public String createdOn = Instant.now().toString(); public String createdOn = Instant.MIN.toString();
} }

View File

@ -38,15 +38,12 @@ import org.json.JSONObject;
import org.openhab.binding.mercedesme.internal.Constants; import org.openhab.binding.mercedesme.internal.Constants;
import org.openhab.binding.mercedesme.internal.config.AccountConfiguration; import org.openhab.binding.mercedesme.internal.config.AccountConfiguration;
import org.openhab.binding.mercedesme.internal.discovery.MercedesMeDiscoveryService; 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.AuthService;
import org.openhab.binding.mercedesme.internal.server.MBWebsocket; import org.openhab.binding.mercedesme.internal.server.MBWebsocket;
import org.openhab.binding.mercedesme.internal.utils.Utils; import org.openhab.binding.mercedesme.internal.utils.Utils;
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener; import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse; 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.i18n.LocaleProvider;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.storage.Storage; import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService; import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.Bridge; 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 static final String COMMAND_APPENDIX = "-commands";
private final Logger logger = LoggerFactory.getLogger(AccountHandler.class); private final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
private final NetworkAddressService networkService;
private final MercedesMeDiscoveryService discoveryService; private final MercedesMeDiscoveryService discoveryService;
private final HttpClient httpClient; private final HttpClient httpClient;
private final LocaleProvider localeProvider; private final LocaleProvider localeProvider;
@ -89,8 +85,6 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr
private final Map<String, VEPUpdate> vepUpdateMap = new HashMap<>(); private final Map<String, VEPUpdate> vepUpdateMap = new HashMap<>();
private final Map<String, Map<String, Object>> capabilitiesMap = new HashMap<>(); private final Map<String, Map<String, Object>> capabilitiesMap = new HashMap<>();
private Optional<AuthServer> server = Optional.empty();
private Optional<AuthService> authService = Optional.empty();
private Optional<ScheduledFuture<?>> refreshScheduler = Optional.empty(); private Optional<ScheduledFuture<?>> refreshScheduler = Optional.empty();
private List<byte[]> eventQueue = new ArrayList<>(); private List<byte[]> eventQueue = new ArrayList<>();
private boolean updateRunning = false; 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 commandCapabilitiesEndpoint = "/v1/vehicle/%s/capabilities/commands";
private String poiEndpoint = "/v1/vehicle/%s/route"; private String poiEndpoint = "/v1/vehicle/%s/route";
Optional<AuthService> authService = Optional.empty();
final MBWebsocket ws; final MBWebsocket ws;
Optional<AccountConfiguration> config = Optional.empty(); AccountConfiguration config = new AccountConfiguration();
@Nullable @Nullable
ClientMessage message; ClientMessage message;
public AccountHandler(Bridge bridge, MercedesMeDiscoveryService mmds, HttpClient hc, LocaleProvider lp, public AccountHandler(Bridge bridge, MercedesMeDiscoveryService mmds, HttpClient hc, LocaleProvider lp,
StorageService store, NetworkAddressService nas) { StorageService store) {
super(bridge); super(bridge);
discoveryService = mmds; discoveryService = mmds;
networkService = nas;
ws = new MBWebsocket(this); ws = new MBWebsocket(this);
httpClient = hc; httpClient = hc;
localeProvider = lp; localeProvider = lp;
@ -122,83 +116,39 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr
@Override @Override
public void initialize() { public void initialize() {
updateStatus(ThingStatus.UNKNOWN); updateStatus(ThingStatus.UNKNOWN);
config = Optional.of(getConfigAs(AccountConfiguration.class)); config = getConfigAs(AccountConfiguration.class);
autodetectCallback();
String configValidReason = configValid(); String configValidReason = configValid();
if (!configValidReason.isEmpty()) { if (!configValidReason.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configValidReason); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configValidReason);
} else { } else {
String callbackUrl = Utils.getCallbackAddress(config.get().callbackIP, config.get().callbackPort); authService = Optional.of(new AuthService(this, httpClient, config, localeProvider.getLocale(), storage,
thing.setProperty("callbackUrl", callbackUrl); config.refreshToken));
server = Optional.of(new AuthServer(httpClient, config.get(), callbackUrl)); refreshScheduler = Optional
authService = Optional .of(scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshInterval, TimeUnit.MINUTES));
.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));
}
} }
} }
public void refresh() { public void refresh() {
if (server.isPresent()) { if (!Constants.NOT_SET.equals(authService.get().getToken())) {
if (!Constants.NOT_SET.equals(authService.get().getToken())) { ws.run();
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") + "\"]");
}
} else { } else {
// server not running - fix first // all failed - start manual authorization
String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId() String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId()
+ Constants.STATUS_SERVER_RESTART; + Constants.STATUS_AUTH_NEEDED;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, textKey); 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() { private String configValid() {
config = Optional.of(getConfigAs(AccountConfiguration.class)); config = getConfigAs(AccountConfiguration.class);
String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId(); String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId();
if (Constants.NOT_SET.equals(config.get().callbackIP)) { if (Constants.NOT_SET.equals(config.refreshToken)) {
return textKey + Constants.STATUS_IP_MISSING; return textKey + Constants.STATUS_REFRESH_TOKEN_MISSING;
} else if (config.get().callbackPort == -1) { } else if (Constants.NOT_SET.equals(config.email)) {
return textKey + Constants.STATUS_PORT_MISSING;
} else if (Constants.NOT_SET.equals(config.get().email)) {
return textKey + Constants.STATUS_EMAIL_MISSING; 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; return textKey + Constants.STATUS_REGION_MISSING;
} else if (config.get().refreshInterval <= 01) { } else if (config.refreshInterval < 5) {
return textKey + Constants.STATUS_REFRESH_INVALID; return textKey + Constants.STATUS_REFRESH_INVALID;
} else { } else {
return Constants.EMPTY; return Constants.EMPTY;
@ -207,13 +157,6 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr
@Override @Override
public void dispose() { 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 -> { refreshScheduler.ifPresent(schedule -> {
if (!schedule.isCancelled()) { if (!schedule.isCancelled()) {
schedule.cancel(true); schedule.cancel(true);
@ -223,6 +166,13 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr
eventQueue.clear(); 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 * 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) { public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
if (!Constants.NOT_SET.equals(tokenResponse.getAccessToken())) { if (!Constants.NOT_SET.equals(tokenResponse.getAccessToken())) {
scheduler.schedule(this::refresh, 2, TimeUnit.SECONDS); 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 { } else {
// all failed - start manual authorization // all failed - start manual authorization
String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId() String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId()
+ Constants.STATUS_AUTH_NEEDED; + Constants.STATUS_AUTH_NEEDED;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, textKey);
textKey + " [\"" + thing.getProperties().get("callbackUrl") + "\"]");
} }
} }
@Override
public String toString() {
return Integer.toString(config.get().callbackPort);
}
public String getWSUri() { public String getWSUri() {
return Utils.getWebsocketServer(config.get().region); return Utils.getWebsocketServer(config.region);
} }
public ClientUpgradeRequest getClientUpgradeRequest() { public ClientUpgradeRequest getClientUpgradeRequest() {
@ -260,12 +199,12 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr
request.setHeader("X-TrackingId", UUID.randomUUID().toString()); request.setHeader("X-TrackingId", UUID.randomUUID().toString());
request.setHeader("Ris-Os-Name", Constants.RIS_OS_NAME); request.setHeader("Ris-Os-Name", Constants.RIS_OS_NAME);
request.setHeader("Ris-Os-Version", Constants.RIS_OS_VERSION); 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", request.setHeader("X-Locale",
localeProvider.getLocale().getLanguage() + "-" + localeProvider.getLocale().getCountry()); // de-DE localeProvider.getLocale().getLanguage() + "-" + localeProvider.getLocale().getCountry()); // de-DE
request.setHeader("User-Agent", Utils.getApplication(config.get().region)); request.setHeader("User-Agent", Utils.getApplication(config.region));
request.setHeader("X-Applicationname", Utils.getUserAgent(config.get().region)); request.setHeader("X-Applicationname", Utils.getUserAgent(config.region));
request.setHeader("Ris-Application-Version", Utils.getRisApplicationVersion(config.get().region)); request.setHeader("Ris-Application-Version", Utils.getRisApplicationVersion(config.region));
return request; return request;
} }
@ -452,8 +391,7 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr
Map<String, Object> featureMap = new HashMap<>(); Map<String, Object> featureMap = new HashMap<>();
try { try {
// add vehicle capabilities // add vehicle capabilities
String capabilitiesUrl = Utils.getRestAPIServer(config.get().region) String capabilitiesUrl = Utils.getRestAPIServer(config.region) + String.format(capabilitiesEndpoint, vin);
+ String.format(capabilitiesEndpoint, vin);
Request capabilitiesRequest = httpClient.newRequest(capabilitiesUrl); Request capabilitiesRequest = httpClient.newRequest(capabilitiesUrl);
authService.get().addBasicHeaders(capabilitiesRequest); authService.get().addBasicHeaders(capabilitiesRequest);
capabilitiesRequest.header("X-SessionId", UUID.randomUUID().toString()); capabilitiesRequest.header("X-SessionId", UUID.randomUUID().toString());
@ -489,7 +427,7 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr
} }
// add command capabilities // add command capabilities
String commandCapabilitiesUrl = Utils.getRestAPIServer(config.get().region) String commandCapabilitiesUrl = Utils.getRestAPIServer(config.region)
+ String.format(commandCapabilitiesEndpoint, vin); + String.format(commandCapabilitiesEndpoint, vin);
Request commandCapabilitiesRequest = httpClient.newRequest(commandCapabilitiesUrl); Request commandCapabilitiesRequest = httpClient.newRequest(commandCapabilitiesUrl);
authService.get().addBasicHeaders(commandCapabilitiesRequest); authService.get().addBasicHeaders(commandCapabilitiesRequest);
@ -557,7 +495,7 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr
*/ */
public void sendPoi(String vin, JSONObject poi) { 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); Request poiRequest = httpClient.POST(poiUrl);
authService.get().addBasicHeaders(poiRequest); authService.get().addBasicHeaders(poiRequest);
poiRequest.header("X-SessionId", UUID.randomUUID().toString()); poiRequest.header("X-SessionId", UUID.randomUUID().toString());

View File

@ -216,7 +216,7 @@ public class VehicleHandler extends BaseThingHandler {
var crBuilder = CommandRequest.newBuilder().setVin(config.get().vin).setRequestId(UUID.randomUUID().toString()); var crBuilder = CommandRequest.newBuilder().setVin(config.get().vin).setRequestId(UUID.randomUUID().toString());
String group = channelUID.getGroupId(); String group = channelUID.getGroupId();
String channel = channelUID.getIdWithoutGroup(); String channel = channelUID.getIdWithoutGroup();
String pin = accountHandler.get().config.get().pin; String pin = accountHandler.get().config.pin;
if (group == null) { if (group == null) {
logger.trace("No command {} found for {}", command, channel); logger.trace("No command {} found for {}", command, channel);
return; return;

View File

@ -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<Integer, AuthServer> SERVER_MAP = new HashMap<>();
private static final AccessTokenResponse INVALID_ACCESS_TOKEN = new AccessTokenResponse();
private final HttpClient httpClient;
private Optional<Server> 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;
}
}

View File

@ -16,9 +16,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -33,7 +31,6 @@ import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.openhab.binding.mercedesme.internal.Constants; import org.openhab.binding.mercedesme.internal.Constants;
import org.openhab.binding.mercedesme.internal.config.AccountConfiguration; 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.dto.TokenResponse;
import org.openhab.binding.mercedesme.internal.utils.Utils; import org.openhab.binding.mercedesme.internal.utils.Utils;
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/** /**
* {@link AuthService} helpers for token management * {@link AuthService} helpers for token management
* *
@ -49,23 +48,19 @@ import org.slf4j.LoggerFactory;
*/ */
@NonNullByDefault @NonNullByDefault
public class AuthService { public class AuthService {
public static final AccessTokenResponse INVALID_TOKEN = new AccessTokenResponse();
private static final int EXPIRATION_BUFFER = 5; private static final int EXPIRATION_BUFFER = 5;
private static final Map<Integer, AuthService> AUTH_MAP = new HashMap<>();
private final Logger logger = LoggerFactory.getLogger(AuthService.class); private final Logger logger = LoggerFactory.getLogger(AuthService.class);
AccessTokenRefreshListener listener; private AccessTokenRefreshListener listener;
private AccountConfiguration config;
private AccessTokenResponse token = Utils.INVALID_TOKEN;
private Storage<String> storage;
private HttpClient httpClient; private HttpClient httpClient;
private String identifier; private String identifier;
private AccountConfiguration config;
private Locale locale; private Locale locale;
private Storage<String> storage;
private AccessTokenResponse token;
public AuthService(AccessTokenRefreshListener atrl, HttpClient hc, AccountConfiguration ac, Locale l, public AuthService(AccessTokenRefreshListener atrl, HttpClient hc, AccountConfiguration ac, Locale l,
Storage<String> store) { Storage<String> store, String refreshToken) {
INVALID_TOKEN.setAccessToken(Constants.NOT_SET);
INVALID_TOKEN.setRefreshToken(Constants.NOT_SET);
listener = atrl; listener = atrl;
httpClient = hc; httpClient = hc;
config = ac; config = ac;
@ -73,129 +68,53 @@ public class AuthService {
locale = l; locale = l;
storage = store; storage = store;
// restore token // restore token from persistence if available
String storedObject = storage.get(identifier); String storedToken = storage.get(identifier);
if (storedObject == null) { if (storedToken != null) {
token = INVALID_TOKEN; // returns INVALID_TOKEN in case of an error
listener.onAccessTokenResponse(token); logger.trace("MB-Auth {} Restore token from persistence", prefix());
} else { try {
token = Utils.fromString(storedObject); logger.trace("MB-Auth {} storedToken {}", prefix(), storedToken);
if (token.isExpired(Instant.now(), EXPIRATION_BUFFER)) { TokenResponse tokenResponseJson = Utils.GSON.fromJson(storedToken, TokenResponse.class);
if (!Constants.NOT_SET.equals(token.getRefreshToken())) { token = decodeToken(tokenResponseJson);
refreshToken(); if (!tokenIsValid()) {
listener.onAccessTokenResponse(token); token = Utils.INVALID_TOKEN;
} else { storage.remove(identifier);
token = INVALID_TOKEN; logger.trace("MB-Auth {} invalid storedToken {}", prefix(), storedToken);
listener.onAccessTokenResponse(token);
} }
} else { } catch (JsonSyntaxException jse) {
listener.onAccessTokenResponse(token); // 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 synchronized String getToken() {
public static AuthService getAuthService(Integer key) { if (token.isExpired(Instant.now(), EXPIRATION_BUFFER)) {
return AUTH_MAP.get(key); if (tokenIsValid()) {
refreshToken();
}
}
return token.getAccessToken();
} }
/** private void refreshToken() {
* logger.trace("MB-Auth {} refreshToken", prefix());
* @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() {
try { try {
String url = Utils.getTokenUrl(config.region); String url = Utils.getTokenUrl(config.region);
Request req = httpClient.POST(url); Request req = httpClient.POST(url);
req.header("X-Device-Id", UUID.randomUUID().toString()); req.header("X-Device-Id", UUID.randomUUID().toString());
req.header("X-Request-Id", UUID.randomUUID().toString()); req.header("X-Request-Id", UUID.randomUUID().toString());
// Content URL form
String grantAttribute = "grant_type=refresh_token"; String grantAttribute = "grant_type=refresh_token";
String refreshTokenAttribute = "refresh_token=" String refreshTokenAttribute = "refresh_token="
+ URLEncoder.encode(token.getRefreshToken(), StandardCharsets.UTF_8.toString()); + 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.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
req.content(new StringContentProvider(content)); req.content(new StringContentProvider(content));
// Send
ContentResponse cr = req.timeout(Constants.REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(); ContentResponse cr = req.timeout(Constants.REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).send();
if (cr.getStatus() == 200) { int tokenResponseStatus = cr.getStatus();
saveTokenResponse(cr.getContentAsString()); String tokenResponse = cr.getContentAsString();
listener.onAccessTokenResponse(token); 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 { } 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) { listener.onAccessTokenResponse(token);
logger.trace("{} Failed to refresh token {}", prefix(), e.getMessage()); } catch (InterruptedException | TimeoutException | ExecutionException | UnsupportedEncodingException
| JsonSyntaxException e) {
logger.info("{} Failed to refresh token {}", prefix(), e.getMessage());
} }
} }
public String getToken() { private AccessTokenResponse decodeToken(@Nullable TokenResponse tokenJson) {
if (token.isExpired(Instant.now(), EXPIRATION_BUFFER)) { if (tokenJson != null) {
if (!Constants.NOT_SET.equals(token.getRefreshToken())) { AccessTokenResponse atr = new AccessTokenResponse();
refreshToken(); atr.setCreatedOn(Instant.parse(tokenJson.createdOn));
// token shall be updated now - retry expired check atr.setExpiresIn(tokenJson.expiresIn);
if (token.isExpired(Instant.now(), EXPIRATION_BUFFER)) { atr.setAccessToken(tokenJson.accessToken);
token = INVALID_TOKEN; if (!Constants.NOT_SET.equals(tokenJson.refreshToken)) {
listener.onAccessTokenResponse(token); atr.setRefreshToken(tokenJson.refreshToken);
return Constants.NOT_SET;
}
} else { } else {
token = INVALID_TOKEN; // Preserve refresh token if available
logger.trace("{} Refresh token empty", prefix()); 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) { public void addBasicHeaders(Request req) {
@ -244,30 +207,6 @@ public class AuthService {
req.header("Ris-Application-Version", Utils.getRisApplicationVersion(config.region)); 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() { private String prefix() {
return "[" + config.email + "] "; return "[" + config.email + "] ";
} }

View File

@ -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("<HTML>");
response.getWriter().println("<BODY>");
response.getWriter().println("<H1>Step 1 - PIN Requested</H1>");
response.getWriter().println("<BR>");
response.getWriter().println("PIN was requested and should be present in your EMail Inbox<BR>");
response.getWriter()
.println("Check first if you received the PIN and then continue with the below Link<BR>");
response.getWriter().println("<a href=\"" + Constants.CALLBACK_ENDPOINT + "?guid=" + requestVal
+ "\">Click here to continue with Step 2</a>");
response.getWriter().println("</BODY>");
response.getWriter().println("</HTML>");
} else {
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println("<HTML>");
response.getWriter().println("<BODY>");
response.getWriter().println("Something went wrong<BR>");
response.getWriter().println("</BODY>");
response.getWriter().println("</HTML>");
}
} 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("<HTML>");
response.getWriter().println("<BODY>");
response.getWriter().println("<H1>Step 2 - Enter PIN</H1>");
response.getWriter().println("<BR>");
response.getWriter().println("Enter PIN in second input field - leave guid as it is!<BR>");
response.getWriter().println("<form action=\"" + Constants.CALLBACK_ENDPOINT + "\">");
response.getWriter().println("<BR>");
response.getWriter().println("<label for=\"GUID\">GUID</label>");
response.getWriter().println("<input type=\"text\" id=\"guid\" name=\"guid\" value=\"" + guid + "\">");
response.getWriter().println("<BR>");
response.getWriter().println("<label for=\"PIN\">PIN</label>");
response.getWriter().println("<input type=\"text\" id=\"pin\" name=\"pin\" placeholder=\"Your PIN\">");
response.getWriter().println("<BR>");
response.getWriter().println("<input type=\"submit\" value=\"Submit\">");
response.getWriter().println("</form>");
response.getWriter().println("</BODY>");
response.getWriter().println("</HTML>");
} 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("<HTML>");
response.getWriter().println("<BODY>");
response.getWriter().println("<H1>Step 3 - Save Token</H1>");
response.getWriter().println("<BR>");
if (result) {
response.getWriter().println("Success - everything done!<BR>");
} else {
response.getWriter().println("Failure - Please check logs for further analysis!<BR>");
}
response.getWriter().println("</BODY>");
response.getWriter().println("</HTML>");
}
}
}

View File

@ -93,11 +93,11 @@ public class MBWebsocket {
client.setStopTimeout(CONNECT_TIMEOUT_MS); client.setStopTimeout(CONNECT_TIMEOUT_MS);
ClientUpgradeRequest request = accountHandler.getClientUpgradeRequest(); ClientUpgradeRequest request = accountHandler.getClientUpgradeRequest();
String websocketURL = accountHandler.getWSUri(); String websocketURL = accountHandler.getWSUri();
logger.trace("Websocket start {}", websocketURL);
if (Constants.JUNIT_TOKEN.equals(request.getHeader("Authorization"))) { if (Constants.JUNIT_TOKEN.equals(request.getHeader("Authorization"))) {
// avoid unit test requesting real websocket - simply return // avoid unit test requesting real websocket - simply return
return; return;
} }
logger.trace("Websocket start {}", websocketURL);
client.start(); client.start();
client.connect(this, new URI(websocketURL), request); client.connect(this, new URI(websocketURL), request);
while (keepAlive || Instant.now().isBefore(runTill)) { while (keepAlive || Instant.now().isBefore(runTill)) {
@ -177,7 +177,8 @@ public class MBWebsocket {
} }
} else { } else {
if (!b) { 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); runTill = Instant.now().plusMillis(KEEP_ALIVE_ADDON);
logger.trace("Websocket - keep alive stop - run till {}", runTill.toString()); 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 * https://community.openhab.org/t/mercedes-me/136866/12
* Release Websocket thread as early as possible to avoid execeptions * Release Websocket thread as early as possible to avoid execeptions
* *
* 1. Websocket thread responsible for reading stream in bytes and enqueue for AccountHandler. * 1. Websocket thread responsible for reading stream in bytes and enqueue for
* 2. AccountHamdler thread responsible for encoding proto message. In case of update enqueue proto message * AccountHandler.
* 2. AccountHamdler thread responsible for encoding proto message. In case of
* update enqueue proto message
* at VehicleHandöer * at VehicleHandöer
* 3. VehicleHandler responsible to update channels * 3. VehicleHandler responsible to update channels
*/ */
@ -225,6 +228,7 @@ public class MBWebsocket {
@OnWebSocketError @OnWebSocketError
public void onError(Throwable t) { public void onError(Throwable t) {
logger.debug("Error during web socket connection - {}", t.getMessage());
accountHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, accountHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/mercedesme.account.status.websocket-failure [\"" + t.getMessage() + "\"]"); "@text/mercedesme.account.status.websocket-failure [\"" + t.getMessage() + "\"]");
} }

View File

@ -13,10 +13,8 @@
package org.openhab.binding.mercedesme.internal.utils; package org.openhab.binding.mercedesme.internal.utils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
@ -37,7 +35,6 @@ import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.openhab.binding.mercedesme.internal.Constants; import org.openhab.binding.mercedesme.internal.Constants;
import org.openhab.binding.mercedesme.internal.MercedesMeHandlerFactory; 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.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider; 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 final int R = 6371; // Radius of the earth
private static int port = 8090; private static int port = 8090;
private static TimeZoneProvider timeZoneProvider = new TimeZoneProvider() { public static TimeZoneProvider timeZoneProvider = new TimeZoneProvider() {
@Override @Override
public ZoneId getTimeZone() { public ZoneId getTimeZone() {
return ZoneId.systemDefault(); return ZoneId.systemDefault();
} }
}; };
private static LocaleProvider localeProvider = new LocaleProvider() { public static LocaleProvider localeProvider = new LocaleProvider() {
@Override @Override
public Locale getLocale() { public Locale getLocale() {
return Locale.getDefault(); return Locale.getDefault();
@ -94,10 +90,13 @@ public class Utils {
public static final Gson GSON = new Gson(); public static final Gson GSON = new Gson();
public static final Map<String, Integer> ZONE_HASHMAP = new HashMap<>(); public static final Map<String, Integer> ZONE_HASHMAP = new HashMap<>();
public static final Map<String, Integer> PROGRAM_HASHMAP = new HashMap<>(); public static final Map<String, Integer> PROGRAM_HASHMAP = new HashMap<>();
public static final AccessTokenResponse INVALID_TOKEN = new AccessTokenResponse();
public static void initialize(TimeZoneProvider tzp, LocaleProvider lp) { public static void initialize(TimeZoneProvider tzp, LocaleProvider lp) {
timeZoneProvider = tzp; timeZoneProvider = tzp;
localeProvider = lp; localeProvider = lp;
INVALID_TOKEN.setAccessToken(Constants.NOT_SET);
INVALID_TOKEN.setRefreshToken(Constants.NOT_SET);
} }
/** /**
@ -138,27 +137,6 @@ public class Utils {
return port; 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 * 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 * Calculate token URL for getting token
* *
@ -337,6 +280,7 @@ public class Utils {
* @param token - Base64 String from storage * @param token - Base64 String from storage
* @return AccessTokenResponse decoded from String, invalid token otherwise * @return AccessTokenResponse decoded from String, invalid token otherwise
*/ */
@Deprecated
public static AccessTokenResponse fromString(String token) { public static AccessTokenResponse fromString(String token) {
try { try {
byte[] data = Base64.getDecoder().decode(token); byte[] data = Base64.getDecoder().decode(token);
@ -347,25 +291,7 @@ public class Utils {
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
LOGGER.warn("Error converting string to token {}", e.getMessage()); LOGGER.warn("Error converting string to token {}", e.getMessage());
} }
return AuthService.INVALID_TOKEN; return 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;
} }
/** /**

View File

@ -19,6 +19,11 @@
<option value="CN">China</option> <option value="CN">China</option>
</options> </options>
</parameter> </parameter>
<parameter name="refreshToken" type="text" required="true">
<label>Refresh Token</label>
<description>Refresh Token from MB Token Requester app</description>
<default>"takeover previous token"</default>
</parameter>
<parameter name="pin" type="text" required="false"> <parameter name="pin" type="text" required="false">
<label>PIN</label> <label>PIN</label>
<description>PIN for commands</description> <description>PIN for commands</description>
@ -29,15 +34,5 @@
<description>Refresh Interval in Minutes</description> <description>Refresh Interval in Minutes</description>
<default>15</default> <default>15</default>
</parameter> </parameter>
<parameter name="callbackIP" type="text">
<label>Callback IP Address</label>
<description>IP address for openHAB callback URL</description>
<advanced>true</advanced>
</parameter>
<parameter name="callbackPort" type="integer">
<label>Callback Port Number</label>
<description>Port Number for openHAB callback URL</description>
<advanced>true</advanced>
</parameter>
</config-description> </config-description>
</config-description:config-descriptions> </config-description:config-descriptions>

View File

@ -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.label = Battery Capacity
thing-type.config.mercedesme.bev.batteryCapacity.description = Battery capacity in kWh of vehicle 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.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.label = MercedesMe EMail
thing-type.config.mercedesme.bridge.email.description = EMail address for MercedesMe account 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.label = PIN
thing-type.config.mercedesme.bridge.pin.description = PIN for commands 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.label = Refresh Interval
thing-type.config.mercedesme.bridge.refreshInterval.description = Refresh Interval in Minutes 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.label = Region
thing-type.config.mercedesme.bridge.region.option.EU = Europe thing-type.config.mercedesme.bridge.region.option.EU = Europe
thing-type.config.mercedesme.bridge.region.option.NA = North America thing-type.config.mercedesme.bridge.region.option.NA = North America
@ -374,13 +372,10 @@ longitudeDescription = Longitude of the location
# thing status types # 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.email-missing = EMail missing
mercedesme.account.status.region-missing = Region missing mercedesme.account.status.region-missing = Region missing
mercedesme.account.status.refresh-invalid = Refresh Interval Invalid 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.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} mercedesme.account.status.websocket-failure = Websocket Exception: Reason: {0}

View File

@ -13,12 +13,18 @@
package org.openhab.binding.mercedesme; package org.openhab.binding.mercedesme;
import static org.junit.jupiter.api.Assertions.*; 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.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.junit.jupiter.api.Test;
import org.openhab.binding.mercedesme.internal.Constants; import org.openhab.binding.mercedesme.internal.Constants;
import org.openhab.binding.mercedesme.internal.handler.AccountHandlerMock; 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; import org.openhab.core.thing.internal.BridgeImpl;
/** /**
* {@link StatusTests} sequencess for testing ThingStatus * {@link StatusTests} sequences for testing ThingStatus
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
*/ */
@ -41,23 +47,41 @@ import org.openhab.core.thing.internal.BridgeImpl;
class StatusTests { class StatusTests {
public static void tearDown(AccountHandlerMock ahm) { public static void tearDown(AccountHandlerMock ahm) {
// ahm.setCallback(null);
ahm.dispose();
try { try {
Thread.sleep(250); Thread.sleep(250);
} catch (InterruptedException e) { } catch (InterruptedException e) {
fail(); 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 @Test
void testInvalidConfig() { void testInvalidConfig() {
BridgeImpl bi = new BridgeImpl(new ThingTypeUID("test", "account"), "MB"); BridgeImpl bi = new BridgeImpl(new ThingTypeUID("test", "account"), "MB");
Map<String, Object> config = new HashMap<>(); Map<String, Object> config = new HashMap<>();
config.put("callbackIP", "999.999.999.999"); config.put("refreshToken", Constants.JUNIT_REFRESH_TOKEN);
config.put("callbackPort", "99999");
bi.setConfiguration(new Configuration(config)); bi.setConfiguration(new Configuration(config));
AccountHandlerMock ahm = new AccountHandlerMock(bi, null); AccountHandlerMock ahm = new AccountHandlerMock(bi, null, getHttpClient(404));
ThingCallbackListener tcl = new ThingCallbackListener(); ThingCallbackListener tcl = new ThingCallbackListener();
ahm.setCallback(tcl); ahm.setCallback(tcl);
ahm.initialize(); ahm.initialize();
@ -83,6 +107,7 @@ class StatusTests {
tcl = new ThingCallbackListener(); tcl = new ThingCallbackListener();
ahm.setCallback(tcl); ahm.setCallback(tcl);
ahm.initialize(); ahm.initialize();
ahm.refreshToken();
tsi = tcl.getThingStatus(); tsi = tcl.getThingStatus();
assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Auth offline"); assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Auth offline");
assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, tsi.getStatusDetail(), "Auth detail"); assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, tsi.getStatusDetail(), "Auth detail");
@ -107,13 +132,13 @@ class StatusTests {
config.put("refreshInterval", Integer.MAX_VALUE); config.put("refreshInterval", Integer.MAX_VALUE);
config.put("region", "row"); config.put("region", "row");
config.put("email", "a@b.c"); config.put("email", "a@b.c");
config.put("callbackIP", "999.999.999.999"); config.put("refreshToken", "abc");
config.put("callbackPort", "99999");
bi.setConfiguration(new Configuration(config)); bi.setConfiguration(new Configuration(config));
AccountHandlerMock ahm = new AccountHandlerMock(bi, null); AccountHandlerMock ahm = new AccountHandlerMock(bi, null, getHttpClient(404));
ThingCallbackListener tcl = new ThingCallbackListener(); ThingCallbackListener tcl = new ThingCallbackListener();
ahm.setCallback(tcl); ahm.setCallback(tcl);
ahm.initialize(); ahm.initialize();
ahm.refreshToken();
ThingStatusInfo tsi = tcl.getThingStatus(); ThingStatusInfo tsi = tcl.getThingStatus();
assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Auth Offline"); assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Auth Offline");
assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, tsi.getStatusDetail(), "Auth details"); assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, tsi.getStatusDetail(), "Auth details");
@ -140,17 +165,10 @@ class StatusTests {
config.put("refreshInterval", Integer.MAX_VALUE); config.put("refreshInterval", Integer.MAX_VALUE);
config.put("region", "row"); config.put("region", "row");
config.put("email", "a@b.c"); config.put("email", "a@b.c");
config.put("callbackIP", "999.999.999.999"); config.put("refreshToken", "abc");
config.put("callbackPort", "99999");
bi.setConfiguration(new Configuration(config)); bi.setConfiguration(new Configuration(config));
AccessTokenResponse token = new AccessTokenResponse(); String tokenResponse = FileReader.readFileInString("src/test/resources/json/TokenResponse.json");
token.setExpiresIn(3000); AccountHandlerMock ahm = new AccountHandlerMock(bi, tokenResponse, getHttpClient(200));
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));
ThingCallbackListener tcl = new ThingCallbackListener(); ThingCallbackListener tcl = new ThingCallbackListener();
ahm.setCallback(tcl); ahm.setCallback(tcl);
ahm.initialize(); ahm.initialize();

View File

@ -15,7 +15,6 @@ package org.openhab.binding.mercedesme.internal.handler;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import java.util.Locale; import java.util.Locale;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.config.AccountConfiguration;
import org.openhab.binding.mercedesme.internal.discovery.MercedesMeDiscoveryService; import org.openhab.binding.mercedesme.internal.discovery.MercedesMeDiscoveryService;
import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.storage.Storage; import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService; import org.openhab.core.storage.StorageService;
import org.openhab.core.test.storage.VolatileStorageService; import org.openhab.core.test.storage.VolatileStorageService;
@ -54,18 +52,17 @@ public class AccountHandlerMock extends AccountHandler {
public AccountHandlerMock() { public AccountHandlerMock() {
super(mock(Bridge.class), mock(MercedesMeDiscoveryService.class), mock(HttpClient.class), super(mock(Bridge.class), mock(MercedesMeDiscoveryService.class), mock(HttpClient.class),
mock(LocaleProvider.class), mock(StorageService.class), mock(NetworkAddressService.class)); mock(LocaleProvider.class), mock(StorageService.class));
config = Optional.of(new AccountConfiguration()); config = new AccountConfiguration();
} }
public AccountHandlerMock(Bridge b, @Nullable String storedObject) { public AccountHandlerMock(Bridge b, @Nullable String storedObject, HttpClient httpClient) {
super(b, mock(MercedesMeDiscoveryService.class), mock(HttpClient.class), localeProvider, storageService, super(b, mock(MercedesMeDiscoveryService.class), httpClient, localeProvider, storageService);
mock(NetworkAddressService.class));
if (storedObject != null) { if (storedObject != null) {
Storage<String> storage = storageService.getStorage(Constants.BINDING_ID); Storage<String> storage = storageService.getStorage(Constants.BINDING_ID);
storage.put("a@b.c", storedObject); storage.put("a@b.c", storedObject);
} }
config = Optional.of(new AccountConfiguration()); config = new AccountConfiguration();
} }
@Override @Override
@ -94,4 +91,8 @@ public class AccountHandlerMock extends AccountHandler {
public void connect() { public void connect() {
super.ws.onConnect(mock(Session.class)); super.ws.onConnect(mock(Session.class));
} }
public void refreshToken() {
authService.get().getToken();
}
} }

View File

@ -21,6 +21,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.binding.mercedesme.FileReader; import org.openhab.binding.mercedesme.FileReader;
import org.openhab.binding.mercedesme.internal.Constants; 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 private static final int EVENT_STORAGE_COUNT = HVAC_UPDATE_COUNT + POSITIONING_UPDATE_COUNT + ECOSCORE_UPDATE_COUNT
+ 76; + 76;
@BeforeAll
public static void init() {
Utils.initialize(Utils.timeZoneProvider, Utils.localeProvider);
}
public static Map<String, Object> createBEV() { public static Map<String, Object> createBEV() {
Thing thingMock = mock(Thing.class); Thing thingMock = mock(Thing.class);
when(thingMock.getThingTypeUID()).thenReturn(Constants.THING_TYPE_BEV); when(thingMock.getThingTypeUID()).thenReturn(Constants.THING_TYPE_BEV);

View File

@ -1,9 +1,9 @@
{ {
"accessToken": "Tkn", "access_token": "junitTestToken",
"tokenType": "Bearer", "token_type": "Bearer",
"expiresIn": 7199, "expires_in": 7199,
"refreshToken": "RfrshTkn", "refresh_token": "RfrshTkn",
"scope": "openid email phone profile offline_access ciam-uid", "scope": "openid email phone profile offline_access ciam-uid",
"state": null, "state": null,
"createdOn": "2023-10-04T01:47:08.007038393Z" "created_on": "2023-10-04T01:47:08.007038393Z"
} }