Skip to content

Commit

Permalink
[neohub] Add support for WebSocket connection to hub (openhab#12915)
Browse files Browse the repository at this point in the history
* [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>
  • Loading branch information
andrewfg authored and nemerdaud committed Feb 28, 2023
1 parent e5a2300 commit 169e386
Show file tree
Hide file tree
Showing 14 changed files with 521 additions and 89 deletions.
34 changes: 22 additions & 12 deletions bundles/org.openhab.binding.neohub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
| 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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ public class NeoHubConfiguration {
public int pollingInterval;
public int socketTimeout;
public boolean preferLegacyApi;
public String apiToken = "";
public boolean useWebSocket;
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
* Discovery service for neo devices
*
* @author Andrew Fiddian-Green - Initial contribution
*
*
*/
@NonNullByDefault
public class NeoHubDiscoveryService extends AbstractDiscoveryService {
Expand Down Expand Up @@ -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;
Expand All @@ -128,7 +128,7 @@ private void discoverDevices() {
continue;
}
deviceType = engineerData.getDeviceType(deviceName);
publishDevice((LiveDataRecord) deviceRecord, deviceType);
publishDevice(deviceRecord, deviceType);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,4 +28,8 @@ public class NeoHubException extends Exception {
public NeoHubException(String message) {
super(message);
}

public NeoHubException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;

/*
Expand Down Expand Up @@ -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;
}
}

/*
Expand All @@ -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;
Expand All @@ -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());
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -128,4 +104,9 @@ public String sendMessage(final String requestJson) throws IOException, NeoHubEx

return responseJson;
}

@Override
public void close() {
// nothing to do
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 169e386

Please sign in to comment.