[mercedesme] New authorization process (#18342)
* change auth process Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>main
parent
a0bae2fd31
commit
79a3c1b242
|
@ -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
|
||||
|
||||
<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
|
||||
|
||||
| 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]
|
||||
}
|
||||
```
|
||||
|
|
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 |
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<String, VEPUpdate> vepUpdateMap = 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 List<byte[]> 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> authService = Optional.empty();
|
||||
final MBWebsocket ws;
|
||||
Optional<AccountConfiguration> 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<String, Object> 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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<Integer, AuthService> 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<String> storage;
|
||||
private HttpClient httpClient;
|
||||
private String identifier;
|
||||
private AccountConfiguration config;
|
||||
private Locale locale;
|
||||
private Storage<String> storage;
|
||||
private AccessTokenResponse token;
|
||||
|
||||
public AuthService(AccessTokenRefreshListener atrl, HttpClient hc, AccountConfiguration ac, Locale l,
|
||||
Storage<String> store) {
|
||||
INVALID_TOKEN.setAccessToken(Constants.NOT_SET);
|
||||
INVALID_TOKEN.setRefreshToken(Constants.NOT_SET);
|
||||
Storage<String> 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 + "] ";
|
||||
}
|
||||
|
|
|
@ -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>");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() + "\"]");
|
||||
}
|
||||
|
|
|
@ -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<String, Integer> ZONE_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) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,6 +19,11 @@
|
|||
<option value="CN">China</option>
|
||||
</options>
|
||||
</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">
|
||||
<label>PIN</label>
|
||||
<description>PIN for commands</description>
|
||||
|
@ -29,15 +34,5 @@
|
|||
<description>Refresh Interval in Minutes</description>
|
||||
<default>15</default>
|
||||
</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-descriptions>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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<String, Object> 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();
|
||||
|
|
|
@ -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<String> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Object> createBEV() {
|
||||
Thing thingMock = mock(Thing.class);
|
||||
when(thingMock.getThingTypeUID()).thenReturn(Constants.THING_TYPE_BEV);
|
||||
|
|
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue