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. (#11228)

Signed-off-by: Jan Gustafsson <jannegpriv@gmail.com>
  • Loading branch information
jannegpriv committed Sep 18, 2021
1 parent 4aa1f14 commit 3194ac6
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 42 deletions.
19 changes: 11 additions & 8 deletions bundles/org.openhab.binding.verisure/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# 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 a rest API behind the Verisure My Pages:

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 +18,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 +30,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 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 +327,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 @@ -145,6 +145,7 @@ public class VerisureBindingConstants {

// GraphQL constants
public static final String STATUS = BASEURL + "/uk/status";
public static final String EXTEND = BASEURL + "/session/extend";
public static final String SETTINGS = BASEURL + "/uk/settings.html?giid=";
public static final String SET_INSTALLATION = BASEURL + "/setinstallation?giid=";
public static final String BASEURL_API = "https://m-api02.verisure.com";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public class VerisureHandlerFactory extends BaseThingHandlerFactory {
}

private final Logger logger = LoggerFactory.getLogger(VerisureHandlerFactory.class);
private final HttpClient httpClient;
private HttpClient httpClient;

@Activate
public VerisureHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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 +87,31 @@ 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 @Nullable String passWord = "";
private @Nullable String vid = "";
private @Nullable String vsAccess = "";
private @Nullable 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, @Nullable String userName,
@Nullable 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 +127,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 +263,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 (cookie.getName().equals(VID)) {
vid = cookie.getValue();
logger.debug("Fetching vid {} from cookie", vid);
} else if (cookie.getName().equals(VS_ACCESS)) {
vsAccess = cookie.getValue();
logger.debug("Fetching vs-access {} from cookie", vsAccess);
} else if (cookie.getName().equals(VS_STEPUP)) {
vsStepup = cookie.getValue();
logger.debug("Fetching vs-stepup {} from cookie", vsStepup);
}
});
}
Expand All @@ -290,7 +301,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 Down Expand Up @@ -325,6 +335,12 @@ 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.OPTIONS);
* ContentResponse optionsRsp = request.send();
* int httpRspCode = optionsRsp.getStatus();
*/

Request request = httpClient.newRequest(url).method(HttpMethod.POST);
if (isJSON) {
request.header("content-type", "application/json");
Expand All @@ -334,14 +350,30 @@ 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);
logger.trace("Basic Auth {}", basicAuhentication);
} else {
logger.debug("Setting cookie with username {} and vid {}", userName, password);
if (vid != null && !vid.isEmpty()) {
request.cookie(new HttpCookie(VID, vid));
logger.debug("Setting cookie with vid {}", vid);
}
if (vsAccess != null && !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 +432,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 +452,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 @@ -489,9 +528,21 @@ private synchronized boolean logIn() {
try {
if (!areWeLoggedIn()) {
logger.debug("Attempting to log in to mypages.verisure.com");
String url = LOGON_SUF;
logger.debug("Remove all cookies");
CookieStore cookieStore = httpClient.getCookieStore();
cookieStore.removeAll();
vid = "";
vsAccess = "";
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 Down Expand Up @@ -617,16 +668,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 +792,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 +841,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 +899,7 @@ private synchronized void updateUserPresenceStatus(VerisureInstallation installa
.getUserTrackings();
userTrackingList.forEach(userTracking -> {
String localUserTrackingStatus = userTracking.getStatus();
if (localUserTrackingStatus != null && localUserTrackingStatus.equals("ACTIVE")) {
if (localUserTrackingStatus != null && "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
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ 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) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
Expand All @@ -116,6 +117,7 @@ public void initialize() {
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) {
Expand All @@ -125,12 +127,11 @@ public void initialize() {
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)) {
logger.warn(
"Failed to login to Verisure, please check your account settings! Is MFA activated!");
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public State getValue(String channelId, VerisureClimatesDTO climateJSON) {
VerisureBatteryStatusDTO batteryStatus = climateJSON.getBatteryStatus();
if (batteryStatus != null) {
String status = batteryStatus.getStatus();
if (status != null && status.equals("CRITICAL")) {
if (status != null && "CRITICAL".equals(status)) {
return OnOffType.from(true);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public State getValue(String channelId, DoorWindow doorWindow, VerisureDoorWindo
VerisureBatteryStatusDTO batteryStatus = doorWindowJSON.getBatteryStatus();
if (batteryStatus != null) {
String status = batteryStatus.getStatus();
if (status != null && status.equals("CRITICAL")) {
if (status != null && "CRITICAL".equals(status)) {
return OnOffType.from(true);
}
}
Expand Down

0 comments on commit 3194ac6

Please sign in to comment.