[verisure] Adapted to new authentication process and support for non MFA activated user. (#11228) (#11265)

* [verisure] Adapted to new authentication process and support for non MFA activated user. (#11228)

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>

* Updated after code review.

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>

* Updated after code review.

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>

* Updated after code review.

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>
This commit is contained in:
Jan Gustafsson
2021-10-16 11:28:08 +02:00
committed by GitHub
parent ef07105b8c
commit 18d26aa821
8 changed files with 145 additions and 89 deletions

View File

@@ -131,22 +131,23 @@ public class VerisureBindingConstants {
// REST URI constants
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String BASEURL = "https://mypages.verisure.com";
public static final String LOGON_SUF = BASEURL + "/j_spring_security_check?locale=en_GB";
public static final String ALARM_COMMAND = BASEURL + "/remotecontrol/armstatechange.cmd";
public static final String SMARTLOCK_LOCK_COMMAND = BASEURL + "/remotecontrol/lockunlock.cmd";
public static final String SMARTLOCK_SET_COMMAND = BASEURL + "/overview/setdoorlock.cmd";
public static final String SMARTLOCK_AUTORELOCK_COMMAND = BASEURL + "/settings/setautorelock.cmd";
public static final String SMARTLOCK_VOLUME_COMMAND = BASEURL + "/settings/setvolume.cmd";
public static final String BASE_URL = "https://mypages.verisure.com";
public static final String LOGON_SUF = BASE_URL + "/j_spring_security_check?locale=en_GB";
public static final String ALARM_COMMAND = BASE_URL + "/remotecontrol/armstatechange.cmd";
public static final String SMARTLOCK_LOCK_COMMAND = BASE_URL + "/remotecontrol/lockunlock.cmd";
public static final String SMARTLOCK_SET_COMMAND = BASE_URL + "/overview/setdoorlock.cmd";
public static final String SMARTLOCK_AUTORELOCK_COMMAND = BASE_URL + "/settings/setautorelock.cmd";
public static final String SMARTLOCK_VOLUME_COMMAND = BASE_URL + "/settings/setvolume.cmd";
public static final String SMARTPLUG_COMMAND = BASEURL + "/settings/smartplug/onoffplug.cmd";
public static final String SMARTPLUG_COMMAND = BASE_URL + "/settings/smartplug/onoffplug.cmd";
public static final String START_REDIRECT = "/uk/start.html";
public static final String START_SUF = BASEURL + START_REDIRECT;
public static final String START_SUF = BASE_URL + START_REDIRECT;
// GraphQL constants
public static final String STATUS = BASEURL + "/uk/status";
public static final String SETTINGS = BASEURL + "/uk/settings.html?giid=";
public static final String SET_INSTALLATION = BASEURL + "/setinstallation?giid=";
public static final String STATUS = BASE_URL + "/uk/status";
public static final String EXTEND = BASE_URL + "/session/extend";
public static final String SETTINGS = BASE_URL + "/uk/settings.html?giid=";
public static final String SET_INSTALLATION = BASE_URL + "/setinstallation?giid=";
public static final String BASEURL_API = "https://m-api02.verisure.com";
public static final String START_GRAPHQL = "/graphql";
public static final String AUTH_TOKEN = "/auth/token";

View File

@@ -23,8 +23,8 @@ import org.eclipse.jdt.annotation.Nullable;
*/
@NonNullByDefault
public class VerisureBridgeConfiguration {
public @Nullable String username;
public @Nullable String password;
public int refresh;
public String username = "";
public String password = "";
public int refresh = 600;
public @Nullable String pin;
}

View File

@@ -17,9 +17,12 @@ import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*;
import java.math.BigDecimal;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -86,24 +89,30 @@ public class VerisureSession {
private int apiServerInUseIndex = 0;
private int numberOfEvents = 15;
private static final String USER_NAME = "username";
private static final String PASSWORD_NAME = "vid";
private static final String VID = "vid";
private static final String VS_STEPUP = "vs-stepup";
private static final String VS_ACCESS = "vs-access";
private String apiServerInUse = APISERVERLIST.get(apiServerInUseIndex);
private String authstring = "";
private @Nullable String csrf;
private @Nullable String pinCode;
private HttpClient httpClient;
private @Nullable String userName = "";
private @Nullable String password = "";
private String userName = "";
private String password = "";
private String vid = "";
private String vsAccess = "";
private String vsStepup = "";
public VerisureSession(HttpClient httpClient) {
this.httpClient = httpClient;
}
public boolean initialize(@Nullable String authstring, @Nullable String pinCode, @Nullable String userName) {
public boolean initialize(@Nullable String authstring, @Nullable String pinCode, String userName, String password) {
if (authstring != null) {
this.authstring = authstring.substring(0);
this.pinCode = pinCode;
this.userName = userName;
this.password = password;
// Try to login to Verisure
if (logIn()) {
return getInstallations();
@@ -119,12 +128,9 @@ public class VerisureSession {
if (logIn()) {
if (updateStatus()) {
return true;
} else {
return false;
}
} else {
return false;
}
return false;
} catch (HttpResponseException e) {
logger.warn("Failed to do a refresh {}", e.getMessage());
return false;
@@ -258,15 +264,21 @@ public class VerisureSession {
}
}
private void setPasswordFromCookie() {
private void analyzeCookies() {
CookieStore c = httpClient.getCookieStore();
List<HttpCookie> cookies = c.getCookies();
final List<HttpCookie> unmodifiableList = List.of(cookies.toArray(new HttpCookie[] {}));
unmodifiableList.forEach(cookie -> {
logger.trace("Response Cookie: {}", cookie);
if (cookie.getName().equals(PASSWORD_NAME)) {
password = cookie.getValue();
logger.debug("Fetching vid {} from cookie", password);
if (VID.equals(cookie.getName())) {
vid = cookie.getValue();
logger.debug("Fetching vid {} from cookie", vid);
} else if (VS_ACCESS.equals(cookie.getName())) {
vsAccess = cookie.getValue();
logger.debug("Fetching vs-access {} from cookie", vsAccess);
} else if (VS_STEPUP.equals(cookie.getName())) {
vsStepup = cookie.getValue();
logger.debug("Fetching vs-stepup {} from cookie", vsStepup);
}
});
}
@@ -290,7 +302,6 @@ public class VerisureSession {
switch (response.getStatus()) {
case HttpStatus.OK_200:
if (content.contains("<link href=\"/newapp")) {
setPasswordFromCookie();
return true;
} else {
logger.debug("We need to login again!");
@@ -313,9 +324,9 @@ public class VerisureSession {
private <T> @Nullable T getJSONVerisureAPI(String url, Class<T> jsonClass)
throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException {
logger.debug("HTTP GET: {}", BASEURL + url);
logger.debug("HTTP GET: {}", BASE_URL + url);
ContentResponse response = httpClient.GET(BASEURL + url + "?_=" + System.currentTimeMillis());
ContentResponse response = httpClient.GET(BASE_URL + url + "?_=" + System.currentTimeMillis());
String content = response.getContentAsString();
logTraceWithPattern(response.getStatus(), content);
@@ -325,6 +336,7 @@ public class VerisureSession {
private ContentResponse postVerisureAPI(String url, String data, boolean isJSON)
throws ExecutionException, InterruptedException, TimeoutException {
logger.debug("postVerisureAPI URL: {} Data:{}", url, data);
Request request = httpClient.newRequest(url).method(HttpMethod.POST);
if (isJSON) {
request.header("content-type", "application/json");
@@ -334,14 +346,29 @@ public class VerisureSession {
}
}
request.header("Accept", "application/json");
if (!data.equals("empty")) {
if (url.contains(AUTH_LOGIN)) {
request.header("APPLICATION_ID", "OpenHAB Verisure");
String basicAuhentication = Base64.getEncoder().encodeToString((userName + ":" + password).getBytes());
request.header("authorization", "Basic " + basicAuhentication);
} else {
if (!vid.isEmpty()) {
request.cookie(new HttpCookie(VID, vid));
logger.debug("Setting cookie with vid {}", vid);
}
if (!vsAccess.isEmpty()) {
request.cookie(new HttpCookie(VS_ACCESS, vsAccess));
logger.debug("Setting cookie with vs-access {}", vsAccess);
}
logger.debug("Setting cookie with username {}", userName);
request.cookie(new HttpCookie(USER_NAME, userName));
}
if (!"empty".equals(data)) {
request.content(new BytesContentProvider(data.getBytes(StandardCharsets.UTF_8)),
"application/x-www-form-urlencoded; charset=UTF-8");
} else {
logger.debug("Setting cookie with username {} and vid {}", userName, password);
request.cookie(new HttpCookie(USER_NAME, userName));
request.cookie(new HttpCookie(PASSWORD_NAME, password));
}
logger.debug("HTTP POST Request {}.", request.toString());
return request.send();
}
@@ -400,6 +427,9 @@ public class VerisureSession {
logTraceWithPattern(httpStatus, content);
return httpStatus;
}
} else if (httpStatus == HttpStatus.BAD_REQUEST_400) {
setApiServerInUse(getNextApiServer());
url = apiServerInUse + urlString;
} else {
logger.debug("Failed to send POST, Http status code: {}", response.getStatus());
}
@@ -417,7 +447,11 @@ public class VerisureSession {
logTraceWithPattern(response.getStatus(), response.getContentAsString());
url = AUTH_LOGIN;
return postVerisureAPI(url, "empty");
int httpStatusCode = postVerisureAPI(url, "empty");
analyzeCookies();
// return response.getStatus();
return httpStatusCode;
}
private boolean getInstallations() {
@@ -488,10 +522,26 @@ public class VerisureSession {
private synchronized boolean logIn() {
try {
if (!areWeLoggedIn()) {
logger.debug("Attempting to log in to mypages.verisure.com");
String url = LOGON_SUF;
vid = "";
vsAccess = "";
logger.debug("Attempting to log in to {}, remove all cookies to ensure a fresh session", BASE_URL);
URI authUri = new URI(BASE_URL);
CookieStore store = httpClient.getCookieStore();
store.get(authUri).forEach(cookie -> {
store.remove(authUri, cookie);
});
String url = AUTH_LOGIN;
int httpStatusCode = postVerisureAPI(url, "empty");
analyzeCookies();
if (!vsStepup.isEmpty()) {
logger.warn("MFA is activated on this user! Not supported by binding!");
return false;
}
url = LOGON_SUF;
logger.debug("Login URL: {}", url);
int httpStatusCode = postVerisureAPI(url, authstring);
httpStatusCode = postVerisureAPI(url, authstring);
if (httpStatusCode != HttpStatus.OK_200) {
logger.debug("Failed to login, HTTP status code: {}", httpStatusCode);
return false;
@@ -500,7 +550,7 @@ public class VerisureSession {
} else {
return true;
}
} catch (ExecutionException | InterruptedException | TimeoutException e) {
} catch (ExecutionException | InterruptedException | TimeoutException | URISyntaxException e) {
logger.warn("Failed to login {}", e.getMessage());
}
return false;
@@ -617,16 +667,17 @@ public class VerisureSession {
// Set location
slThing.setLocation(doorLock.getDevice().getArea());
slThing.setDeviceId(deviceId);
// Fetch more info from old endpoint
try {
VerisureSmartLockDTO smartLockThing = getJSONVerisureAPI(SMARTLOCK_PATH + slThing.getDeviceId(),
VerisureSmartLockDTO.class);
logger.debug("REST Response ({})", smartLockThing);
slThing.setSmartLockJSON(smartLockThing);
notifyListenersIfChanged(slThing, installation, deviceId);
} catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
logger.warn("Failed to query for smartlock status: {}", e.getMessage());
}
notifyListenersIfChanged(slThing, installation, deviceId);
}
});
@@ -740,7 +791,7 @@ public class VerisureSession {
cThing.setBatteryStatus(batteryStatus);
}
} catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
logger.warn("Failed to query for smartlock status: {}", e.getMessage());
logger.debug("Failed to query for battery status: {}", e.getMessage());
}
// Set location
cThing.setLocation(climate.getDevice().getArea());
@@ -789,7 +840,7 @@ public class VerisureSession {
dThing.setBatteryStatus(batteryStatus);
}
} catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
logger.warn("Failed to query for smartlock status: {}", e.getMessage());
logger.warn("Failed to query for door&window status: {}", e.getMessage());
}
// Set location
dThing.setLocation(doorWindow.getDevice().getArea());
@@ -847,7 +898,7 @@ public class VerisureSession {
.getUserTrackings();
userTrackingList.forEach(userTracking -> {
String localUserTrackingStatus = userTracking.getStatus();
if (localUserTrackingStatus != null && localUserTrackingStatus.equals("ACTIVE")) {
if ("ACTIVE".equals(localUserTrackingStatus)) {
VerisureUserPresencesDTO upThing = new VerisureUserPresencesDTO();
VerisureUserPresencesDTO.Installation inst = new VerisureUserPresencesDTO.Installation();
inst.setUserTrackings(Collections.singletonList(userTracking));

View File

@@ -75,7 +75,8 @@ public class VerisureThingDiscoveryService extends AbstractDiscoveryService
String deviceId = thing.getDeviceId();
if (thingUID != null) {
if (verisureBridgeHandler != null) {
String label = "Device Id: " + deviceId;
String className = thing.getClass().getSimpleName();
String label = "Type: " + className + " Device Id: " + deviceId;
if (thing.getLocation() != null) {
label += ", Location: " + thing.getLocation();
}
@@ -84,7 +85,7 @@ public class VerisureThingDiscoveryService extends AbstractDiscoveryService
}
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID)
.withLabel(label).withProperty(VerisureThingConfiguration.DEVICE_ID_LABEL, deviceId)
.withRepresentationProperty(deviceId).build();
.withRepresentationProperty(VerisureThingConfiguration.DEVICE_ID_LABEL).build();
logger.debug("thinguid: {}, bridge {}, label {}", thingUID, bridgeUID, deviceId);
thingDiscovered(discoveryResult);
}

View File

@@ -14,9 +14,6 @@ package org.openhab.binding.verisure.internal.handler;
import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
@@ -65,7 +62,6 @@ public class VerisureBridgeHandler extends BaseBridgeHandler {
private String authstring = "";
private @Nullable String pinCode;
private static int REFRESH_SEC = 600;
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable ScheduledFuture<?> immediateRefreshJob;
private @Nullable VerisureSession session;
@@ -104,20 +100,19 @@ public class VerisureBridgeHandler extends BaseBridgeHandler {
public void initialize() {
logger.debug("Initializing Verisure Binding");
VerisureBridgeConfiguration config = getConfigAs(VerisureBridgeConfiguration.class);
REFRESH_SEC = config.refresh;
this.pinCode = config.pin;
if (config.username == null || config.password == null) {
if (config.username.isBlank() || config.password.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Configuration of username and password is mandatory");
} else if (REFRESH_SEC < 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Refresh time cannot negative!");
} else if (config.refresh < 10) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Refresh time is lower than min value of 10!");
} else {
try {
authstring = "j_username=" + config.username + "&j_password="
+ URLEncoder.encode(config.password, StandardCharsets.UTF_8.toString())
+ "&spring-security-redirect=" + START_REDIRECT;
authstring = "j_username=" + config.username;
scheduler.execute(() -> {
if (session == null) {
logger.debug("Session is null, let's create a new one");
session = new VerisureSession(this.httpClient);
@@ -125,18 +120,15 @@ public class VerisureBridgeHandler extends BaseBridgeHandler {
VerisureSession session = this.session;
updateStatus(ThingStatus.UNKNOWN);
if (session != null) {
if (!session.initialize(authstring, pinCode, config.username)) {
logger.warn("Failed to initialize bridge, please check your credentials!");
if (!session.initialize(authstring, pinCode, config.username, config.password)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_REGISTERING_ERROR,
"Failed to login to Verisure, please check your credentials!");
dispose();
initialize();
"Failed to login to Verisure, please check your account settings! Is MFA activated?");
return;
}
startAutomaticRefresh();
startAutomaticRefresh(config.refresh);
}
});
} catch (RuntimeException | UnsupportedEncodingException e) {
} catch (RuntimeException e) {
logger.warn("Failed to initialize: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
@@ -227,12 +219,12 @@ public class VerisureBridgeHandler extends BaseBridgeHandler {
}
}
private void startAutomaticRefresh() {
private void startAutomaticRefresh(int refresh) {
ScheduledFuture<?> refreshJob = this.refreshJob;
logger.debug("Start automatic refresh {}", refreshJob);
if (refreshJob == null || refreshJob.isCancelled()) {
try {
this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshAndUpdateStatus, 0, REFRESH_SEC,
this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshAndUpdateStatus, 0, refresh,
TimeUnit.SECONDS);
logger.debug("Scheduling at fixed delay refreshjob {}", this.refreshJob);
} catch (RejectedExecutionException e) {

View File

@@ -15,6 +15,7 @@ package org.openhab.binding.verisure.internal.handler;
import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.measure.quantity.Dimensionless;
@@ -23,6 +24,7 @@ import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.verisure.internal.dto.VerisureBatteryStatusDTO;
import org.openhab.binding.verisure.internal.dto.VerisureClimatesDTO;
import org.openhab.binding.verisure.internal.dto.VerisureClimatesDTO.Climate;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
@@ -74,27 +76,35 @@ public class VerisureClimateDeviceThingHandler extends VerisureThingHandler<Veri
State state = getValue(channelUID.getId(), climateJSON);
updateState(channelUID, state);
});
String timeStamp = climateJSON.getData().getInstallation().getClimates().get(0).getTemperatureTimestamp();
if (timeStamp != null) {
updateTimeStamp(timeStamp);
List<Climate> climateList = climateJSON.getData().getInstallation().getClimates();
if (climateList != null && !climateList.isEmpty()) {
String timeStamp = climateList.get(0).getTemperatureTimestamp();
if (timeStamp != null) {
updateTimeStamp(timeStamp);
}
}
updateInstallationChannels(climateJSON);
}
public State getValue(String channelId, VerisureClimatesDTO climateJSON) {
List<Climate> climateList = climateJSON.getData().getInstallation().getClimates();
switch (channelId) {
case CHANNEL_TEMPERATURE:
double temperature = climateJSON.getData().getInstallation().getClimates().get(0).getTemperatureValue();
return new QuantityType<Temperature>(temperature, SIUnits.CELSIUS);
if (climateList != null && !climateList.isEmpty()) {
double temperature = climateList.get(0).getTemperatureValue();
return new QuantityType<Temperature>(temperature, SIUnits.CELSIUS);
}
case CHANNEL_HUMIDITY:
if (climateJSON.getData().getInstallation().getClimates().get(0).isHumidityEnabled()) {
double humidity = climateJSON.getData().getInstallation().getClimates().get(0).getHumidityValue();
if (climateList != null && !climateList.isEmpty() && climateList.get(0).isHumidityEnabled()) {
double humidity = climateList.get(0).getHumidityValue();
return new QuantityType<Dimensionless>(humidity, Units.PERCENT);
}
case CHANNEL_HUMIDITY_ENABLED:
boolean humidityEnabled = climateJSON.getData().getInstallation().getClimates().get(0)
.isHumidityEnabled();
return OnOffType.from(humidityEnabled);
if (climateList != null && !climateList.isEmpty()) {
boolean humidityEnabled = climateList.get(0).isHumidityEnabled();
return OnOffType.from(humidityEnabled);
}
case CHANNEL_LOCATION:
String location = climateJSON.getLocation();
return location != null ? new StringType(location) : UnDefType.NULL;
@@ -102,7 +112,7 @@ public class VerisureClimateDeviceThingHandler extends VerisureThingHandler<Veri
VerisureBatteryStatusDTO batteryStatus = climateJSON.getBatteryStatus();
if (batteryStatus != null) {
String status = batteryStatus.getStatus();
if (status != null && status.equals("CRITICAL")) {
if ("CRITICAL".equals(status)) {
return OnOffType.from(true);
}
}

View File

@@ -88,7 +88,7 @@ public class VerisureDoorWindowThingHandler extends VerisureThingHandler<Verisur
VerisureBatteryStatusDTO batteryStatus = doorWindowJSON.getBatteryStatus();
if (batteryStatus != null) {
String status = batteryStatus.getStatus();
if (status != null && status.equals("CRITICAL")) {
if ("CRITICAL".equals(status)) {
return OnOffType.from(true);
}
}