2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.siemenshvac.internal.network;
15 import java.net.CookieStore;
16 import java.net.HttpCookie;
18 import java.net.URISyntaxException;
19 import java.util.HashMap;
20 import java.util.Hashtable;
21 import java.util.List;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
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;
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;
58 * @author Laurent Arnal - Initial contribution
61 @Component(immediate = true)
62 public class SiemensHvacConnectorImpl implements SiemensHvacConnector {
64 private final Logger logger = LoggerFactory.getLogger(SiemensHvacConnectorImpl.class);
66 private Map<SiemensHvacRequestHandler, SiemensHvacRequestHandler> currentHandlerRegistry = new ConcurrentHashMap<>();
67 private Map<SiemensHvacRequestHandler, SiemensHvacRequestHandler> handlerInErrorRegistry = new ConcurrentHashMap<>();
69 private Map<String, Boolean> oldSessionId = new HashMap<>();
71 private final Gson gson;
72 private final Gson gsonWithAdapter;
74 private @Nullable String sessionId = null;
75 private @Nullable String sessionIdHttp = null;
76 private @Nullable SiemensHvacBridgeConfig config = null;
78 protected final HttpClientFactory httpClientFactory;
80 protected HttpClient httpClient;
82 private Map<String, Type> updateCommand;
84 private int requestCount = 0;
85 private int errorCount = 0;
86 private int timeout = 10;
87 private SiemensHvacRequestListener.ErrorSource errorSource = SiemensHvacRequestListener.ErrorSource.ErrorBridge;
89 private @Nullable SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler;
92 public SiemensHvacConnectorImpl(@Reference HttpClientFactory httpClientFactory) {
93 GsonBuilder builder = new GsonBuilder();
94 gson = builder.setPrettyPrinting().create();
96 RuntimeTypeAdapterFactory<SiemensHvacMetadata> adapter = RuntimeTypeAdapterFactory
97 .of(SiemensHvacMetadata.class);
98 adapter.registerSubtype(SiemensHvacMetadataMenu.class);
99 adapter.registerSubtype(SiemensHvacMetadataDataPoint.class);
101 gsonWithAdapter = new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create();
103 this.updateCommand = new Hashtable<String, Type>();
104 this.httpClientFactory = httpClientFactory;
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);
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);
122 this.httpClient.start();
123 } catch (Exception e) {
124 logger.error("Failed to start http client: {}", e.getMessage());
129 public void setSiemensHvacBridgeBaseThingHandler(
130 @Nullable SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler) {
131 this.hvacBridgeBaseThingHandler = hvacBridgeBaseThingHandler;
134 public void unsetSiemensHvacBridgeBaseThingHandler(SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler) {
135 this.hvacBridgeBaseThingHandler = null;
139 public void onComplete(@Nullable Request request, SiemensHvacRequestHandler reqHandler)
140 throws SiemensHvacException {
141 unregisterRequestHandler(reqHandler);
144 public static String extractSessionId(String query) {
145 int idx1 = query.indexOf("SessionId=");
146 int idx2 = query.indexOf("&", idx1 + 1);
148 idx2 = query.length();
151 String sessionId = query.substring(idx1 + 10, idx2);
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");
162 boolean doRetry = mayRetry;
163 // Don't retry if we have do it multiple time
164 if (reqHandler.getRetryCount() >= 5) {
168 // Don't retry if we lost session, just abort the request, and wait next loop
169 if (sessionIdHttp == null || sessionId == null) {
174 logger.debug("unable to handle request, doRetry = false, cancel it");
175 unregisterRequestHandler(reqHandler);
176 registerHandlerError(reqHandler);
178 this.errorSource = errorSource;
183 // Wait one second before retrying the request to avoid flooding the gateway
185 } catch (InterruptedException ex) {
186 // We can silently ignore this one
189 if (sessionIdHttp == null) {
193 if (sessionId == null) {
198 URI uri = request.getURI();
199 String query = uri.toString();
201 String sessionIdInQuery = extractSessionId(query);
202 if (query.indexOf("main.app") >= 0) {
203 String sessionIdHttpLc = sessionIdHttp;
205 if (sessionIdHttpLc != null && !sessionIdHttpLc.equals(sessionIdInQuery)) {
206 uri = new URI(query.replace(sessionIdInQuery, sessionIdHttpLc));
209 String sessionIdLc = sessionId;
211 if (sessionIdLc != null && !sessionIdLc.equals(sessionIdInQuery)) {
212 uri = new URI(query.replace(sessionIdInQuery, sessionIdLc));
216 final Request retryRequest = httpClient.newRequest(uri);
217 request.method(HttpMethod.GET);
218 reqHandler.setRequest(retryRequest);
219 reqHandler.incrementRetryCount();
221 if (retryRequest != null) {
222 executeRequest(retryRequest, reqHandler);
224 } catch (URISyntaxException ex) {
225 throw new SiemensHvacException("Error during gateway request", ex);
229 private @Nullable ContentResponse executeRequest(final Request request) throws SiemensHvacException {
230 return executeRequest(request, (SiemensHvacCallback) null);
233 private @Nullable ContentResponse executeRequest(final Request request, @Nullable SiemensHvacCallback callback)
234 throws SiemensHvacException {
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);
245 return executeRequest(request, requestHandler);
248 private void unregisterRequestHandler(SiemensHvacRequestHandler handler) throws SiemensHvacException {
249 synchronized (currentHandlerRegistry) {
250 if (currentHandlerRegistry.containsKey(handler)) {
251 currentHandlerRegistry.remove(handler);
256 private void registerHandlerError(SiemensHvacRequestHandler handler) {
257 synchronized (handlerInErrorRegistry) {
258 handlerInErrorRegistry.put(handler, handler);
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);
268 ContentResponse response = null;
271 if (requestHandler != null) {
272 SiemensHvacRequestListener requestListener = new SiemensHvacRequestListener(requestHandler);
273 request.send(requestListener);
275 response = request.send();
277 } catch (InterruptedException | TimeoutException | ExecutionException e) {
278 throw new SiemensHvacException("siemensHvac:Exception by executing request: "
279 + anominized(request.getURI().toString()) + " ; " + e.getLocalizedMessage());
284 private void initConfig() throws SiemensHvacException {
285 SiemensHvacBridgeThingHandler lcHvacBridgeBaseThingHandler = hvacBridgeBaseThingHandler;
287 if (lcHvacBridgeBaseThingHandler != null) {
288 config = lcHvacBridgeBaseThingHandler.getBridgeConfiguration();
290 throw new SiemensHvacException(
291 "siemensHvac:Exception unable to get config because hvacBridgeBaseThingHandler is null");
296 public @Nullable SiemensHvacBridgeConfig getBridgeConfiguration() {
300 private void doAuth(boolean http) throws SiemensHvacException {
301 synchronized (this) {
302 logger.debug("siemensHvac:doAuth()");
306 SiemensHvacBridgeConfig config = this.config;
307 if (config == null) {
308 throw new SiemensHvacException("Missing SiemensHvacOZW Bridge configuration");
311 String baseUri = config.baseUrl;
317 uri = String.format("api/auth/login.json?user=%s&pwd=%s", config.userName, config.userPassword);
320 final Request request = httpClient.newRequest(baseUri + uri);
322 request.method(HttpMethod.POST).param("user", config.userName).param("pwd", config.userPassword);
324 request.method(HttpMethod.GET);
327 logger.debug("siemensHvac:doAuth:connect()");
329 ContentResponse response = executeRequest(request);
330 if (response != null) {
331 int statusCode = response.getStatus();
333 if (statusCode == HttpStatus.OK_200) {
334 String result = response.getContentAsString();
337 CookieStore cookieStore = httpClient.getCookieStore();
338 List<HttpCookie> cookies = cookieStore.getCookies();
340 for (HttpCookie httpCookie : cookies) {
341 if (httpCookie.getName().equals("SessionId")) {
342 sessionIdHttp = httpCookie.getValue();
347 if (sessionIdHttp == null) {
348 logger.debug("Session request auth was unsuccessful in _doAuth()");
351 if (result != null) {
352 JsonObject resultObj = getGson().fromJson(result, JsonObject.class);
354 if (resultObj != null && resultObj.has("Result")) {
355 JsonElement resultVal = resultObj.get("Result");
356 JsonObject resultObj2 = resultVal.getAsJsonObject();
358 if (resultObj2.has("Success")) {
359 boolean successVal = resultObj2.get("Success").getAsBoolean();
362 if (resultObj.has("SessionId")) {
363 sessionId = resultObj.get("SessionId").getAsString();
364 logger.debug("Have new SessionId: {} ", sessionId);
370 logger.debug("siemensHvac:doAuth:decodeResponse:()");
372 if (sessionId == null) {
373 throw new SiemensHvacException(
374 "Session request auth was unsuccessful in _doAuth(), please verify login parameters");
382 logger.trace("siemensHvac:doAuth:connect()");
387 public @Nullable String doBasicRequest(String uri) throws SiemensHvacException {
388 return doBasicRequest(uri, null);
391 public @Nullable String doBasicRequestAsync(String uri, @Nullable SiemensHvacCallback callback)
392 throws SiemensHvacException {
393 return doBasicRequest(uri, callback);
396 public @Nullable String doBasicRequest(String uri, @Nullable SiemensHvacCallback callback)
397 throws SiemensHvacException {
398 if (sessionIdHttp == null) {
402 if (sessionId == null) {
406 SiemensHvacBridgeConfig config = this.config;
407 if (config == null) {
408 throw new SiemensHvacException("Missing SiemensHvac OZW Bridge configuration");
411 String baseUri = config.baseUrl;
414 if (!mUri.endsWith("?")) {
417 if (mUri.indexOf("main.app") >= 0) {
418 mUri = mUri + "SessionId=" + sessionIdHttp;
420 mUri = mUri + "SessionId=" + sessionId;
423 CookieStore c = httpClient.getCookieStore();
424 java.net.HttpCookie cookie = new HttpCookie("SessionId", sessionIdHttp);
426 cookie.setVersion(0);
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);
434 logger.debug("Execute request: {}", uri);
435 final Request request = httpClient.newRequest(baseUri + mUri);
436 request.method(HttpMethod.GET);
438 ContentResponse response = executeRequest(request, callback);
439 if (callback == null && response != null) {
440 int statusCode = response.getStatus();
442 if (statusCode == HttpStatus.OK_200) {
443 return response.getContentAsString();
451 public @Nullable JsonObject doRequest(String req) {
452 return doRequest(req, null);
456 public @Nullable JsonObject doRequest(String req, @Nullable SiemensHvacCallback callback) {
458 String response = doBasicRequest(req, callback);
460 if (response != null) {
461 JsonObject resultObj = getGson().fromJson(response, JsonObject.class);
463 if (resultObj != null && resultObj.has("Result")) {
464 JsonObject subResultObj = resultObj.getAsJsonObject("Result");
466 if (subResultObj.has("Success")) {
467 boolean result = subResultObj.get("Success").getAsBoolean();
477 } catch (SiemensHvacException e) {
478 logger.warn("siemensHvac:DoRequest:Exception by executing jsonRequest: {} ; {} ", req,
479 e.getLocalizedMessage());
486 public void displayRequestStats() {
487 logger.debug("DisplayRequestStats: ");
488 logger.debug(" currentRuning : {}", getCurrentHandlerRegistryCount());
489 logger.debug(" errors : {}", getHandlerInErrorRegistryCount());
493 public void waitAllPendingRequest() {
494 logger.debug("WaitAllPendingRequest:start");
496 boolean allRequestDone = false;
499 while (!allRequestDone) {
500 allRequestDone = false;
501 int currentRequestCount = getCurrentHandlerRegistryCount();
503 logger.debug("WaitAllPendingRequest:waitAllRequestDone {}: {}", idx, currentRequestCount);
505 if (currentRequestCount == 0) {
506 allRequestDone = true;
510 if ((idx % 50) == 0) {
515 } catch (InterruptedException ex) {
516 logger.debug("WaitAllPendingRequest:interrupted in WaitAllRequest");
519 logger.debug("WaitAllPendingRequest:end WaitAllPendingRequest");
522 public void checkStaleRequest() {
523 synchronized (currentHandlerRegistry) {
524 logger.debug("check stale request::begin");
525 int staleRequest = 0;
527 for (SiemensHvacRequestHandler handler : currentHandlerRegistry.keySet()) {
528 long elapseTime = handler.getElapsedTime();
529 if (elapseTime > 150) {
531 Request request = handler.getRequest();
532 if (request != null) {
533 uri = request.getURI().toString();
535 logger.debug("find stale request: {} {}", elapseTime, anominized(uri));
539 unregisterRequestHandler(handler);
540 registerHandlerError(handler);
541 } catch (SiemensHvacException ex) {
542 logger.debug("error unregistring handler: {}", handler);
548 logger.debug("check stale request::end: {}", staleRequest);
552 public String anominized(String uri) {
553 int p0 = uri.indexOf("pwd=");
555 return uri.substring(0, p0) + "pwd=xxxxx";
561 private int getCurrentHandlerRegistryCount() {
562 synchronized (currentHandlerRegistry) {
563 return currentHandlerRegistry.keySet().size();
567 private int getHandlerInErrorRegistryCount() {
568 synchronized (handlerInErrorRegistry) {
569 return handlerInErrorRegistry.keySet().size();
574 public void waitNoNewRequest() {
575 logger.debug("WaitNoNewRequest:start");
577 int lastRequestCount = getCurrentHandlerRegistryCount();
578 boolean newRequest = true;
581 int newRequestCount = getCurrentHandlerRegistryCount();
582 if (newRequestCount != lastRequestCount) {
583 logger.debug("waitNoNewRequest {}/{})", newRequestCount, lastRequestCount);
584 lastRequestCount = newRequestCount;
589 } catch (InterruptedException ex) {
590 logger.debug("WaitAllPendingRequest:interrupted in WaitAllRequest");
593 logger.debug("WaitNoNewRequest:end WaitAllStartingRequest");
597 public Gson getGson() {
602 public Gson getGsonWithAdapter() {
603 return gsonWithAdapter;
606 public void addDpUpdate(String itemName, Type dp) {
607 synchronized (updateCommand) {
608 updateCommand.put(itemName, dp);
613 public void resetSessionId(@Nullable String sessionIdToInvalidate, boolean web) {
615 if (sessionIdToInvalidate == null) {
616 sessionIdHttp = null;
618 if (!oldSessionId.containsKey(sessionIdToInvalidate) && sessionIdToInvalidate.equals(sessionIdHttp)) {
619 oldSessionId.put(sessionIdToInvalidate, true);
621 logger.debug("Invalidate sessionIdHttp: {}", sessionIdToInvalidate);
622 sessionIdHttp = null;
626 if (sessionIdToInvalidate == null) {
629 if (!oldSessionId.containsKey(sessionIdToInvalidate) && sessionIdToInvalidate.equals(sessionId)) {
630 oldSessionId.put(sessionIdToInvalidate, true);
632 logger.debug("Invalidate sessionId: {}", sessionIdToInvalidate);
640 public int getRequestCount() {
645 public int getErrorCount() {
650 public SiemensHvacRequestListener.ErrorSource getErrorSource() {
655 public void invalidate() {
657 sessionIdHttp = null;
659 synchronized (currentHandlerRegistry) {
660 currentHandlerRegistry.clear();
661 handlerInErrorRegistry.clear();
666 public void setTimeOut(int timeout) {
667 this.timeout = timeout;