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) |
+| 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 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"
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";
}
public int pollingInterval;
public int socketTimeout;
public boolean preferLegacyApi;
+ public String apiToken = "";
+ public boolean useWebSocket;
}
* Discovery service for neo devices
*
* @author Andrew Fiddian-Green - Initial contribution
- *
+ *
*/
@NonNullByDefault
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;
continue;
}
deviceType = engineerData.getDeviceType(deviceName);
- publishDevice((LiveDataRecord) deviceRecord, deviceType);
+ publishDevice(deviceRecord, deviceType);
}
}
}
* The {@link NeoHubException} is a custom exception for NeoHub
*
* @author Andrew Fiddian-Green - Initial contribution
- *
+ *
*/
@NonNullByDefault
public class NeoHubException extends Exception {
public NeoHubException(String message) {
super(message);
}
+
+ public NeoHubException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
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;
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;
}
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;
/*
fast.cancel(true);
this.fastPollingScheduler = null;
}
+
+ NeoHubSocketBase socket = this.socket;
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ }
+ this.socket = null;
+ }
}
/*
* 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;
* @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());
* @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;
boolean supportsLegacyApi = false;
boolean supportsFutureApi = false;
- NeoHubSocket socket = this.socket;
+ NeoHubSocketBase socket = this.socket;
if (socket != null) {
String responseJson;
NeoHubReadDcbResponse systemData;
* get the Engineers data
*/
public @Nullable NeoHubGetEngineersData fromNeoHubGetEngineersData() {
- NeoHubSocket socket = this.socket;
+ NeoHubSocketBase socket = this.socket;
if (socket != null) {
String responseJson;
try {
}
public @Nullable String getFirmwareVersion() {
+ BigDecimal firmwareVersionNew = this.firmwareVersionNew;
if (firmwareVersionNew != null) {
return firmwareVersionNew.toString();
}
+ BigDecimal firmwareVersionOld = this.firmwareVersionOld;
if (firmwareVersionOld != null) {
return firmwareVersionOld.toString();
}
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)) {
return responseJson;
}
+
+ @Override
+ public void close() {
+ // nothing to do
+ }
}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
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
<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>
<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>
--- /dev/null
+/**
+ * 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 static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.util.regex.Pattern;
+
+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;
+import org.openhab.binding.neohub.internal.NeoHubLiveDeviceData;
+import org.openhab.binding.neohub.internal.NeoHubReadDcbResponse;
+import org.openhab.binding.neohub.internal.NeoHubSocket;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * JUnit for testing JSON parsing.
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@NonNullByDefault
+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 HUB_IP_ADDRESS = "192.168.1.xxx";
+
+ 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) {
+ try (FileReader file = new FileReader(String.format("src/test/resources/%s.json", fileName));
+ BufferedReader reader = new BufferedReader(file)) {
+ StringBuilder builder = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line).append("\n");
+ }
+ return builder.toString();
+ } catch (IOException e) {
+ fail(e.getMessage());
+ }
+ return "";
+ }
+
+ /**
+ * Test an INFO JSON response string as produced by older firmware versions
+ */
+ @Test
+ public void testInfoJsonOld() {
+ // load INFO JSON response string in old JSON format
+ NeoHubAbstractDeviceData infoResponse = NeoHubInfoResponse.createDeviceData(load("info_old"));
+ assertNotNull(infoResponse);
+
+ // missing device
+ AbstractRecord device = infoResponse.getDeviceRecord("Aardvark");
+ assertNull(device);
+
+ // existing type 12 thermostat device
+ device = infoResponse.getDeviceRecord("Dining Room");
+ assertNotNull(device);
+ assertEquals("Dining Room", device.getDeviceName());
+ assertEquals(new BigDecimal("22.0"), device.getTargetTemperature());
+ assertEquals(new BigDecimal("22.2"), device.getActualTemperature());
+ assertEquals(new BigDecimal("23"), device.getFloorTemperature());
+ assertTrue(device instanceof InfoRecord);
+ assertEquals(12, ((InfoRecord) device).getDeviceType());
+ assertFalse(device.isStandby());
+ assertFalse(device.isHeating());
+ assertFalse(device.isPreHeating());
+ assertFalse(device.isTimerOn());
+ assertFalse(device.offline());
+ assertFalse(device.stateManual());
+ assertTrue(device.stateAuto());
+ assertFalse(device.isWindowOpen());
+ assertFalse(device.isBatteryLow());
+
+ // existing type 6 plug device (MANUAL OFF)
+ device = infoResponse.getDeviceRecord("Plug South");
+ assertNotNull(device);
+ assertEquals("Plug South", device.getDeviceName());
+ assertTrue(device instanceof InfoRecord);
+ assertEquals(6, ((InfoRecord) device).getDeviceType());
+ assertFalse(device.isTimerOn());
+ assertTrue(device.stateManual());
+
+ // existing type 6 plug device (MANUAL ON)
+ device = infoResponse.getDeviceRecord("Plug North");
+ assertNotNull(device);
+ assertEquals("Plug North", device.getDeviceName());
+ assertTrue(device instanceof InfoRecord);
+ assertEquals(6, ((InfoRecord) device).getDeviceType());
+ assertTrue(device.isTimerOn());
+ assertTrue(device.stateManual());
+
+ // existing type 6 plug device (AUTO OFF)
+ device = infoResponse.getDeviceRecord("Watering System");
+ assertNotNull(device);
+ assertEquals("Watering System", device.getDeviceName());
+ assertTrue(device instanceof InfoRecord);
+ assertEquals(6, ((InfoRecord) device).getDeviceType());
+ assertFalse(device.isTimerOn());
+ assertFalse(device.stateManual());
+ }
+
+ /**
+ * Test an INFO JSON response string as produced by newer firmware versions
+ */
+ @Test
+ public void testInfoJsonNew() {
+ // load INFO JSON response string in new JSON format
+ NeoHubAbstractDeviceData infoResponse = NeoHubInfoResponse.createDeviceData(load("info_new"));
+ assertNotNull(infoResponse);
+
+ // existing device (new JSON format)
+ AbstractRecord device = infoResponse.getDeviceRecord("Dining Room");
+ assertNotNull(device);
+ assertEquals("Dining Room", device.getDeviceName());
+ assertFalse(device.offline());
+ assertFalse(device.isWindowOpen());
+
+ // existing repeater device
+ device = infoResponse.getDeviceRecord("repeaternode54473");
+ assertNotNull(device);
+ assertEquals("repeaternode54473", device.getDeviceName());
+ assertEquals(new BigDecimal("127"), device.getFloorTemperature());
+ assertEquals(new BigDecimal("255.255"), device.getActualTemperature());
+ }
+
+ /**
+ * Test for a READ_DCB JSON string that has valid CORF C response
+ */
+ @Test
+ public void testReadDcbJson() {
+ // load READ_DCB JSON response string with valid CORF C response
+ NeoHubReadDcbResponse dcbResponse = NeoHubReadDcbResponse.createSystemData(load("dcb_celsius"));
+ assertNotNull(dcbResponse);
+ assertEquals(SIUnits.CELSIUS, dcbResponse.getTemperatureUnit());
+ assertEquals("2134", dcbResponse.getFirmwareVersion());
+
+ // load READ_DCB JSON response string with valid CORF F response
+ dcbResponse = NeoHubReadDcbResponse.createSystemData(load("dcb_fahrenheit"));
+ assertNotNull(dcbResponse);
+ assertEquals(ImperialUnits.FAHRENHEIT, dcbResponse.getTemperatureUnit());
+
+ // load READ_DCB JSON response string with missing CORF element
+ dcbResponse = NeoHubReadDcbResponse.createSystemData(load("dcb_corf_missing"));
+ assertNotNull(dcbResponse);
+ assertEquals(SIUnits.CELSIUS, dcbResponse.getTemperatureUnit());
+
+ // load READ_DCB JSON response string where CORF element is an empty string
+ dcbResponse = NeoHubReadDcbResponse.createSystemData(load("dcb_corf_empty"));
+ assertNotNull(dcbResponse);
+ assertEquals(SIUnits.CELSIUS, dcbResponse.getTemperatureUnit());
+ }
+
+ /**
+ * Test an INFO JSON string that has a door contact and a temperature sensor
+ */
+ @Test
+ public void testInfoJsonWithSensors() {
+ /*
+ * load an INFO JSON response string that has a closed door contact and a
+ * temperature sensor
+ */
+ // save("info_sensors_closed", NEOHUB_JSON_TEST_STRING_INFO_SENSORS_CLOSED);
+ NeoHubAbstractDeviceData infoResponse = NeoHubInfoResponse.createDeviceData(load("info_sensors_closed"));
+ assertNotNull(infoResponse);
+
+ // existing contact device type 5 (CLOSED)
+ AbstractRecord device = infoResponse.getDeviceRecord("Back Door");
+ assertNotNull(device);
+ assertEquals("Back Door", device.getDeviceName());
+ assertTrue(device instanceof InfoRecord);
+ assertEquals(5, ((InfoRecord) device).getDeviceType());
+ assertFalse(device.isWindowOpen());
+ assertFalse(device.isBatteryLow());
+
+ // existing temperature sensor type 14
+ device = infoResponse.getDeviceRecord("Master Bedroom");
+ assertNotNull(device);
+ assertEquals("Master Bedroom", device.getDeviceName());
+ assertTrue(device instanceof InfoRecord);
+ assertEquals(14, ((InfoRecord) device).getDeviceType());
+ assertEquals(new BigDecimal("19.5"), device.getActualTemperature());
+
+ // existing thermostat type 1
+ device = infoResponse.getDeviceRecord("Living Room Floor");
+ assertNotNull(device);
+ assertEquals("Living Room Floor", device.getDeviceName());
+ assertTrue(device instanceof InfoRecord);
+ assertEquals(1, ((InfoRecord) device).getDeviceType());
+ assertEquals(new BigDecimal("19.8"), device.getActualTemperature());
+
+ // load an INFO JSON response string that has an open door contact
+ // save("info_sensors_open", NEOHUB_JSON_TEST_STRING_INFO_SENSORS_OPEN);
+ infoResponse = NeoHubInfoResponse.createDeviceData(load("info_sensors_open"));
+ assertNotNull(infoResponse);
+
+ // existing contact device type 5 (OPEN)
+ device = infoResponse.getDeviceRecord("Back Door");
+ assertNotNull(device);
+ assertEquals("Back Door", device.getDeviceName());
+ assertTrue(device instanceof InfoRecord);
+ assertEquals(5, ((InfoRecord) device).getDeviceType());
+ assertTrue(device.isWindowOpen());
+ 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)
+ */
+ @Test
+ public void testGetSystemJson() {
+ // load GET_SYSTEM JSON response string
+ NeoHubReadDcbResponse dcbResponse;
+ dcbResponse = NeoHubReadDcbResponse.createSystemData(load("system"));
+ assertNotNull(dcbResponse);
+ assertEquals(SIUnits.CELSIUS, dcbResponse.getTemperatureUnit());
+ assertEquals("2134", dcbResponse.getFirmwareVersion());
+ }
+
+ /**
+ * From NeoHub rev2.6 onwards the INFO command is "deprecated" so we must test
+ * the replacement GET_LIVE_DATA command
+ */
+ @Test
+ public void testGetLiveDataJson() {
+ // load GET_LIVE_DATA JSON response string
+ NeoHubLiveDeviceData liveDataResponse = NeoHubLiveDeviceData.createDeviceData(load("live_data"));
+ assertNotNull(liveDataResponse);
+
+ // test the time stamps
+ assertEquals(1588494785, liveDataResponse.getTimestampEngineers());
+ assertEquals(0, liveDataResponse.getTimestampSystem());
+
+ // missing device
+ AbstractRecord device = liveDataResponse.getDeviceRecord("Aardvark");
+ assertNull(device);
+
+ // test an existing thermostat device
+ device = liveDataResponse.getDeviceRecord("Dining Room");
+ assertNotNull(device);
+ assertEquals("Dining Room", device.getDeviceName());
+ assertEquals(new BigDecimal("22.0"), device.getTargetTemperature());
+ assertEquals(new BigDecimal("22.2"), device.getActualTemperature());
+ assertEquals(new BigDecimal("20.50"), device.getFloorTemperature());
+ assertFalse(device.isStandby());
+ assertFalse(device.isHeating());
+ assertFalse(device.isPreHeating());
+ assertFalse(device.isTimerOn());
+ assertFalse(device.offline());
+ assertFalse(device.stateManual());
+ assertTrue(device.stateAuto());
+ assertFalse(device.isWindowOpen());
+ assertFalse(device.isBatteryLow());
+
+ // test a plug device (MANUAL OFF)
+ device = liveDataResponse.getDeviceRecord("Living Room South");
+ assertNotNull(device);
+ assertEquals("Living Room South", device.getDeviceName());
+ assertFalse(device.isTimerOn());
+ assertTrue(device.stateManual());
+
+ // test a plug device (MANUAL ON)
+ device = liveDataResponse.getDeviceRecord("Living Room North");
+ assertNotNull(device);
+ assertEquals("Living Room North", device.getDeviceName());
+ assertTrue(device.isTimerOn());
+ assertTrue(device.stateManual());
+
+ // test a plug device (AUTO OFF)
+ device = liveDataResponse.getDeviceRecord("Green Wall Watering");
+ assertNotNull(device);
+ assertEquals("Green Wall Watering", device.getDeviceName());
+ assertFalse(device.isTimerOn());
+ assertFalse(device.stateManual());
+
+ // test a device that is offline
+ device = liveDataResponse.getDeviceRecord("Shower Room");
+ assertNotNull(device);
+ assertEquals("Shower Room", device.getDeviceName());
+ assertTrue(device.offline());
+
+ // test a device with a low battery
+ device = liveDataResponse.getDeviceRecord("Conservatory");
+ assertNotNull(device);
+ assertEquals("Conservatory", device.getDeviceName());
+ assertTrue(device.isBatteryLow());
+
+ // test a device with an open window alarm
+ device = liveDataResponse.getDeviceRecord("Door Contact");
+ assertNotNull(device);
+ assertEquals("Door Contact", device.getDeviceName());
+ assertTrue(device.isWindowOpen());
+
+ // test a wireless temperature sensor
+ device = liveDataResponse.getDeviceRecord("Room Sensor");
+ assertNotNull(device);
+ assertEquals("Room Sensor", device.getDeviceName());
+ assertEquals(new BigDecimal("21.5"), device.getActualTemperature());
+
+ // test a repeater node
+ device = liveDataResponse.getDeviceRecord("repeaternode54473");
+ assertNotNull(device);
+ assertEquals("repeaternode54473", device.getDeviceName());
+ 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
+ */
+ @Test
+ public void testGetEngineersJson() {
+ // load GET_ENGINEERS JSON response string
+ NeoHubGetEngineersData engResponse = NeoHubGetEngineersData.createEngineersData(load("engineers"));
+ assertNotNull(engResponse);
+
+ // test device ID (type 12 thermostat device)
+ assertEquals(12, engResponse.getDeviceType("Dining Room"));
+
+ // test device ID (type 6 plug device)
+ assertEquals(6, engResponse.getDeviceType("Living Room South"));
+ }
+
+ /**
+ * send JSON request to the socket and retrieve JSON response
+ */
+ private String testCommunicationInner(String requestJson) {
+ NeoHubConfiguration config = new NeoHubConfiguration();
+ config.hostName = HUB_IP_ADDRESS;
+ config.socketTimeout = 5;
+ try {
+ NeoHubSocket socket = new NeoHubSocket(config);
+ String responseJson = socket.sendMessage(requestJson);
+ socket.close();
+ return responseJson;
+ } catch (Exception e) {
+ assertTrue(false);
+ }
+ return "";
+ }
+
+ /**
+ * Test the communications
+ */
+ @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(HUB_IP_ADDRESS).matches()) {
+ return;
+ }
+
+ String responseJson = testCommunicationInner(CMD_CODE_INFO);
+ assertFalse(responseJson.isEmpty());
+
+ responseJson = testCommunicationInner(CMD_CODE_READ_DCB);
+ assertFalse(responseJson.isEmpty());
+
+ NeoHubReadDcbResponse dcbResponse = NeoHubReadDcbResponse.createSystemData(responseJson);
+ assertNotNull(dcbResponse);
+
+ long timeStamp = dcbResponse.timeStamp;
+ assertEquals(Instant.now().getEpochSecond(), timeStamp, 1);
+
+ responseJson = testCommunicationInner(CMD_CODE_GET_LIVE_DATA);
+ assertFalse(responseJson.isEmpty());
+
+ NeoHubLiveDeviceData liveDataResponse = NeoHubLiveDeviceData.createDeviceData(responseJson);
+ assertNotNull(liveDataResponse);
+
+ assertTrue(timeStamp > liveDataResponse.getTimestampEngineers());
+ assertTrue(timeStamp > liveDataResponse.getTimestampSystem());
+
+ responseJson = testCommunicationInner(CMD_CODE_GET_ENGINEERS);
+ assertFalse(responseJson.isEmpty());
+
+ responseJson = testCommunicationInner(CMD_CODE_GET_SYSTEM);
+ assertFalse(responseJson.isEmpty());
+
+ responseJson = testCommunicationInner(String.format(CMD_CODE_TEMP, "20", "Hallway"));
+ assertFalse(responseJson.isEmpty());
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+ }
+}
+++ /dev/null
-/**
- * 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 static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*;
-
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.io.IOException;
-import java.math.BigDecimal;
-import java.time.Instant;
-import java.util.regex.Pattern;
-
-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.NeoHubGetEngineersData;
-import org.openhab.binding.neohub.internal.NeoHubInfoResponse;
-import org.openhab.binding.neohub.internal.NeoHubInfoResponse.InfoRecord;
-import org.openhab.binding.neohub.internal.NeoHubLiveDeviceData;
-import org.openhab.binding.neohub.internal.NeoHubReadDcbResponse;
-import org.openhab.binding.neohub.internal.NeoHubSocket;
-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.
- *
- * @author Andrew Fiddian-Green - Initial contribution
- */
-@NonNullByDefault
-public class NeoHubTestData {
-
- /*
- * 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 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) {
- try (FileReader file = new FileReader(String.format("src/test/resources/%s.json", fileName));
- BufferedReader reader = new BufferedReader(file)) {
- StringBuilder builder = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- builder.append(line).append("\n");
- }
- return builder.toString();
- } catch (IOException e) {
- fail(e.getMessage());
- }
- 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
- NeoHubAbstractDeviceData infoResponse = NeoHubInfoResponse.createDeviceData(load("info_old"));
- assertNotNull(infoResponse);
-
- // missing device
- AbstractRecord device = infoResponse.getDeviceRecord("Aardvark");
- assertNull(device);
-
- // existing type 12 thermostat device
- device = infoResponse.getDeviceRecord("Dining Room");
- assertNotNull(device);
- assertEquals("Dining Room", device.getDeviceName());
- assertEquals(new BigDecimal("22.0"), device.getTargetTemperature());
- assertEquals(new BigDecimal("22.2"), device.getActualTemperature());
- assertEquals(new BigDecimal("23"), device.getFloorTemperature());
- assertTrue(device instanceof InfoRecord);
- assertEquals(12, ((InfoRecord) device).getDeviceType());
- assertFalse(device.isStandby());
- assertFalse(device.isHeating());
- assertFalse(device.isPreHeating());
- assertFalse(device.isTimerOn());
- assertFalse(device.offline());
- assertFalse(device.stateManual());
- assertTrue(device.stateAuto());
- assertFalse(device.isWindowOpen());
- assertFalse(device.isBatteryLow());
-
- // existing type 6 plug device (MANUAL OFF)
- device = infoResponse.getDeviceRecord("Plug South");
- assertNotNull(device);
- assertEquals("Plug South", device.getDeviceName());
- assertTrue(device instanceof InfoRecord);
- assertEquals(6, ((InfoRecord) device).getDeviceType());
- assertFalse(device.isTimerOn());
- assertTrue(device.stateManual());
-
- // existing type 6 plug device (MANUAL ON)
- device = infoResponse.getDeviceRecord("Plug North");
- assertNotNull(device);
- assertEquals("Plug North", device.getDeviceName());
- assertTrue(device instanceof InfoRecord);
- assertEquals(6, ((InfoRecord) device).getDeviceType());
- assertTrue(device.isTimerOn());
- assertTrue(device.stateManual());
-
- // existing type 6 plug device (AUTO OFF)
- device = infoResponse.getDeviceRecord("Watering System");
- assertNotNull(device);
- assertEquals("Watering System", device.getDeviceName());
- assertTrue(device instanceof InfoRecord);
- assertEquals(6, ((InfoRecord) device).getDeviceType());
- assertFalse(device.isTimerOn());
- 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
- NeoHubAbstractDeviceData infoResponse = NeoHubInfoResponse.createDeviceData(load("info_new"));
- assertNotNull(infoResponse);
-
- // existing device (new JSON format)
- AbstractRecord device = infoResponse.getDeviceRecord("Dining Room");
- assertNotNull(device);
- assertEquals("Dining Room", device.getDeviceName());
- assertFalse(device.offline());
- assertFalse(device.isWindowOpen());
-
- // existing repeater device
- device = infoResponse.getDeviceRecord("repeaternode54473");
- assertNotNull(device);
- assertEquals("repeaternode54473", device.getDeviceName());
- assertEquals(new BigDecimal("127"), device.getFloorTemperature());
- 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
- NeoHubReadDcbResponse dcbResponse = NeoHubReadDcbResponse.createSystemData(load("dcb_celsius"));
- assertNotNull(dcbResponse);
- assertEquals(SIUnits.CELSIUS, dcbResponse.getTemperatureUnit());
- assertEquals("2134", dcbResponse.getFirmwareVersion());
-
- // load READ_DCB JSON response string with valid CORF F response
- dcbResponse = NeoHubReadDcbResponse.createSystemData(load("dcb_fahrenheit"));
- assertNotNull(dcbResponse);
- assertEquals(ImperialUnits.FAHRENHEIT, dcbResponse.getTemperatureUnit());
-
- // load READ_DCB JSON response string with missing CORF element
- dcbResponse = NeoHubReadDcbResponse.createSystemData(load("dcb_corf_missing"));
- assertNotNull(dcbResponse);
- assertEquals(SIUnits.CELSIUS, dcbResponse.getTemperatureUnit());
-
- // load READ_DCB JSON response string where CORF element is an empty string
- dcbResponse = NeoHubReadDcbResponse.createSystemData(load("dcb_corf_empty"));
- assertNotNull(dcbResponse);
- 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() {
- /*
- * load an INFO JSON response string that has a closed door contact and a
- * temperature sensor
- */
- // save("info_sensors_closed", NEOHUB_JSON_TEST_STRING_INFO_SENSORS_CLOSED);
- NeoHubAbstractDeviceData infoResponse = NeoHubInfoResponse.createDeviceData(load("info_sensors_closed"));
- assertNotNull(infoResponse);
-
- // existing contact device type 5 (CLOSED)
- AbstractRecord device = infoResponse.getDeviceRecord("Back Door");
- assertNotNull(device);
- assertEquals("Back Door", device.getDeviceName());
- assertTrue(device instanceof InfoRecord);
- assertEquals(5, ((InfoRecord) device).getDeviceType());
- assertFalse(device.isWindowOpen());
- assertFalse(device.isBatteryLow());
-
- // existing temperature sensor type 14
- device = infoResponse.getDeviceRecord("Master Bedroom");
- assertNotNull(device);
- assertEquals("Master Bedroom", device.getDeviceName());
- assertTrue(device instanceof InfoRecord);
- assertEquals(14, ((InfoRecord) device).getDeviceType());
- assertEquals(new BigDecimal("19.5"), device.getActualTemperature());
-
- // existing thermostat type 1
- device = infoResponse.getDeviceRecord("Living Room Floor");
- assertNotNull(device);
- assertEquals("Living Room Floor", device.getDeviceName());
- assertTrue(device instanceof InfoRecord);
- assertEquals(1, ((InfoRecord) device).getDeviceType());
- assertEquals(new BigDecimal("19.8"), device.getActualTemperature());
-
- // load an INFO JSON response string that has an open door contact
- // save("info_sensors_open", NEOHUB_JSON_TEST_STRING_INFO_SENSORS_OPEN);
- infoResponse = NeoHubInfoResponse.createDeviceData(load("info_sensors_open"));
- assertNotNull(infoResponse);
-
- // existing contact device type 5 (OPEN)
- device = infoResponse.getDeviceRecord("Back Door");
- assertNotNull(device);
- assertEquals("Back Door", device.getDeviceName());
- assertTrue(device instanceof InfoRecord);
- assertEquals(5, ((InfoRecord) device).getDeviceType());
- assertTrue(device.isWindowOpen());
- 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
- NeoHubReadDcbResponse dcbResponse;
- dcbResponse = NeoHubReadDcbResponse.createSystemData(load("system"));
- assertNotNull(dcbResponse);
- assertEquals(SIUnits.CELSIUS, dcbResponse.getTemperatureUnit());
- 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
- NeoHubLiveDeviceData liveDataResponse = NeoHubLiveDeviceData.createDeviceData(load("live_data"));
- assertNotNull(liveDataResponse);
-
- // test the time stamps
- assertEquals(1588494785, liveDataResponse.getTimestampEngineers());
- assertEquals(0, liveDataResponse.getTimestampSystem());
-
- // missing device
- AbstractRecord device = liveDataResponse.getDeviceRecord("Aardvark");
- assertNull(device);
-
- // test an existing thermostat device
- device = liveDataResponse.getDeviceRecord("Dining Room");
- assertNotNull(device);
- assertEquals("Dining Room", device.getDeviceName());
- assertEquals(new BigDecimal("22.0"), device.getTargetTemperature());
- assertEquals(new BigDecimal("22.2"), device.getActualTemperature());
- assertEquals(new BigDecimal("20.50"), device.getFloorTemperature());
- assertFalse(device.isStandby());
- assertFalse(device.isHeating());
- assertFalse(device.isPreHeating());
- assertFalse(device.isTimerOn());
- assertFalse(device.offline());
- assertFalse(device.stateManual());
- assertTrue(device.stateAuto());
- assertFalse(device.isWindowOpen());
- assertFalse(device.isBatteryLow());
-
- // test a plug device (MANUAL OFF)
- device = liveDataResponse.getDeviceRecord("Living Room South");
- assertNotNull(device);
- assertEquals("Living Room South", device.getDeviceName());
- assertFalse(device.isTimerOn());
- assertTrue(device.stateManual());
-
- // test a plug device (MANUAL ON)
- device = liveDataResponse.getDeviceRecord("Living Room North");
- assertNotNull(device);
- assertEquals("Living Room North", device.getDeviceName());
- assertTrue(device.isTimerOn());
- assertTrue(device.stateManual());
-
- // test a plug device (AUTO OFF)
- device = liveDataResponse.getDeviceRecord("Green Wall Watering");
- assertNotNull(device);
- assertEquals("Green Wall Watering", device.getDeviceName());
- assertFalse(device.isTimerOn());
- assertFalse(device.stateManual());
-
- // test a device that is offline
- device = liveDataResponse.getDeviceRecord("Shower Room");
- assertNotNull(device);
- assertEquals("Shower Room", device.getDeviceName());
- assertTrue(device.offline());
-
- // test a device with a low battery
- device = liveDataResponse.getDeviceRecord("Conservatory");
- assertNotNull(device);
- assertEquals("Conservatory", device.getDeviceName());
- assertTrue(device.isBatteryLow());
-
- // test a device with an open window alarm
- device = liveDataResponse.getDeviceRecord("Door Contact");
- assertNotNull(device);
- assertEquals("Door Contact", device.getDeviceName());
- assertTrue(device.isWindowOpen());
-
- // test a wireless temperature sensor
- device = liveDataResponse.getDeviceRecord("Room Sensor");
- assertNotNull(device);
- assertEquals("Room Sensor", device.getDeviceName());
- assertEquals(new BigDecimal("21.5"), device.getActualTemperature());
-
- // test a repeater node
- device = liveDataResponse.getDeviceRecord("repeaternode54473");
- assertNotNull(device);
- assertEquals("repeaternode54473", device.getDeviceName());
- 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
- NeoHubGetEngineersData engResponse = NeoHubGetEngineersData.createEngineersData(load("engineers"));
- assertNotNull(engResponse);
-
- // test device ID (type 12 thermostat device)
- assertEquals(12, engResponse.getDeviceType("Dining Room"));
-
- // test device ID (type 6 plug device)
- 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 = "";
- try {
- responseJson = socket.sendMessage(requestJson);
- } catch (Exception e) {
- assertTrue(false);
- }
- return responseJson;
- }
-
- /*
- * 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()) {
- return;
- }
-
- String responseJson = testCommunicationInner(CMD_CODE_INFO);
- assertFalse(responseJson.isEmpty());
-
- responseJson = testCommunicationInner(CMD_CODE_READ_DCB);
- assertFalse(responseJson.isEmpty());
-
- NeoHubReadDcbResponse dcbResponse = NeoHubReadDcbResponse.createSystemData(responseJson);
- assertNotNull(dcbResponse);
-
- long timeStamp = dcbResponse.timeStamp;
- assertEquals(Instant.now().getEpochSecond(), timeStamp, 1);
-
- responseJson = testCommunicationInner(CMD_CODE_GET_LIVE_DATA);
- assertFalse(responseJson.isEmpty());
-
- NeoHubLiveDeviceData liveDataResponse = NeoHubLiveDeviceData.createDeviceData(responseJson);
- assertNotNull(liveDataResponse);
-
- assertTrue(timeStamp > liveDataResponse.getTimestampEngineers());
- assertTrue(timeStamp > liveDataResponse.getTimestampSystem());
-
- responseJson = testCommunicationInner(CMD_CODE_GET_ENGINEERS);
- assertFalse(responseJson.isEmpty());
-
- responseJson = testCommunicationInner(CMD_CODE_GET_SYSTEM);
- assertFalse(responseJson.isEmpty());
-
- responseJson = testCommunicationInner(String.format(CMD_CODE_TEMP, "20", "Hallway"));
- assertFalse(responseJson.isEmpty());
- }
-}