[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
|
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 |
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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.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 + "] ";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
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() + "\"]");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
Loading…
Reference in New Issue