]> git.basschouten.com Git - openhab-addons.git/blob
8f77edfce2f286b08bad9b5c5ebfd66fd380eb21
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.kodi.internal.protocol;
14
15 import java.io.IOException;
16 import java.net.URI;
17 import java.util.concurrent.CountDownLatch;
18 import java.util.concurrent.Future;
19 import java.util.concurrent.ScheduledExecutorService;
20 import java.util.concurrent.TimeUnit;
21
22 import org.eclipse.jetty.websocket.api.Session;
23 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
24 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
25 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
26 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
27 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
28 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
29 import org.eclipse.jetty.websocket.client.WebSocketClient;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 import com.google.gson.Gson;
34 import com.google.gson.JsonElement;
35 import com.google.gson.JsonObject;
36 import com.google.gson.JsonParser;
37
38 /**
39  * KodiClientSocket implements the low level communication to Kodi through
40  * websocket. Usually this communication is done through port 9090
41  *
42  * @author Paul Frank - Initial contribution
43  */
44 public class KodiClientSocket {
45
46     private final Logger logger = LoggerFactory.getLogger(KodiClientSocket.class);
47
48     private final ScheduledExecutorService scheduler;
49     private static final int REQUEST_TIMEOUT_MS = 60000;
50
51     private CountDownLatch commandLatch = null;
52     private JsonObject commandResponse = null;
53     private int nextMessageId = 1;
54
55     private boolean connected = false;
56
57     private final JsonParser parser = new JsonParser();
58     private final Gson mapper = new Gson();
59     private final URI uri;
60     private final WebSocketClient client;
61     private Session session;
62     private Future<?> sessionFuture;
63
64     private final KodiClientSocketEventListener eventHandler;
65
66     public KodiClientSocket(KodiClientSocketEventListener eventHandler, URI uri, ScheduledExecutorService scheduler,
67             WebSocketClient webSocketClient) {
68         this.eventHandler = eventHandler;
69         this.uri = uri;
70         this.scheduler = scheduler;
71         this.client = webSocketClient;
72     }
73
74     /**
75      * Attempts to create a connection to the Kodi host and begin listening for updates over the async http web socket
76      *
77      * @throws IOException
78      */
79     public synchronized void open() throws IOException {
80         if (isConnected()) {
81             logger.warn("open: connection is already open");
82         }
83         KodiWebSocketListener socket = new KodiWebSocketListener();
84         ClientUpgradeRequest request = new ClientUpgradeRequest();
85
86         sessionFuture = client.connect(socket, uri, request);
87     }
88
89     /***
90      * Close this connection to the Kodi instance
91      */
92     public void close() {
93         // if there is an old web socket then clean up and destroy
94         if (session != null) {
95             session.close();
96             session = null;
97         }
98
99         if (sessionFuture != null && !sessionFuture.isDone()) {
100             sessionFuture.cancel(true);
101         }
102     }
103
104     public boolean isConnected() {
105         if (session == null || !session.isOpen()) {
106             return false;
107         }
108         return connected;
109     }
110
111     @WebSocket
112     public class KodiWebSocketListener {
113
114         @OnWebSocketConnect
115         public void onConnect(Session wssession) {
116             logger.trace("Connected to server");
117             session = wssession;
118             connected = true;
119             if (eventHandler != null) {
120                 scheduler.submit(() -> {
121                     try {
122                         eventHandler.onConnectionOpened();
123                     } catch (Exception e) {
124                         logger.debug("Error handling onConnectionOpened(): {}", e.getMessage(), e);
125                     }
126                 });
127             }
128         }
129
130         @OnWebSocketMessage
131         public void onMessage(String message) {
132             logger.trace("Message received from server: {}", message);
133             final JsonObject json = parser.parse(message).getAsJsonObject();
134             if (json.has("id")) {
135                 int messageId = json.get("id").getAsInt();
136                 if (messageId == nextMessageId - 1) {
137                     commandResponse = json;
138                     commandLatch.countDown();
139                 }
140             } else {
141                 logger.trace("Event received from server: {}", json);
142                 if (eventHandler != null) {
143                     scheduler.submit(() -> {
144                         try {
145                             eventHandler.handleEvent(json);
146                         } catch (Exception e) {
147                             logger.debug("Error handling event {} player state change message: {}", json,
148                                     e.getMessage(), e);
149                         }
150                     });
151                 }
152             }
153         }
154
155         @OnWebSocketClose
156         public void onClose(int statusCode, String reason) {
157             logger.trace("Closing a WebSocket due to {}", reason);
158             session = null;
159             connected = false;
160             if (eventHandler != null) {
161                 scheduler.submit(() -> {
162                     try {
163                         eventHandler.onConnectionClosed();
164                     } catch (Exception e) {
165                         logger.debug("Error handling onConnectionClosed(): {}", e.getMessage(), e);
166                     }
167                 });
168             }
169         }
170
171         @OnWebSocketError
172         public void onError(Throwable error) {
173             logger.trace("Error occured: {}", error.getMessage());
174             onClose(0, error.getMessage());
175         }
176     }
177
178     private void sendMessage(String str) throws IOException {
179         if (isConnected()) {
180             logger.trace("send message: {}", str);
181             session.getRemote().sendString(str);
182         } else {
183             throw new IOException("Socket not initialized");
184         }
185     }
186
187     public JsonElement callMethod(String methodName) {
188         return callMethod(methodName, null);
189     }
190
191     public synchronized JsonElement callMethod(String methodName, JsonObject params) {
192         try {
193             JsonObject payloadObject = new JsonObject();
194             payloadObject.addProperty("jsonrpc", "2.0");
195             payloadObject.addProperty("id", nextMessageId);
196             payloadObject.addProperty("method", methodName);
197
198             if (params != null) {
199                 payloadObject.add("params", params);
200             }
201
202             String message = mapper.toJson(payloadObject);
203
204             commandLatch = new CountDownLatch(1);
205             commandResponse = null;
206             nextMessageId++;
207
208             sendMessage(message);
209             if (commandLatch.await(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
210                 logger.debug("callMethod returns: {}", commandResponse);
211                 if (commandResponse.has("result")) {
212                     return commandResponse.get("result");
213                 } else {
214                     JsonElement error = commandResponse.get("error");
215                     logger.debug("Error received from server: {}", error);
216                     return null;
217                 }
218             } else {
219                 logger.debug("Timeout during callMethod({}, {})", methodName, params);
220                 return null;
221             }
222         } catch (IOException | InterruptedException e) {
223             logger.debug("Error during callMethod({}, {}): {}", methodName, params, e.getMessage(), e);
224             return null;
225         }
226     }
227 }