]> git.basschouten.com Git - openhab-addons.git/blob
45e13592d0b54e0942f4f97d859132e86e88c572
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.verisure.internal;
14
15 import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.net.CookieStore;
19 import java.net.HttpCookie;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.CopyOnWriteArrayList;
29 import java.util.concurrent.ExecutionException;
30 import java.util.concurrent.TimeoutException;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.eclipse.jetty.client.HttpResponseException;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.client.util.BytesContentProvider;
39 import org.eclipse.jetty.http.HttpMethod;
40 import org.eclipse.jetty.http.HttpStatus;
41 import org.jsoup.Jsoup;
42 import org.jsoup.nodes.Document;
43 import org.jsoup.nodes.Element;
44 import org.openhab.binding.verisure.internal.dto.VerisureAlarmsDTO;
45 import org.openhab.binding.verisure.internal.dto.VerisureBroadbandConnectionsDTO;
46 import org.openhab.binding.verisure.internal.dto.VerisureClimatesDTO;
47 import org.openhab.binding.verisure.internal.dto.VerisureDoorWindowsDTO;
48 import org.openhab.binding.verisure.internal.dto.VerisureEventLogDTO;
49 import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO;
50 import org.openhab.binding.verisure.internal.dto.VerisureGatewayDTO.CommunicationState;
51 import org.openhab.binding.verisure.internal.dto.VerisureInstallationsDTO;
52 import org.openhab.binding.verisure.internal.dto.VerisureInstallationsDTO.Owainstallation;
53 import org.openhab.binding.verisure.internal.dto.VerisureMiceDetectionDTO;
54 import org.openhab.binding.verisure.internal.dto.VerisureSmartLockDTO;
55 import org.openhab.binding.verisure.internal.dto.VerisureSmartLocksDTO;
56 import org.openhab.binding.verisure.internal.dto.VerisureSmartPlugsDTO;
57 import org.openhab.binding.verisure.internal.dto.VerisureThingDTO;
58 import org.openhab.binding.verisure.internal.dto.VerisureUserPresencesDTO;
59 import org.openhab.binding.verisure.internal.handler.VerisureThingHandler;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 import com.google.gson.Gson;
64 import com.google.gson.JsonSyntaxException;
65
66 /**
67  * This class performs the communication with Verisure My Pages.
68  *
69  * @author Jarle Hjortland - Initial contribution
70  * @author Jan Gustafsson - Re-design and support for several sites and update to new Verisure API
71  *
72  */
73 @NonNullByDefault
74 public class VerisureSession {
75
76     @NonNullByDefault({})
77     private final Map<String, VerisureThingDTO> verisureThings = new ConcurrentHashMap<>();
78     private final Map<String, VerisureThingHandler<?>> verisureHandlers = new ConcurrentHashMap<>();
79     private final Logger logger = LoggerFactory.getLogger(VerisureSession.class);
80     private final Gson gson = new Gson();
81     private final List<DeviceStatusListener<VerisureThingDTO>> deviceStatusListeners = new CopyOnWriteArrayList<>();
82     private final Map<BigDecimal, VerisureInstallation> verisureInstallations = new ConcurrentHashMap<>();
83     private static final List<String> APISERVERLIST = Arrays.asList("https://m-api01.verisure.com",
84             "https://m-api02.verisure.com");
85     private int apiServerInUseIndex = 0;
86     private int numberOfEvents = 15;
87     private static final String USER_NAME = "username";
88     private static final String PASSWORD_NAME = "vid";
89     private String apiServerInUse = APISERVERLIST.get(apiServerInUseIndex);
90     private String authstring = "";
91     private @Nullable String csrf;
92     private @Nullable String pinCode;
93     private HttpClient httpClient;
94     private @Nullable String userName = "";
95     private @Nullable String password = "";
96
97     public VerisureSession(HttpClient httpClient) {
98         this.httpClient = httpClient;
99     }
100
101     public boolean initialize(@Nullable String authstring, @Nullable String pinCode, @Nullable String userName) {
102         if (authstring != null) {
103             this.authstring = authstring.substring(0);
104             this.pinCode = pinCode;
105             this.userName = userName;
106             // Try to login to Verisure
107             if (logIn()) {
108                 return getInstallations();
109             } else {
110                 return false;
111             }
112         }
113         return false;
114     }
115
116     public boolean refresh() {
117         try {
118             if (logIn()) {
119                 updateStatus();
120                 return true;
121             } else {
122                 return false;
123             }
124         } catch (HttpResponseException e) {
125             logger.warn("Failed to do a refresh {}", e.getMessage());
126             return false;
127         }
128     }
129
130     public int sendCommand(String url, String data, BigDecimal installationId) {
131         logger.debug("Sending command with URL {} and data {}", url, data);
132         try {
133             configureInstallationInstance(installationId);
134             int httpResultCode = setSessionCookieAuthLogin();
135             if (httpResultCode == HttpStatus.OK_200) {
136                 return postVerisureAPI(url, data);
137             } else {
138                 return httpResultCode;
139             }
140         } catch (ExecutionException | InterruptedException | TimeoutException e) {
141             logger.debug("Failed to send command {}", e.getMessage());
142         }
143         return HttpStatus.BAD_REQUEST_400;
144     }
145
146     public boolean unregisterDeviceStatusListener(
147             DeviceStatusListener<? extends VerisureThingDTO> deviceStatusListener) {
148         return deviceStatusListeners.remove(deviceStatusListener);
149     }
150
151     @SuppressWarnings("unchecked")
152     public boolean registerDeviceStatusListener(DeviceStatusListener<? extends VerisureThingDTO> deviceStatusListener) {
153         return deviceStatusListeners.add((DeviceStatusListener<VerisureThingDTO>) deviceStatusListener);
154     }
155
156     @SuppressWarnings({ "unchecked" })
157     public <T extends VerisureThingDTO> @Nullable T getVerisureThing(String deviceId, Class<T> thingType) {
158         VerisureThingDTO thing = verisureThings.get(deviceId);
159         if (thingType.isInstance(thing)) {
160             return (T) thing;
161         }
162         return null;
163     }
164
165     public <T extends VerisureThingDTO> @Nullable T getVerisureThing(String deviceId) {
166         VerisureThingDTO thing = verisureThings.get(deviceId);
167         if (thing != null) {
168             @SuppressWarnings("unchecked")
169             T thing2 = (T) thing;
170             return thing2;
171         }
172         return null;
173     }
174
175     public @Nullable VerisureThingHandler<?> getVerisureThinghandler(String deviceId) {
176         VerisureThingHandler<?> thingHandler = verisureHandlers.get(deviceId);
177         return thingHandler;
178     }
179
180     public void setVerisureThingHandler(VerisureThingHandler<?> vth, String deviceId) {
181         verisureHandlers.put(deviceId, vth);
182     };
183
184     public void removeVerisureThingHandler(String deviceId) {
185         verisureHandlers.remove(deviceId);
186     }
187
188     public Collection<VerisureThingDTO> getVerisureThings() {
189         return verisureThings.values();
190     }
191
192     public @Nullable String getCsrf() {
193         return csrf;
194     }
195
196     public @Nullable String getPinCode() {
197         return pinCode;
198     }
199
200     public String getApiServerInUse() {
201         return apiServerInUse;
202     }
203
204     public void setApiServerInUse(String apiServerInUse) {
205         this.apiServerInUse = apiServerInUse;
206     }
207
208     public String getNextApiServer() {
209         apiServerInUseIndex++;
210         if (apiServerInUseIndex > (APISERVERLIST.size() - 1)) {
211             apiServerInUseIndex = 0;
212         }
213         return APISERVERLIST.get(apiServerInUseIndex);
214     }
215
216     public void setNumberOfEvents(int numberOfEvents) {
217         this.numberOfEvents = numberOfEvents;
218     }
219
220     public void configureInstallationInstance(BigDecimal installationId)
221             throws ExecutionException, InterruptedException, TimeoutException {
222         csrf = getCsrfToken(installationId);
223         logger.debug("Got CSRF: {}", csrf);
224         // Set installation
225         String url = SET_INSTALLATION + installationId;
226         httpClient.GET(url);
227     }
228
229     public @Nullable String getCsrfToken(BigDecimal installationId)
230             throws ExecutionException, InterruptedException, TimeoutException {
231         String html = null;
232         String url = SETTINGS + installationId;
233
234         ContentResponse resp = httpClient.GET(url);
235         html = resp.getContentAsString();
236         logger.trace("url: {} html: {}", url, html);
237
238         Document htmlDocument = Jsoup.parse(html);
239         Element nameInput = htmlDocument.select("input[name=_csrf]").first();
240         if (nameInput != null) {
241             return nameInput.attr("value");
242         } else {
243             return null;
244         }
245     }
246
247     public @Nullable String getPinCode(BigDecimal installationId) {
248         return verisureInstallations.get(installationId).getPinCode();
249     }
250
251     private void setPasswordFromCookie() {
252         CookieStore c = httpClient.getCookieStore();
253         List<HttpCookie> cookies = c.getCookies();
254         cookies.forEach(cookie -> {
255             logger.trace("Response Cookie: {}", cookie);
256             if (cookie.getName().equals(PASSWORD_NAME)) {
257                 password = cookie.getValue();
258                 logger.debug("Fetching vid {} from cookie", password);
259             }
260         });
261     }
262
263     private void logTraceWithPattern(int responseStatus, String content) {
264         if (logger.isTraceEnabled()) {
265             String pattern = "(?m)^\\s*\\r?\\n|\\r?\\n\\s*(?!.*\\r?\\n)";
266             String replacement = "";
267             logger.trace("HTTP Response ({}) Body:{}", responseStatus, content.replaceAll(pattern, replacement));
268         }
269     }
270
271     private boolean areWeLoggedIn() throws ExecutionException, InterruptedException, TimeoutException {
272         logger.debug("Checking if we are logged in");
273         String url = STATUS;
274
275         ContentResponse response = httpClient.newRequest(url).method(HttpMethod.GET).send();
276         String content = response.getContentAsString();
277         logTraceWithPattern(response.getStatus(), content);
278
279         switch (response.getStatus()) {
280             case HttpStatus.OK_200:
281                 if (content.contains("<link href=\"/newapp")) {
282                     setPasswordFromCookie();
283                     return true;
284                 } else {
285                     logger.debug("We need to login again!");
286                     return false;
287                 }
288             case HttpStatus.MOVED_TEMPORARILY_302:
289                 // Redirection
290                 logger.debug("Status code 302. Redirected. Probably not logged in");
291                 return false;
292             case HttpStatus.INTERNAL_SERVER_ERROR_500:
293             case HttpStatus.SERVICE_UNAVAILABLE_503:
294                 throw new HttpResponseException(
295                         "Status code " + response.getStatus() + ". Verisure service temporarily down", response);
296             default:
297                 logger.debug("Status code {} body {}", response.getStatus(), content);
298                 break;
299         }
300         return false;
301     }
302
303     private <T> @Nullable T getJSONVerisureAPI(String url, Class<T> jsonClass)
304             throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException {
305         logger.debug("HTTP GET: {}", BASEURL + url);
306
307         ContentResponse response = httpClient.GET(BASEURL + url + "?_=" + System.currentTimeMillis());
308         String content = response.getContentAsString();
309         logTraceWithPattern(response.getStatus(), content);
310
311         return gson.fromJson(content, jsonClass);
312     }
313
314     private ContentResponse postVerisureAPI(String url, String data, boolean isJSON)
315             throws ExecutionException, InterruptedException, TimeoutException {
316         logger.debug("postVerisureAPI URL: {} Data:{}", url, data);
317         Request request = httpClient.newRequest(url).method(HttpMethod.POST);
318         if (isJSON) {
319             request.header("content-type", "application/json");
320         } else {
321             if (csrf != null) {
322                 request.header("X-CSRF-TOKEN", csrf);
323             }
324         }
325         request.header("Accept", "application/json");
326         if (!data.equals("empty")) {
327             request.content(new BytesContentProvider(data.getBytes(StandardCharsets.UTF_8)),
328                     "application/x-www-form-urlencoded; charset=UTF-8");
329         } else {
330             logger.debug("Setting cookie with username {} and vid {}", userName, password);
331             request.cookie(new HttpCookie(USER_NAME, userName));
332             request.cookie(new HttpCookie(PASSWORD_NAME, password));
333         }
334         logger.debug("HTTP POST Request {}.", request.toString());
335         return request.send();
336     }
337
338     private <T> T postJSONVerisureAPI(String url, String data, Class<T> jsonClass)
339             throws ExecutionException, InterruptedException, TimeoutException, JsonSyntaxException, PostToAPIException {
340         for (int cnt = 0; cnt < APISERVERLIST.size(); cnt++) {
341             ContentResponse response = postVerisureAPI(apiServerInUse + url, data, Boolean.TRUE);
342             logger.debug("HTTP Response ({})", response.getStatus());
343             if (response.getStatus() == HttpStatus.OK_200) {
344                 String content = response.getContentAsString();
345                 if (content.contains("\"message\":\"Request Failed") && content.contains("503")) {
346                     // Maybe Verisure has switched API server in use?
347                     logger.debug("Changed API server! Response: {}", content);
348                     setApiServerInUse(getNextApiServer());
349                 } else {
350                     String contentChomped = content.trim();
351                     logger.trace("Response body: {}", content);
352                     return gson.fromJson(contentChomped, jsonClass);
353                 }
354             } else {
355                 logger.debug("Failed to send POST, Http status code: {}", response.getStatus());
356             }
357         }
358         throw new PostToAPIException("Failed to POST to API");
359     }
360
361     private int postVerisureAPI(String urlString, String data) {
362         String url;
363         if (urlString.contains("https://mypages")) {
364             url = urlString;
365         } else {
366             url = apiServerInUse + urlString;
367         }
368
369         for (int cnt = 0; cnt < APISERVERLIST.size(); cnt++) {
370             try {
371                 ContentResponse response = postVerisureAPI(url, data, Boolean.FALSE);
372                 logger.debug("HTTP Response ({})", response.getStatus());
373                 int httpStatus = response.getStatus();
374                 if (httpStatus == HttpStatus.OK_200) {
375                     String content = response.getContentAsString();
376                     if (content.contains("\"message\":\"Request Failed. Code 503 from")) {
377                         if (url.contains("https://mypages")) {
378                             // Not an API URL
379                             return HttpStatus.SERVICE_UNAVAILABLE_503;
380                         } else {
381                             // Maybe Verisure has switched API server in use
382                             setApiServerInUse(getNextApiServer());
383                             url = apiServerInUse + urlString;
384                         }
385                     } else {
386                         logTraceWithPattern(httpStatus, content);
387                         return httpStatus;
388                     }
389                 } else {
390                     logger.debug("Failed to send POST, Http status code: {}", response.getStatus());
391                 }
392             } catch (ExecutionException | InterruptedException | TimeoutException e) {
393                 logger.warn("Failed to send a POST to the API {}", e.getMessage());
394             }
395         }
396         return HttpStatus.SERVICE_UNAVAILABLE_503;
397     }
398
399     private int setSessionCookieAuthLogin() throws ExecutionException, InterruptedException, TimeoutException {
400         // URL to set status which will give us 2 cookies with username and password used for the session
401         String url = STATUS;
402         ContentResponse response = httpClient.GET(url);
403         logTraceWithPattern(response.getStatus(), response.getContentAsString());
404
405         url = AUTH_LOGIN;
406         return postVerisureAPI(url, "empty");
407     }
408
409     private boolean getInstallations() {
410         int httpResultCode = 0;
411
412         try {
413             httpResultCode = setSessionCookieAuthLogin();
414         } catch (ExecutionException | InterruptedException | TimeoutException e) {
415             logger.warn("Failed to set session cookie {}", e.getMessage());
416             return false;
417         }
418
419         if (httpResultCode == HttpStatus.OK_200) {
420             String url = START_GRAPHQL;
421
422             String queryQLAccountInstallations = "[{\"operationName\":\"AccountInstallations\",\"variables\":{\"email\":\""
423                     + userName
424                     + "\"},\"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\"}]";
425             try {
426                 VerisureInstallationsDTO installations = postJSONVerisureAPI(url, queryQLAccountInstallations,
427                         VerisureInstallationsDTO.class);
428                 logger.debug("Installation: {}", installations.toString());
429                 List<Owainstallation> owaInstList = installations.getData().getAccount().getOwainstallations();
430                 boolean pinCodesMatchInstallations = true;
431                 List<String> pinCodes = null;
432                 String pinCode = this.pinCode;
433                 if (pinCode != null) {
434                     pinCodes = Arrays.asList(pinCode.split(","));
435                     if (owaInstList.size() != pinCodes.size()) {
436                         logger.debug("Number of installations {} does not match number of pin codes configured {}",
437                                 owaInstList.size(), pinCodes.size());
438                         pinCodesMatchInstallations = false;
439                     }
440                 } else {
441                     logger.debug("No pin-code defined for user {}", userName);
442                 }
443
444                 for (int i = 0; i < owaInstList.size(); i++) {
445                     VerisureInstallation vInst = new VerisureInstallation();
446                     Owainstallation owaInstallation = owaInstList.get(i);
447                     String installationId = owaInstallation.getGiid();
448                     if (owaInstallation.getAlias() != null && installationId != null) {
449                         vInst.setInstallationId(new BigDecimal(installationId));
450                         vInst.setInstallationName(owaInstallation.getAlias());
451                         if (pinCode != null && pinCodes != null) {
452                             int pinCodeIndex = pinCodesMatchInstallations ? i : 0;
453                             vInst.setPinCode(pinCodes.get(pinCodeIndex));
454                             logger.debug("Setting configured pincode index[{}] to installation ID {}", pinCodeIndex,
455                                     installationId);
456                         }
457                         verisureInstallations.put(new BigDecimal(installationId), vInst);
458                     } else {
459                         logger.warn("Failed to get alias and/or giid");
460                         return false;
461                     }
462                 }
463             } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
464                     | PostToAPIException e) {
465                 logger.warn("Failed to send a POST to the API {}", e.getMessage());
466             }
467         } else {
468             logger.warn("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode);
469             return false;
470         }
471         return true;
472     }
473
474     private synchronized boolean logIn() {
475         try {
476             if (!areWeLoggedIn()) {
477                 logger.debug("Attempting to log in to mypages.verisure.com");
478                 String url = LOGON_SUF;
479                 logger.debug("Login URL: {}", url);
480                 int httpStatusCode = postVerisureAPI(url, authstring);
481                 if (httpStatusCode != HttpStatus.OK_200) {
482                     logger.debug("Failed to login, HTTP status code: {}", httpStatusCode);
483                     return false;
484                 }
485                 return true;
486             } else {
487                 return true;
488             }
489         } catch (ExecutionException | InterruptedException | TimeoutException e) {
490             logger.warn("Failed to login {}", e.getMessage());
491         }
492         return false;
493     }
494
495     private <T extends VerisureThingDTO> void notifyListeners(T thing) {
496         deviceStatusListeners.forEach(listener -> {
497             if (listener.getVerisureThingClass().equals(thing.getClass())) {
498                 listener.onDeviceStateChanged(thing);
499             }
500         });
501     }
502
503     private void notifyListenersIfChanged(VerisureThingDTO thing, VerisureInstallation installation, String deviceId) {
504         String normalizedDeviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId);
505         thing.setDeviceId(normalizedDeviceId);
506         thing.setSiteId(installation.getInstallationId());
507         thing.setSiteName(installation.getInstallationName());
508         VerisureThingDTO oldObj = verisureThings.get(normalizedDeviceId);
509         if (!thing.equals(oldObj)) {
510             verisureThings.put(thing.getDeviceId(), thing);
511             notifyListeners(thing);
512         } else {
513             logger.trace("No need to notify listeners for thing: {}", thing);
514         }
515     }
516
517     private void updateStatus() {
518         logger.debug("Update status");
519         verisureInstallations.forEach((installationId, installation) -> {
520             try {
521                 configureInstallationInstance(installation.getInstallationId());
522                 int httpResultCode = setSessionCookieAuthLogin();
523                 if (httpResultCode == HttpStatus.OK_200) {
524                     updateAlarmStatus(installation);
525                     updateSmartLockStatus(installation);
526                     updateMiceDetectionStatus(installation);
527                     updateClimateStatus(installation);
528                     updateDoorWindowStatus(installation);
529                     updateUserPresenceStatus(installation);
530                     updateSmartPlugStatus(installation);
531                     updateBroadbandConnectionStatus(installation);
532                     updateEventLogStatus(installation);
533                     updateGatewayStatus(installation);
534                 } else {
535                     logger.debug("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode);
536                 }
537             } catch (ExecutionException | InterruptedException | TimeoutException e) {
538                 logger.debug("Failed to update status {}", e.getMessage());
539             }
540         });
541     }
542
543     private String createOperationJSON(String operation, VariablesDTO variables, String query) {
544         OperationDTO operationJSON = new OperationDTO();
545         operationJSON.setOperationName(operation);
546         operationJSON.setVariables(variables);
547         operationJSON.setQuery(query);
548         return gson.toJson(Collections.singletonList(operationJSON));
549     }
550
551     private synchronized void updateAlarmStatus(VerisureInstallation installation) {
552         BigDecimal installationId = installation.getInstallationId();
553         String url = START_GRAPHQL;
554         String operation = "ArmState";
555         VariablesDTO variables = new VariablesDTO();
556         variables.setGiid(installationId.toString());
557         String query = "query " + operation
558                 + "($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";
559
560         String queryQLAlarmStatus = createOperationJSON(operation, variables, query);
561         logger.debug("Quering API for alarm status!");
562         try {
563             VerisureThingDTO thing = postJSONVerisureAPI(url, queryQLAlarmStatus, VerisureAlarmsDTO.class);
564             logger.debug("REST Response ({})", thing);
565             // Set unique deviceID
566             String deviceId = "alarm" + installationId;
567             thing.setDeviceId(deviceId);
568             notifyListenersIfChanged(thing, installation, deviceId);
569         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
570                 | PostToAPIException e) {
571             logger.warn("Failed to send a POST to the API {}", e.getMessage());
572         }
573     }
574
575     private synchronized void updateSmartLockStatus(VerisureInstallation installation) {
576         BigDecimal installationId = installation.getInstallationId();
577         String url = START_GRAPHQL;
578         String operation = "DoorLock";
579         String query = "query " + operation
580                 + "($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";
581         VariablesDTO variables = new VariablesDTO();
582         variables.setGiid(installationId.toString());
583         String queryQLSmartLock = createOperationJSON(operation, variables, query);
584         logger.debug("Quering API for smart lock status");
585
586         try {
587             VerisureSmartLocksDTO thing = postJSONVerisureAPI(url, queryQLSmartLock, VerisureSmartLocksDTO.class);
588             logger.debug("REST Response ({})", thing);
589             List<VerisureSmartLocksDTO.Doorlock> doorLockList = thing.getData().getInstallation().getDoorlocks();
590             doorLockList.forEach(doorLock -> {
591                 VerisureSmartLocksDTO slThing = new VerisureSmartLocksDTO();
592                 VerisureSmartLocksDTO.Installation inst = new VerisureSmartLocksDTO.Installation();
593                 inst.setDoorlocks(Collections.singletonList(doorLock));
594                 VerisureSmartLocksDTO.Data data = new VerisureSmartLocksDTO.Data();
595                 data.setInstallation(inst);
596                 slThing.setData(data);
597                 // Set unique deviceID
598                 String deviceId = doorLock.getDevice().getDeviceLabel();
599                 if (deviceId != null) {
600                     // Set location
601                     slThing.setLocation(doorLock.getDevice().getArea());
602                     slThing.setDeviceId(deviceId);
603                     // Fetch more info from old endpoint
604                     try {
605                         VerisureSmartLockDTO smartLockThing = getJSONVerisureAPI(SMARTLOCK_PATH + slThing.getDeviceId(),
606                                 VerisureSmartLockDTO.class);
607                         logger.debug("REST Response ({})", smartLockThing);
608                         slThing.setSmartLockJSON(smartLockThing);
609                         notifyListenersIfChanged(slThing, installation, deviceId);
610                     } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException e) {
611                         logger.warn("Failed to query for smartlock status: {}", e.getMessage());
612                     }
613                 }
614             });
615
616         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
617                 | PostToAPIException e) {
618             logger.warn("Failed to send a POST to the API {}", e.getMessage());
619         }
620     }
621
622     private synchronized void updateSmartPlugStatus(VerisureInstallation installation) {
623         BigDecimal installationId = installation.getInstallationId();
624         String url = START_GRAPHQL;
625         String operation = "SmartPlug";
626         VariablesDTO variables = new VariablesDTO();
627         variables.setGiid(installationId.toString());
628         String query = "query " + operation
629                 + "($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";
630         String queryQLSmartPlug = createOperationJSON(operation, variables, query);
631         logger.debug("Quering API for smart plug status");
632
633         try {
634             VerisureSmartPlugsDTO thing = postJSONVerisureAPI(url, queryQLSmartPlug, VerisureSmartPlugsDTO.class);
635             logger.debug("REST Response ({})", thing);
636             List<VerisureSmartPlugsDTO.Smartplug> smartPlugList = thing.getData().getInstallation().getSmartplugs();
637             smartPlugList.forEach(smartPlug -> {
638                 VerisureSmartPlugsDTO spThing = new VerisureSmartPlugsDTO();
639                 VerisureSmartPlugsDTO.Installation inst = new VerisureSmartPlugsDTO.Installation();
640                 inst.setSmartplugs(Collections.singletonList(smartPlug));
641                 VerisureSmartPlugsDTO.Data data = new VerisureSmartPlugsDTO.Data();
642                 data.setInstallation(inst);
643                 spThing.setData(data);
644                 // Set unique deviceID
645                 String deviceId = smartPlug.getDevice().getDeviceLabel();
646                 if (deviceId != null) {
647                     // Set location
648                     spThing.setLocation(smartPlug.getDevice().getArea());
649                     notifyListenersIfChanged(spThing, installation, deviceId);
650                 }
651             });
652         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
653                 | PostToAPIException e) {
654             logger.warn("Failed to send a POST to the API {}", e.getMessage());
655         }
656     }
657
658     private synchronized void updateClimateStatus(VerisureInstallation installation) {
659         BigDecimal installationId = installation.getInstallationId();
660         String url = START_GRAPHQL;
661         VariablesDTO variables = new VariablesDTO();
662         variables.setGiid(installationId.toString());
663         String operation = "Climate";
664         String query = "query " + operation
665                 + "($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";
666
667         String queryQLClimates = createOperationJSON(operation, variables, query);
668         logger.debug("Quering API for climate status");
669
670         try {
671             VerisureClimatesDTO thing = postJSONVerisureAPI(url, queryQLClimates, VerisureClimatesDTO.class);
672             logger.debug("REST Response ({})", thing);
673             List<VerisureClimatesDTO.Climate> climateList = thing.getData().getInstallation().getClimates();
674             climateList.forEach(climate -> {
675                 // If thing is Mouse detection device, then skip it, but fetch temperature from it
676                 String type = climate.getDevice().getGui().getLabel();
677                 if ("MOUSE".equals(type)) {
678                     logger.debug("Mouse detection device!");
679                     String deviceId = climate.getDevice().getDeviceLabel();
680                     if (deviceId != null) {
681                         deviceId = VerisureThingConfiguration.normalizeDeviceId(deviceId);
682                         VerisureThingDTO mouseThing = verisureThings.get(deviceId);
683                         if (mouseThing != null && mouseThing instanceof VerisureMiceDetectionDTO) {
684                             VerisureMiceDetectionDTO miceDetectorThing = (VerisureMiceDetectionDTO) mouseThing;
685                             miceDetectorThing.setTemperatureValue(climate.getTemperatureValue());
686                             miceDetectorThing.setTemperatureTime(climate.getTemperatureTimestamp());
687                             notifyListeners(miceDetectorThing);
688                             logger.debug("Found climate thing for a Verisure Mouse Detector");
689                         }
690                     }
691                     return;
692                 }
693                 VerisureClimatesDTO cThing = new VerisureClimatesDTO();
694                 VerisureClimatesDTO.Installation inst = new VerisureClimatesDTO.Installation();
695                 inst.setClimates(Collections.singletonList(climate));
696                 VerisureClimatesDTO.Data data = new VerisureClimatesDTO.Data();
697                 data.setInstallation(inst);
698                 cThing.setData(data);
699                 // Set unique deviceID
700                 String deviceId = climate.getDevice().getDeviceLabel();
701                 if (deviceId != null) {
702                     // Set location
703                     cThing.setLocation(climate.getDevice().getArea());
704                     notifyListenersIfChanged(cThing, installation, deviceId);
705                 }
706             });
707         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
708                 | PostToAPIException e) {
709             logger.warn("Failed to send a POST to the API {}", e.getMessage());
710         }
711     }
712
713     private synchronized void updateDoorWindowStatus(VerisureInstallation installation) {
714         BigDecimal installationId = installation.getInstallationId();
715         String url = START_GRAPHQL;
716         String operation = "DoorWindow";
717         VariablesDTO variables = new VariablesDTO();
718         variables.setGiid(installationId.toString());
719         String query = "query " + operation
720                 + "($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";
721
722         String queryQLDoorWindow = createOperationJSON(operation, variables, query);
723         logger.debug("Quering API for door&window status");
724
725         try {
726             VerisureDoorWindowsDTO thing = postJSONVerisureAPI(url, queryQLDoorWindow, VerisureDoorWindowsDTO.class);
727             logger.debug("REST Response ({})", thing);
728             List<VerisureDoorWindowsDTO.DoorWindow> doorWindowList = thing.getData().getInstallation().getDoorWindows();
729             doorWindowList.forEach(doorWindow -> {
730                 VerisureDoorWindowsDTO dThing = new VerisureDoorWindowsDTO();
731                 VerisureDoorWindowsDTO.Installation inst = new VerisureDoorWindowsDTO.Installation();
732                 inst.setDoorWindows(Collections.singletonList(doorWindow));
733                 VerisureDoorWindowsDTO.Data data = new VerisureDoorWindowsDTO.Data();
734                 data.setInstallation(inst);
735                 dThing.setData(data);
736                 // Set unique deviceID
737                 String deviceId = doorWindow.getDevice().getDeviceLabel();
738                 if (deviceId != null) {
739                     // Set location
740                     dThing.setLocation(doorWindow.getDevice().getArea());
741                     notifyListenersIfChanged(dThing, installation, deviceId);
742                 }
743             });
744         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
745                 | PostToAPIException e) {
746             logger.warn("Failed to send a POST to the API {}", e.getMessage());
747         }
748     }
749
750     private synchronized void updateBroadbandConnectionStatus(VerisureInstallation inst) {
751         BigDecimal installationId = inst.getInstallationId();
752         String url = START_GRAPHQL;
753         String operation = "Broadband";
754         VariablesDTO variables = new VariablesDTO();
755         variables.setGiid(installationId.toString());
756         String query = "query " + operation
757                 + "($giid: String!) {\n installation(giid: $giid) {\n broadband {\n testDate\n isBroadbandConnected\n __typename\n }\n __typename\n}\n}\n";
758
759         String queryQLBroadbandConnection = createOperationJSON(operation, variables, query);
760         logger.debug("Quering API for broadband connection status");
761
762         try {
763             VerisureThingDTO thing = postJSONVerisureAPI(url, queryQLBroadbandConnection,
764                     VerisureBroadbandConnectionsDTO.class);
765             logger.debug("REST Response ({})", thing);
766             // Set unique deviceID
767             String deviceId = "bc" + installationId;
768             notifyListenersIfChanged(thing, inst, deviceId);
769         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
770                 | PostToAPIException e) {
771             logger.warn("Failed to send a POST to the API {}", e.getMessage());
772         }
773     }
774
775     private synchronized void updateUserPresenceStatus(VerisureInstallation installation) {
776         BigDecimal installationId = installation.getInstallationId();
777         String url = START_GRAPHQL;
778         String operation = "userTrackings";
779         VariablesDTO variables = new VariablesDTO();
780         variables.setGiid(installationId.toString());
781         String query = "query " + operation
782                 + "($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";
783
784         String queryQLUserPresence = createOperationJSON(operation, variables, query);
785         logger.debug("Quering API for user presence status");
786
787         try {
788             VerisureUserPresencesDTO thing = postJSONVerisureAPI(url, queryQLUserPresence,
789                     VerisureUserPresencesDTO.class);
790             logger.debug("REST Response ({})", thing);
791             List<VerisureUserPresencesDTO.UserTracking> userTrackingList = thing.getData().getInstallation()
792                     .getUserTrackings();
793             userTrackingList.forEach(userTracking -> {
794                 String localUserTrackingStatus = userTracking.getStatus();
795                 if (localUserTrackingStatus != null && localUserTrackingStatus.equals("ACTIVE")) {
796                     VerisureUserPresencesDTO upThing = new VerisureUserPresencesDTO();
797                     VerisureUserPresencesDTO.Installation inst = new VerisureUserPresencesDTO.Installation();
798                     inst.setUserTrackings(Collections.singletonList(userTracking));
799                     VerisureUserPresencesDTO.Data data = new VerisureUserPresencesDTO.Data();
800                     data.setInstallation(inst);
801                     upThing.setData(data);
802                     // Set unique deviceID
803                     String deviceId = "up" + userTracking.getWebAccount() + installationId;
804                     notifyListenersIfChanged(upThing, installation, deviceId);
805                 }
806             });
807         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
808                 | PostToAPIException e) {
809             logger.warn("Failed to send a POST to the API {}", e.getMessage());
810         }
811     }
812
813     private synchronized void updateMiceDetectionStatus(VerisureInstallation installation) {
814         BigDecimal installationId = installation.getInstallationId();
815         String url = START_GRAPHQL;
816         String operation = "Mouse";
817         VariablesDTO variables = new VariablesDTO();
818         variables.setGiid(installationId.toString());
819         String query = "query " + operation
820                 + "($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";
821
822         String queryQLMiceDetection = createOperationJSON(operation, variables, query);
823         logger.debug("Quering API for mice detection status");
824
825         try {
826             VerisureMiceDetectionDTO thing = postJSONVerisureAPI(url, queryQLMiceDetection,
827                     VerisureMiceDetectionDTO.class);
828             logger.debug("REST Response ({})", thing);
829             List<VerisureMiceDetectionDTO.Mouse> miceList = thing.getData().getInstallation().getMice();
830             miceList.forEach(mouse -> {
831                 VerisureMiceDetectionDTO miceThing = new VerisureMiceDetectionDTO();
832                 VerisureMiceDetectionDTO.Installation inst = new VerisureMiceDetectionDTO.Installation();
833                 inst.setMice(Collections.singletonList(mouse));
834                 VerisureMiceDetectionDTO.Data data = new VerisureMiceDetectionDTO.Data();
835                 data.setInstallation(inst);
836                 miceThing.setData(data);
837                 // Set unique deviceID
838                 String deviceId = mouse.getDevice().getDeviceLabel();
839                 logger.debug("Mouse id: {} for thing: {}", deviceId, mouse);
840                 if (deviceId != null) {
841                     // Set location
842                     miceThing.setLocation(mouse.getDevice().getArea());
843                     notifyListenersIfChanged(miceThing, installation, deviceId);
844                 }
845             });
846         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
847                 | PostToAPIException e) {
848             logger.warn("Failed to send a POST to the API {}", e.getMessage());
849         }
850     }
851
852     private synchronized void updateEventLogStatus(VerisureInstallation installation) {
853         BigDecimal installationId = installation.getInstallationId();
854         String url = START_GRAPHQL;
855         String operation = "EventLog";
856         int offset = 0;
857         int numberOfEvents = this.numberOfEvents;
858         List<String> eventCategories = new ArrayList<>(Arrays.asList("INTRUSION", "FIRE", "SOS", "WATER", "ANIMAL",
859                 "TECHNICAL", "WARNING", "ARM", "DISARM", "LOCK", "UNLOCK", "PICTURE", "CLIMATE", "CAMERA_SETTINGS",
860                 "DOORWINDOW_STATE_OPENED", "DOORWINDOW_STATE_CLOSED", "USERTRACKING"));
861         VariablesDTO variables = new VariablesDTO();
862         variables.setGiid(installationId.toString());
863         variables.setHideNotifications(true);
864         variables.setOffset(offset);
865         variables.setPagesize(numberOfEvents);
866         variables.setEventCategories(eventCategories);
867         String query = "query " + operation
868                 + "($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";
869
870         String queryQLEventLog = createOperationJSON(operation, variables, query);
871         logger.debug("Quering API for event log status");
872
873         try {
874             VerisureEventLogDTO thing = postJSONVerisureAPI(url, queryQLEventLog, VerisureEventLogDTO.class);
875             logger.debug("REST Response ({})", thing);
876             // Set unique deviceID
877             String deviceId = "el" + installationId;
878             notifyListenersIfChanged(thing, installation, deviceId);
879         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
880                 | PostToAPIException e) {
881             logger.warn("Failed to send a POST to the API {}", e.getMessage());
882         }
883     }
884
885     private synchronized void updateGatewayStatus(VerisureInstallation installation) {
886         BigDecimal installationId = installation.getInstallationId();
887         String url = START_GRAPHQL;
888         String operation = "communicationState";
889         VariablesDTO variables = new VariablesDTO();
890         variables.setGiid(installationId.toString());
891
892         String query = "query " + operation
893                 + "($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}";
894
895         String queryQLEventLog = createOperationJSON(operation, variables, query);
896         logger.debug("Quering API for gateway status");
897
898         try {
899             VerisureGatewayDTO thing = postJSONVerisureAPI(url, queryQLEventLog, VerisureGatewayDTO.class);
900             logger.debug("REST Response ({})", thing);
901             // Set unique deviceID
902             List<CommunicationState> communicationStateList = thing.getData().getInstallation().getCommunicationState();
903             if (!communicationStateList.isEmpty()) {
904                 String deviceId = communicationStateList.get(0).getDevice().getDeviceLabel();
905                 if (deviceId != null) {
906                     notifyListenersIfChanged(thing, installation, deviceId);
907                 }
908             }
909         } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException
910                 | PostToAPIException e) {
911             logger.warn("Failed to send a POST to the API {}", e.getMessage());
912         }
913     }
914
915     private final class VerisureInstallation {
916         private @Nullable String installationName;
917         private BigDecimal installationId = BigDecimal.ZERO;
918         private @Nullable String pinCode;
919
920         public @Nullable String getPinCode() {
921             return pinCode;
922         }
923
924         public void setPinCode(@Nullable String pinCode) {
925             this.pinCode = pinCode;
926         }
927
928         public VerisureInstallation() {
929         }
930
931         public BigDecimal getInstallationId() {
932             return installationId;
933         }
934
935         public @Nullable String getInstallationName() {
936             return installationName;
937         }
938
939         public void setInstallationId(BigDecimal installationId) {
940             this.installationId = installationId;
941         }
942
943         public void setInstallationName(@Nullable String installationName) {
944             this.installationName = installationName;
945         }
946     }
947
948     private static class OperationDTO {
949
950         @SuppressWarnings("unused")
951         private @Nullable String operationName;
952         @SuppressWarnings("unused")
953         private VariablesDTO variables = new VariablesDTO();
954         @SuppressWarnings("unused")
955         private @Nullable String query;
956
957         public void setOperationName(String operationName) {
958             this.operationName = operationName;
959         }
960
961         public void setVariables(VariablesDTO variables) {
962             this.variables = variables;
963         }
964
965         public void setQuery(String query) {
966             this.query = query;
967         }
968     }
969
970     public static class VariablesDTO {
971
972         @SuppressWarnings("unused")
973         private boolean hideNotifications;
974         @SuppressWarnings("unused")
975         private int offset;
976         @SuppressWarnings("unused")
977         private int pagesize;
978         @SuppressWarnings("unused")
979         private @Nullable List<String> eventCategories = null;
980         @SuppressWarnings("unused")
981         private @Nullable String giid;
982
983         public void setHideNotifications(boolean hideNotifications) {
984             this.hideNotifications = hideNotifications;
985         }
986
987         public void setOffset(int offset) {
988             this.offset = offset;
989         }
990
991         public void setPagesize(int pagesize) {
992             this.pagesize = pagesize;
993         }
994
995         public void setEventCategories(List<String> eventCategories) {
996             this.eventCategories = eventCategories;
997         }
998
999         public void setGiid(String giid) {
1000             this.giid = giid;
1001         }
1002     }
1003
1004     private class PostToAPIException extends Exception {
1005
1006         private static final long serialVersionUID = 1L;
1007
1008         public PostToAPIException(String message) {
1009             super(message);
1010         }
1011     }
1012 }