]> git.basschouten.com Git - openhab-addons.git/blob
7d24d9b80788058e091fe2439d96d18d0569061f
[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.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;
35
36 import com.google.gson.Gson;
37 import com.google.gson.JsonParser;
38 import com.google.gson.JsonSyntaxException;
39
40 /**
41  * Handles the text based communication via web socket between openHAB and NeoHub
42  *
43  * @author Andrew Fiddian-Green - Initial contribution
44  *
45  */
46 @NonNullByDefault
47 @WebSocket
48 public class NeoHubWebSocket extends NeoHubSocketBase {
49
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}]}";
53
54     private final Logger logger = LoggerFactory.getLogger(NeoHubWebSocket.class);
55     private final Gson gson = new Gson();
56     private final WebSocketClient webSocketClient;
57
58     private @Nullable Session session = null;
59     private String responseOuter = "";
60     private boolean responsePending;
61
62     /**
63      * DTO to receive and parse the response JSON.
64      *
65      * @author Andrew Fiddian-Green - Initial contribution
66      */
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;
74     }
75
76     public NeoHubWebSocket(NeoHubConfiguration config, WebSocketFactory webSocketFactory, ThingUID bridgeUID)
77             throws IOException {
78         super(config, bridgeUID.getAsString());
79
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);
85         try {
86             webSocketClient.start();
87         } catch (Exception e) {
88             throw new IOException("Error starting Web Socket client", e);
89         }
90     }
91
92     /**
93      * Open the web socket session.
94      *
95      * @throws IOException if unable to open the web socket
96      */
97     private void startSession() throws IOException {
98         Session session = this.session;
99         if (session == null || !session.isOpen()) {
100             closeSession();
101             try {
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);
110             }
111         }
112     }
113
114     /**
115      * Close the web socket session.
116      */
117     private void closeSession() {
118         Session session = this.session;
119         if (session != null) {
120             session.close();
121             this.session = null;
122         }
123     }
124
125     /**
126      * Helper to escape the quote marks in a JSON string.
127      *
128      * @param json the input JSON string.
129      * @return the escaped JSON version.
130      */
131     private String jsonEscape(String json) {
132         return json.replace("\"", "\\\"");
133     }
134
135     /**
136      * Helper to remove quote escape marks from an escaped JSON string.
137      *
138      * @param escapedJson the escaped input string.
139      * @return the clean JSON version.
140      */
141     private String jsonUnEscape(String escapedJson) {
142         return escapedJson.replace("\\\"", "\"");
143     }
144
145     /**
146      * Helper to replace double quote marks in a JSON string with single quote marks.
147      *
148      * @param json the input string.
149      * @return the modified version.
150      */
151     private String jsonReplaceQuotes(String json) {
152         return json.replace("\"", "'");
153     }
154
155     @Override
156     public synchronized String sendMessage(final String requestJson) throws IOException, NeoHubException {
157         // start the session
158         startSession();
159
160         // session start failed
161         Session session = this.session;
162         if (session == null) {
163             throw new IOException("Session is null");
164         }
165
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))));
169
170         // initialise the response
171         responseOuter = "";
172         responsePending = true;
173
174         IOException caughtException = null;
175         try {
176             // send the request
177             logger.debug("hub '{}' sending characters:{}", hubId, requestOuter.length());
178             session.getRemote().sendString(requestOuter);
179             logger.trace("hub '{}' sent:{}", hubId, requestOuter);
180
181             // sleep and loop until we get a response or the socket is closed
182             int sleepRemainingMilliseconds = config.socketTimeout * 1000;
183             while (responsePending) {
184                 try {
185                     Thread.sleep(SLEEP_MILLISECONDS);
186                     sleepRemainingMilliseconds = sleepRemainingMilliseconds - SLEEP_MILLISECONDS;
187                     if (sleepRemainingMilliseconds <= 0) {
188                         throw new IOException("Read timed out");
189                     }
190                 } catch (InterruptedException e) {
191                     throw new IOException("Read interrupted", e);
192                 }
193             }
194         } catch (IOException e) {
195             caughtException = e;
196         }
197
198         logger.debug("hub '{}' received characters:{}", hubId, responseOuter.length());
199         logger.trace("hub '{}' received:{}", hubId, responseOuter);
200
201         // if an IOException was caught above, re-throw it again
202         if (caughtException != null) {
203             throw caughtException;
204         }
205
206         try {
207             Response responseDto = gson.fromJson(responseOuter, Response.class);
208             if (responseDto == null) {
209                 throw new JsonSyntaxException("Response DTO is invalid");
210             }
211             if (!NeoHubBindingConstants.HM_SET_COMMAND_RESPONSE.equals(responseDto.message_type)) {
212                 throw new JsonSyntaxException("DTO 'message_type' field is invalid");
213             }
214             String responseJson = responseDto.response;
215             if (responseJson == null) {
216                 throw new JsonSyntaxException("DTO 'response' field is null");
217             }
218             responseJson = jsonUnEscape(responseJson).strip();
219             if (!JsonParser.parseString(responseJson).isJsonObject()) {
220                 throw new JsonSyntaxException("DTO 'response' field is not a JSON object");
221             }
222             return responseJson;
223         } catch (JsonSyntaxException e) {
224             logger.debug("hub '{}' {}; response:{}", hubId, e.getMessage(), responseOuter);
225             throw new NeoHubException("Invalid response");
226         }
227     }
228
229     @Override
230     public void close() {
231         closeSession();
232         try {
233             webSocketClient.stop();
234         } catch (Exception e) {
235         }
236     }
237
238     @OnWebSocketConnect
239     public void onConnect(Session session) {
240         logger.debug("hub '{}' onConnect() ok", hubId);
241         this.session = session;
242     }
243
244     @OnWebSocketClose
245     public void onClose(int statusCode, String reason) {
246         logger.debug("hub '{}' onClose() statusCode:{}, reason:{}", hubId, statusCode, reason);
247         responsePending = false;
248         this.session = null;
249     }
250
251     @OnWebSocketError
252     public void onError(Throwable cause) {
253         logger.debug("hub '{}' onError() cause:{}", hubId, cause.getMessage());
254         closeSession();
255     }
256
257     @OnWebSocketMessage
258     public void onMessage(String msg) {
259         responseOuter = msg.strip();
260         responsePending = false;
261     }
262 }