[neohub] Add support for WebSocket connection to hub (#12915)

* [neohub] add support for secure web socket connection
* [neohub] clean code
* [neohub] synchronize api calls
* [neohub] rename classes, fix compiler errors, remove SuppressWarnings

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green 2022-08-01 20:58:39 +01:00 committed by GitHub
parent 8d3828a9a4
commit f60e324229
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 521 additions and 89 deletions

View File

@ -34,20 +34,30 @@ The NeoHub supports two Application Programming Interfaces "API" (an older "lega
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) |
| useWebSocket<sup>1)</sup> | Use secure WebSocket to connect to the NeoHub (example `true`) |
| apiToken<sup>1)</sup> | 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 the binding to use older API calls; if these are not supported, it switches to the new calls (Default=false) |
| preferLegacyApi | ADVANCED: Prefer to use older API calls; but if not supported, it switches to new calls (Default=false) |
| portNumber<sup>2)</sup> | ADVANCED: Port number for connection to the NeoHub (Default=0 (automatic)) |
<sup>1)</sup> 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).
<sup>2)</sup> 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"

View File

@ -196,4 +196,16 @@ public class NeoHubBindingConstants {
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";
}

View File

@ -30,4 +30,6 @@ public class NeoHubConfiguration {
public int pollingInterval;
public int socketTimeout;
public boolean preferLegacyApi;
public String apiToken = "";
public boolean useWebSocket;
}

View File

@ -113,11 +113,11 @@ public class NeoHubDiscoveryService extends AbstractDiscoveryService {
// 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 @@ public class NeoHubDiscoveryService extends AbstractDiscoveryService {
continue;
}
deviceType = engineerData.getDeviceType(deviceName);
publishDevice((LiveDataRecord) deviceRecord, deviceType);
publishDevice(deviceRecord, deviceType);
}
}
}

View File

@ -28,4 +28,8 @@ public class NeoHubException extends Exception {
public NeoHubException(String message) {
super(message);
}
public NeoHubException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -65,7 +65,7 @@ public class NeoHubHandler extends BaseBridgeHandler {
private final Map<String, Boolean> 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 class NeoHubHandler extends BaseBridgeHandler {
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 class NeoHubHandler extends BaseBridgeHandler {
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 class NeoHubHandler extends BaseBridgeHandler {
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 class NeoHubHandler extends BaseBridgeHandler {
* 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 class NeoHubHandler extends BaseBridgeHandler {
* @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 class NeoHubHandler extends BaseBridgeHandler {
* @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 @@ public class NeoHubHandler extends BaseBridgeHandler {
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 @@ public class NeoHubHandler extends BaseBridgeHandler {
* get the Engineers data
*/
public @Nullable NeoHubGetEngineersData fromNeoHubGetEngineersData() {
NeoHubSocket socket = this.socket;
NeoHubSocketBase socket = this.socket;
if (socket != null) {
String responseJson;
try {

View File

@ -59,9 +59,11 @@ public class NeoHubReadDcbResponse {
}
public @Nullable String getFirmwareVersion() {
BigDecimal firmwareVersionNew = this.firmwareVersionNew;
if (firmwareVersionNew != null) {
return firmwareVersionNew.toString();
}
BigDecimal firmwareVersionOld = this.firmwareVersionOld;
if (firmwareVersionOld != null) {
return firmwareVersionOld.toString();
}

View File

@ -25,54 +25,30 @@ import org.slf4j.Logger;
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 class NeoHubSocket {
return responseJson;
}
@Override
public void close() {
// nothing to do
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -28,8 +28,8 @@
<parameter name="portNumber" type="integer" required="false">
<label>Port Number</label>
<description>Port number of the NeoHub</description>
<default>4242</default>
<description>Override port number to use to connect to the NeoHub (0=automatic)</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
@ -53,6 +53,17 @@
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="useWebSocket" type="boolean" required="false">
<label>Connect via WebSocket</label>
<description>Select whether to communicate with the Neohub via WebSocket or TCP</description>
<default>false</default>
</parameter>
<parameter name="apiToken" type="text" required="false">
<label>API Access Token</label>
<description>API access token for the hub (created with the Heatmiser mobile app)</description>
</parameter>
</config-description>
</bridge-type>

View File

@ -26,6 +26,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
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.ImperialUnits;
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 @@ public class NeoHubTestData {
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 class NeoHubTestData {
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 class NeoHubTestData {
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 class NeoHubTestData {
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 class NeoHubTestData {
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 class NeoHubTestData {
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 class NeoHubTestData {
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 class NeoHubTestData {
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;
}

View File

@ -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();
}
}
}