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