2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.verisure.internal;
15 import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.net.CookieStore;
19 import java.net.HttpCookie;
21 import java.net.URISyntaxException;
22 import java.nio.charset.StandardCharsets;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Base64;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.List;
31 import java.util.concurrent.ConcurrentHashMap;
32 import java.util.concurrent.ExecutionException;
33 import java.util.concurrent.TimeoutException;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.eclipse.jetty.client.HttpClient;
38 import org.eclipse.jetty.client.HttpResponseException;
39 import org.eclipse.jetty.client.api.ContentResponse;
40 import org.eclipse.jetty.client.api.Request;
41 import org.eclipse.jetty.client.util.BytesContentProvider;
42 import org.eclipse.jetty.http.HttpMethod;
43 import org.eclipse.jetty.http.HttpStatus;
44 import org.jsoup.Jsoup;
45 import org.jsoup.nodes.Document;
46 import org.jsoup.nodes.Element;
47 import org.openhab.binding.verisure.internal.dto.VerisureAlarmsDTO;
48 import org.openhab.binding.verisure.internal.dto.VerisureBatteryStatusDTO;
49 import org.openhab.binding.verisure.internal.dto.VerisureBroadbandConnectionsDTO;
50 import org.openhab.binding.verisure.internal.dto.VerisureClimatesDTO;
51 import org.openhab.binding.verisure.internal.dto.VerisureDoorWindowsDTO;
52 import org.openhab.binding.verisure.internal.dto.VerisureEventLogDTO;
53 import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO;
54 import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO.CommunicationState;
55 import org.openhab.binding.verisure.internal.dto.VerisureInstallationsDTO;
56 import org.openhab.binding.verisure.internal.dto.VerisureInstallationsDTO.Owainstallation;
57 import org.openhab.binding.verisure.internal.dto.VerisureMiceDetectionDTO;
58 import org.openhab.binding.verisure.internal.dto.VerisureSmartLockDTO;
59 import org.openhab.binding.verisure.internal.dto.VerisureSmartLocksDTO;
60 import org.openhab.binding.verisure.internal.dto.VerisureSmartPlugsDTO;
61 import org.openhab.binding.verisure.internal.dto.VerisureThingDTO;
62 import org.openhab.binding.verisure.internal.dto.VerisureUserPresencesDTO;
63 import org.openhab.binding.verisure.internal.handler.VerisureThingHandler;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
67 import com.google.gson.Gson;
68 import com.google.gson.JsonSyntaxException;
71 * This class performs the communication with Verisure My Pages.
73 * @author Jarle Hjortland - Initial contribution
74 * @author Jan Gustafsson - Re-design and support for several sites and update to new Verisure API
78 public class VerisureSession {
81 private final Map<String, VerisureThingDTO> verisureThings = new ConcurrentHashMap<>();
82 private final Map<String, VerisureThingHandler<?>> verisureHandlers = new ConcurrentHashMap<>();
83 private final Logger logger = LoggerFactory.getLogger(VerisureSession.class);
84 private final Gson gson = new Gson();
85 private final Set<DeviceStatusListener<VerisureThingDTO>> deviceStatusListeners = ConcurrentHashMap.newKeySet();
86 private final Map<BigDecimal, VerisureInstallation> verisureInstallations = new ConcurrentHashMap<>();
87 private static final List<String> APISERVERLIST = Arrays.asList("https://m-api01.verisure.com",
88 "https://m-api02.verisure.com");
89 private int apiServerInUseIndex = 0;
90 private int numberOfEvents = 15;
91 private static final String USER_NAME = "username";
92 private static final String VID = "vid";
93 private static final String VS_STEPUP = "vs-stepup";
94 private static final String VS_ACCESS = "vs-access";
95 private String apiServerInUse = APISERVERLIST.get(apiServerInUseIndex);
96 private String authstring = "";
97 private @Nullable String csrf;
98 private @Nullable String pinCode;
99 private HttpClient httpClient;
100 private String userName = "";
101 private String password = "";
102 private String vid = "";
103 private String vsAccess = "";
104 private String vsStepup = "";
106 public VerisureSession(HttpClient httpClient) {
107 this.httpClient = httpClient;
110 public boolean initialize(@Nullable String authstring, @Nullable String pinCode, String userName, String password) {
111 if (authstring != null) {
112 this.authstring = authstring;
113 this.pinCode = pinCode;
114 this.userName = userName;
115 this.password = password;
116 // Try to login to Verisure
118 return getInstallations();
126 public boolean refresh() {
128 if (updateStatus()) {
135 public int sendCommand(String url, String data, BigDecimal installationId) {
136 logger.debug("Sending command with URL {} and data {}", url, data);
138 configureInstallationInstance(installationId);
139 int httpResultCode = setSessionCookieAuthLogin();
140 if (httpResultCode == HttpStatus.OK_200) {
141 return postVerisureAPI(url, data);
143 return httpResultCode;
145 } catch (ExecutionException | InterruptedException | TimeoutException e) {
146 logger.debug("Failed to send command {}", e.getMessage());
148 return HttpStatus.BAD_REQUEST_400;
151 public boolean unregisterDeviceStatusListener(
152 DeviceStatusListener<? extends VerisureThingDTO> deviceStatusListener) {
153 return deviceStatusListeners.remove(deviceStatusListener);
156 @SuppressWarnings("unchecked")
157 public boolean registerDeviceStatusListener(DeviceStatusListener<? extends VerisureThingDTO> deviceStatusListener) {
158 return deviceStatusListeners.add((DeviceStatusListener<VerisureThingDTO>) deviceStatusListener);
161 @SuppressWarnings({ "unchecked" })
162 public <T extends VerisureThingDTO> @Nullable T getVerisureThing(String deviceId, Class<T> thingType) {
163 VerisureThingDTO thing = verisureThings.get(deviceId);
164 if (thingType.isInstance(thing)) {
170 public <T extends VerisureThingDTO> @Nullable T getVerisureThing(String deviceId) {
171 VerisureThingDTO thing = verisureThings.get(deviceId);
173 @SuppressWarnings("unchecked")
174 T thing2 = (T) thing;
180 public @Nullable VerisureThingHandler<?> getVerisureThinghandler(String deviceId) {
181 VerisureThingHandler<?> thingHandler = verisureHandlers.get(deviceId);
185 public void setVerisureThingHandler(VerisureThingHandler<?> vth, String deviceId) {
186 verisureHandlers.put(deviceId, vth);
189 public void removeVerisureThingHandler(String deviceId) {
190 verisureHandlers.remove(deviceId);
193 public Collection<VerisureThingDTO> getVerisureThings() {
194 return verisureThings.values();
197 public @Nullable String getCsrf() {
201 public @Nullable String getPinCode() {
205 public String getApiServerInUse() {
206 return apiServerInUse;
209 public void setApiServerInUse(String apiServerInUse) {
210 this.apiServerInUse = apiServerInUse;
213 public String getNextApiServer() {
214 apiServerInUseIndex++;
215 if (apiServerInUseIndex > (APISERVERLIST.size() - 1)) {
216 apiServerInUseIndex = 0;
218 return APISERVERLIST.get(apiServerInUseIndex);
221 public void setNumberOfEvents(int numberOfEvents) {
222 this.numberOfEvents = numberOfEvents;
225 public void configureInstallationInstance(BigDecimal installationId)
226 throws ExecutionException, InterruptedException, TimeoutException {
227 csrf = getCsrfToken(installationId);
228 logger.debug("Got CSRF: {}", csrf);
230 String url = SET_INSTALLATION + installationId;
234 public @Nullable String getCsrfToken(BigDecimal installationId)
235 throws ExecutionException, InterruptedException, TimeoutException {
237 String url = SETTINGS + installationId;
239 ContentResponse resp = httpClient.GET(url);
240 html = resp.getContentAsString();
241 logger.trace("url: {} html: {}", url, html);
243 Document htmlDocument = Jsoup.parse(html);
244 Element nameInput = htmlDocument.select("input[name=_csrf]").first();
245 if (nameInput != null) {
246 return nameInput.attr("value");
252 public @Nullable String getPinCode(BigDecimal installationId) {
253 VerisureInstallation inst = verisureInstallations.get(installationId);
255 return inst.getPinCode();
257 logger.debug("Installation is null!");
262 private void analyzeCookies() {
263 CookieStore c = httpClient.getCookieStore();
264 List<HttpCookie> cookies = c.getCookies();
265 final List<HttpCookie> unmodifiableList = List.of(cookies.toArray(new HttpCookie[] {}));
266 unmodifiableList.forEach(cookie -> {
267 logger.trace("Response Cookie: {}", cookie);
268 if (VID.equals(cookie.getName())) {
269 vid = cookie.getValue();
270 logger.debug("Fetching vid {} from cookie", vid);
271 } else if (VS_ACCESS.equals(cookie.getName())) {
272 vsAccess = cookie.getValue();
273 logger.debug("Fetching vs-access {} from cookie", vsAccess);
274 } else if (VS_STEPUP.equals(cookie.getName())) {
275 vsStepup = cookie.getValue();
276 logger.debug("Fetching vs-stepup {} from cookie", vsStepup);
281 private void logTraceWithPattern(int responseStatus, String content) {
282 if (logger.isTraceEnabled()) {
283 String pattern = "(?m)^\\s*\\r?\\n|\\r?\\n\\s*(?!.*\\r?\\n)";
284 String replacement = "";
285 logger.trace("HTTP Response ({}) Body:{}", responseStatus, content.replaceAll(pattern, replacement));
289 private boolean areWeLoggedIn() throws ExecutionException, InterruptedException, TimeoutException {
290 logger.debug("Checking if we are logged in");
293 ContentResponse response = httpClient.newRequest(url).method(HttpMethod.GET).send();
294 String content = response.getContentAsString();
295 logTraceWithPattern(response.getStatus(), content);
297 switch (response.getStatus()) {
298 case HttpStatus.OK_200:
299 if (content.contains("<link href=\"/newapp")) {
302 logger.debug("We need to login again!");
305 case HttpStatus.MOVED_TEMPORARILY_302:
307 logger.debug("Status code 302. Redirected. Probably not logged in");
309 case HttpStatus.INTERNAL_SERVER_ERROR_500:
310 case HttpStatus.SERVICE_UNAVAILABLE_503:
311 throw new HttpResponseException(
312 "Status code " + response.getStatus() + ". Verisure service temporarily down", response);
314 logger.debug("Status code {} body {}", response.getStatus(), content);
320 private <T> @Nullable T getJSONVerisureAPI(String url, Class<T> jsonClass)
321 throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException {
322 logger.debug("HTTP GET: {}", BASE_URL + url);
324 ContentResponse response = httpClient.GET(BASE_URL + url + "?_=" + System.currentTimeMillis());
325 String content = response.getContentAsString();
326 logTraceWithPattern(response.getStatus(), content);
328 return gson.fromJson(content, jsonClass);
331 private ContentResponse postVerisureAPI(String url, String data, boolean isJSON)
332 throws ExecutionException, InterruptedException, TimeoutException {
333 logger.debug("postVerisureAPI URL: {} Data:{}", url, data);
335 Request request = httpClient.newRequest(url).method(HttpMethod.POST);
337 request.header("content-type", "application/json");
340 request.header("X-CSRF-TOKEN", csrf);
343 request.header("Accept", "application/json");
345 if (url.contains(AUTH_LOGIN)) {
346 request.header("APPLICATION_ID", "OpenHAB Verisure");
347 String basicAuhentication = Base64.getEncoder().encodeToString((userName + ":" + password).getBytes());
348 request.header("authorization", "Basic " + basicAuhentication);
350 if (!vid.isEmpty()) {
351 request.cookie(new HttpCookie(VID, vid));
352 logger.debug("Setting cookie with vid {}", vid);
354 if (!vsAccess.isEmpty()) {
355 request.cookie(new HttpCookie(VS_ACCESS, vsAccess));
356 logger.debug("Setting cookie with vs-access {}", vsAccess);
358 logger.debug("Setting cookie with username {}", userName);
359 request.cookie(new HttpCookie(USER_NAME, userName));
362 if (!"empty".equals(data)) {
363 request.content(new BytesContentProvider(data.getBytes(StandardCharsets.UTF_8)),
364 "application/x-www-form-urlencoded; charset=UTF-8");
367 logger.debug("HTTP POST Request {}.", request.toString());
368 return request.send();
371 private <T> T postJSONVerisureAPI(String url, String data, Class<T> jsonClass)
372 throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException, PostToAPIException {
373 for (int cnt = 0; cnt < APISERVERLIST.size(); cnt++) {
374 ContentResponse response = postVerisureAPI(apiServerInUse + url, data, Boolean.TRUE);
375 logger.debug("HTTP Response ({})", response.getStatus());
376 if (response.getStatus() == HttpStatus.OK_200) {
377 String content = response.getContentAsString();
378 if (content.contains("\"message\":\"Request Failed") && content.contains("503")) {
379 // Maybe Verisure has switched API server in use?
380 logger.debug("Changed API server! Response: {}", content);
381 setApiServerInUse(getNextApiServer());
382 } else if (content.contains("\"message\":\"Request Failed")
383 && content.contains("Invalid session cookie")) {
384 throw new PostToAPIException("Invalid session cookie");
386 String contentChomped = content.trim();
387 logger.trace("Response body: {}", content);
388 return gson.fromJson(contentChomped, jsonClass);
391 logger.debug("Failed to send POST, Http status code: {}", response.getStatus());
394 throw new PostToAPIException("Failed to POST to API");
397 private int postVerisureAPI(String urlString, String data) {
399 if (urlString.contains("https://mypages")) {
402 url = apiServerInUse + urlString;
405 for (int cnt = 0; cnt < APISERVERLIST.size(); cnt++) {
407 ContentResponse response = postVerisureAPI(url, data, Boolean.FALSE);
408 logger.debug("HTTP Response ({})", response.getStatus());
409 int httpStatus = response.getStatus();
410 if (httpStatus == HttpStatus.OK_200) {
411 String content = response.getContentAsString();
412 if (content.contains("\"message\":\"Request Failed. Code 503 from")) {
413 if (url.contains("https://mypages")) {
415 return HttpStatus.SERVICE_UNAVAILABLE_503;
417 // Maybe Verisure has switched API server in use
418 setApiServerInUse(getNextApiServer());
419 url = apiServerInUse + urlString;
422 logTraceWithPattern(httpStatus, content);
425 } else if (httpStatus == HttpStatus.BAD_REQUEST_400) {
426 setApiServerInUse(getNextApiServer());
427 url = apiServerInUse + urlString;
429 logger.debug("Failed to send POST, Http status code: {}", response.getStatus());
431 } catch (ExecutionException | InterruptedException | TimeoutException e) {
432 logger.warn("Failed to send a POST to the API {}", e.getMessage());
435 return HttpStatus.SERVICE_UNAVAILABLE_503;
438 private int setSessionCookieAuthLogin() throws ExecutionException, InterruptedException, TimeoutException {
439 // URL to set status which will give us 2 cookies with username and password used for the session
441 ContentResponse response = httpClient.GET(url);
442 logTraceWithPattern(response.getStatus(), response.getContentAsString());
445 int httpStatusCode = postVerisureAPI(url, "empty");
448 // return response.getStatus();
449 return httpStatusCode;
452 private boolean getInstallations() {
453 int httpResultCode = 0;
456 httpResultCode = setSessionCookieAuthLogin();
457 } catch (ExecutionException | InterruptedException | TimeoutException e) {
458 logger.warn("Failed to set session cookie {}", e.getMessage());
462 if (httpResultCode == HttpStatus.OK_200) {
463 String url = START_GRAPHQL;
465 String queryQLAccountInstallations = "[{\"operationName\":\"AccountInstallations\",\"variables\":{\"email\":\""
467 + "\"},\"query\":\"query AccountInstallations($email: String!) {\\n account(email: $email) {\\n owainstallations {\\n giid\\n alias\\n type\\n subsidiary\\n dealerId\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]";
469 VerisureInstallationsDTO installations = postJSONVerisureAPI(url, queryQLAccountInstallations,
470 VerisureInstallationsDTO.class);
471 logger.debug("Installation: {}", installations.toString());
472 List<Owainstallation> owaInstList = installations.getData().getAccount().getOwainstallations();
473 boolean pinCodesMatchInstallations = true;
474 List<String> pinCodes = null;
475 String pinCode = this.pinCode;
476 if (pinCode != null) {
477 pinCodes = Arrays.asList(pinCode.split(","));
478 if (owaInstList.size() != pinCodes.size()) {
479 logger.debug("Number of installations {} does not match number of pin codes configured {}",
480 owaInstList.size(), pinCodes.size());
481 pinCodesMatchInstallations = false;
484 logger.debug("No pin-code defined for user {}", userName);
487 for (int i = 0; i < owaInstList.size(); i++) {
488 VerisureInstallation vInst = new VerisureInstallation();
489 Owainstallation owaInstallation = owaInstList.get(i);
490 String installationId = owaInstallation.getGiid();
491 if (owaInstallation.getAlias() != null && installationId != null) {
492 vInst.setInstallationId(new BigDecimal(installationId));
493 vInst.setInstallationName(owaInstallation.getAlias());
494 if (pinCode != null && pinCodes != null) {
495 int pinCodeIndex = pinCodesMatchInstallations ? i : 0;
496 vInst.setPinCode(pinCodes.get(pinCodeIndex));
497 logger.debug("Setting configured pincode index[{}] to installation ID {}", pinCodeIndex,
500 verisureInstallations.put(new BigDecimal(installationId), vInst);
502 logger.warn("Failed to get alias and/or giid");
506 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
507 | PostToAPIException e) {
508 logger.warn("Failed to send a POST to the API {}", e.getMessage());
511 logger.warn("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode);
517 private synchronized boolean logIn() {
519 if (!areWeLoggedIn()) {
522 logger.debug("Attempting to log in to {}, remove all cookies to ensure a fresh session", BASE_URL);
523 URI authUri = new URI(BASE_URL);
524 CookieStore store = httpClient.getCookieStore();
525 store.get(authUri).forEach(cookie -> {
526 store.remove(authUri, cookie);
529 String url = AUTH_LOGIN;
530 int httpStatusCode = postVerisureAPI(url, "empty");
532 if (!vsStepup.isEmpty()) {
533 logger.warn("MFA is activated on this user! Not supported by binding!");
538 logger.debug("Login URL: {}", url);
539 httpStatusCode = postVerisureAPI(url, authstring);
540 if (httpStatusCode != HttpStatus.OK_200) {
541 logger.debug("Failed to login, HTTP status code: {}", httpStatusCode);
548 } catch (ExecutionException | InterruptedException | TimeoutException | URISyntaxException
549 | HttpResponseException e) {
550 logger.warn("Failed to login {}", e.getMessage());
555 private <T extends VerisureThingDTO> void notifyListeners(T thing) {
556 deviceStatusListeners.forEach(listener -> {
557 if (listener.getVerisureThingClass().equals(thing.getClass())) {
558 listener.onDeviceStateChanged(thing);
563 private void notifyListenersIfChanged(VerisureThingDTO thing, VerisureInstallation installation, String deviceId) {
564 String normalizedDeviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId);
565 thing.setDeviceId(normalizedDeviceId);
566 thing.setSiteId(installation.getInstallationId());
567 thing.setSiteName(installation.getInstallationName());
568 VerisureThingDTO oldObj = verisureThings.get(normalizedDeviceId);
569 if (!thing.equals(oldObj)) {
570 verisureThings.put(thing.getDeviceId(), thing);
571 notifyListeners(thing);
573 logger.trace("No need to notify listeners for thing: {}", thing);
577 private boolean updateStatus() {
578 logger.debug("Update status");
579 for (Map.Entry<BigDecimal, VerisureInstallation> verisureInstallations : verisureInstallations.entrySet()) {
580 VerisureInstallation installation = verisureInstallations.getValue();
582 configureInstallationInstance(installation.getInstallationId());
583 int httpResultCode = setSessionCookieAuthLogin();
584 if (httpResultCode == HttpStatus.OK_200) {
585 updateAlarmStatus(installation);
586 updateSmartLockStatus(installation);
587 updateMiceDetectionStatus(installation);
588 updateClimateStatus(installation);
589 updateDoorWindowStatus(installation);
590 updateUserPresenceStatus(installation);
591 updateSmartPlugStatus(installation);
592 updateBroadbandConnectionStatus(installation);
593 updateEventLogStatus(installation);
594 updateGatewayStatus(installation);
596 logger.debug("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode);
599 } catch (ExecutionException | InterruptedException | TimeoutException | PostToAPIException e) {
600 logger.debug("Failed to update status {}", e.getMessage());
607 private String createOperationJSON(String operation, VariablesDTO variables, String query) {
608 OperationDTO operationJSON = new OperationDTO();
609 operationJSON.setOperationName(operation);
610 operationJSON.setVariables(variables);
611 operationJSON.setQuery(query);
612 return gson.toJson(Collections.singletonList(operationJSON));
615 private synchronized void updateAlarmStatus(VerisureInstallation installation) throws PostToAPIException {
616 BigDecimal installationId = installation.getInstallationId();
617 String url = START_GRAPHQL;
618 String operation = "ArmState";
619 VariablesDTO variables = new VariablesDTO();
620 variables.setGiid(installationId.toString());
621 String query = "query " + operation
622 + "($giid: String!) {\n installation(giid: $giid) {\n armState {\n type\n statusType\n date\n name\n changedVia\n allowedForFirstLine\n allowed\n errorCodes {\n value\n message\n __typename\n}\n __typename\n}\n __typename\n}\n}\n";
624 String queryQLAlarmStatus = createOperationJSON(operation, variables, query);
625 logger.debug("Quering API for alarm status!");
627 VerisureThingDTO thing = postJSONVerisureAPI(url, queryQLAlarmStatus, VerisureAlarmsDTO.class);
628 logger.debug("REST Response ({})", thing);
629 // Set unique deviceID
630 String deviceId = "alarm" + installationId;
631 thing.setDeviceId(deviceId);
632 notifyListenersIfChanged(thing, installation, deviceId);
633 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
634 logger.warn("Failed to send a POST to the API {}", e.getMessage());
638 private synchronized void updateSmartLockStatus(VerisureInstallation installation) {
639 BigDecimal installationId = installation.getInstallationId();
640 String url = START_GRAPHQL;
641 String operation = "DoorLock";
642 String query = "query " + operation
643 + "($giid: String!) {\n installation(giid: $giid) {\n doorlocks {\n device {\n deviceLabel\n area\n __typename\n}\n currentLockState\n eventTime\n secureModeActive\n motorJam\n userString\n method\n __typename\n}\n __typename\n}\n}\n";
644 VariablesDTO variables = new VariablesDTO();
645 variables.setGiid(installationId.toString());
646 String queryQLSmartLock = createOperationJSON(operation, variables, query);
647 logger.debug("Quering API for smart lock status");
650 VerisureSmartLocksDTO thing = postJSONVerisureAPI(url, queryQLSmartLock, VerisureSmartLocksDTO.class);
651 logger.debug("REST Response ({})", thing);
652 List<VerisureSmartLocksDTO.Doorlock> doorLockList = thing.getData().getInstallation().getDoorlocks();
653 if (doorLockList != null) {
654 doorLockList.forEach(doorLock -> {
655 VerisureSmartLocksDTO slThing = new VerisureSmartLocksDTO();
656 VerisureSmartLocksDTO.Installation inst = new VerisureSmartLocksDTO.Installation();
657 inst.setDoorlocks(Collections.singletonList(doorLock));
658 VerisureSmartLocksDTO.Data data = new VerisureSmartLocksDTO.Data();
659 data.setInstallation(inst);
660 slThing.setData(data);
661 // Set unique deviceID
662 String deviceId = doorLock.getDevice().getDeviceLabel();
663 if (deviceId != null) {
665 slThing.setLocation(doorLock.getDevice().getArea());
666 slThing.setDeviceId(deviceId);
668 // Fetch more info from old endpoint
670 VerisureSmartLockDTO smartLockThing = getJSONVerisureAPI(
671 SMARTLOCK_PATH + slThing.getDeviceId(), VerisureSmartLockDTO.class);
672 logger.debug("REST Response ({})", smartLockThing);
673 slThing.setSmartLockJSON(smartLockThing);
674 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
675 logger.warn("Failed to query for smartlock status: {}", e.getMessage());
677 notifyListenersIfChanged(slThing, installation, deviceId);
681 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
682 | PostToAPIException e) {
683 logger.warn("Failed to send a POST to the API {}", e.getMessage());
687 private synchronized void updateSmartPlugStatus(VerisureInstallation installation) {
688 BigDecimal installationId = installation.getInstallationId();
689 String url = START_GRAPHQL;
690 String operation = "SmartPlug";
691 VariablesDTO variables = new VariablesDTO();
692 variables.setGiid(installationId.toString());
693 String query = "query " + operation
694 + "($giid: String!) {\n installation(giid: $giid) {\n smartplugs {\n device {\n deviceLabel\n area\n gui {\n support\n label\n __typename\n}\n __typename\n}\n currentState\n icon\n isHazardous\n __typename\n}\n __typename\n}\n}\n";
695 String queryQLSmartPlug = createOperationJSON(operation, variables, query);
696 logger.debug("Quering API for smart plug status");
699 VerisureSmartPlugsDTO thing = postJSONVerisureAPI(url, queryQLSmartPlug, VerisureSmartPlugsDTO.class);
700 logger.debug("REST Response ({})", thing);
701 List<VerisureSmartPlugsDTO.Smartplug> smartPlugList = thing.getData().getInstallation().getSmartplugs();
702 if (smartPlugList != null) {
703 smartPlugList.forEach(smartPlug -> {
704 VerisureSmartPlugsDTO spThing = new VerisureSmartPlugsDTO();
705 VerisureSmartPlugsDTO.Installation inst = new VerisureSmartPlugsDTO.Installation();
706 inst.setSmartplugs(Collections.singletonList(smartPlug));
707 VerisureSmartPlugsDTO.Data data = new VerisureSmartPlugsDTO.Data();
708 data.setInstallation(inst);
709 spThing.setData(data);
710 // Set unique deviceID
711 String deviceId = smartPlug.getDevice().getDeviceLabel();
712 if (deviceId != null) {
714 spThing.setLocation(smartPlug.getDevice().getArea());
715 notifyListenersIfChanged(spThing, installation, deviceId);
719 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
720 | PostToAPIException e) {
721 logger.warn("Failed to send a POST to the API {}", e.getMessage());
725 private @Nullable VerisureBatteryStatusDTO getBatteryStatus(String deviceId,
726 VerisureBatteryStatusDTO @Nullable [] batteryStatus) {
727 if (batteryStatus != null) {
728 for (VerisureBatteryStatusDTO verisureBatteryStatusDTO : batteryStatus) {
729 String id = verisureBatteryStatusDTO.getId();
730 if (id != null && id.equals(deviceId)) {
731 return verisureBatteryStatusDTO;
738 private synchronized void updateClimateStatus(VerisureInstallation installation) {
739 BigDecimal installationId = installation.getInstallationId();
740 String url = START_GRAPHQL;
741 VariablesDTO variables = new VariablesDTO();
742 variables.setGiid(installationId.toString());
743 String operation = "Climate";
744 String query = "query " + operation
745 + "($giid: String!) {\n installation(giid: $giid) {\n climates {\n device {\n deviceLabel\n area\n gui {\n label\n __typename\n }\n __typename\n }\n humidityEnabled\n humidityTimestamp\n humidityValue\n temperatureTimestamp\n temperatureValue\n __typename\n }\n __typename\n}\n}\n";
747 String queryQLClimates = createOperationJSON(operation, variables, query);
748 logger.debug("Quering API for climate status");
751 VerisureClimatesDTO thing = postJSONVerisureAPI(url, queryQLClimates, VerisureClimatesDTO.class);
752 logger.debug("REST Response ({})", thing);
753 List<VerisureClimatesDTO.Climate> climateList = thing.getData().getInstallation().getClimates();
754 if (climateList != null) {
755 climateList.forEach(climate -> {
756 // If thing is Mouse detection device, then skip it, but fetch temperature from it
757 String type = climate.getDevice().getGui().getLabel();
758 if ("MOUSE".equals(type)) {
759 logger.debug("Mouse detection device!");
760 String deviceId = climate.getDevice().getDeviceLabel();
761 if (deviceId != null) {
762 deviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId);
763 VerisureThingDTO mouseThing = verisureThings.get(deviceId);
764 if (mouseThing instanceof VerisureMiceDetectionDTO) {
765 VerisureMiceDetectionDTO miceDetectorThing = (VerisureMiceDetectionDTO) mouseThing;
766 miceDetectorThing.setTemperatureValue(climate.getTemperatureValue());
767 miceDetectorThing.setTemperatureTime(climate.getTemperatureTimestamp());
768 notifyListeners(miceDetectorThing);
769 logger.debug("Found climate thing for a Verisure Mouse Detector");
774 VerisureClimatesDTO cThing = new VerisureClimatesDTO();
775 VerisureClimatesDTO.Installation inst = new VerisureClimatesDTO.Installation();
776 inst.setClimates(Collections.singletonList(climate));
777 VerisureClimatesDTO.Data data = new VerisureClimatesDTO.Data();
778 data.setInstallation(inst);
779 cThing.setData(data);
780 // Set unique deviceID
781 String deviceId = climate.getDevice().getDeviceLabel();
782 if (deviceId != null) {
784 VerisureBatteryStatusDTO[] batteryStatusThingArray = getJSONVerisureAPI(BATTERY_STATUS,
785 VerisureBatteryStatusDTO[].class);
786 VerisureBatteryStatusDTO batteryStatus = getBatteryStatus(deviceId,
787 batteryStatusThingArray);
788 if (batteryStatus != null) {
789 logger.debug("REST Response ({})", batteryStatus);
790 cThing.setBatteryStatus(batteryStatus);
792 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
793 logger.debug("Failed to query for battery status: {}", e.getMessage());
796 cThing.setLocation(climate.getDevice().getArea());
797 notifyListenersIfChanged(cThing, installation, deviceId);
801 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
802 | PostToAPIException e) {
803 logger.warn("Failed to send a POST to the API {}", e.getMessage());
807 private synchronized void updateDoorWindowStatus(VerisureInstallation installation) {
808 BigDecimal installationId = installation.getInstallationId();
809 String url = START_GRAPHQL;
810 String operation = "DoorWindow";
811 VariablesDTO variables = new VariablesDTO();
812 variables.setGiid(installationId.toString());
813 String query = "query " + operation
814 + "($giid: String!) {\n installation(giid: $giid) {\n doorWindows {\n device {\n deviceLabel\n area\n __typename\n }\n type\n state\n wired\n reportTime\n __typename\n }\n __typename\n}\n}\n";
816 String queryQLDoorWindow = createOperationJSON(operation, variables, query);
817 logger.debug("Quering API for door&window status");
820 VerisureDoorWindowsDTO thing = postJSONVerisureAPI(url, queryQLDoorWindow, VerisureDoorWindowsDTO.class);
821 logger.debug("REST Response ({})", thing);
822 List<VerisureDoorWindowsDTO.DoorWindow> doorWindowList = thing.getData().getInstallation().getDoorWindows();
823 if (doorWindowList != null) {
824 doorWindowList.forEach(doorWindow -> {
825 VerisureDoorWindowsDTO dThing = new VerisureDoorWindowsDTO();
826 VerisureDoorWindowsDTO.Installation inst = new VerisureDoorWindowsDTO.Installation();
827 inst.setDoorWindows(Collections.singletonList(doorWindow));
828 VerisureDoorWindowsDTO.Data data = new VerisureDoorWindowsDTO.Data();
829 data.setInstallation(inst);
830 dThing.setData(data);
831 // Set unique deviceID
832 String deviceId = doorWindow.getDevice().getDeviceLabel();
833 if (deviceId != null) {
835 VerisureBatteryStatusDTO[] batteryStatusThingArray = getJSONVerisureAPI(BATTERY_STATUS,
836 VerisureBatteryStatusDTO[].class);
837 VerisureBatteryStatusDTO batteryStatus = getBatteryStatus(deviceId,
838 batteryStatusThingArray);
839 if (batteryStatus != null) {
840 logger.debug("REST Response ({})", batteryStatus);
841 dThing.setBatteryStatus(batteryStatus);
843 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
844 logger.warn("Failed to query for door&window status: {}", e.getMessage());
847 dThing.setLocation(doorWindow.getDevice().getArea());
848 notifyListenersIfChanged(dThing, installation, deviceId);
852 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
853 | PostToAPIException e) {
854 logger.warn("Failed to send a POST to the API {}", e.getMessage());
858 private synchronized void updateBroadbandConnectionStatus(VerisureInstallation inst) {
859 BigDecimal installationId = inst.getInstallationId();
860 String url = START_GRAPHQL;
861 String operation = "Broadband";
862 VariablesDTO variables = new VariablesDTO();
863 variables.setGiid(installationId.toString());
864 String query = "query " + operation
865 + "($giid: String!) {\n installation(giid: $giid) {\n broadband {\n testDate\n isBroadbandConnected\n __typename\n }\n __typename\n}\n}\n";
867 String queryQLBroadbandConnection = createOperationJSON(operation, variables, query);
868 logger.debug("Quering API for broadband connection status");
871 VerisureThingDTO thing = postJSONVerisureAPI(url, queryQLBroadbandConnection,
872 VerisureBroadbandConnectionsDTO.class);
873 logger.debug("REST Response ({})", thing);
874 // Set unique deviceID
875 String deviceId = "bc" + installationId;
876 notifyListenersIfChanged(thing, inst, deviceId);
877 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
878 | PostToAPIException e) {
879 logger.warn("Failed to send a POST to the API {}", e.getMessage());
883 private synchronized void updateUserPresenceStatus(VerisureInstallation installation) {
884 BigDecimal installationId = installation.getInstallationId();
885 String url = START_GRAPHQL;
886 String operation = "userTrackings";
887 VariablesDTO variables = new VariablesDTO();
888 variables.setGiid(installationId.toString());
889 String query = "query " + operation
890 + "($giid: String!) {\ninstallation(giid: $giid) {\n userTrackings {\n isCallingUser\n webAccount\n status\n xbnContactId\n currentLocationName\n deviceId\n name\n currentLocationTimestamp\n deviceName\n currentLocationId\n __typename\n}\n __typename\n}\n}\n";
892 String queryQLUserPresence = createOperationJSON(operation, variables, query);
893 logger.debug("Quering API for user presence status");
896 VerisureUserPresencesDTO thing = postJSONVerisureAPI(url, queryQLUserPresence,
897 VerisureUserPresencesDTO.class);
898 logger.debug("REST Response ({})", thing);
899 List<VerisureUserPresencesDTO.UserTracking> userTrackingList = thing.getData().getInstallation()
901 if (userTrackingList != null) {
902 userTrackingList.forEach(userTracking -> {
903 String localUserTrackingStatus = userTracking.getStatus();
904 if ("ACTIVE".equals(localUserTrackingStatus)) {
905 VerisureUserPresencesDTO upThing = new VerisureUserPresencesDTO();
906 VerisureUserPresencesDTO.Installation inst = new VerisureUserPresencesDTO.Installation();
907 inst.setUserTrackings(Collections.singletonList(userTracking));
908 VerisureUserPresencesDTO.Data data = new VerisureUserPresencesDTO.Data();
909 data.setInstallation(inst);
910 upThing.setData(data);
911 // Set unique deviceID
912 String deviceId = "up" + userTracking.getWebAccount() + installationId;
913 notifyListenersIfChanged(upThing, installation, deviceId);
917 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
918 | PostToAPIException e) {
919 logger.warn("Failed to send a POST to the API {}", e.getMessage());
923 private synchronized void updateMiceDetectionStatus(VerisureInstallation installation) {
924 BigDecimal installationId = installation.getInstallationId();
925 String url = START_GRAPHQL;
926 String operation = "Mouse";
927 VariablesDTO variables = new VariablesDTO();
928 variables.setGiid(installationId.toString());
929 String query = "query " + operation
930 + "($giid: String!) {\n installation(giid: $giid) {\n mice {\n device {\n deviceLabel\n area\n gui {\n support\n __typename\n}\n __typename\n}\n type\n detections {\n count\n gatewayTime\n nodeTime\n duration\n __typename\n}\n __typename\n}\n __typename\n}\n}\n";
932 String queryQLMiceDetection = createOperationJSON(operation, variables, query);
933 logger.debug("Quering API for mice detection status");
936 VerisureMiceDetectionDTO thing = postJSONVerisureAPI(url, queryQLMiceDetection,
937 VerisureMiceDetectionDTO.class);
938 logger.debug("REST Response ({})", thing);
939 List<VerisureMiceDetectionDTO.Mouse> miceList = thing.getData().getInstallation().getMice();
940 if (miceList != null) {
941 miceList.forEach(mouse -> {
942 VerisureMiceDetectionDTO miceThing = new VerisureMiceDetectionDTO();
943 VerisureMiceDetectionDTO.Installation inst = new VerisureMiceDetectionDTO.Installation();
944 inst.setMice(Collections.singletonList(mouse));
945 VerisureMiceDetectionDTO.Data data = new VerisureMiceDetectionDTO.Data();
946 data.setInstallation(inst);
947 miceThing.setData(data);
948 // Set unique deviceID
949 String deviceId = mouse.getDevice().getDeviceLabel();
950 logger.debug("Mouse id: {} for thing: {}", deviceId, mouse);
951 if (deviceId != null) {
953 miceThing.setLocation(mouse.getDevice().getArea());
954 notifyListenersIfChanged(miceThing, installation, deviceId);
958 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
959 | PostToAPIException e) {
960 logger.warn("Failed to send a POST to the API {}", e.getMessage());
964 private synchronized void updateEventLogStatus(VerisureInstallation installation) {
965 BigDecimal installationId = installation.getInstallationId();
966 String url = START_GRAPHQL;
967 String operation = "EventLog";
969 int numberOfEvents = this.numberOfEvents;
970 List<String> eventCategories = new ArrayList<>(Arrays.asList("INTRUSION", "FIRE", "SOS", "WATER", "ANIMAL",
971 "TECHNICAL", "WARNING", "ARM", "DISARM", "LOCK", "UNLOCK", "PICTURE", "CLIMATE", "CAMERA_SETTINGS",
972 "DOORWINDOW_STATE_OPENED", "DOORWINDOW_STATE_CLOSED", "USERTRACKING"));
973 VariablesDTO variables = new VariablesDTO();
974 variables.setGiid(installationId.toString());
975 variables.setHideNotifications(true);
976 variables.setOffset(offset);
977 variables.setPagesize(numberOfEvents);
978 variables.setEventCategories(eventCategories);
979 String query = "query " + operation
980 + "($giid: String!, $offset: Int!, $pagesize: Int!, $eventCategories: [String], $fromDate: String, $toDate: String, $eventContactIds: [String]) {\n installation(giid: $giid) {\n eventLog(offset: $offset, pagesize: $pagesize, eventCategories: $eventCategories, eventContactIds: $eventContactIds, fromDate: $fromDate, toDate: $toDate) {\n moreDataAvailable\n pagedList {\n device {\n deviceLabel\n area\n gui {\n label\n __typename\n }\n __typename\n }\n gatewayArea\n eventType\n eventCategory\n eventSource\n eventId\n eventTime\n userName\n armState\n userType\n climateValue\n sensorType\n eventCount\n __typename\n }\n __typename\n }\n __typename\n }\n}\n";
982 String queryQLEventLog = createOperationJSON(operation, variables, query);
983 logger.debug("Quering API for event log status");
986 VerisureEventLogDTO thing = postJSONVerisureAPI(url, queryQLEventLog, VerisureEventLogDTO.class);
987 logger.debug("REST Response ({})", thing);
988 // Set unique deviceID
989 String deviceId = "el" + installationId;
990 notifyListenersIfChanged(thing, installation, deviceId);
991 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
992 | PostToAPIException e) {
993 logger.warn("Failed to send a POST to the API {}", e.getMessage());
997 private synchronized void updateGatewayStatus(VerisureInstallation installation) {
998 BigDecimal installationId = installation.getInstallationId();
999 String url = START_GRAPHQL;
1000 String operation = "communicationState";
1001 VariablesDTO variables = new VariablesDTO();
1002 variables.setGiid(installationId.toString());
1004 String query = "query " + operation
1005 + "($giid: String!) {\n installation(giid: $giid) {\n communicationState {\n hardwareCarrierType\n result\n mediaType\n device {\n deviceLabel\n area\n gui {\n label\n __typename\n }\n __typename\n }\n testDate\n __typename\n }\n __typename\n }\n}";
1007 String queryQLEventLog = createOperationJSON(operation, variables, query);
1008 logger.debug("Quering API for gateway status");
1011 VerisureGatewayDTO thing = postJSONVerisureAPI(url, queryQLEventLog, VerisureGatewayDTO.class);
1012 logger.debug("REST Response ({})", thing);
1013 // Set unique deviceID
1014 List<CommunicationState> communicationStateList = thing.getData().getInstallation().getCommunicationState();
1015 if (communicationStateList != null) {
1016 if (!communicationStateList.isEmpty()) {
1017 String deviceId = communicationStateList.get(0).getDevice().getDeviceLabel();
1018 if (deviceId != null) {
1019 notifyListenersIfChanged(thing, installation, deviceId);
1023 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
1024 | PostToAPIException e) {
1025 logger.warn("Failed to send a POST to the API {}", e.getMessage());
1029 private final class VerisureInstallation {
1030 private @Nullable String installationName;
1031 private BigDecimal installationId = BigDecimal.ZERO;
1032 private @Nullable String pinCode;
1034 public @Nullable String getPinCode() {
1038 public void setPinCode(@Nullable String pinCode) {
1039 this.pinCode = pinCode;
1042 public VerisureInstallation() {
1045 public BigDecimal getInstallationId() {
1046 return installationId;
1049 public @Nullable String getInstallationName() {
1050 return installationName;
1053 public void setInstallationId(BigDecimal installationId) {
1054 this.installationId = installationId;
1057 public void setInstallationName(@Nullable String installationName) {
1058 this.installationName = installationName;
1062 private static class OperationDTO {
1064 @SuppressWarnings("unused")
1065 private @Nullable String operationName;
1066 @SuppressWarnings("unused")
1067 private VariablesDTO variables = new VariablesDTO();
1068 @SuppressWarnings("unused")
1069 private @Nullable String query;
1071 public void setOperationName(String operationName) {
1072 this.operationName = operationName;
1075 public void setVariables(VariablesDTO variables) {
1076 this.variables = variables;
1079 public void setQuery(String query) {
1084 public static class VariablesDTO {
1086 @SuppressWarnings("unused")
1087 private boolean hideNotifications;
1088 @SuppressWarnings("unused")
1090 @SuppressWarnings("unused")
1091 private int pagesize;
1092 @SuppressWarnings("unused")
1093 private @Nullable List<String> eventCategories = null;
1094 @SuppressWarnings("unused")
1095 private @Nullable String giid;
1097 public void setHideNotifications(boolean hideNotifications) {
1098 this.hideNotifications = hideNotifications;
1101 public void setOffset(int offset) {
1102 this.offset = offset;
1105 public void setPagesize(int pagesize) {
1106 this.pagesize = pagesize;
1109 public void setEventCategories(List<String> eventCategories) {
1110 this.eventCategories = eventCategories;
1113 public void setGiid(String giid) {
1118 private class PostToAPIException extends Exception {
1120 private static final long serialVersionUID = 1L;
1122 public PostToAPIException(String message) {