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