Skip to content

Commit

Permalink
[verisure] Adapted to new authentication process and support for non …
Browse files Browse the repository at this point in the history
…MFA activated user. (openhab#11228) (openhab#11265)

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

Signed-off-by: Jan Gustafsson <[email protected]>

* Updated after code review.

Signed-off-by: Jan Gustafsson <[email protected]>

* Updated after code review.

Signed-off-by: Jan Gustafsson <[email protected]>

* Updated after code review.

Signed-off-by: Jan Gustafsson <[email protected]>
  • Loading branch information
jannegpriv authored and frederictobiasc committed Oct 26, 2021
1 parent 2eaae47 commit 23585a1
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 89 deletions.
21 changes: 11 additions & 10 deletions bundles/org.openhab.binding.verisure/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
# Verisure Binding

This is an openHAB binding for Verisure Alarm System, by Securitas Direct.
This is an openHAB binding for Verisure Smart Alarms by Verisure Securitas.

This binding uses the rest API behind the Verisure My Pages:
This binding uses a rest API used by the [Verisure My Pages webpage](https://mypages.verisure.com/login.html)

https://mypages.verisure.com/login.html.

Be aware that Verisure don't approve if you update to often, I have gotten no complaints running with a 10 minutes update interval, but officially you should use 30 minutes.


## Supported Things
Expand All @@ -19,7 +16,7 @@ This binding supports the following thing types:
- Water Detector (climate)
- Siren (climate)
- Night Control
- Yaleman SmartLock
- Yaleman Doorman SmartLock
- SmartPlug
- Door/Window Status
- User Presence Status
Expand All @@ -31,11 +28,14 @@ This binding supports the following thing types:

## Binding Configuration

You will have to configure the bridge with username and password, these must be the same credentials as used when logging into https://mypages.verisure.com.
You will have to configure the bridge with username and password of a pre-defined user on [Verisure page](https://mypages.verisure.com) that has not activated Multi Factor Authentication (MFA/2FA).

Verisure allows you to have more than one user so the suggestion is to use a specific user for automation that has MFA/2FA deactivated.
**NOTE:** To be able to have full control over all SmartLock/alarm functionality, the user also needs to have Administrator rights.

You must also configure pin-code(s) to be able to lock/unlock the SmartLock(s) and arm/unarm the Alarm(s).

You must also configure your pin-code(s) to be able to lock/unlock the SmartLock(s) and arm/unarm the Alarm(s).

**NOTE:** To be able to have full control over all SmartLock functionality, the user has to have Administrator rights.

## Discovery

Expand Down Expand Up @@ -325,7 +325,8 @@ The following channels are supported:
#### Configuration Options

* `deviceId` - Device Id
* Since Event Log lacks a Verisure ID, the following naming convention is used for Event Log on site id 123456789: 'el123456789'. Installation ID can be found using DEBUG log settings.
* Since Event Log lacks a Verisure ID, the following naming convention is used for Event Log on site id 123456789: 'el123456789'. Installation ID can be found using DEBUG log settings.


#### Channels

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
*/
@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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
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;
Expand Down Expand Up @@ -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();
Expand All @@ -119,12 +128,9 @@ public boolean refresh() {
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;
Expand Down Expand Up @@ -258,15 +264,21 @@ public void configureInstallationInstance(BigDecimal installationId)
}
}

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);
}
});
}
Expand All @@ -290,7 +302,6 @@ private boolean areWeLoggedIn() throws ExecutionException, InterruptedException,
switch (response.getStatus()) {
case HttpStatus.OK_200:
if (content.contains("<link href=\"/newapp")) {
setPasswordFromCookie();
return true;
} else {
logger.debug("We need to login again!");
Expand All @@ -313,9 +324,9 @@ private boolean areWeLoggedIn() throws ExecutionException, InterruptedException,

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);

Expand All @@ -325,6 +336,7 @@ private boolean areWeLoggedIn() throws ExecutionException, InterruptedException,
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");
Expand All @@ -334,14 +346,29 @@ private ContentResponse postVerisureAPI(String url, String data, boolean isJSON)
}
}
request.header("Accept", "application/json");
if (!data.equals("empty")) {
request.content(new BytesContentProvider(data.getBytes(StandardCharsets.UTF_8)),
"application/x-www-form-urlencoded; charset=UTF-8");

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 {
logger.debug("Setting cookie with username {} and vid {}", userName, password);
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));
request.cookie(new HttpCookie(PASSWORD_NAME, password));
}

if (!"empty".equals(data)) {
request.content(new BytesContentProvider(data.getBytes(StandardCharsets.UTF_8)),
"application/x-www-form-urlencoded; charset=UTF-8");
}

logger.debug("HTTP POST Request {}.", request.toString());
return request.send();
}
Expand Down Expand Up @@ -400,6 +427,9 @@ private int postVerisureAPI(String urlString, String data) {
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());
}
Expand All @@ -417,7 +447,11 @@ private int setSessionCookieAuthLogin() throws ExecutionException, InterruptedEx
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() {
Expand Down Expand Up @@ -488,10 +522,26 @@ private boolean getInstallations() {
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;
Expand All @@ -500,7 +550,7 @@ private synchronized boolean logIn() {
} else {
return true;
}
} catch (ExecutionException | InterruptedException | TimeoutException e) {
} catch (ExecutionException | InterruptedException | TimeoutException | URISyntaxException e) {
logger.warn("Failed to login {}", e.getMessage());
}
return false;
Expand Down Expand Up @@ -617,16 +667,17 @@ private synchronized void updateSmartLockStatus(VerisureInstallation installatio
// 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);
}
});

Expand Down Expand Up @@ -740,7 +791,7 @@ private synchronized void updateClimateStatus(VerisureInstallation installation)
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());
Expand Down Expand Up @@ -789,7 +840,7 @@ private synchronized void updateDoorWindowStatus(VerisureInstallation installati
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());
Expand Down Expand Up @@ -847,7 +898,7 @@ private synchronized void updateUserPresenceStatus(VerisureInstallation installa
.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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ private void onThingAddedInternal(VerisureThingDTO thing) {
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();
}
Expand All @@ -84,7 +85,7 @@ private void onThingAddedInternal(VerisureThingDTO thing) {
}
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);
}
Expand Down
Loading

0 comments on commit 23585a1

Please sign in to comment.