]> git.basschouten.com Git - openhab-addons.git/blob
fddfd06e0c03faf7f6206fa69f1336d86a624c49
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.neohub.internal;
14
15 import java.io.IOException;
16 import java.net.URI;
17 import java.net.URISyntaxException;
18 import java.util.concurrent.ExecutionException;
19
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;
33
34 import com.google.gson.Gson;
35 import com.google.gson.JsonParser;
36 import com.google.gson.JsonSyntaxException;
37
38 /**
39  * Handles the text based communication via web socket between openHAB and NeoHub
40  *
41  * @author Andrew Fiddian-Green - Initial contribution
42  *
43  */
44 @NonNullByDefault
45 @WebSocket
46 public class NeoHubWebSocket extends NeoHubSocketBase {
47
48     private static final int SLEEP_MILLISECONDS = 100;
49     private static final String REQUEST_OUTER = "{\"message_type\":\"hm_get_command_queue\",\"message\":\"%s\"}";
50     private static final String REQUEST_INNER = "{\"token\":\"%s\",\"COMMANDS\":[{\"COMMAND\":\"%s\",\"COMMANDID\":1}]}";
51
52     private final Logger logger = LoggerFactory.getLogger(NeoHubWebSocket.class);
53     private final Gson gson = new Gson();
54     private final WebSocketClient webSocketClient;
55
56     private @Nullable Session session = null;
57     private String responseOuter = "";
58     private boolean responsePending;
59
60     /**
61      * DTO to receive and parse the response JSON.
62      *
63      * @author Andrew Fiddian-Green - Initial contribution
64      */
65     private static class Response {
66         @SuppressWarnings("unused")
67         public @Nullable String command_id;
68         @SuppressWarnings("unused")
69         public @Nullable String device_id;
70         public @Nullable String message_type;
71         public @Nullable String response;
72     }
73
74     public NeoHubWebSocket(NeoHubConfiguration config, String hubId) throws IOException {
75         super(config, hubId);
76
77         // initialise and start ssl context factory, http client, web socket client
78         SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
79         sslContextFactory.setTrustAll(true);
80         HttpClient httpClient = new HttpClient(sslContextFactory);
81         try {
82             httpClient.start();
83         } catch (Exception e) {
84             throw new IOException("Error starting HTTP client", e);
85         }
86         webSocketClient = new WebSocketClient(httpClient);
87         webSocketClient.setConnectTimeout(config.socketTimeout * 1000);
88         try {
89             webSocketClient.start();
90         } catch (Exception e) {
91             throw new IOException("Error starting Web Socket client", e);
92         }
93     }
94
95     /**
96      * Open the web socket session.
97      *
98      * @throws IOException if unable to open the web socket
99      */
100     private void startSession() throws IOException {
101         Session session = this.session;
102         if (session == null || !session.isOpen()) {
103             closeSession();
104             try {
105                 int port = config.portNumber > 0 ? config.portNumber : NeoHubBindingConstants.PORT_WSS;
106                 URI uri = new URI(String.format("wss://%s:%d", config.hostName, port));
107                 webSocketClient.connect(this, uri).get();
108             } catch (InterruptedException e) {
109                 Thread.currentThread().interrupt();
110                 throw new IOException("Error starting session", e);
111             } catch (ExecutionException | IOException | URISyntaxException e) {
112                 throw new IOException("Error starting session", e);
113             }
114         }
115     }
116
117     /**
118      * Close the web socket session.
119      */
120     private void closeSession() {
121         Session session = this.session;
122         if (session != null) {
123             session.close();
124             this.session = null;
125         }
126     }
127
128     /**
129      * Helper to escape the quote marks in a JSON string.
130      *
131      * @param json the input JSON string.
132      * @return the escaped JSON version.
133      */
134     private String jsonEscape(String json) {
135         return json.replace("\"", "\\\"");
136     }
137
138     /**
139      * Helper to remove quote escape marks from an escaped JSON string.
140      *
141      * @param escapedJson the escaped input string.
142      * @return the clean JSON version.
143      */
144     private String jsonUnEscape(String escapedJson) {
145         return escapedJson.replace("\\\"", "\"");
146     }
147
148     /**
149      * Helper to replace double quote marks in a JSON string with single quote marks.
150      *
151      * @param json the input string.
152      * @return the modified version.
153      */
154     private String jsonReplaceQuotes(String json) {
155         return json.replace("\"", "'");
156     }
157
158     @Override
159     public synchronized String sendMessage(final String requestJson) throws IOException, NeoHubException {
160         // start the session
161         startSession();
162
163         // session start failed
164         Session session = this.session;
165         if (session == null) {
166             throw new IOException("Session is null");
167         }
168
169         // wrap the inner request in an outer request string
170         String requestOuter = String.format(REQUEST_OUTER,
171                 jsonEscape(String.format(REQUEST_INNER, config.apiToken, jsonReplaceQuotes(requestJson))));
172
173         // initialise the response
174         responseOuter = "";
175         responsePending = true;
176
177         IOException caughtException = null;
178         try {
179             // send the request
180             logger.debug("hub '{}' sending characters:{}", hubId, requestOuter.length());
181             session.getRemote().sendString(requestOuter);
182             logger.trace("hub '{}' sent:{}", hubId, requestOuter);
183
184             // sleep and loop until we get a response or the socket is closed
185             int sleepRemainingMilliseconds = config.socketTimeout * 1000;
186             while (responsePending) {
187                 try {
188                     Thread.sleep(SLEEP_MILLISECONDS);
189                     sleepRemainingMilliseconds = sleepRemainingMilliseconds - SLEEP_MILLISECONDS;
190                     if (sleepRemainingMilliseconds <= 0) {
191                         throw new IOException("Read timed out");
192                     }
193                 } catch (InterruptedException e) {
194                     throw new IOException("Read interrupted", e);
195                 }
196             }
197         } catch (IOException e) {
198             caughtException = e;
199         }
200
201         logger.debug("hub '{}' received characters:{}", hubId, responseOuter.length());
202         logger.trace("hub '{}' received:{}", hubId, responseOuter);
203
204         // if an IOException was caught above, re-throw it again
205         if (caughtException != null) {
206             throw caughtException;
207         }
208
209         try {
210             Response responseDto = gson.fromJson(responseOuter, Response.class);
211             if (responseDto == null) {
212                 throw new JsonSyntaxException("Response DTO is invalid");
213             }
214             if (!NeoHubBindingConstants.HM_SET_COMMAND_RESPONSE.equals(responseDto.message_type)) {
215                 throw new JsonSyntaxException("DTO 'message_type' field is invalid");
216             }
217             String responseJson = responseDto.response;
218             if (responseJson == null) {
219                 throw new JsonSyntaxException("DTO 'response' field is null");
220             }
221             responseJson = jsonUnEscape(responseJson).strip();
222             if (!JsonParser.parseString(responseJson).isJsonObject()) {
223                 throw new JsonSyntaxException("DTO 'response' field is not a JSON object");
224             }
225             return responseJson;
226         } catch (JsonSyntaxException e) {
227             logger.debug("hub '{}' {}; response:{}", hubId, e.getMessage(), responseOuter);
228             throw new NeoHubException("Invalid response");
229         }
230     }
231
232     @Override
233     public void close() {
234         closeSession();
235         try {
236             webSocketClient.stop();
237         } catch (Exception e) {
238         }
239     }
240
241     @OnWebSocketConnect
242     public void onConnect(Session session) {
243         logger.debug("hub '{}' onConnect() ok", hubId);
244         this.session = session;
245     }
246
247     @OnWebSocketClose
248     public void onClose(int statusCode, String reason) {
249         logger.debug("hub '{}' onClose() statusCode:{}, reason:{}", hubId, statusCode, reason);
250         responsePending = false;
251         this.session = null;
252     }
253
254     @OnWebSocketError
255     public void onError(Throwable cause) {
256         logger.debug("hub '{}' onError() cause:{}", hubId, cause.getMessage());
257         closeSession();
258     }
259
260     @OnWebSocketMessage
261     public void onMessage(String msg) {
262         responseOuter = msg.strip();
263         responsePending = false;
264     }
265 }