diff --git a/bundles/org.openhab.binding.neohub/README.md b/bundles/org.openhab.binding.neohub/README.md
index 9b0b64eb174f3..ea8f85a5fea4b 100644
--- a/bundles/org.openhab.binding.neohub/README.md
+++ b/bundles/org.openhab.binding.neohub/README.md
@@ -33,21 +33,31 @@ It signs on to the hub using the supplied connection parameters, and it polls th
The NeoHub supports two Application Programming Interfaces "API" (an older "legacy" one, and a modern one), and this binding can use either of them to communicate with it.
Before the binding can communicate with the hub, the following Configuration Parameters must be entered.
-| Configuration Parameter | Description |
-|-------------------------|---------------------------------------------------------------------------------------------|
-| hostName | Host name (IP address) of the NeoHub (example 192.168.1.123) |
-| portNumber | Port number of the NeoHub (Default=4242) |
-| pollingInterval | Time (seconds) between polling requests to the NeoHub (Min=4, Max=60, Default=60) |
-| socketTimeout | Time (seconds) to allow for TCP socket connections to the hub to succeed (Min=4, Max=20, Default=5) |
-| preferLegacyApi | ADVANCED: Prefer the binding to use older API calls; if these are not supported, it switches to the new calls (Default=false) |
+| Configuration Parameter | Description |
+|----------------------------|----------------------------------------------------------------------------------------------------------|
+| hostName | Host name (IP address) of the NeoHub (example 192.168.1.123) |
+| useWebSocket1) | Use secure WebSocket to connect to the NeoHub (example `true`) |
+| apiToken1) | API Access Token for secure connection to hub. Create the token in the Heatmiser mobile App |
+| pollingInterval | Time (seconds) between polling requests to the NeoHub (Min=4, Max=60, Default=60) |
+| socketTimeout | Time (seconds) to allow for TCP socket connections to the hub to succeed (Min=4, Max=20, Default=5) |
+| preferLegacyApi | ADVANCED: Prefer to use older API calls; but if not supported, it switches to new calls (Default=false) |
+| portNumber2) | ADVANCED: Port number for connection to the NeoHub (Default=0 (automatic)) |
+
+1) If `useWebSocket` is false, the binding will connect via an older and less secure TCP connection, in which case `apiToken` is not required.
+However see the chapter "Connection Refused Errors" below.
+Whereas if you prefer to connect via more secure WebSocket connections then an API access token `apiToken` is required.
+You can create an API access token in the Heatmiser mobile App (Settings | System | API Access).
+
+2) Normally the port number is chosen automatically (for TCP it is 4242 and for WebSocket it is 4243).
+But you can override this in special cases if you want to use (say) port forwarding.
## Connection Refused Errors
-From early 2022 Heatmiser introduced NeoHub firmware that has the ability to enable / disable the NeoHub `portNumber` 4242.
-If this port is disabled the OpenHAB binding cannot connect and the binding will report a *"Connection Refused"* warning in the log.
-In prior firmware versions the port was always enabled.
-But in the new firmware the port is initially enabled on power up but if no communication occurs for 48 hours it is automatically disabled.
-Alternatively the Heatmiser mobile App has a setting (Settings | System | API Access | Legacy API Enable | On) whereby the port can be permanently enabled.
+From early 2022 Heatmiser introduced NeoHub firmware that has the ability to enable / disable connecting to it via a TCP port.
+If the TCP port is disabled the OpenHAB binding cannot connect and the binding will report a *"Connection Refused"* warning in the log.
+In prior firmware versions the TCP port was always enabled.
+But in the new firmware the TCP port is initially enabled on power up but if no communication occurs for 48 hours it is automatically disabled.
+Alternatively the Heatmiser mobile app has a setting (Settings | System | API Access | Legacy API Enable | On) whereby the TCP port can be permanently enabled.
## Thing Configuration for "NeoStat" and "NeoPlug"
diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubBindingConstants.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubBindingConstants.java
index 710328cf325b1..e4f950037d82d 100644
--- a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubBindingConstants.java
+++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubBindingConstants.java
@@ -196,4 +196,16 @@ public static enum NeoHubReturnResult {
public static final String PROPERTY_FIRMWARE_VERSION = "Firmware version";
public static final String PROPERTY_API_VERSION = "API version";
public static final String PROPERTY_API_DEVICEINFO = "Devices [online/total]";
+
+ /*
+ * reserved ports on the hub
+ */
+ public static final int PORT_TCP = 4242;
+ public static final int PORT_WSS = 4243;
+
+ /*
+ * web socket communication constants
+ */
+ public static final String HM_GET_COMMAND_QUEUE = "hm_get_command_queue";
+ public static final String HM_SET_COMMAND_RESPONSE = "hm_set_command_response";
}
diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubConfiguration.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubConfiguration.java
index 88977d1281afb..afebbce9112f2 100644
--- a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubConfiguration.java
+++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubConfiguration.java
@@ -30,4 +30,6 @@ public class NeoHubConfiguration {
public int pollingInterval;
public int socketTimeout;
public boolean preferLegacyApi;
+ public String apiToken = "";
+ public boolean useWebSocket;
}
diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDiscoveryService.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDiscoveryService.java
index ca6e2f28ff3d5..cff7cf137953e 100644
--- a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDiscoveryService.java
+++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDiscoveryService.java
@@ -40,7 +40,7 @@
* Discovery service for neo devices
*
* @author Andrew Fiddian-Green - Initial contribution
- *
+ *
*/
@NonNullByDefault
public class NeoHubDiscoveryService extends AbstractDiscoveryService {
@@ -113,11 +113,11 @@ private void discoverDevices() {
// the record came from the legacy API (deviceType included)
if (deviceRecord instanceof InfoRecord) {
deviceType = ((InfoRecord) deviceRecord).getDeviceType();
- publishDevice((InfoRecord) deviceRecord, deviceType);
+ publishDevice(deviceRecord, deviceType);
continue;
}
- // the record came from the now API (deviceType NOT included)
+ // the record came from the new API (deviceType NOT included)
if (deviceRecord instanceof LiveDataRecord) {
if (engineerData == null) {
break;
@@ -128,7 +128,7 @@ private void discoverDevices() {
continue;
}
deviceType = engineerData.getDeviceType(deviceName);
- publishDevice((LiveDataRecord) deviceRecord, deviceType);
+ publishDevice(deviceRecord, deviceType);
}
}
}
diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubException.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubException.java
index 1541dac1333a7..55c4bd2a243b0 100644
--- a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubException.java
+++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubException.java
@@ -18,7 +18,7 @@
* The {@link NeoHubException} is a custom exception for NeoHub
*
* @author Andrew Fiddian-Green - Initial contribution
- *
+ *
*/
@NonNullByDefault
public class NeoHubException extends Exception {
@@ -28,4 +28,8 @@ public class NeoHubException extends Exception {
public NeoHubException(String message) {
super(message);
}
+
+ public NeoHubException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandler.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandler.java
index be17626a9a719..15bb98ce1a745 100644
--- a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandler.java
+++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandler.java
@@ -65,7 +65,7 @@ public class NeoHubHandler extends BaseBridgeHandler {
private final Map connectionStates = new HashMap<>();
private @Nullable NeoHubConfiguration config;
- private @Nullable NeoHubSocket socket;
+ private @Nullable NeoHubSocketBase socket;
private @Nullable ScheduledFuture> lazyPollingScheduler;
private @Nullable ScheduledFuture> fastPollingScheduler;
@@ -113,7 +113,7 @@ public void initialize() {
logger.debug("hub '{}' port={}", getThing().getUID(), config.portNumber);
}
- if (config.portNumber <= 0 || config.portNumber > 0xFFFF) {
+ if (config.portNumber < 0 || config.portNumber > 0xFFFF) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "portNumber is invalid!");
return;
}
@@ -142,7 +142,20 @@ public void initialize() {
logger.debug("hub '{}' preferLegacyApi={}", getThing().getUID(), config.preferLegacyApi);
}
- NeoHubSocket socket = this.socket = new NeoHubSocket(config.hostName, config.portNumber, config.socketTimeout);
+ // create a web or TCP socket based on the port number in the configuration
+ NeoHubSocketBase socket;
+ try {
+ if (config.useWebSocket) {
+ socket = new NeoHubWebSocket(config);
+ } else {
+ socket = new NeoHubSocket(config);
+ }
+ } catch (NeoHubException e) {
+ logger.debug("\"hub '{}' error creating web/tcp socket: '{}'", getThing().getUID(), e.getMessage());
+ return;
+ }
+
+ this.socket = socket;
this.config = config;
/*
@@ -206,6 +219,15 @@ public void dispose() {
fast.cancel(true);
this.fastPollingScheduler = null;
}
+
+ NeoHubSocketBase socket = this.socket;
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ }
+ this.socket = null;
+ }
}
/*
@@ -220,7 +242,7 @@ public void startFastPollingBurst() {
* device handlers call this method to issue commands to the NeoHub
*/
public synchronized NeoHubReturnResult toNeoHubSendChannelValue(String commandStr) {
- NeoHubSocket socket = this.socket;
+ NeoHubSocketBase socket = this.socket;
if (socket == null || config == null) {
return NeoHubReturnResult.ERR_INITIALIZATION;
@@ -246,7 +268,7 @@ public synchronized NeoHubReturnResult toNeoHubSendChannelValue(String commandSt
* @return a class that contains the full status of all devices
*/
protected @Nullable NeoHubAbstractDeviceData fromNeoHubGetDeviceData() {
- NeoHubSocket socket = this.socket;
+ NeoHubSocketBase socket = this.socket;
if (socket == null || config == null) {
logger.warn(MSG_HUB_CONFIG, getThing().getUID());
@@ -322,7 +344,7 @@ public synchronized NeoHubReturnResult toNeoHubSendChannelValue(String commandSt
* @return a class that contains the status of the system
*/
protected @Nullable NeoHubReadDcbResponse fromNeoHubReadSystemData() {
- NeoHubSocket socket = this.socket;
+ NeoHubSocketBase socket = this.socket;
if (socket == null) {
return null;
@@ -443,7 +465,7 @@ private void selectApi() {
boolean supportsLegacyApi = false;
boolean supportsFutureApi = false;
- NeoHubSocket socket = this.socket;
+ NeoHubSocketBase socket = this.socket;
if (socket != null) {
String responseJson;
NeoHubReadDcbResponse systemData;
@@ -498,7 +520,7 @@ private void selectApi() {
* get the Engineers data
*/
public @Nullable NeoHubGetEngineersData fromNeoHubGetEngineersData() {
- NeoHubSocket socket = this.socket;
+ NeoHubSocketBase socket = this.socket;
if (socket != null) {
String responseJson;
try {
diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubReadDcbResponse.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubReadDcbResponse.java
index 05d4f8f6b22c3..00e0cb7adbf7b 100644
--- a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubReadDcbResponse.java
+++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubReadDcbResponse.java
@@ -59,9 +59,11 @@ public Unit> getTemperatureUnit() {
}
public @Nullable String getFirmwareVersion() {
+ BigDecimal firmwareVersionNew = this.firmwareVersionNew;
if (firmwareVersionNew != null) {
return firmwareVersionNew.toString();
}
+ BigDecimal firmwareVersionOld = this.firmwareVersionOld;
if (firmwareVersionOld != null) {
return firmwareVersionOld.toString();
}
diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocket.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocket.java
index 7d289ef644125..301fefb8e1755 100644
--- a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocket.java
+++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocket.java
@@ -25,54 +25,30 @@
import org.slf4j.LoggerFactory;
/**
- * NeoHubConnector handles the ASCII based communication via TCP between openHAB
- * and NeoHub
+ * Handles the ASCII based communication via TCP socket between openHAB and NeoHub
*
* @author Sebastian Prehn - Initial contribution
* @author Andrew Fiddian-Green - Refactoring for openHAB v2.x
*
*/
@NonNullByDefault
-public class NeoHubSocket {
+public class NeoHubSocket extends NeoHubSocketBase {
private final Logger logger = LoggerFactory.getLogger(NeoHubSocket.class);
- /**
- * Name of host or IP to connect to.
- */
- private final String hostname;
-
- /**
- * The port to connect to
- */
- private final int port;
-
- /**
- * The socket connect resp. read timeout value
- */
- private final int timeout;
-
- public NeoHubSocket(final String hostname, final int portNumber, final int timeoutSeconds) {
- this.hostname = hostname;
- this.port = portNumber;
- this.timeout = timeoutSeconds * 1000;
+ public NeoHubSocket(NeoHubConfiguration config) {
+ super(config);
}
- /**
- * sends the message over the network to the NeoHub and returns its response
- *
- * @param requestJson the message to be sent to the NeoHub
- * @return responseJson received from NeoHub
- * @throws NeoHubException, IOException
- *
- */
- public String sendMessage(final String requestJson) throws IOException, NeoHubException {
+ @Override
+ public synchronized String sendMessage(final String requestJson) throws IOException, NeoHubException {
IOException caughtException = null;
StringBuilder builder = new StringBuilder();
try (Socket socket = new Socket()) {
- socket.connect(new InetSocketAddress(hostname, port), timeout);
- socket.setSoTimeout(timeout);
+ int port = config.portNumber > 0 ? config.portNumber : NeoHubBindingConstants.PORT_TCP;
+ socket.connect(new InetSocketAddress(config.hostName, port), config.socketTimeout * 1000);
+ socket.setSoTimeout(config.socketTimeout * 1000);
try (InputStreamReader reader = new InputStreamReader(socket.getInputStream(), US_ASCII);
OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream(), US_ASCII)) {
@@ -128,4 +104,9 @@ public String sendMessage(final String requestJson) throws IOException, NeoHubEx
return responseJson;
}
+
+ @Override
+ public void close() {
+ // nothing to do
+ }
}
diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocketBase.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocketBase.java
new file mode 100644
index 0000000000000..3ee2b9665245e
--- /dev/null
+++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocketBase.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2022 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.neohub.internal;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Base abstract class for ASCII based communication between openHAB and NeoHub
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ *
+ */
+@NonNullByDefault
+public abstract class NeoHubSocketBase implements Closeable {
+
+ protected final NeoHubConfiguration config;
+
+ public NeoHubSocketBase(NeoHubConfiguration config) {
+ this.config = config;
+ }
+
+ /**
+ * Sends the message over the network to the NeoHub and returns its response
+ *
+ * @param requestJson the message to be sent to the NeoHub
+ * @return responseJson received from NeoHub
+ * @throws NeoHubException, IOException
+ *
+ */
+ public abstract String sendMessage(final String requestJson) throws IOException, NeoHubException;
+}
diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubWebSocket.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubWebSocket.java
new file mode 100644
index 0000000000000..f62dc860e5e16
--- /dev/null
+++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubWebSocket.java
@@ -0,0 +1,238 @@
+/**
+ * Copyright (c) 2010-2022 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.neohub.internal;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.ExecutionException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * Handles the ASCII based communication via web socket between openHAB and NeoHub
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ *
+ */
+@NonNullByDefault
+@WebSocket
+public class NeoHubWebSocket extends NeoHubSocketBase {
+
+ private static final int SLEEP_MILLISECONDS = 100;
+ private static final String REQUEST_OUTER = "{\"message_type\":\"hm_get_command_queue\",\"message\":\"%s\"}";
+ private static final String REQUEST_INNER = "{\"token\":\"%s\",\"COMMANDS\":[{\"COMMAND\":\"%s\",\"COMMANDID\":1}]}";
+
+ private final Logger logger = LoggerFactory.getLogger(NeoHubWebSocket.class);
+ private final Gson gson = new Gson();
+ private final WebSocketClient webSocketClient;
+
+ private @Nullable Session session = null;
+ private String responseOuter = "";
+ private boolean responseWaiting;
+
+ /**
+ * DTO to receive and parse the response JSON.
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+ private static class Response {
+ @SuppressWarnings("unused")
+ public @Nullable String command_id;
+ @SuppressWarnings("unused")
+ public @Nullable String device_id;
+ public @Nullable String message_type;
+ public @Nullable String response;
+ }
+
+ public NeoHubWebSocket(NeoHubConfiguration config) throws NeoHubException {
+ super(config);
+
+ // initialise and start ssl context factory, http client, web socket client
+ SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+ sslContextFactory.setTrustAll(true);
+ HttpClient httpClient = new HttpClient(sslContextFactory);
+ try {
+ httpClient.start();
+ } catch (Exception e) {
+ throw new NeoHubException(String.format("Error starting http client: '%s'", e.getMessage()));
+ }
+ webSocketClient = new WebSocketClient(httpClient);
+ webSocketClient.setConnectTimeout(config.socketTimeout * 1000);
+ try {
+ webSocketClient.start();
+ } catch (Exception e) {
+ throw new NeoHubException(String.format("Error starting web socket client: '%s'", e.getMessage()));
+ }
+ }
+
+ /**
+ * Open the web socket session.
+ *
+ * @throws NeoHubException
+ */
+ private void startSession() throws NeoHubException {
+ Session session = this.session;
+ if (session == null || !session.isOpen()) {
+ closeSession();
+ try {
+ int port = config.portNumber > 0 ? config.portNumber : NeoHubBindingConstants.PORT_WSS;
+ URI uri = new URI(String.format("wss://%s:%d", config.hostName, port));
+ webSocketClient.connect(this, uri).get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new NeoHubException(String.format("Error starting session: '%s'", e.getMessage(), e));
+ } catch (ExecutionException | IOException | URISyntaxException e) {
+ throw new NeoHubException(String.format("Error starting session: '%s'", e.getMessage(), e));
+ }
+ }
+ }
+
+ /**
+ * Close the web socket session.
+ */
+ private void closeSession() {
+ Session session = this.session;
+ if (session != null) {
+ session.close();
+ this.session = null;
+ }
+ }
+
+ /**
+ * Helper to escape the quote marks in a JSON string.
+ *
+ * @param json the input JSON string.
+ * @return the escaped JSON version.
+ */
+ private String jsonEscape(String json) {
+ return json.replace("\"", "\\\"");
+ }
+
+ /**
+ * Helper to remove quote escape marks from an escaped JSON string.
+ *
+ * @param escapedJson the escaped input string.
+ * @return the clean JSON version.
+ */
+ private String jsonUnEscape(String escapedJson) {
+ return escapedJson.replace("\\\"", "\"");
+ }
+
+ /**
+ * Helper to replace double quote marks in a JSON string with single quote marks.
+ *
+ * @param json the input string.
+ * @return the modified version.
+ */
+ private String jsonReplaceQuotes(String json) {
+ return json.replace("\"", "'");
+ }
+
+ @Override
+ public synchronized String sendMessage(final String requestJson) throws IOException, NeoHubException {
+ // start the session
+ startSession();
+
+ // session start failed
+ Session session = this.session;
+ if (session == null) {
+ throw new NeoHubException("Session is null.");
+ }
+
+ // wrap the inner request in an outer request string
+ String requestOuter = String.format(REQUEST_OUTER,
+ jsonEscape(String.format(REQUEST_INNER, config.apiToken, jsonReplaceQuotes(requestJson))));
+
+ // initialise the response
+ responseOuter = "";
+ responseWaiting = true;
+
+ // send the request
+ logger.trace("Sending request: {}", requestOuter);
+ session.getRemote().sendString(requestOuter);
+
+ // sleep and loop until we get a response or the socket is closed
+ int sleepRemainingMilliseconds = config.socketTimeout * 1000;
+ while (responseWaiting && (sleepRemainingMilliseconds > 0)) {
+ try {
+ Thread.sleep(SLEEP_MILLISECONDS);
+ sleepRemainingMilliseconds = sleepRemainingMilliseconds - SLEEP_MILLISECONDS;
+ } catch (InterruptedException e) {
+ throw new NeoHubException(String.format("Read timeout '%s'", e.getMessage()));
+ }
+ }
+
+ // extract the inner response from the outer response string
+ Response responseDto = gson.fromJson(responseOuter, Response.class);
+ if (responseDto != null && NeoHubBindingConstants.HM_SET_COMMAND_RESPONSE.equals(responseDto.message_type)) {
+ String responseJson = responseDto.response;
+ if (responseJson != null) {
+ responseJson = jsonUnEscape(responseJson);
+ logger.trace("Received response: {}", responseJson);
+ return responseJson;
+ }
+ }
+ logger.debug("Null or invalid response.");
+ return "";
+ }
+
+ @Override
+ public void close() {
+ closeSession();
+ try {
+ webSocketClient.stop();
+ } catch (Exception e) {
+ }
+ }
+
+ @OnWebSocketConnect
+ public void onConnect(Session session) {
+ logger.trace("onConnect: ok");
+ this.session = session;
+ }
+
+ @OnWebSocketClose
+ public void onClose(int statusCode, String reason) {
+ logger.trace("onClose: code:{}, reason:{}", statusCode, reason);
+ responseWaiting = false;
+ this.session = null;
+ }
+
+ @OnWebSocketError
+ public void onError(Throwable cause) {
+ logger.trace("onError: cause:{}", cause.getMessage());
+ closeSession();
+ }
+
+ @OnWebSocketMessage
+ public void onMessage(String msg) {
+ logger.trace("onMessage: msg:{}", msg);
+ responseOuter = msg;
+ responseWaiting = false;
+ }
+}
diff --git a/bundles/org.openhab.binding.neohub/src/main/resources/OH-INF/i18n/neohub.properties b/bundles/org.openhab.binding.neohub/src/main/resources/OH-INF/i18n/neohub.properties
index dddda2cf9d342..d29d025218702 100644
--- a/bundles/org.openhab.binding.neohub/src/main/resources/OH-INF/i18n/neohub.properties
+++ b/bundles/org.openhab.binding.neohub/src/main/resources/OH-INF/i18n/neohub.properties
@@ -35,11 +35,15 @@ thing-type.config.neohub.neohub.hostName.description = Host name (IP address) of
thing-type.config.neohub.neohub.pollingInterval.label = Polling Interval
thing-type.config.neohub.neohub.pollingInterval.description = Time (seconds) between polling the NeoHub (min=4, max/default=60)
thing-type.config.neohub.neohub.portNumber.label = Port Number
-thing-type.config.neohub.neohub.portNumber.description = Port number of the NeoHub
+thing-type.config.neohub.neohub.portNumber.description = Override port number to use to connect to the NeoHub (0=automatic)
thing-type.config.neohub.neohub.preferLegacyApi.label = Prefer Legacy API
thing-type.config.neohub.neohub.preferLegacyApi.description = Use the legacy API instead of the new API (if available)
thing-type.config.neohub.neohub.socketTimeout.label = Socket Timeout
thing-type.config.neohub.neohub.socketTimeout.description = Time (seconds) to wait for connections to the Hub (min/default=5, max=20)
+thing-type.config.neohub.neohub.apiToken.label = API Access Token
+thing-type.config.neohub.neohub.apiToken.description = API access token for the hub (created on the Heatmiser mobile App)
+thing-type.config.neohub.neohub.useWebSocket.label = Connect via WebSocket
+thing-type.config.neohub.neohub.useWebSocket.description = Select whether to communicate with the Neohub via WebSocket or TCP
thing-type.config.neohub.neoplug.deviceNameInHub.label = Device Name
thing-type.config.neohub.neoplug.deviceNameInHub.description = Device Name that identifies the NeoPlug device in the NeoHub and Heatmiser App
thing-type.config.neohub.neostat.deviceNameInHub.label = Device Name
diff --git a/bundles/org.openhab.binding.neohub/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.neohub/src/main/resources/OH-INF/thing/thing-types.xml
index 1aa673651a8bc..75e9aef900926 100644
--- a/bundles/org.openhab.binding.neohub/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.neohub/src/main/resources/OH-INF/thing/thing-types.xml
@@ -28,8 +28,8 @@
- Port number of the NeoHub
- 4242
+ Override port number to use to connect to the NeoHub (0=automatic)
+ 0true
@@ -53,6 +53,17 @@
falsetrue
+
+
+
+ Select whether to communicate with the Neohub via WebSocket or TCP
+ false
+
+
+
+
+ API access token for the hub (created with the Heatmiser mobile app)
+
diff --git a/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubTestData.java b/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubJsonTests.java
similarity index 95%
rename from bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubTestData.java
rename to bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubJsonTests.java
index 0422c1f7af483..0417e15359b39 100644
--- a/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubTestData.java
+++ b/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubJsonTests.java
@@ -26,6 +26,7 @@
import org.junit.jupiter.api.Test;
import org.openhab.binding.neohub.internal.NeoHubAbstractDeviceData;
import org.openhab.binding.neohub.internal.NeoHubAbstractDeviceData.AbstractRecord;
+import org.openhab.binding.neohub.internal.NeoHubConfiguration;
import org.openhab.binding.neohub.internal.NeoHubGetEngineersData;
import org.openhab.binding.neohub.internal.NeoHubInfoResponse;
import org.openhab.binding.neohub.internal.NeoHubInfoResponse.InfoRecord;
@@ -36,25 +37,24 @@
import org.openhab.core.library.unit.SIUnits;
/**
- * The {@link NeoHubTestData} class defines common constants, which are used
- * across the whole binding.
+ * JUnit for testing JSON parsing.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
-public class NeoHubTestData {
+public class NeoHubJsonTests {
/*
* to actually run tests on a physical device you must have a hub physically available, and its IP address must be
* correctly configured in the "hubIPAddress" string constant e.g. "192.168.1.123"
* note: only run the test if such a device is actually available
*/
- private static final String hubIpAddress = "192.168.1.xxx";
+ private static final String HUB_IP_ADDRESS = "192.168.1.xxx";
- private static final Pattern VALID_IP_V4_ADDRESS = Pattern
+ public static final Pattern VALID_IP_V4_ADDRESS = Pattern
.compile("\\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}\\b");
- /*
+ /**
* Load the test JSON payload string from a file
*/
private String load(String fileName) {
@@ -72,10 +72,9 @@ private String load(String fileName) {
return "";
}
- /*
+ /**
* Test an INFO JSON response string as produced by older firmware versions
*/
- @SuppressWarnings("null")
@Test
public void testInfoJsonOld() {
// load INFO JSON response string in old JSON format
@@ -133,10 +132,9 @@ public void testInfoJsonOld() {
assertFalse(device.stateManual());
}
- /*
+ /**
* Test an INFO JSON response string as produced by newer firmware versions
*/
- @SuppressWarnings("null")
@Test
public void testInfoJsonNew() {
// load INFO JSON response string in new JSON format
@@ -158,10 +156,9 @@ public void testInfoJsonNew() {
assertEquals(new BigDecimal("255.255"), device.getActualTemperature());
}
- /*
+ /**
* Test for a READ_DCB JSON string that has valid CORF C response
*/
- @SuppressWarnings("null")
@Test
public void testReadDcbJson() {
// load READ_DCB JSON response string with valid CORF C response
@@ -186,10 +183,9 @@ public void testReadDcbJson() {
assertEquals(SIUnits.CELSIUS, dcbResponse.getTemperatureUnit());
}
- /*
+ /**
* Test an INFO JSON string that has a door contact and a temperature sensor
*/
- @SuppressWarnings("null")
@Test
public void testInfoJsonWithSensors() {
/*
@@ -240,11 +236,10 @@ public void testInfoJsonWithSensors() {
assertTrue(device.isBatteryLow());
}
- /*
+ /**
* From NeoHub rev2.6 onwards the READ_DCB command is "deprecated" so we can
* also test the replacement GET_SYSTEM command (valid CORF response)
*/
- @SuppressWarnings("null")
@Test
public void testGetSystemJson() {
// load GET_SYSTEM JSON response string
@@ -255,11 +250,10 @@ public void testGetSystemJson() {
assertEquals("2134", dcbResponse.getFirmwareVersion());
}
- /*
+ /**
* From NeoHub rev2.6 onwards the INFO command is "deprecated" so we must test
* the replacement GET_LIVE_DATA command
*/
- @SuppressWarnings("null")
@Test
public void testGetLiveDataJson() {
// load GET_LIVE_DATA JSON response string
@@ -343,12 +337,11 @@ public void testGetLiveDataJson() {
assertTrue(MATCHER_HEATMISER_REPEATER.matcher(device.getDeviceName()).matches());
}
- /*
+ /**
* From NeoHub rev2.6 onwards the INFO command is "deprecated" and the DEVICE_ID
* element is not returned in the GET_LIVE_DATA call so we must test the
* replacement GET_ENGINEERS command
*/
- @SuppressWarnings("null")
@Test
public void testGetEngineersJson() {
// load GET_ENGINEERS JSON response string
@@ -362,31 +355,34 @@ public void testGetEngineersJson() {
assertEquals(6, engResponse.getDeviceType("Living Room South"));
}
- /*
+ /**
* send JSON request to the socket and retrieve JSON response
*/
private String testCommunicationInner(String requestJson) {
- NeoHubSocket socket = new NeoHubSocket(hubIpAddress, 4242, 5);
- String responseJson = "";
+ NeoHubConfiguration config = new NeoHubConfiguration();
+ config.hostName = HUB_IP_ADDRESS;
+ config.socketTimeout = 5;
try {
- responseJson = socket.sendMessage(requestJson);
+ NeoHubSocket socket = new NeoHubSocket(config);
+ String responseJson = socket.sendMessage(requestJson);
+ socket.close();
+ return responseJson;
} catch (Exception e) {
assertTrue(false);
}
- return responseJson;
+ return "";
}
- /*
+ /**
* Test the communications
*/
- @SuppressWarnings("null")
@Test
public void testCommunications() {
/*
* tests the actual communication with a real physical device on 'hubIpAddress'
* note: only run the test if such a device is actually available
*/
- if (!VALID_IP_V4_ADDRESS.matcher(hubIpAddress).matches()) {
+ if (!VALID_IP_V4_ADDRESS.matcher(HUB_IP_ADDRESS).matches()) {
return;
}
diff --git a/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubProtocolTests.java b/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubProtocolTests.java
new file mode 100644
index 0000000000000..86cc7ae79b1df
--- /dev/null
+++ b/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubProtocolTests.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2010-2022 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.neohub.test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.neohub.internal.NeoHubBindingConstants;
+import org.openhab.binding.neohub.internal.NeoHubConfiguration;
+import org.openhab.binding.neohub.internal.NeoHubException;
+import org.openhab.binding.neohub.internal.NeoHubSocket;
+import org.openhab.binding.neohub.internal.NeoHubWebSocket;
+
+/**
+ * JUnit for testing WSS and TCP socket protocols.
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class NeoHubProtocolTests {
+
+ /**
+ * Test online communication. Requires an actual Neohub to be present on the LAN. Configuration parameters must be
+ * entered for the actual specific Neohub instance as follows:
+ *
+ * - HUB_IP_ADDRESS the dotted ip address of the hub
+ * - HUB_API_TOKEN the api access token for the hub
+ * - SOCKET_TIMEOUT the connection time out
+ * - RUN_WSS_TEST enable testing the WSS communication
+ * - RUN_TCP_TEST enable testing the TCP communication
+ *
+ * NOTE: only run these tests if a device is actually available
+ *
+ */
+ private static final String HUB_IP_ADDRESS = "192.168.1.xxx";
+ private static final String HUB_API_TOKEN = "12345678-1234-1234-1234-123456789ABC";
+ private static final int SOCKET_TIMEOUT = 5;
+ private static final boolean RUN_WSS_TEST = false;
+ private static final boolean RUN_TCP_TEST = false;
+
+ /**
+ * Use web socket to send a request, and check for a response.
+ *
+ * @throws NeoHubException
+ * @throws IOException
+ */
+ @Test
+ void testWssConnection() throws NeoHubException, IOException {
+ if (RUN_WSS_TEST) {
+ if (!NeoHubJsonTests.VALID_IP_V4_ADDRESS.matcher(HUB_IP_ADDRESS).matches()) {
+ fail();
+ }
+
+ NeoHubConfiguration config = new NeoHubConfiguration();
+ config.hostName = HUB_IP_ADDRESS;
+ config.socketTimeout = SOCKET_TIMEOUT;
+ config.apiToken = HUB_API_TOKEN;
+
+ NeoHubWebSocket socket = new NeoHubWebSocket(config);
+ String requestJson = NeoHubBindingConstants.CMD_CODE_FIRMWARE;
+ String responseJson = socket.sendMessage(requestJson);
+ assertNotEquals(0, responseJson.length());
+ socket.close();
+ }
+ }
+
+ /**
+ * Use TCP socket to send a request, and check for a response.
+ *
+ * @throws NeoHubException
+ * @throws IOException
+ */
+ @Test
+ void testTcpConnection() throws IOException, NeoHubException {
+ if (RUN_TCP_TEST) {
+ if (!NeoHubJsonTests.VALID_IP_V4_ADDRESS.matcher(HUB_IP_ADDRESS).matches()) {
+ fail();
+ }
+
+ NeoHubConfiguration config = new NeoHubConfiguration();
+ config.hostName = HUB_IP_ADDRESS;
+ config.socketTimeout = SOCKET_TIMEOUT;
+ config.apiToken = HUB_API_TOKEN;
+
+ NeoHubSocket socket = new NeoHubSocket(config);
+ String requestJson = NeoHubBindingConstants.CMD_CODE_FIRMWARE;
+ String responseJson = socket.sendMessage(requestJson);
+ assertNotEquals(0, responseJson.length());
+ socket.close();
+ }
+ }
+}