]> git.basschouten.com Git - openhab-addons.git/blob
eaa5bc649d69f022c43769f6a03481a8aa4228c8
[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.siemenshvac.internal.network;
14
15 import java.net.CookieStore;
16 import java.net.HttpCookie;
17 import java.net.URI;
18 import java.net.URISyntaxException;
19 import java.util.HashMap;
20 import java.util.Hashtable;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.eclipse.jetty.client.api.ContentResponse;
32 import org.eclipse.jetty.client.api.Request;
33 import org.eclipse.jetty.http.HttpMethod;
34 import org.eclipse.jetty.http.HttpStatus;
35 import org.eclipse.jetty.util.ssl.SslContextFactory;
36 import org.openhab.binding.siemenshvac.internal.handler.SiemensHvacBridgeConfig;
37 import org.openhab.binding.siemenshvac.internal.handler.SiemensHvacBridgeThingHandler;
38 import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadata;
39 import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
40 import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataMenu;
41 import org.openhab.binding.siemenshvac.internal.type.SiemensHvacException;
42 import org.openhab.core.io.net.http.HttpClientFactory;
43 import org.openhab.core.types.Type;
44 import org.osgi.service.component.annotations.Activate;
45 import org.osgi.service.component.annotations.Component;
46 import org.osgi.service.component.annotations.Reference;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import com.google.gson.Gson;
51 import com.google.gson.GsonBuilder;
52 import com.google.gson.JsonElement;
53 import com.google.gson.JsonObject;
54 import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
55
56 /**
57  *
58  * @author Laurent Arnal - Initial contribution
59  */
60 @NonNullByDefault
61 @Component(immediate = true)
62 public class SiemensHvacConnectorImpl implements SiemensHvacConnector {
63
64     private final Logger logger = LoggerFactory.getLogger(SiemensHvacConnectorImpl.class);
65
66     private Map<SiemensHvacRequestHandler, SiemensHvacRequestHandler> currentHandlerRegistry = new ConcurrentHashMap<>();
67     private Map<SiemensHvacRequestHandler, SiemensHvacRequestHandler> handlerInErrorRegistry = new ConcurrentHashMap<>();
68
69     private Map<String, Boolean> oldSessionId = new HashMap<>();
70
71     private final Gson gson;
72     private final Gson gsonWithAdapter;
73
74     private @Nullable String sessionId = null;
75     private @Nullable String sessionIdHttp = null;
76     private @Nullable SiemensHvacBridgeConfig config = null;
77
78     protected final HttpClientFactory httpClientFactory;
79
80     protected HttpClient httpClient;
81
82     private Map<String, Type> updateCommand;
83
84     private int requestCount = 0;
85     private int errorCount = 0;
86     private int timeout = 10;
87     private SiemensHvacRequestListener.ErrorSource errorSource = SiemensHvacRequestListener.ErrorSource.ErrorBridge;
88
89     private @Nullable SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler;
90
91     @Activate
92     public SiemensHvacConnectorImpl(@Reference HttpClientFactory httpClientFactory) {
93         GsonBuilder builder = new GsonBuilder();
94         gson = builder.setPrettyPrinting().create();
95
96         RuntimeTypeAdapterFactory<SiemensHvacMetadata> adapter = RuntimeTypeAdapterFactory
97                 .of(SiemensHvacMetadata.class);
98         adapter.registerSubtype(SiemensHvacMetadataMenu.class);
99         adapter.registerSubtype(SiemensHvacMetadataDataPoint.class);
100
101         gsonWithAdapter = new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create();
102
103         this.updateCommand = new Hashtable<String, Type>();
104         this.httpClientFactory = httpClientFactory;
105
106         SslContextFactory ctxFactory = new SslContextFactory.Client(true);
107         ctxFactory.setRenegotiationAllowed(false);
108         ctxFactory.setEnableCRLDP(false);
109         ctxFactory.setEnableOCSP(false);
110         ctxFactory.setTrustAll(true);
111         ctxFactory.setValidateCerts(false);
112         ctxFactory.setValidatePeerCerts(false);
113         ctxFactory.setEndpointIdentificationAlgorithm(null);
114
115         this.httpClient = new HttpClient(ctxFactory);
116         this.httpClient.setMaxConnectionsPerDestination(10);
117         this.httpClient.setMaxRequestsQueuedPerDestination(10000);
118         this.httpClient.setConnectTimeout(10000);
119         this.httpClient.setFollowRedirects(false);
120
121         try {
122             this.httpClient.start();
123         } catch (Exception e) {
124             logger.error("Failed to start http client: {}", e.getMessage());
125         }
126     }
127
128     @Override
129     public void setSiemensHvacBridgeBaseThingHandler(
130             @Nullable SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler) {
131         this.hvacBridgeBaseThingHandler = hvacBridgeBaseThingHandler;
132     }
133
134     public void unsetSiemensHvacBridgeBaseThingHandler(SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler) {
135         this.hvacBridgeBaseThingHandler = null;
136     }
137
138     @Override
139     public void onComplete(@Nullable Request request, SiemensHvacRequestHandler reqHandler)
140             throws SiemensHvacException {
141         unregisterRequestHandler(reqHandler);
142     }
143
144     public static String extractSessionId(String query) {
145         int idx1 = query.indexOf("SessionId=");
146         int idx2 = query.indexOf("&", idx1 + 1);
147         if (idx2 < 0) {
148             idx2 = query.length();
149         }
150
151         String sessionId = query.substring(idx1 + 10, idx2);
152         return sessionId;
153     }
154
155     @Override
156     public void onError(@Nullable Request request, @Nullable SiemensHvacRequestHandler reqHandler,
157             SiemensHvacRequestListener.ErrorSource errorSource, boolean mayRetry) throws SiemensHvacException {
158         if (reqHandler == null || request == null) {
159             throw new SiemensHvacException("internalError: onError call with reqHandler == null");
160         }
161
162         boolean doRetry = mayRetry;
163         // Don't retry if we have do it multiple time
164         if (reqHandler.getRetryCount() >= 5) {
165             doRetry = false;
166         }
167
168         // Don't retry if we lost session, just abort the request, and wait next loop
169         if (sessionIdHttp == null || sessionId == null) {
170             doRetry = false;
171         }
172
173         if (!doRetry) {
174             logger.debug("unable to handle request, doRetry = false, cancel it");
175             unregisterRequestHandler(reqHandler);
176             registerHandlerError(reqHandler);
177             errorCount++;
178             this.errorSource = errorSource;
179             return;
180         }
181
182         try {
183             // Wait one second before retrying the request to avoid flooding the gateway
184             Thread.sleep(1000);
185         } catch (InterruptedException ex) {
186             // We can silently ignore this one
187         }
188
189         if (sessionIdHttp == null) {
190             doAuth(true);
191         }
192
193         if (sessionId == null) {
194             doAuth(false);
195         }
196
197         try {
198             URI uri = request.getURI();
199             String query = uri.toString();
200
201             String sessionIdInQuery = extractSessionId(query);
202             if (query.indexOf("main.app") >= 0) {
203                 String sessionIdHttpLc = sessionIdHttp;
204
205                 if (sessionIdHttpLc != null && !sessionIdHttpLc.equals(sessionIdInQuery)) {
206                     uri = new URI(query.replace(sessionIdInQuery, sessionIdHttpLc));
207                 }
208             } else {
209                 String sessionIdLc = sessionId;
210
211                 if (sessionIdLc != null && !sessionIdLc.equals(sessionIdInQuery)) {
212                     uri = new URI(query.replace(sessionIdInQuery, sessionIdLc));
213                 }
214             }
215
216             final Request retryRequest = httpClient.newRequest(uri);
217             request.method(HttpMethod.GET);
218             reqHandler.setRequest(retryRequest);
219             reqHandler.incrementRetryCount();
220
221             if (retryRequest != null) {
222                 executeRequest(retryRequest, reqHandler);
223             }
224         } catch (URISyntaxException ex) {
225             throw new SiemensHvacException("Error during gateway request", ex);
226         }
227     }
228
229     private @Nullable ContentResponse executeRequest(final Request request) throws SiemensHvacException {
230         return executeRequest(request, (SiemensHvacCallback) null);
231     }
232
233     private @Nullable ContentResponse executeRequest(final Request request, @Nullable SiemensHvacCallback callback)
234             throws SiemensHvacException {
235         requestCount++;
236
237         // For asynchronous request, we create a RequestHandler that will enable us to follow request state
238         SiemensHvacRequestHandler requestHandler = null;
239         if (callback != null) {
240             requestHandler = new SiemensHvacRequestHandler(callback, this);
241             requestHandler.setRequest(request);
242             currentHandlerRegistry.put(requestHandler, requestHandler);
243         }
244
245         return executeRequest(request, requestHandler);
246     }
247
248     private void unregisterRequestHandler(SiemensHvacRequestHandler handler) throws SiemensHvacException {
249         synchronized (currentHandlerRegistry) {
250             if (currentHandlerRegistry.containsKey(handler)) {
251                 currentHandlerRegistry.remove(handler);
252             }
253         }
254     }
255
256     private void registerHandlerError(SiemensHvacRequestHandler handler) {
257         synchronized (handlerInErrorRegistry) {
258             handlerInErrorRegistry.put(handler, handler);
259         }
260     }
261
262     private @Nullable ContentResponse executeRequest(final Request request,
263             @Nullable SiemensHvacRequestHandler requestHandler) throws SiemensHvacException {
264         // Give a high timeout because we queue a lot of async request,
265         // so enqueued them will take some times ...
266         request.timeout(timeout, TimeUnit.SECONDS);
267
268         ContentResponse response = null;
269
270         try {
271             if (requestHandler != null) {
272                 SiemensHvacRequestListener requestListener = new SiemensHvacRequestListener(requestHandler);
273                 request.send(requestListener);
274             } else {
275                 response = request.send();
276             }
277         } catch (InterruptedException | TimeoutException | ExecutionException e) {
278             throw new SiemensHvacException("siemensHvac:Exception by executing request: "
279                     + anominized(request.getURI().toString()) + " ; " + e.getLocalizedMessage());
280         }
281         return response;
282     }
283
284     private void initConfig() throws SiemensHvacException {
285         SiemensHvacBridgeThingHandler lcHvacBridgeBaseThingHandler = hvacBridgeBaseThingHandler;
286
287         if (lcHvacBridgeBaseThingHandler != null) {
288             config = lcHvacBridgeBaseThingHandler.getBridgeConfiguration();
289         } else {
290             throw new SiemensHvacException(
291                     "siemensHvac:Exception unable to get config because hvacBridgeBaseThingHandler is null");
292         }
293     }
294
295     @Override
296     public @Nullable SiemensHvacBridgeConfig getBridgeConfiguration() {
297         return config;
298     }
299
300     private void doAuth(boolean http) throws SiemensHvacException {
301         synchronized (this) {
302             logger.debug("siemensHvac:doAuth()");
303
304             initConfig();
305
306             SiemensHvacBridgeConfig config = this.config;
307             if (config == null) {
308                 throw new SiemensHvacException("Missing SiemensHvacOZW Bridge configuration");
309             }
310
311             String baseUri = config.baseUrl;
312             String uri = "";
313
314             if (http) {
315                 uri = "main.app";
316             } else {
317                 uri = String.format("api/auth/login.json?user=%s&pwd=%s", config.userName, config.userPassword);
318             }
319
320             final Request request = httpClient.newRequest(baseUri + uri);
321             if (http) {
322                 request.method(HttpMethod.POST).param("user", config.userName).param("pwd", config.userPassword);
323             } else {
324                 request.method(HttpMethod.GET);
325             }
326
327             logger.debug("siemensHvac:doAuth:connect()");
328
329             ContentResponse response = executeRequest(request);
330             if (response != null) {
331                 int statusCode = response.getStatus();
332
333                 if (statusCode == HttpStatus.OK_200) {
334                     String result = response.getContentAsString();
335
336                     if (http) {
337                         CookieStore cookieStore = httpClient.getCookieStore();
338                         List<HttpCookie> cookies = cookieStore.getCookies();
339
340                         for (HttpCookie httpCookie : cookies) {
341                             if (httpCookie.getName().equals("SessionId")) {
342                                 sessionIdHttp = httpCookie.getValue();
343                             }
344
345                         }
346
347                         if (sessionIdHttp == null) {
348                             logger.debug("Session request auth was unsuccessful in _doAuth()");
349                         }
350                     } else {
351                         if (result != null) {
352                             JsonObject resultObj = getGson().fromJson(result, JsonObject.class);
353
354                             if (resultObj != null && resultObj.has("Result")) {
355                                 JsonElement resultVal = resultObj.get("Result");
356                                 JsonObject resultObj2 = resultVal.getAsJsonObject();
357
358                                 if (resultObj2.has("Success")) {
359                                     boolean successVal = resultObj2.get("Success").getAsBoolean();
360
361                                     if (successVal) {
362                                         if (resultObj.has("SessionId")) {
363                                             sessionId = resultObj.get("SessionId").getAsString();
364                                             logger.debug("Have new SessionId: {} ", sessionId);
365                                         }
366                                     }
367                                 }
368                             }
369
370                             logger.debug("siemensHvac:doAuth:decodeResponse:()");
371
372                             if (sessionId == null) {
373                                 throw new SiemensHvacException(
374                                         "Session request auth was unsuccessful in _doAuth(), please verify login parameters");
375                             }
376                         }
377
378                     }
379                 }
380             }
381
382             logger.trace("siemensHvac:doAuth:connect()");
383         }
384     }
385
386     @Override
387     public @Nullable String doBasicRequest(String uri) throws SiemensHvacException {
388         return doBasicRequest(uri, null);
389     }
390
391     public @Nullable String doBasicRequestAsync(String uri, @Nullable SiemensHvacCallback callback)
392             throws SiemensHvacException {
393         return doBasicRequest(uri, callback);
394     }
395
396     public @Nullable String doBasicRequest(String uri, @Nullable SiemensHvacCallback callback)
397             throws SiemensHvacException {
398         if (sessionIdHttp == null) {
399             doAuth(true);
400         }
401
402         if (sessionId == null) {
403             doAuth(false);
404         }
405
406         SiemensHvacBridgeConfig config = this.config;
407         if (config == null) {
408             throw new SiemensHvacException("Missing SiemensHvac OZW Bridge configuration");
409         }
410
411         String baseUri = config.baseUrl;
412
413         String mUri = uri;
414         if (!mUri.endsWith("?")) {
415             mUri = mUri + "&";
416         }
417         if (mUri.indexOf("main.app") >= 0) {
418             mUri = mUri + "SessionId=" + sessionIdHttp;
419         } else {
420             mUri = mUri + "SessionId=" + sessionId;
421         }
422
423         CookieStore c = httpClient.getCookieStore();
424         java.net.HttpCookie cookie = new HttpCookie("SessionId", sessionIdHttp);
425         cookie.setPath("/");
426         cookie.setVersion(0);
427
428         try {
429             c.add(new URI(baseUri), cookie);
430         } catch (URISyntaxException ex) {
431             throw new SiemensHvacException(String.format("URI is not correctly formatted: %s", baseUri), ex);
432         }
433
434         logger.debug("Execute request: {}", uri);
435         final Request request = httpClient.newRequest(baseUri + mUri);
436         request.method(HttpMethod.GET);
437
438         ContentResponse response = executeRequest(request, callback);
439         if (callback == null && response != null) {
440             int statusCode = response.getStatus();
441
442             if (statusCode == HttpStatus.OK_200) {
443                 return response.getContentAsString();
444             }
445         }
446
447         return null;
448     }
449
450     @Override
451     public @Nullable JsonObject doRequest(String req) {
452         return doRequest(req, null);
453     }
454
455     @Override
456     public @Nullable JsonObject doRequest(String req, @Nullable SiemensHvacCallback callback) {
457         try {
458             String response = doBasicRequest(req, callback);
459
460             if (response != null) {
461                 JsonObject resultObj = getGson().fromJson(response, JsonObject.class);
462
463                 if (resultObj != null && resultObj.has("Result")) {
464                     JsonObject subResultObj = resultObj.getAsJsonObject("Result");
465
466                     if (subResultObj.has("Success")) {
467                         boolean result = subResultObj.get("Success").getAsBoolean();
468                         if (result) {
469                             return resultObj;
470                         }
471                     }
472
473                 }
474
475                 return null;
476             }
477         } catch (SiemensHvacException e) {
478             logger.warn("siemensHvac:DoRequest:Exception by executing jsonRequest: {} ; {} ", req,
479                     e.getLocalizedMessage());
480         }
481
482         return null;
483     }
484
485     @Override
486     public void displayRequestStats() {
487         logger.debug("DisplayRequestStats: ");
488         logger.debug("    currentRuning   : {}", getCurrentHandlerRegistryCount());
489         logger.debug("    errors          : {}", getHandlerInErrorRegistryCount());
490     }
491
492     @Override
493     public void waitAllPendingRequest() {
494         logger.debug("WaitAllPendingRequest:start");
495         try {
496             boolean allRequestDone = false;
497             int idx = 0;
498
499             while (!allRequestDone) {
500                 allRequestDone = false;
501                 int currentRequestCount = getCurrentHandlerRegistryCount();
502
503                 logger.debug("WaitAllPendingRequest:waitAllRequestDone {}: {}", idx, currentRequestCount);
504
505                 if (currentRequestCount == 0) {
506                     allRequestDone = true;
507                 }
508                 Thread.sleep(1000);
509
510                 if ((idx % 50) == 0) {
511                     checkStaleRequest();
512                 }
513                 idx++;
514             }
515         } catch (InterruptedException ex) {
516             logger.debug("WaitAllPendingRequest:interrupted in WaitAllRequest");
517         }
518
519         logger.debug("WaitAllPendingRequest:end WaitAllPendingRequest");
520     }
521
522     public void checkStaleRequest() {
523         synchronized (currentHandlerRegistry) {
524             logger.debug("check stale request::begin");
525             int staleRequest = 0;
526
527             for (SiemensHvacRequestHandler handler : currentHandlerRegistry.keySet()) {
528                 long elapseTime = handler.getElapsedTime();
529                 if (elapseTime > 150) {
530                     String uri = "";
531                     Request request = handler.getRequest();
532                     if (request != null) {
533                         uri = request.getURI().toString();
534                     }
535                     logger.debug("find stale request: {} {}", elapseTime, anominized(uri));
536                     staleRequest++;
537
538                     try {
539                         unregisterRequestHandler(handler);
540                         registerHandlerError(handler);
541                     } catch (SiemensHvacException ex) {
542                         logger.debug("error unregistring handler: {}", handler);
543                     }
544
545                 }
546             }
547
548             logger.debug("check stale request::end: {}", staleRequest);
549         }
550     }
551
552     public String anominized(String uri) {
553         int p0 = uri.indexOf("pwd=");
554         if (p0 > 0) {
555             return uri.substring(0, p0) + "pwd=xxxxx";
556         }
557
558         return uri;
559     }
560
561     private int getCurrentHandlerRegistryCount() {
562         synchronized (currentHandlerRegistry) {
563             return currentHandlerRegistry.keySet().size();
564         }
565     }
566
567     private int getHandlerInErrorRegistryCount() {
568         synchronized (handlerInErrorRegistry) {
569             return handlerInErrorRegistry.keySet().size();
570         }
571     }
572
573     @Override
574     public void waitNoNewRequest() {
575         logger.debug("WaitNoNewRequest:start");
576         try {
577             int lastRequestCount = getCurrentHandlerRegistryCount();
578             boolean newRequest = true;
579             while (newRequest) {
580                 Thread.sleep(5000);
581                 int newRequestCount = getCurrentHandlerRegistryCount();
582                 if (newRequestCount != lastRequestCount) {
583                     logger.debug("waitNoNewRequest  {}/{})", newRequestCount, lastRequestCount);
584                     lastRequestCount = newRequestCount;
585                 } else {
586                     newRequest = false;
587                 }
588             }
589         } catch (InterruptedException ex) {
590             logger.debug("WaitAllPendingRequest:interrupted in WaitAllRequest");
591         }
592
593         logger.debug("WaitNoNewRequest:end WaitAllStartingRequest");
594     }
595
596     @Override
597     public Gson getGson() {
598         return gson;
599     }
600
601     @Override
602     public Gson getGsonWithAdapter() {
603         return gsonWithAdapter;
604     }
605
606     public void addDpUpdate(String itemName, Type dp) {
607         synchronized (updateCommand) {
608             updateCommand.put(itemName, dp);
609         }
610     }
611
612     @Override
613     public void resetSessionId(@Nullable String sessionIdToInvalidate, boolean web) {
614         if (web) {
615             if (sessionIdToInvalidate == null) {
616                 sessionIdHttp = null;
617             } else {
618                 if (!oldSessionId.containsKey(sessionIdToInvalidate) && sessionIdToInvalidate.equals(sessionIdHttp)) {
619                     oldSessionId.put(sessionIdToInvalidate, true);
620
621                     logger.debug("Invalidate sessionIdHttp: {}", sessionIdToInvalidate);
622                     sessionIdHttp = null;
623                 }
624             }
625         } else {
626             if (sessionIdToInvalidate == null) {
627                 sessionId = null;
628             } else {
629                 if (!oldSessionId.containsKey(sessionIdToInvalidate) && sessionIdToInvalidate.equals(sessionId)) {
630                     oldSessionId.put(sessionIdToInvalidate, true);
631
632                     logger.debug("Invalidate sessionId: {}", sessionIdToInvalidate);
633                     sessionId = null;
634                 }
635             }
636         }
637     }
638
639     @Override
640     public int getRequestCount() {
641         return requestCount;
642     }
643
644     @Override
645     public int getErrorCount() {
646         return errorCount;
647     }
648
649     @Override
650     public SiemensHvacRequestListener.ErrorSource getErrorSource() {
651         return errorSource;
652     }
653
654     @Override
655     public void invalidate() {
656         sessionId = null;
657         sessionIdHttp = null;
658
659         synchronized (currentHandlerRegistry) {
660             currentHandlerRegistry.clear();
661             handlerInErrorRegistry.clear();
662         }
663     }
664
665     @Override
666     public void setTimeOut(int timeout) {
667         this.timeout = timeout;
668     }
669 }