2 * Copyright (c) 2010-2023 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.util.ssl.SslContextFactory;
23 import org.eclipse.jetty.websocket.api.Session;
24 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
25 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
26 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
27 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
28 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
29 import org.eclipse.jetty.websocket.client.WebSocketClient;
30 import org.openhab.core.io.net.http.WebSocketFactory;
31 import org.openhab.core.thing.ThingUID;
32 import org.openhab.core.thing.util.ThingWebClientUtil;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
36 import com.google.gson.Gson;
37 import com.google.gson.JsonParser;
38 import com.google.gson.JsonSyntaxException;
41 * Handles the text based communication via web socket between openHAB and NeoHub
43 * @author Andrew Fiddian-Green - Initial contribution
48 public class NeoHubWebSocket extends NeoHubSocketBase {
50 private static final int SLEEP_MILLISECONDS = 100;
51 private static final String REQUEST_OUTER = "{\"message_type\":\"hm_get_command_queue\",\"message\":\"%s\"}";
52 private static final String REQUEST_INNER = "{\"token\":\"%s\",\"COMMANDS\":[{\"COMMAND\":\"%s\",\"COMMANDID\":1}]}";
54 private final Logger logger = LoggerFactory.getLogger(NeoHubWebSocket.class);
55 private final Gson gson = new Gson();
56 private final WebSocketClient webSocketClient;
58 private @Nullable Session session = null;
59 private String responseOuter = "";
60 private boolean responsePending;
63 * DTO to receive and parse the response JSON.
65 * @author Andrew Fiddian-Green - Initial contribution
67 private static class Response {
68 @SuppressWarnings("unused")
69 public @Nullable String command_id;
70 @SuppressWarnings("unused")
71 public @Nullable String device_id;
72 public @Nullable String message_type;
73 public @Nullable String response;
76 public NeoHubWebSocket(NeoHubConfiguration config, WebSocketFactory webSocketFactory, ThingUID bridgeUID)
78 super(config, bridgeUID.getAsString());
80 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
81 sslContextFactory.setTrustAll(true);
82 String name = ThingWebClientUtil.buildWebClientConsumerName(bridgeUID, null);
83 webSocketClient = webSocketFactory.createWebSocketClient(name, sslContextFactory);
84 webSocketClient.setConnectTimeout(config.socketTimeout * 1000);
86 webSocketClient.start();
87 } catch (Exception e) {
88 throw new IOException("Error starting Web Socket client", e);
93 * Open the web socket session.
95 * @throws IOException if unable to open the web socket
97 private void startSession() throws IOException {
98 Session session = this.session;
99 if (session == null || !session.isOpen()) {
102 int port = config.portNumber > 0 ? config.portNumber : NeoHubBindingConstants.PORT_WSS;
103 URI uri = new URI(String.format("wss://%s:%d", config.hostName, port));
104 webSocketClient.connect(this, uri).get();
105 } catch (InterruptedException e) {
106 Thread.currentThread().interrupt();
107 throw new IOException("Error starting session", e);
108 } catch (ExecutionException | IOException | URISyntaxException e) {
109 throw new IOException("Error starting session", e);
115 * Close the web socket session.
117 private void closeSession() {
118 Session session = this.session;
119 if (session != null) {
126 * Helper to escape the quote marks in a JSON string.
128 * @param json the input JSON string.
129 * @return the escaped JSON version.
131 private String jsonEscape(String json) {
132 return json.replace("\"", "\\\"");
136 * Helper to remove quote escape marks from an escaped JSON string.
138 * @param escapedJson the escaped input string.
139 * @return the clean JSON version.
141 private String jsonUnEscape(String escapedJson) {
142 return escapedJson.replace("\\\"", "\"");
146 * Helper to replace double quote marks in a JSON string with single quote marks.
148 * @param json the input string.
149 * @return the modified version.
151 private String jsonReplaceQuotes(String json) {
152 return json.replace("\"", "'");
156 public synchronized String sendMessage(final String requestJson) throws IOException, NeoHubException {
160 // session start failed
161 Session session = this.session;
162 if (session == null) {
163 throw new IOException("Session is null");
166 // wrap the inner request in an outer request string
167 String requestOuter = String.format(REQUEST_OUTER,
168 jsonEscape(String.format(REQUEST_INNER, config.apiToken, jsonReplaceQuotes(requestJson))));
170 // initialise the response
172 responsePending = true;
174 IOException caughtException = null;
177 logger.debug("hub '{}' sending characters:{}", hubId, requestOuter.length());
178 session.getRemote().sendString(requestOuter);
179 logger.trace("hub '{}' sent:{}", hubId, requestOuter);
181 // sleep and loop until we get a response or the socket is closed
182 int sleepRemainingMilliseconds = config.socketTimeout * 1000;
183 while (responsePending) {
185 Thread.sleep(SLEEP_MILLISECONDS);
186 sleepRemainingMilliseconds = sleepRemainingMilliseconds - SLEEP_MILLISECONDS;
187 if (sleepRemainingMilliseconds <= 0) {
188 throw new IOException("Read timed out");
190 } catch (InterruptedException e) {
191 throw new IOException("Read interrupted", e);
194 } catch (IOException e) {
198 logger.debug("hub '{}' received characters:{}", hubId, responseOuter.length());
199 logger.trace("hub '{}' received:{}", hubId, responseOuter);
201 // if an IOException was caught above, re-throw it again
202 if (caughtException != null) {
203 throw caughtException;
207 Response responseDto = gson.fromJson(responseOuter, Response.class);
208 if (responseDto == null) {
209 throw new JsonSyntaxException("Response DTO is invalid");
211 if (!NeoHubBindingConstants.HM_SET_COMMAND_RESPONSE.equals(responseDto.message_type)) {
212 throw new JsonSyntaxException("DTO 'message_type' field is invalid");
214 String responseJson = responseDto.response;
215 if (responseJson == null) {
216 throw new JsonSyntaxException("DTO 'response' field is null");
218 responseJson = jsonUnEscape(responseJson).strip();
219 if (!JsonParser.parseString(responseJson).isJsonObject()) {
220 throw new JsonSyntaxException("DTO 'response' field is not a JSON object");
223 } catch (JsonSyntaxException e) {
224 logger.debug("hub '{}' {}; response:{}", hubId, e.getMessage(), responseOuter);
225 throw new NeoHubException("Invalid response");
230 public void close() {
233 webSocketClient.stop();
234 } catch (Exception e) {
239 public void onConnect(Session session) {
240 logger.debug("hub '{}' onConnect() ok", hubId);
241 this.session = session;
245 public void onClose(int statusCode, String reason) {
246 logger.debug("hub '{}' onClose() statusCode:{}, reason:{}", hubId, statusCode, reason);
247 responsePending = false;
252 public void onError(Throwable cause) {
253 logger.debug("hub '{}' onError() cause:{}", hubId, cause.getMessage());
258 public void onMessage(String msg) {
259 responseOuter = msg.strip();
260 responsePending = false;