2 * Copyright (c) 2010-2024 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.TimeUnit;
34 import java.util.concurrent.TimeoutException;
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.eclipse.jetty.client.HttpClient;
39 import org.eclipse.jetty.client.HttpResponseException;
40 import org.eclipse.jetty.client.api.ContentResponse;
41 import org.eclipse.jetty.client.api.Request;
42 import org.eclipse.jetty.client.util.BytesContentProvider;
43 import org.eclipse.jetty.http.HttpMethod;
44 import org.eclipse.jetty.http.HttpStatus;
45 import org.jsoup.Jsoup;
46 import org.jsoup.nodes.Document;
47 import org.jsoup.nodes.Element;
48 import org.openhab.binding.verisure.internal.dto.VerisureAlarmsDTO;
49 import org.openhab.binding.verisure.internal.dto.VerisureBatteryStatusDTO;
50 import org.openhab.binding.verisure.internal.dto.VerisureBroadbandConnectionsDTO;
51 import org.openhab.binding.verisure.internal.dto.VerisureClimatesDTO;
52 import org.openhab.binding.verisure.internal.dto.VerisureDoorWindowsDTO;
53 import org.openhab.binding.verisure.internal.dto.VerisureEventLogDTO;
54 import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO;
55 import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO.CommunicationState;
56 import org.openhab.binding.verisure.internal.dto.VerisureInstallationsDTO;
57 import org.openhab.binding.verisure.internal.dto.VerisureInstallationsDTO.Owainstallation;
58 import org.openhab.binding.verisure.internal.dto.VerisureMiceDetectionDTO;
59 import org.openhab.binding.verisure.internal.dto.VerisureSmartLockDTO;
60 import org.openhab.binding.verisure.internal.dto.VerisureSmartLocksDTO;
61 import org.openhab.binding.verisure.internal.dto.VerisureSmartPlugsDTO;
62 import org.openhab.binding.verisure.internal.dto.VerisureThingDTO;
63 import org.openhab.binding.verisure.internal.dto.VerisureUserPresencesDTO;
64 import org.openhab.binding.verisure.internal.handler.VerisureThingHandler;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
68 import com.google.gson.Gson;
69 import com.google.gson.JsonSyntaxException;
72 * This class performs the communication with Verisure My Pages.
74 * @author Jarle Hjortland - Initial contribution
75 * @author Jan Gustafsson - Re-design and support for several sites and update to new Verisure API
79 public class VerisureSession {
82 private final Map<String, VerisureThingDTO> verisureThings = new ConcurrentHashMap<>();
83 private final Map<String, VerisureThingHandler<?>> verisureHandlers = new ConcurrentHashMap<>();
84 private final Logger logger = LoggerFactory.getLogger(VerisureSession.class);
85 private final Gson gson = new Gson();
86 private final Set<DeviceStatusListener<VerisureThingDTO>> deviceStatusListeners = ConcurrentHashMap.newKeySet();
87 private final Map<BigDecimal, VerisureInstallation> verisureInstallations = new ConcurrentHashMap<>();
88 private static final List<String> APISERVERLIST = Arrays.asList("https://m-api01.verisure.com",
89 "https://m-api02.verisure.com");
90 private int apiServerInUseIndex = 0;
91 private int numberOfEvents = 15;
92 private static final int REQUEST_TIMEOUT_MS = 10_000;
93 private static final String USER_NAME = "username";
94 private static final String VID = "vid";
95 private static final String VS_STEPUP = "vs-stepup";
96 private static final String VS_ACCESS = "vs-access";
97 private String apiServerInUse = APISERVERLIST.get(apiServerInUseIndex);
98 private String authstring = "";
99 private @Nullable String csrf;
100 private @Nullable String pinCode;
101 private HttpClient httpClient;
102 private String userName = "";
103 private String password = "";
104 private String vid = "";
105 private String vsAccess = "";
106 private String vsStepup = "";
108 public VerisureSession(HttpClient httpClient) {
109 this.httpClient = httpClient;
112 public boolean initialize(@Nullable String authstring, @Nullable String pinCode, String userName, String password) {
113 if (authstring != null) {
114 this.authstring = authstring;
115 this.pinCode = pinCode;
116 this.userName = userName;
117 this.password = password;
118 // Try to login to Verisure
120 return getInstallations();
128 public boolean refresh() {
130 if (updateStatus()) {
137 public int sendCommand(String url, String data, BigDecimal installationId) {
138 logger.debug("Sending command with URL {} and data {}", url, data);
140 configureInstallationInstance(installationId);
141 int httpResultCode = setSessionCookieAuthLogin();
142 if (httpResultCode == HttpStatus.OK_200) {
143 return postVerisureAPI(url, data);
145 return httpResultCode;
147 } catch (ExecutionException | InterruptedException | TimeoutException e) {
148 logger.debug("Failed to send command {}", e.getMessage());
150 return HttpStatus.BAD_REQUEST_400;
153 public boolean unregisterDeviceStatusListener(
154 DeviceStatusListener<? extends VerisureThingDTO> deviceStatusListener) {
155 return deviceStatusListeners.remove(deviceStatusListener);
158 @SuppressWarnings("unchecked")
159 public boolean registerDeviceStatusListener(DeviceStatusListener<? extends VerisureThingDTO> deviceStatusListener) {
160 return deviceStatusListeners.add((DeviceStatusListener<VerisureThingDTO>) deviceStatusListener);
163 @SuppressWarnings({ "unchecked" })
164 public <T extends VerisureThingDTO> @Nullable T getVerisureThing(String deviceId, Class<T> thingType) {
165 VerisureThingDTO thing = verisureThings.get(deviceId);
166 if (thingType.isInstance(thing)) {
172 public <T extends VerisureThingDTO> @Nullable T getVerisureThing(String deviceId) {
173 VerisureThingDTO thing = verisureThings.get(deviceId);
175 @SuppressWarnings("unchecked")
176 T thing2 = (T) thing;
182 public @Nullable VerisureThingHandler<?> getVerisureThinghandler(String deviceId) {
183 VerisureThingHandler<?> thingHandler = verisureHandlers.get(deviceId);
187 public void setVerisureThingHandler(VerisureThingHandler<?> vth, String deviceId) {
188 verisureHandlers.put(deviceId, vth);
191 public void removeVerisureThingHandler(String deviceId) {
192 verisureHandlers.remove(deviceId);
195 public Collection<VerisureThingDTO> getVerisureThings() {
196 return verisureThings.values();
199 public @Nullable String getCsrf() {
203 public @Nullable String getPinCode() {
207 public String getApiServerInUse() {
208 return apiServerInUse;
211 public void setApiServerInUse(String apiServerInUse) {
212 this.apiServerInUse = apiServerInUse;
215 public String getNextApiServer() {
216 apiServerInUseIndex++;
217 if (apiServerInUseIndex > (APISERVERLIST.size() - 1)) {
218 apiServerInUseIndex = 0;
220 return APISERVERLIST.get(apiServerInUseIndex);
223 public void setNumberOfEvents(int numberOfEvents) {
224 this.numberOfEvents = numberOfEvents;
227 public void configureInstallationInstance(BigDecimal installationId)
228 throws ExecutionException, InterruptedException, TimeoutException {
229 csrf = getCsrfToken(installationId);
230 logger.debug("Got CSRF: {}", csrf);
232 String url = SET_INSTALLATION + installationId;
236 public @Nullable String getCsrfToken(BigDecimal installationId)
237 throws ExecutionException, InterruptedException, TimeoutException {
239 String url = SETTINGS + installationId;
241 ContentResponse resp = httpClient.GET(url);
242 html = resp.getContentAsString();
243 logger.trace("url: {} html: {}", url, html);
245 Document htmlDocument = Jsoup.parse(html);
246 Element nameInput = htmlDocument.select("input[name=_csrf]").first();
247 if (nameInput != null) {
248 return nameInput.attr("value");
254 public @Nullable String getPinCode(BigDecimal installationId) {
255 VerisureInstallation inst = verisureInstallations.get(installationId);
257 return inst.getPinCode();
259 logger.debug("Installation is null!");
264 private void analyzeCookies() {
265 CookieStore c = httpClient.getCookieStore();
266 List<HttpCookie> cookies = c.getCookies();
267 final List<HttpCookie> unmodifiableList = List.of(cookies.toArray(new HttpCookie[] {}));
268 unmodifiableList.forEach(cookie -> {
269 logger.trace("Response Cookie: {}", cookie);
270 if (VID.equals(cookie.getName())) {
271 vid = cookie.getValue();
272 logger.debug("Fetching vid {} from cookie", vid);
273 } else if (VS_ACCESS.equals(cookie.getName())) {
274 vsAccess = cookie.getValue();
275 logger.debug("Fetching vs-access {} from cookie", vsAccess);
276 } else if (VS_STEPUP.equals(cookie.getName())) {
277 vsStepup = cookie.getValue();
278 logger.debug("Fetching vs-stepup {} from cookie", vsStepup);
283 private void logTraceWithPattern(int responseStatus, String content) {
284 if (logger.isTraceEnabled()) {
285 String pattern = "(?m)^\\s*\\r?\\n|\\r?\\n\\s*(?!.*\\r?\\n)";
286 String replacement = "";
287 logger.trace("HTTP Response ({}) Body:{}", responseStatus, content.replaceAll(pattern, replacement));
291 private boolean areWeLoggedIn() throws ExecutionException, InterruptedException, TimeoutException {
292 logger.debug("Checking if we are logged in");
295 ContentResponse response = httpClient.newRequest(url).method(HttpMethod.GET).send();
296 String content = response.getContentAsString();
297 logTraceWithPattern(response.getStatus(), content);
299 switch (response.getStatus()) {
300 case HttpStatus.OK_200:
301 if (content.contains("<link href=\"/newapp")) {
304 logger.debug("We need to login again!");
307 case HttpStatus.MOVED_TEMPORARILY_302:
309 logger.debug("Status code 302. Redirected. Probably not logged in");
311 case HttpStatus.INTERNAL_SERVER_ERROR_500:
312 case HttpStatus.SERVICE_UNAVAILABLE_503:
313 throw new HttpResponseException(
314 "Status code " + response.getStatus() + ". Verisure service temporarily down", response);
316 logger.debug("Status code {} body {}", response.getStatus(), content);
322 private <T> @Nullable T getJSONVerisureAPI(String url, Class<T> jsonClass)
323 throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException {
324 logger.debug("HTTP GET: {}", BASE_URL + url);
326 ContentResponse response = httpClient.GET(BASE_URL + url + "?_=" + System.currentTimeMillis());
327 String content = response.getContentAsString();
328 logTraceWithPattern(response.getStatus(), content);
330 return gson.fromJson(content, jsonClass);
333 private ContentResponse postVerisureAPI(String url, String data, boolean isJSON)
334 throws ExecutionException, InterruptedException, TimeoutException {
335 logger.debug("postVerisureAPI URL: {} Data:{}", url, data);
337 Request request = httpClient.newRequest(url).method(HttpMethod.POST);
338 request.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
340 request.header("content-type", "application/json");
343 request.header("X-CSRF-TOKEN", csrf);
346 request.header("Accept", "application/json");
348 if (url.contains(AUTH_LOGIN)) {
349 request.header("APPLICATION_ID", "OpenHAB Verisure");
350 String basicAuhentication = Base64.getEncoder().encodeToString((userName + ":" + password).getBytes());
351 request.header("authorization", "Basic " + basicAuhentication);
353 if (!vid.isEmpty()) {
354 request.cookie(new HttpCookie(VID, vid));
355 logger.debug("Setting cookie with vid {}", vid);
357 if (!vsAccess.isEmpty()) {
358 request.cookie(new HttpCookie(VS_ACCESS, vsAccess));
359 logger.debug("Setting cookie with vs-access {}", vsAccess);
361 logger.debug("Setting cookie with username {}", userName);
362 request.cookie(new HttpCookie(USER_NAME, userName));
365 if (!"empty".equals(data)) {
366 request.content(new BytesContentProvider(data.getBytes(StandardCharsets.UTF_8)),
367 "application/x-www-form-urlencoded; charset=UTF-8");
370 logger.debug("HTTP POST Request {}.", request.toString());
371 return request.send();
374 private <T> T postJSONVerisureAPI(String url, String data, Class<T> jsonClass)
375 throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException, PostToAPIException {
376 for (int cnt = 0; cnt < APISERVERLIST.size(); cnt++) {
377 ContentResponse response = postVerisureAPI(apiServerInUse + url, data, Boolean.TRUE);
378 logger.debug("HTTP Response ({})", response.getStatus());
379 if (response.getStatus() == HttpStatus.OK_200) {
380 String content = response.getContentAsString();
381 if (content.contains("\"message\":\"Request Failed") && content.contains("503")) {
382 // Maybe Verisure has switched API server in use?
383 logger.debug("Changed API server! Response: {}", content);
384 setApiServerInUse(getNextApiServer());
385 } else if (content.contains("\"message\":\"Request Failed")
386 && content.contains("Invalid session cookie")) {
387 throw new PostToAPIException("Invalid session cookie");
389 String contentChomped = content.trim();
390 logger.trace("Response body: {}", content);
391 return gson.fromJson(contentChomped, jsonClass);
394 logger.debug("Failed to send POST, Http status code: {}", response.getStatus());
397 throw new PostToAPIException("Failed to POST to API");
400 private int postVerisureAPI(String urlString, String data) {
402 if (urlString.contains("https://mypages")) {
405 url = apiServerInUse + urlString;
408 for (int cnt = 0; cnt < APISERVERLIST.size(); cnt++) {
410 ContentResponse response = postVerisureAPI(url, data, Boolean.FALSE);
411 logger.debug("HTTP Response ({})", response.getStatus());
412 int httpStatus = response.getStatus();
413 if (httpStatus == HttpStatus.OK_200) {
414 String content = response.getContentAsString();
415 if (content.contains("\"message\":\"Request Failed. Code 503 from")) {
416 if (url.contains("https://mypages")) {
418 return HttpStatus.SERVICE_UNAVAILABLE_503;
420 // Maybe Verisure has switched API server in use
421 setApiServerInUse(getNextApiServer());
422 url = apiServerInUse + urlString;
425 logTraceWithPattern(httpStatus, content);
428 } else if (httpStatus == HttpStatus.BAD_REQUEST_400) {
429 setApiServerInUse(getNextApiServer());
430 url = apiServerInUse + urlString;
432 logger.debug("Failed to send POST, Http status code: {}", response.getStatus());
434 } catch (ExecutionException | InterruptedException | TimeoutException e) {
435 logger.warn("Failed to send a POST to the API {}", e.getMessage());
438 return HttpStatus.SERVICE_UNAVAILABLE_503;
441 private int setSessionCookieAuthLogin() throws ExecutionException, InterruptedException, TimeoutException {
442 // URL to set status which will give us 2 cookies with username and password used for the session
444 ContentResponse response = httpClient.GET(url);
445 logTraceWithPattern(response.getStatus(), response.getContentAsString());
448 int httpStatusCode = postVerisureAPI(url, "empty");
451 // return response.getStatus();
452 return httpStatusCode;
455 private boolean getInstallations() {
456 int httpResultCode = 0;
459 httpResultCode = setSessionCookieAuthLogin();
460 } catch (ExecutionException | InterruptedException | TimeoutException e) {
461 logger.warn("Failed to set session cookie {}", e.getMessage());
465 if (httpResultCode == HttpStatus.OK_200) {
466 String url = START_GRAPHQL;
468 String queryQLAccountInstallations = "[{\"operationName\":\"AccountInstallations\",\"variables\":{\"email\":\""
470 + "\"},\"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\"}]";
472 VerisureInstallationsDTO installations = postJSONVerisureAPI(url, queryQLAccountInstallations,
473 VerisureInstallationsDTO.class);
474 logger.debug("Installation: {}", installations.toString());
475 List<Owainstallation> owaInstList = installations.getData().getAccount().getOwainstallations();
476 boolean pinCodesMatchInstallations = true;
477 List<String> pinCodes = null;
478 String pinCode = this.pinCode;
479 if (pinCode != null) {
480 pinCodes = Arrays.asList(pinCode.split(","));
481 if (owaInstList.size() != pinCodes.size()) {
482 logger.debug("Number of installations {} does not match number of pin codes configured {}",
483 owaInstList.size(), pinCodes.size());
484 pinCodesMatchInstallations = false;
487 logger.debug("No pin-code defined for user {}", userName);
490 for (int i = 0; i < owaInstList.size(); i++) {
491 VerisureInstallation vInst = new VerisureInstallation();
492 Owainstallation owaInstallation = owaInstList.get(i);
493 String installationId = owaInstallation.getGiid();
494 if (owaInstallation.getAlias() != null && installationId != null) {
495 vInst.setInstallationId(new BigDecimal(installationId));
496 vInst.setInstallationName(owaInstallation.getAlias());
497 if (pinCode != null && pinCodes != null) {
498 int pinCodeIndex = pinCodesMatchInstallations ? i : 0;
499 vInst.setPinCode(pinCodes.get(pinCodeIndex));
500 logger.debug("Setting configured pincode index[{}] to installation ID {}", pinCodeIndex,
503 verisureInstallations.put(new BigDecimal(installationId), vInst);
505 logger.warn("Failed to get alias and/or giid");
509 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
510 | PostToAPIException e) {
511 logger.warn("Failed to send a POST to the API {}", e.getMessage());
514 logger.warn("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode);
520 private synchronized boolean logIn() {
522 if (!areWeLoggedIn()) {
525 logger.debug("Attempting to log in to {}, remove all cookies to ensure a fresh session", BASE_URL);
526 URI authUri = new URI(BASE_URL);
527 CookieStore store = httpClient.getCookieStore();
528 store.get(authUri).forEach(cookie -> {
529 store.remove(authUri, cookie);
532 String url = AUTH_LOGIN;
533 int httpStatusCode = postVerisureAPI(url, "empty");
535 if (!vsStepup.isEmpty()) {
536 logger.warn("MFA is activated on this user! Not supported by binding!");
541 logger.debug("Login URL: {}", url);
542 httpStatusCode = postVerisureAPI(url, authstring);
543 if (httpStatusCode != HttpStatus.OK_200) {
544 logger.debug("Failed to login, HTTP status code: {}", httpStatusCode);
551 } catch (ExecutionException | InterruptedException | TimeoutException | URISyntaxException
552 | HttpResponseException e) {
553 logger.warn("Failed to login {}", e.getMessage());
558 private <T extends VerisureThingDTO> void notifyListeners(T thing) {
559 deviceStatusListeners.forEach(listener -> {
560 if (listener.getVerisureThingClass().equals(thing.getClass())) {
561 listener.onDeviceStateChanged(thing);
566 private void notifyListenersIfChanged(VerisureThingDTO thing, VerisureInstallation installation, String deviceId) {
567 String normalizedDeviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId);
568 thing.setDeviceId(normalizedDeviceId);
569 thing.setSiteId(installation.getInstallationId());
570 thing.setSiteName(installation.getInstallationName());
571 VerisureThingDTO oldObj = verisureThings.get(normalizedDeviceId);
572 if (!thing.equals(oldObj)) {
573 verisureThings.put(thing.getDeviceId(), thing);
574 notifyListeners(thing);
576 logger.trace("No need to notify listeners for thing: {}", thing);
580 private boolean updateStatus() {
581 logger.debug("Update status");
582 for (Map.Entry<BigDecimal, VerisureInstallation> verisureInstallations : verisureInstallations.entrySet()) {
583 VerisureInstallation installation = verisureInstallations.getValue();
585 configureInstallationInstance(installation.getInstallationId());
586 int httpResultCode = setSessionCookieAuthLogin();
587 if (httpResultCode == HttpStatus.OK_200) {
588 updateAlarmStatus(installation);
589 updateSmartLockStatus(installation);
590 updateMiceDetectionStatus(installation);
591 updateClimateStatus(installation);
592 updateDoorWindowStatus(installation);
593 updateUserPresenceStatus(installation);
594 updateSmartPlugStatus(installation);
595 updateBroadbandConnectionStatus(installation);
596 updateEventLogStatus(installation);
597 updateGatewayStatus(installation);
599 logger.debug("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode);
602 } catch (ExecutionException | InterruptedException | TimeoutException | PostToAPIException e) {
603 logger.debug("Failed to update status {}", e.getMessage());
610 private String createOperationJSON(String operation, VariablesDTO variables, String query) {
611 OperationDTO operationJSON = new OperationDTO();
612 operationJSON.setOperationName(operation);
613 operationJSON.setVariables(variables);
614 operationJSON.setQuery(query);
615 return gson.toJson(Collections.singletonList(operationJSON));
618 private synchronized void updateAlarmStatus(VerisureInstallation installation) throws PostToAPIException {
619 BigDecimal installationId = installation.getInstallationId();
620 String url = START_GRAPHQL;
621 String operation = "ArmState";
622 VariablesDTO variables = new VariablesDTO();
623 variables.setGiid(installationId.toString());
624 String query = "query " + operation
625 + "($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";
627 String queryQLAlarmStatus = createOperationJSON(operation, variables, query);
628 logger.debug("Quering API for alarm status!");
630 VerisureThingDTO thing = postJSONVerisureAPI(url, queryQLAlarmStatus, VerisureAlarmsDTO.class);
631 logger.debug("REST Response ({})", thing);
632 // Set unique deviceID
633 String deviceId = "alarm" + installationId;
634 thing.setDeviceId(deviceId);
635 notifyListenersIfChanged(thing, installation, deviceId);
636 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
637 logger.warn("Failed to send a POST to the API {}", e.getMessage());
641 private synchronized void updateSmartLockStatus(VerisureInstallation installation) {
642 BigDecimal installationId = installation.getInstallationId();
643 String url = START_GRAPHQL;
644 String operation = "DoorLock";
645 String query = "query " + operation
646 + "($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";
647 VariablesDTO variables = new VariablesDTO();
648 variables.setGiid(installationId.toString());
649 String queryQLSmartLock = createOperationJSON(operation, variables, query);
650 logger.debug("Quering API for smart lock status");
653 VerisureSmartLocksDTO thing = postJSONVerisureAPI(url, queryQLSmartLock, VerisureSmartLocksDTO.class);
654 logger.debug("REST Response ({})", thing);
655 List<VerisureSmartLocksDTO.Doorlock> doorLockList = thing.getData().getInstallation().getDoorlocks();
656 if (doorLockList != null) {
657 doorLockList.forEach(doorLock -> {
658 VerisureSmartLocksDTO slThing = new VerisureSmartLocksDTO();
659 VerisureSmartLocksDTO.Installation inst = new VerisureSmartLocksDTO.Installation();
660 inst.setDoorlocks(Collections.singletonList(doorLock));
661 VerisureSmartLocksDTO.Data data = new VerisureSmartLocksDTO.Data();
662 data.setInstallation(inst);
663 slThing.setData(data);
664 // Set unique deviceID
665 String deviceId = doorLock.getDevice().getDeviceLabel();
666 if (deviceId != null) {
668 slThing.setLocation(doorLock.getDevice().getArea());
669 slThing.setDeviceId(deviceId);
671 // Fetch more info from old endpoint
673 VerisureSmartLockDTO smartLockThing = getJSONVerisureAPI(
674 SMARTLOCK_PATH + slThing.getDeviceId(), VerisureSmartLockDTO.class);
675 logger.debug("REST Response ({})", smartLockThing);
676 slThing.setSmartLockJSON(smartLockThing);
677 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
678 logger.warn("Failed to query for smartlock status: {}", e.getMessage());
680 notifyListenersIfChanged(slThing, installation, deviceId);
684 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
685 | PostToAPIException e) {
686 logger.warn("Failed to send a POST to the API {}", e.getMessage());
690 private synchronized void updateSmartPlugStatus(VerisureInstallation installation) {
691 BigDecimal installationId = installation.getInstallationId();
692 String url = START_GRAPHQL;
693 String operation = "SmartPlug";
694 VariablesDTO variables = new VariablesDTO();
695 variables.setGiid(installationId.toString());
696 String query = "query " + operation
697 + "($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";
698 String queryQLSmartPlug = createOperationJSON(operation, variables, query);
699 logger.debug("Quering API for smart plug status");
702 VerisureSmartPlugsDTO thing = postJSONVerisureAPI(url, queryQLSmartPlug, VerisureSmartPlugsDTO.class);
703 logger.debug("REST Response ({})", thing);
704 List<VerisureSmartPlugsDTO.Smartplug> smartPlugList = thing.getData().getInstallation().getSmartplugs();
705 if (smartPlugList != null) {
706 smartPlugList.forEach(smartPlug -> {
707 VerisureSmartPlugsDTO spThing = new VerisureSmartPlugsDTO();
708 VerisureSmartPlugsDTO.Installation inst = new VerisureSmartPlugsDTO.Installation();
709 inst.setSmartplugs(Collections.singletonList(smartPlug));
710 VerisureSmartPlugsDTO.Data data = new VerisureSmartPlugsDTO.Data();
711 data.setInstallation(inst);
712 spThing.setData(data);
713 // Set unique deviceID
714 String deviceId = smartPlug.getDevice().getDeviceLabel();
715 if (deviceId != null) {
717 spThing.setLocation(smartPlug.getDevice().getArea());
718 notifyListenersIfChanged(spThing, installation, deviceId);
722 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
723 | PostToAPIException e) {
724 logger.warn("Failed to send a POST to the API {}", e.getMessage());
728 private @Nullable VerisureBatteryStatusDTO getBatteryStatus(String deviceId,
729 VerisureBatteryStatusDTO @Nullable [] batteryStatus) {
730 if (batteryStatus != null) {
731 for (VerisureBatteryStatusDTO verisureBatteryStatusDTO : batteryStatus) {
732 String id = verisureBatteryStatusDTO.getId();
733 if (id != null && id.equals(deviceId)) {
734 return verisureBatteryStatusDTO;
741 private synchronized void updateClimateStatus(VerisureInstallation installation) {
742 BigDecimal installationId = installation.getInstallationId();
743 String url = START_GRAPHQL;
744 VariablesDTO variables = new VariablesDTO();
745 variables.setGiid(installationId.toString());
746 String operation = "Climate";
747 String query = "query " + operation
748 + "($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";
750 String queryQLClimates = createOperationJSON(operation, variables, query);
751 logger.debug("Quering API for climate status");
754 VerisureClimatesDTO thing = postJSONVerisureAPI(url, queryQLClimates, VerisureClimatesDTO.class);
755 logger.debug("REST Response ({})", thing);
756 List<VerisureClimatesDTO.Climate> climateList = thing.getData().getInstallation().getClimates();
757 if (climateList != null) {
758 climateList.forEach(climate -> {
759 // If thing is Mouse detection device, then skip it, but fetch temperature from it
760 String type = climate.getDevice().getGui().getLabel();
761 if ("MOUSE".equals(type)) {
762 logger.debug("Mouse detection device!");
763 String deviceId = climate.getDevice().getDeviceLabel();
764 if (deviceId != null) {
765 deviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId);
766 VerisureThingDTO mouseThing = verisureThings.get(deviceId);
767 if (mouseThing instanceof VerisureMiceDetectionDTO) {
768 VerisureMiceDetectionDTO miceDetectorThing = (VerisureMiceDetectionDTO) mouseThing;
769 miceDetectorThing.setTemperatureValue(climate.getTemperatureValue());
770 miceDetectorThing.setTemperatureTime(climate.getTemperatureTimestamp());
771 notifyListeners(miceDetectorThing);
772 logger.debug("Found climate thing for a Verisure Mouse Detector");
777 VerisureClimatesDTO cThing = new VerisureClimatesDTO();
778 VerisureClimatesDTO.Installation inst = new VerisureClimatesDTO.Installation();
779 inst.setClimates(Collections.singletonList(climate));
780 VerisureClimatesDTO.Data data = new VerisureClimatesDTO.Data();
781 data.setInstallation(inst);
782 cThing.setData(data);
783 // Set unique deviceID
784 String deviceId = climate.getDevice().getDeviceLabel();
785 if (deviceId != null) {
787 VerisureBatteryStatusDTO[] batteryStatusThingArray = getJSONVerisureAPI(BATTERY_STATUS,
788 VerisureBatteryStatusDTO[].class);
789 VerisureBatteryStatusDTO batteryStatus = getBatteryStatus(deviceId,
790 batteryStatusThingArray);
791 if (batteryStatus != null) {
792 logger.debug("REST Response ({})", batteryStatus);
793 cThing.setBatteryStatus(batteryStatus);
795 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
796 logger.debug("Failed to query for battery status: {}", e.getMessage());
799 cThing.setLocation(climate.getDevice().getArea());
800 notifyListenersIfChanged(cThing, installation, deviceId);
804 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
805 | PostToAPIException e) {
806 logger.warn("Failed to send a POST to the API {}", e.getMessage());
810 private synchronized void updateDoorWindowStatus(VerisureInstallation installation) {
811 BigDecimal installationId = installation.getInstallationId();
812 String url = START_GRAPHQL;
813 String operation = "DoorWindow";
814 VariablesDTO variables = new VariablesDTO();
815 variables.setGiid(installationId.toString());
816 String query = "query " + operation
817 + "($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";
819 String queryQLDoorWindow = createOperationJSON(operation, variables, query);
820 logger.debug("Quering API for door&window status");
823 VerisureDoorWindowsDTO thing = postJSONVerisureAPI(url, queryQLDoorWindow, VerisureDoorWindowsDTO.class);
824 logger.debug("REST Response ({})", thing);
825 List<VerisureDoorWindowsDTO.DoorWindow> doorWindowList = thing.getData().getInstallation().getDoorWindows();
826 if (doorWindowList != null) {
827 doorWindowList.forEach(doorWindow -> {
828 VerisureDoorWindowsDTO dThing = new VerisureDoorWindowsDTO();
829 VerisureDoorWindowsDTO.Installation inst = new VerisureDoorWindowsDTO.Installation();
830 inst.setDoorWindows(Collections.singletonList(doorWindow));
831 VerisureDoorWindowsDTO.Data data = new VerisureDoorWindowsDTO.Data();
832 data.setInstallation(inst);
833 dThing.setData(data);
834 // Set unique deviceID
835 String deviceId = doorWindow.getDevice().getDeviceLabel();
836 if (deviceId != null) {
838 VerisureBatteryStatusDTO[] batteryStatusThingArray = getJSONVerisureAPI(BATTERY_STATUS,
839 VerisureBatteryStatusDTO[].class);
840 VerisureBatteryStatusDTO batteryStatus = getBatteryStatus(deviceId,
841 batteryStatusThingArray);
842 if (batteryStatus != null) {
843 logger.debug("REST Response ({})", batteryStatus);
844 dThing.setBatteryStatus(batteryStatus);
846 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
847 logger.warn("Failed to query for door&window status: {}", e.getMessage());
850 dThing.setLocation(doorWindow.getDevice().getArea());
851 notifyListenersIfChanged(dThing, installation, deviceId);
855 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
856 | PostToAPIException e) {
857 logger.warn("Failed to send a POST to the API {}", e.getMessage());
861 private synchronized void updateBroadbandConnectionStatus(VerisureInstallation inst) {
862 BigDecimal installationId = inst.getInstallationId();
863 String url = START_GRAPHQL;
864 String operation = "Broadband";
865 VariablesDTO variables = new VariablesDTO();
866 variables.setGiid(installationId.toString());
867 String query = "query " + operation
868 + "($giid: String!) {\n installation(giid: $giid) {\n broadband {\n testDate\n isBroadbandConnected\n __typename\n }\n __typename\n}\n}\n";
870 String queryQLBroadbandConnection = createOperationJSON(operation, variables, query);
871 logger.debug("Quering API for broadband connection status");
874 VerisureThingDTO thing = postJSONVerisureAPI(url, queryQLBroadbandConnection,
875 VerisureBroadbandConnectionsDTO.class);
876 logger.debug("REST Response ({})", thing);
877 // Set unique deviceID
878 String deviceId = "bc" + installationId;
879 notifyListenersIfChanged(thing, inst, deviceId);
880 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
881 | PostToAPIException e) {
882 logger.warn("Failed to send a POST to the API {}", e.getMessage());
886 private synchronized void updateUserPresenceStatus(VerisureInstallation installation) {
887 BigDecimal installationId = installation.getInstallationId();
888 String url = START_GRAPHQL;
889 String operation = "userTrackings";
890 VariablesDTO variables = new VariablesDTO();
891 variables.setGiid(installationId.toString());
892 String query = "query " + operation
893 + "($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";
895 String queryQLUserPresence = createOperationJSON(operation, variables, query);
896 logger.debug("Quering API for user presence status");
899 VerisureUserPresencesDTO thing = postJSONVerisureAPI(url, queryQLUserPresence,
900 VerisureUserPresencesDTO.class);
901 logger.debug("REST Response ({})", thing);
902 List<VerisureUserPresencesDTO.UserTracking> userTrackingList = thing.getData().getInstallation()
904 if (userTrackingList != null) {
905 userTrackingList.forEach(userTracking -> {
906 String localUserTrackingStatus = userTracking.getStatus();
907 if ("ACTIVE".equals(localUserTrackingStatus)) {
908 VerisureUserPresencesDTO upThing = new VerisureUserPresencesDTO();
909 VerisureUserPresencesDTO.Installation inst = new VerisureUserPresencesDTO.Installation();
910 inst.setUserTrackings(Collections.singletonList(userTracking));
911 VerisureUserPresencesDTO.Data data = new VerisureUserPresencesDTO.Data();
912 data.setInstallation(inst);
913 upThing.setData(data);
914 // Set unique deviceID
915 String deviceId = "up" + userTracking.getWebAccount() + installationId;
916 notifyListenersIfChanged(upThing, installation, deviceId);
920 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
921 | PostToAPIException e) {
922 logger.warn("Failed to send a POST to the API {}", e.getMessage());
926 private synchronized void updateMiceDetectionStatus(VerisureInstallation installation) {
927 BigDecimal installationId = installation.getInstallationId();
928 String url = START_GRAPHQL;
929 String operation = "Mouse";
930 VariablesDTO variables = new VariablesDTO();
931 variables.setGiid(installationId.toString());
932 String query = "query " + operation
933 + "($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";
935 String queryQLMiceDetection = createOperationJSON(operation, variables, query);
936 logger.debug("Quering API for mice detection status");
939 VerisureMiceDetectionDTO thing = postJSONVerisureAPI(url, queryQLMiceDetection,
940 VerisureMiceDetectionDTO.class);
941 logger.debug("REST Response ({})", thing);
942 List<VerisureMiceDetectionDTO.Mouse> miceList = thing.getData().getInstallation().getMice();
943 if (miceList != null) {
944 miceList.forEach(mouse -> {
945 VerisureMiceDetectionDTO miceThing = new VerisureMiceDetectionDTO();
946 VerisureMiceDetectionDTO.Installation inst = new VerisureMiceDetectionDTO.Installation();
947 inst.setMice(Collections.singletonList(mouse));
948 VerisureMiceDetectionDTO.Data data = new VerisureMiceDetectionDTO.Data();
949 data.setInstallation(inst);
950 miceThing.setData(data);
951 // Set unique deviceID
952 String deviceId = mouse.getDevice().getDeviceLabel();
953 logger.debug("Mouse id: {} for thing: {}", deviceId, mouse);
954 if (deviceId != null) {
956 miceThing.setLocation(mouse.getDevice().getArea());
957 notifyListenersIfChanged(miceThing, installation, deviceId);
961 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
962 | PostToAPIException e) {
963 logger.warn("Failed to send a POST to the API {}", e.getMessage());
967 private synchronized void updateEventLogStatus(VerisureInstallation installation) {
968 BigDecimal installationId = installation.getInstallationId();
969 String url = START_GRAPHQL;
970 String operation = "EventLog";
972 int numberOfEvents = this.numberOfEvents;
973 List<String> eventCategories = new ArrayList<>(Arrays.asList("INTRUSION", "FIRE", "SOS", "WATER", "ANIMAL",
974 "TECHNICAL", "WARNING", "ARM", "DISARM", "LOCK", "UNLOCK", "PICTURE", "CLIMATE", "CAMERA_SETTINGS",
975 "DOORWINDOW_STATE_OPENED", "DOORWINDOW_STATE_CLOSED", "USERTRACKING"));
976 VariablesDTO variables = new VariablesDTO();
977 variables.setGiid(installationId.toString());
978 variables.setHideNotifications(true);
979 variables.setOffset(offset);
980 variables.setPagesize(numberOfEvents);
981 variables.setEventCategories(eventCategories);
982 String query = "query " + operation
983 + "($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";
985 String queryQLEventLog = createOperationJSON(operation, variables, query);
986 logger.debug("Quering API for event log status");
989 VerisureEventLogDTO thing = postJSONVerisureAPI(url, queryQLEventLog, VerisureEventLogDTO.class);
990 logger.debug("REST Response ({})", thing);
991 // Set unique deviceID
992 String deviceId = "el" + installationId;
993 notifyListenersIfChanged(thing, installation, deviceId);
994 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
995 | PostToAPIException e) {
996 logger.warn("Failed to send a POST to the API {}", e.getMessage());
1000 private synchronized void updateGatewayStatus(VerisureInstallation installation) {
1001 BigDecimal installationId = installation.getInstallationId();
1002 String url = START_GRAPHQL;
1003 String operation = "communicationState";
1004 VariablesDTO variables = new VariablesDTO();
1005 variables.setGiid(installationId.toString());
1007 String query = "query " + operation
1008 + "($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}";
1010 String queryQLEventLog = createOperationJSON(operation, variables, query);
1011 logger.debug("Quering API for gateway status");
1014 VerisureGatewayDTO thing = postJSONVerisureAPI(url, queryQLEventLog, VerisureGatewayDTO.class);
1015 logger.debug("REST Response ({})", thing);
1016 // Set unique deviceID
1017 List<CommunicationState> communicationStateList = thing.getData().getInstallation().getCommunicationState();
1018 if (communicationStateList != null) {
1019 if (!communicationStateList.isEmpty()) {
1020 String deviceId = communicationStateList.get(0).getDevice().getDeviceLabel();
1021 if (deviceId != null) {
1022 notifyListenersIfChanged(thing, installation, deviceId);
1026 } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
1027 | PostToAPIException e) {
1028 logger.warn("Failed to send a POST to the API {}", e.getMessage());
1032 private final class VerisureInstallation {
1033 private @Nullable String installationName;
1034 private BigDecimal installationId = BigDecimal.ZERO;
1035 private @Nullable String pinCode;
1037 public @Nullable String getPinCode() {
1041 public void setPinCode(@Nullable String pinCode) {
1042 this.pinCode = pinCode;
1045 public VerisureInstallation() {
1048 public BigDecimal getInstallationId() {
1049 return installationId;
1052 public @Nullable String getInstallationName() {
1053 return installationName;
1056 public void setInstallationId(BigDecimal installationId) {
1057 this.installationId = installationId;
1060 public void setInstallationName(@Nullable String installationName) {
1061 this.installationName = installationName;
1065 private static class OperationDTO {
1067 @SuppressWarnings("unused")
1068 private @Nullable String operationName;
1069 @SuppressWarnings("unused")
1070 private VariablesDTO variables = new VariablesDTO();
1071 @SuppressWarnings("unused")
1072 private @Nullable String query;
1074 public void setOperationName(String operationName) {
1075 this.operationName = operationName;
1078 public void setVariables(VariablesDTO variables) {
1079 this.variables = variables;
1082 public void setQuery(String query) {
1087 public static class VariablesDTO {
1089 @SuppressWarnings("unused")
1090 private boolean hideNotifications;
1091 @SuppressWarnings("unused")
1093 @SuppressWarnings("unused")
1094 private int pagesize;
1095 @SuppressWarnings("unused")
1096 private @Nullable List<String> eventCategories = null;
1097 @SuppressWarnings("unused")
1098 private @Nullable String giid;
1100 public void setHideNotifications(boolean hideNotifications) {
1101 this.hideNotifications = hideNotifications;
1104 public void setOffset(int offset) {
1105 this.offset = offset;
1108 public void setPagesize(int pagesize) {
1109 this.pagesize = pagesize;
1112 public void setEventCategories(List<String> eventCategories) {
1113 this.eventCategories = eventCategories;
1116 public void setGiid(String giid) {
1121 private class PostToAPIException extends Exception {
1123 private static final long serialVersionUID = 1L;
1125 public PostToAPIException(String message) {