2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.neohub.internal;
15 import java.io.IOException;
17 import java.net.URISyntaxException;
18 import java.util.concurrent.ExecutionException;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.eclipse.jetty.client.HttpClient;
23 import org.eclipse.jetty.util.ssl.SslContextFactory;
24 import org.eclipse.jetty.websocket.api.Session;
25 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
26 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
27 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
28 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
29 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
30 import org.eclipse.jetty.websocket.client.WebSocketClient;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
34 import com.google.gson.Gson;
37 * Handles the ASCII based communication via web socket between openHAB and NeoHub
39 * @author Andrew Fiddian-Green - Initial contribution
44 public class NeoHubWebSocket extends NeoHubSocketBase {
46 private static final int SLEEP_MILLISECONDS = 100;
47 private static final String REQUEST_OUTER = "{\"message_type\":\"hm_get_command_queue\",\"message\":\"%s\"}";
48 private static final String REQUEST_INNER = "{\"token\":\"%s\",\"COMMANDS\":[{\"COMMAND\":\"%s\",\"COMMANDID\":1}]}";
50 private final Logger logger = LoggerFactory.getLogger(NeoHubWebSocket.class);
51 private final Gson gson = new Gson();
52 private final WebSocketClient webSocketClient;
54 private @Nullable Session session = null;
55 private String responseOuter = "";
56 private boolean responseWaiting;
59 * DTO to receive and parse the response JSON.
61 * @author Andrew Fiddian-Green - Initial contribution
63 private static class Response {
64 @SuppressWarnings("unused")
65 public @Nullable String command_id;
66 @SuppressWarnings("unused")
67 public @Nullable String device_id;
68 public @Nullable String message_type;
69 public @Nullable String response;
72 public NeoHubWebSocket(NeoHubConfiguration config) throws NeoHubException {
75 // initialise and start ssl context factory, http client, web socket client
76 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
77 sslContextFactory.setTrustAll(true);
78 HttpClient httpClient = new HttpClient(sslContextFactory);
81 } catch (Exception e) {
82 throw new NeoHubException(String.format("Error starting http client: '%s'", e.getMessage()));
84 webSocketClient = new WebSocketClient(httpClient);
85 webSocketClient.setConnectTimeout(config.socketTimeout * 1000);
87 webSocketClient.start();
88 } catch (Exception e) {
89 throw new NeoHubException(String.format("Error starting web socket client: '%s'", e.getMessage()));
94 * Open the web socket session.
96 * @throws NeoHubException
98 private void startSession() throws NeoHubException {
99 Session session = this.session;
100 if (session == null || !session.isOpen()) {
103 int port = config.portNumber > 0 ? config.portNumber : NeoHubBindingConstants.PORT_WSS;
104 URI uri = new URI(String.format("wss://%s:%d", config.hostName, port));
105 webSocketClient.connect(this, uri).get();
106 } catch (InterruptedException e) {
107 Thread.currentThread().interrupt();
108 throw new NeoHubException(String.format("Error starting session: '%s'", e.getMessage(), e));
109 } catch (ExecutionException | IOException | URISyntaxException e) {
110 throw new NeoHubException(String.format("Error starting session: '%s'", e.getMessage(), e));
116 * Close the web socket session.
118 private void closeSession() {
119 Session session = this.session;
120 if (session != null) {
127 * Helper to escape the quote marks in a JSON string.
129 * @param json the input JSON string.
130 * @return the escaped JSON version.
132 private String jsonEscape(String json) {
133 return json.replace("\"", "\\\"");
137 * Helper to remove quote escape marks from an escaped JSON string.
139 * @param escapedJson the escaped input string.
140 * @return the clean JSON version.
142 private String jsonUnEscape(String escapedJson) {
143 return escapedJson.replace("\\\"", "\"");
147 * Helper to replace double quote marks in a JSON string with single quote marks.
149 * @param json the input string.
150 * @return the modified version.
152 private String jsonReplaceQuotes(String json) {
153 return json.replace("\"", "'");
157 public synchronized String sendMessage(final String requestJson) throws IOException, NeoHubException {
161 // session start failed
162 Session session = this.session;
163 if (session == null) {
164 throw new NeoHubException("Session is null.");
167 // wrap the inner request in an outer request string
168 String requestOuter = String.format(REQUEST_OUTER,
169 jsonEscape(String.format(REQUEST_INNER, config.apiToken, jsonReplaceQuotes(requestJson))));
171 // initialise the response
173 responseWaiting = true;
176 logger.trace("Sending request: {}", requestOuter);
177 session.getRemote().sendString(requestOuter);
179 // sleep and loop until we get a response or the socket is closed
180 int sleepRemainingMilliseconds = config.socketTimeout * 1000;
181 while (responseWaiting && (sleepRemainingMilliseconds > 0)) {
183 Thread.sleep(SLEEP_MILLISECONDS);
184 sleepRemainingMilliseconds = sleepRemainingMilliseconds - SLEEP_MILLISECONDS;
185 } catch (InterruptedException e) {
186 throw new NeoHubException(String.format("Read timeout '%s'", e.getMessage()));
190 // extract the inner response from the outer response string
191 Response responseDto = gson.fromJson(responseOuter, Response.class);
192 if (responseDto != null && NeoHubBindingConstants.HM_SET_COMMAND_RESPONSE.equals(responseDto.message_type)) {
193 String responseJson = responseDto.response;
194 if (responseJson != null) {
195 responseJson = jsonUnEscape(responseJson);
196 logger.trace("Received response: {}", responseJson);
200 logger.debug("Null or invalid response.");
205 public void close() {
208 webSocketClient.stop();
209 } catch (Exception e) {
214 public void onConnect(Session session) {
215 logger.trace("onConnect: ok");
216 this.session = session;
220 public void onClose(int statusCode, String reason) {
221 logger.trace("onClose: code:{}, reason:{}", statusCode, reason);
222 responseWaiting = false;
227 public void onError(Throwable cause) {
228 logger.trace("onError: cause:{}", cause.getMessage());
233 public void onMessage(String msg) {
234 logger.trace("onMessage: msg:{}", msg);
236 responseWaiting = false;