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