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