[linktap] Add communication timeout configuration parameter (#18679)

Signed-off-by: David Goodyear <david.goodyear@gmail.com>
This commit is contained in:
David Goodyear
2025-05-24 00:26:56 +02:00
committed by GitHub
parent 06a00e515d
commit fe1dddd70e
10 changed files with 142 additions and 35 deletions
+10 -9
View File
@@ -70,14 +70,15 @@ If the gateway cannot publish to openHAB, then the gateway is checked every 2 mi
### Gateway Configuration
| Name | Type | Description | Recommended Values | Required | Advanced |
|-----------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|----------|----------|
| host | String | The hostname / IP address of the gateway device | | Yes | No |
| username | String | The username if set for the gateway device | | No | No |
| password | String | The password if set for the gateway device | | No | No |
| enableMDNS | Switch | On connection whether the mDNS responder should be enabled on the gateway device | true | No | Yes |
| enforceProtocolLimits | Switch | If true data outside of the allowed ranges against the protocol will be logged and not sent | true | No | Yes |
| enableJSONComms | Switch | false by default for backwards compatibility, if using up to date firmware with no other local network applications set this to true, for more efficient communications | true | No | Yes |
| Name | Type | Description | Recommended Values | Required | Advanced |
|------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|----------|----------|
| host | text | The hostname / IP address of the gateway device | | Yes | No |
| username | text | The username if set for the gateway device | | No | No |
| password | text | The password if set for the gateway device | | No | No |
| enableMDNS | boolean | On connection whether the mDNS responder should be enabled on the gateway device | true | No | Yes |
| enforceProtocolLimits | boolean | If true data outside of the allowed ranges against the protocol will be logged and not sent | true | No | Yes |
| enableJSONComms | boolean | false by default for backwards compatibility, if using up to date firmware with no other local network applications set this to true, for more efficient communications | true | No | Yes |
| gatewayResponseTimeout | integer | For slow or heavily loaded systems this may need increasing, if communication errors are seen (seconds allowed for responses from the gateway) | 3 | No | Yes |
**NOTE** When enableMDNS is enabled, upon connection to the gateway option "Enable mDNS responder" is switched on.
@@ -153,7 +154,7 @@ There are 4 different areas of channels:
- **Device Model**: Q1
```java
Bridge linktap:gateway:home "LinkTap GW02" [ host="192.168.0.21", enableMDNS=true, enableJSONComms=false, enforceProtocolLimits=true ] {
Bridge linktap:gateway:home "LinkTap GW02" [ host="192.168.0.21", enableMDNS=true, enableJSONComms=false, enforceProtocolLimits=true, gatewayResponseTimeout=3 ] {
Thing device TapValve1 "Outdoor Tap 1" [ id="D71BC52E985B1200_1", name="ValveLinker_1", enableAlerts=true ]
Thing device TapValve2 "Outdoor Tap 2" [ id="D71BC52E985B1200_2", name="ValveLinker_2", enableAlerts=true ]
Thing device TapValve3 "Outdoor Tap 3" [ id="D71BC52E985B1200_3", name="ValveLinker_3", enableAlerts=true ]
@@ -28,4 +28,5 @@ public class LinkTapBridgeConfiguration {
public boolean enableMDNS = true;
public boolean enableJSONComms = false;
public boolean enforceProtocolLimits = true;
public int gatewayResponseTimeout = 3;
}
@@ -59,6 +59,7 @@ public class LinkTapBindingConstants {
public static final String BRIDGE_CONFIG_MDNS_ENABLE = "enableMDNS";
public static final String BRIDGE_CONFIG_NON_HTML_COMM_ENABLE = "enableJSONComms";
public static final String BRIDGE_CONFIG_ENFORCE_COMM_LIMITS = "enforceProtocolLimits";
public static final String BRIDGE_CONFIG_GATEWAY_RESPONSE_TIMEOUT = "gatewayResponseTimeout";
public static final String DEVICE_PROP_DEV_ID = "deviceId";
public static final String DEVICE_PROP_DEV_NAME = "deviceName";
@@ -143,6 +143,10 @@ public class LinkTapBridgeHandler extends BaseBridgeHandler {
return Objects.nonNull(result) ? result : key;
}
public int getResponseTimeout() {
return config.gatewayResponseTimeout;
}
private void startGwPolling() {
synchronized (schedulerLock) {
cancelGwPolling();
@@ -379,7 +383,7 @@ public class LinkTapBridgeHandler extends BaseBridgeHandler {
}
final String reqData = LinkTapBindingConstants.GSON.toJson(req);
logger.debug("{} = APP BRIDGE -> GW -> Request {}", uid, reqData);
final String respData = api.sendRequest(host, reqData);
final String respData = api.sendRequest(host, getResponseTimeout(), reqData);
logger.debug("{} = APP BRIDGE -> GW -> Response {}", uid, respData);
final GatewayDeviceResponse gwResponseFrame = LinkTapBindingConstants.GSON.fromJson(respData,
GatewayDeviceResponse.class);
@@ -429,7 +433,7 @@ public class LinkTapBridgeHandler extends BaseBridgeHandler {
final WebServerApi api = WebServerApi.getInstance();
api.setHttpClient(httpClientProvider.getHttpClient());
try {
final Map<String, String> bridgeProps = api.getBridgeProperities(bridgeKey);
final Map<String, String> bridgeProps = api.getBridgeProperities(bridgeKey, getResponseTimeout());
if (!bridgeProps.isEmpty()) {
final String readGwId = bridgeProps.get(BRIDGE_PROP_GW_ID);
if (readGwId != null) {
@@ -439,7 +443,7 @@ public class LinkTapBridgeHandler extends BaseBridgeHandler {
currentProps.putAll(bridgeProps);
updateProperties(currentProps);
} else {
if (!api.unlockWebInterface(bridgeKey, config.username, config.password)) {
if (!api.unlockWebInterface(bridgeKey, getResponseTimeout(), config.username, config.password)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getLocalizedText("bridge.error.check-credentials"));
return;
@@ -481,13 +485,13 @@ public class LinkTapBridgeHandler extends BaseBridgeHandler {
final String servletEp = BindingServlet.getServletAddress(localServerAddr,
getLocalizedText("warning.no-http-server-port"));
final Optional<String> servletEpOpt = (!servletEp.isEmpty()) ? Optional.of(servletEp) : Optional.empty();
api.configureBridge(hostname, Optional.of(config.enableMDNS), Optional.of(config.enableJSONComms),
servletEpOpt);
api.configureBridge(hostname, getResponseTimeout(), Optional.of(config.enableMDNS),
Optional.of(config.enableJSONComms), servletEpOpt);
if (Thread.currentThread().isInterrupted()) {
return;
}
// Ensure we have a response with data in if not schedule a reconnect in 15 seconds, theres no reason
// Ensure we have a response with data in if not schedule a reconnect in 15 seconds, there's no reason
// for a gateway with no devices.
if (!getGatewayConfigurationFreshCheck()) {
logger.debug("{}", getLocalizedText("bridge.info.awaiting-init"));
@@ -532,7 +536,8 @@ public class LinkTapBridgeHandler extends BaseBridgeHandler {
}
private void scheduleReconnect() {
scheduleReconnect(15);
scheduleReconnect(getResponseTimeout() * 5); // 5 is due to the number of req/resp required for
// connection
}
public void attemptReconnectIfNeeded() {
@@ -235,7 +235,7 @@ public class LinkTapHandler extends PollingDeviceHandler {
strStore.put(DEVICE_CHANNEL_OH_VOLUME_LIMIT, String.valueOf(targetValue));
break;
}
} else if (command instanceof StringType stringCmd) {
} else if (command instanceof StringType) {
switch (channelUID.getId()) {
case DEVICE_CHANNEL_CHILD_LOCK: {
sendRequest(new LockReq(Integer.valueOf(command.toString())));
@@ -239,7 +239,7 @@ public final class TransactionProcessor {
final String payloadJson = GSON.toJson(request);
logger.debug("{} = APP -> GW Request {} -> Payload {}", uid, targetHost, payloadJson);
String response = API.sendRequest(targetHost, GSON.toJson(request));
String response = API.sendRequest(targetHost, handler.getResponseTimeout(), GSON.toJson(request));
logger.debug("{} = APP -> GW Response {} -> Payload {}", uid, targetHost, response.trim());
GatewayDeviceResponse gatewayFrame = GSON.fromJson(response, GatewayDeviceResponse.class);
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.Request;
import org.slf4j.Logger;
/**
* The {@link JettyTraceListener} defines a basic listener that can be utilised for logging jetty client states.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public final class JettyTraceListener implements Request.Listener {
private final Logger logger;
public JettyTraceListener(final Logger logger) {
this.logger = logger;
}
@Override
public void onQueued(@Nullable Request request) {
if (request != null) {
logger.trace("HTTP Comms request is queued to be processed to {}", request.getURI());
} else {
logger.trace("HTTP Comms request is queued to be processed");
}
Request.Listener.super.onQueued(request);
}
@Override
public void onBegin(@Nullable Request request) {
if (request != null) {
logger.trace("HTTP Comms request is beginning to be processed to {}", request.getURI());
} else {
logger.trace("HTTP Comms request is beginning to be processed");
}
Request.Listener.super.onBegin(request);
}
@Override
public void onSuccess(@Nullable Request request) {
if (request != null) {
logger.trace("HTTP Comms request has been reported as successful to {}", request.getURI());
} else {
logger.trace("HTTP Comms request has been reported as successful");
}
Request.Listener.super.onSuccess(request);
}
@Override
public void onFailure(@Nullable Request request, @Nullable Throwable failure) {
if (request != null) {
logger.trace("HTTP Comms request has failed {}", request.getHost());
} else {
logger.trace("HTTP Comms request has failed with error");
}
if (failure != null) {
logger.trace("HTTP Comms request has failed due to cause {}", failure.toString());
}
Request.Listener.super.onFailure(request, failure);
}
}
@@ -87,10 +87,10 @@ public final class WebServerApi {
private static final String FIELD_ADMIN_USER = "admin";
private static final String FIELD_ADMIN_USER_PWD = "adminpwd";
private static final int REQ_TIMEOUT_SECONDS = 3;
private static final WebServerApi INSTANCE = new WebServerApi();
private static final String REQ_HDR_APPLICATION_JSON = new MediaType("application", "json", "UTF-8").toString();
private final Logger logger = LoggerFactory.getLogger(WebServerApi.class);
private final JettyTraceListener jettyTraceListener = new JettyTraceListener(logger);
private @NonNullByDefault({}) HttpClient httpClient;
private @Nullable TranslationProvider translationProvider;
@@ -132,11 +132,11 @@ public final class WebServerApi {
}
}
public Map<String, String> getBridgeProperities(final String hostname)
public Map<String, String> getBridgeProperities(final String hostname, final int timeoutSeconds)
throws LinkTapException, NotTapLinkGatewayException, TransientCommunicationIssueException {
try {
final Request request = httpClient.newRequest(URI_HOST_PREFIX + hostname).method(HttpMethod.GET);
final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
final ContentResponse cr = addTraceListener(request).timeout(timeoutSeconds, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
@@ -375,8 +375,9 @@ public final class WebServerApi {
return "";
}
public boolean configureBridge(final @Nullable String hostname, final Optional<Boolean> mdnsEnable,
final Optional<Boolean> nonHtmlEnable, final Optional<String> localServer)
public boolean configureBridge(final @Nullable String hostname, final int timeoutSeconds,
final Optional<Boolean> mdnsEnable, final Optional<Boolean> nonHtmlEnable,
final Optional<String> localServer)
throws InterruptedException, NotTapLinkGatewayException, TransientCommunicationIssueException {
try {
if (hostname == null) {
@@ -384,10 +385,11 @@ public final class WebServerApi {
}
final String targetHost = URI_HOST_PREFIX + hostname;
final Request request = httpClient.newRequest(targetHost).method(HttpMethod.GET);
final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
final ContentResponse cr = addTraceListener(request).timeout(timeoutSeconds, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
logger.trace("Validating response from Gateway web UI");
validateHeaders(cr.getHeaders());
final String responseData = cr.getContentAsString();
final Document doc = Jsoup.parse(responseData);
@@ -409,7 +411,8 @@ public final class WebServerApi {
logger.debug("Updating mdns server settings on gateway");
final Request mdnsRequest = httpClient
.newRequest(targetHost + "/index.shtml?flag=4&" + mdnsEnableReqStr).method(HttpMethod.GET);
final ContentResponse mdnsCr = mdnsRequest.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
final ContentResponse mdnsCr = addTraceListener(mdnsRequest)
.timeout(timeoutSeconds, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != mdnsCr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
@@ -432,8 +435,8 @@ public final class WebServerApi {
final Request lhttpApiRequest = httpClient
.newRequest(targetHost + "/index.shtml?flag=5&" + localHttpApiReqStr)
.method(HttpMethod.GET);
final ContentResponse mdnsCr = lhttpApiRequest.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.send();
final ContentResponse mdnsCr = addTraceListener(lhttpApiRequest)
.timeout(timeoutSeconds, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != mdnsCr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
@@ -445,7 +448,8 @@ public final class WebServerApi {
logger.debug("Rebooting gateway to apply new settings");
final Request restartReq = httpClient.newRequest(targetHost + "/index.shtml?flag=0")
.method(HttpMethod.GET);
final ContentResponse mdnsCr = restartReq.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
final ContentResponse mdnsCr = addTraceListener(restartReq).timeout(timeoutSeconds, TimeUnit.SECONDS)
.send();
if (HttpURLConnection.HTTP_OK != mdnsCr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
@@ -470,7 +474,8 @@ public final class WebServerApi {
}
}
public boolean unlockWebInterface(final String hostname, final String username, final String password)
public boolean unlockWebInterface(final String hostname, final int timeoutSeconds, final String username,
final String password)
throws LinkTapException, NotTapLinkGatewayException, TransientCommunicationIssueException {
try {
org.eclipse.jetty.util.Fields fields = new org.eclipse.jetty.util.Fields();
@@ -478,12 +483,13 @@ public final class WebServerApi {
fields.put(FIELD_ADMIN_USER_PWD, password);
final Request request = httpClient.newRequest(URI_HOST_PREFIX + hostname + "/login.shtml")
.method(HttpMethod.POST).content(new FormContentProvider(fields));
final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
final ContentResponse cr = addTraceListener(request).timeout(timeoutSeconds, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
validateHeaders(cr.getHeaders());
return !getBridgeProperities(hostname).isEmpty();
return !getBridgeProperities(hostname, timeoutSeconds).isEmpty();
} catch (InterruptedException e) {
return false;
} catch (TimeoutException e) {
@@ -516,15 +522,15 @@ public final class WebServerApi {
}
}
public String sendRequest(final String hostname, final String requestBody)
public String sendRequest(final String hostname, final int timeoutSeconds, final String requestBody)
throws NotTapLinkGatewayException, TransientCommunicationIssueException {
try {
final InetAddress address = InetAddress.getByName(hostname);
logger.trace("API Endpoint: {}", URI_HOST_PREFIX + address.getHostAddress() + "/api.shtml");
final Request request = httpClient.POST(URI_HOST_PREFIX + address.getHostAddress() + "/api.shtml");
request.content(new StringContentProvider(requestBody), REQ_HDR_APPLICATION_JSON);
final Request request = httpClient.POST(URI_HOST_PREFIX + address.getHostAddress() + "/api.shtml")
.content(new StringContentProvider(requestBody), REQ_HDR_APPLICATION_JSON);
final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
final ContentResponse cr = addTraceListener(request).timeout(timeoutSeconds, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
@@ -571,4 +577,12 @@ public final class WebServerApi {
}
}
}
private org.eclipse.jetty.client.api.Request addTraceListener(final Request request) {
if (logger.isTraceEnabled()) {
return request.onRequestQueued(jettyTraceListener).onRequestBegin(jettyTraceListener)
.onRequestSuccess(jettyTraceListener).onRequestFailure(jettyTraceListener);
}
return request;
}
}
@@ -24,6 +24,8 @@ thing-type.config.linktap.gateway.enableMDNS.label = Enable mDNS Responder
thing-type.config.linktap.gateway.enableMDNS.description = On connection whether the mDNS responder should be enabled on the gateway device
thing-type.config.linktap.gateway.enforceProtocolLimits.label = Enforce protocol limits
thing-type.config.linktap.gateway.enforceProtocolLimits.description = If parameters outside the limits acceptable to the device's are sent they will be blocked and logged
thing-type.config.linktap.gateway.gatewayResponseTimeout.label = Gateway Response Timeout
thing-type.config.linktap.gateway.gatewayResponseTimeout.description = For slow or heavily loaded systems this may need increasing, if communication errors are seen (seconds allowed for responses from the gateway)
thing-type.config.linktap.gateway.host.label = Hostname / IP
thing-type.config.linktap.gateway.host.description = The hostname / IP address of the gateway device
thing-type.config.linktap.gateway.password.label = Device Password
@@ -53,6 +53,13 @@
<default>true</default>
<advanced>true</advanced>
</parameter>
<parameter name="gatewayResponseTimeout" type="integer" required="true" min="3" max="30">
<label>Gateway Response Timeout</label>
<description>For slow or heavily loaded systems this may need increasing, if communication errors are seen (seconds
allowed for responses from the gateway)</description>
<default>3</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>