]> git.basschouten.com Git - openhab-addons.git/blob
1c23de2cb1fe0737e5a30aaebd34c44982cc1b62
[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.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 Gson mapper = new Gson();
58     private final URI uri;
59     private final WebSocketClient client;
60     private Session session;
61     private Future<?> sessionFuture;
62
63     private final KodiClientSocketEventListener eventHandler;
64
65     public KodiClientSocket(KodiClientSocketEventListener eventHandler, URI uri, ScheduledExecutorService scheduler,
66             WebSocketClient webSocketClient) {
67         this.eventHandler = eventHandler;
68         this.uri = uri;
69         this.scheduler = scheduler;
70         this.client = webSocketClient;
71     }
72
73     /**
74      * Attempts to create a connection to the Kodi host and begin listening for updates over the async http web socket
75      *
76      * @throws IOException
77      */
78     public synchronized void open() throws IOException {
79         if (isConnected()) {
80             logger.warn("open: connection is already open");
81         }
82         KodiWebSocketListener socket = new KodiWebSocketListener();
83         ClientUpgradeRequest request = new ClientUpgradeRequest();
84
85         sessionFuture = client.connect(socket, uri, request);
86     }
87
88     /***
89      * Close this connection to the Kodi instance
90      */
91     public void close() {
92         // if there is an old web socket then clean up and destroy
93         if (session != null) {
94             session.close();
95             session = null;
96         }
97
98         if (sessionFuture != null && !sessionFuture.isDone()) {
99             sessionFuture.cancel(true);
100         }
101     }
102
103     public boolean isConnected() {
104         if (session == null || !session.isOpen()) {
105             return false;
106         }
107         return connected;
108     }
109
110     @WebSocket
111     public class KodiWebSocketListener {
112
113         @OnWebSocketConnect
114         public void onConnect(Session wssession) {
115             logger.trace("Connected to server");
116             session = wssession;
117             connected = true;
118             if (eventHandler != null) {
119                 scheduler.submit(() -> {
120                     try {
121                         eventHandler.onConnectionOpened();
122                     } catch (Exception e) {
123                         logger.debug("Error handling onConnectionOpened(): {}", e.getMessage(), e);
124                     }
125                 });
126             }
127         }
128
129         @OnWebSocketMessage
130         public void onMessage(String message) {
131             logger.trace("Message received from server: {}", message);
132             final JsonObject json = JsonParser.parseString(message).getAsJsonObject();
133             if (json.has("id")) {
134                 int messageId = json.get("id").getAsInt();
135                 if (messageId == nextMessageId - 1) {
136                     commandResponse = json;
137                     commandLatch.countDown();
138                 }
139             } else {
140                 logger.trace("Event received from server: {}", json);
141                 if (eventHandler != null) {
142                     scheduler.submit(() -> {
143                         try {
144                             eventHandler.handleEvent(json);
145                         } catch (Exception e) {
146                             logger.debug("Error handling event {} player state change message: {}", json,
147                                     e.getMessage(), e);
148                         }
149                     });
150                 }
151             }
152         }
153
154         @OnWebSocketClose
155         public void onClose(int statusCode, String reason) {
156             logger.trace("Closing a WebSocket due to {}", reason);
157             session = null;
158             connected = false;
159             if (eventHandler != null) {
160                 scheduler.submit(() -> {
161                     try {
162                         eventHandler.onConnectionClosed();
163                     } catch (Exception e) {
164                         logger.debug("Error handling onConnectionClosed(): {}", e.getMessage(), e);
165                     }
166                 });
167             }
168         }
169
170         @OnWebSocketError
171         public void onError(Throwable error) {
172             logger.trace("Error occured: {}", error.getMessage());
173             onClose(0, error.getMessage());
174         }
175     }
176
177     private void sendMessage(String str) throws IOException {
178         if (isConnected()) {
179             logger.trace("send message: {}", str);
180             session.getRemote().sendString(str);
181         } else {
182             throw new IOException("Socket not initialized");
183         }
184     }
185
186     public JsonElement callMethod(String methodName) {
187         return callMethod(methodName, null);
188     }
189
190     public synchronized JsonElement callMethod(String methodName, JsonObject params) {
191         try {
192             JsonObject payloadObject = new JsonObject();
193             payloadObject.addProperty("jsonrpc", "2.0");
194             payloadObject.addProperty("id", nextMessageId);
195             payloadObject.addProperty("method", methodName);
196
197             if (params != null) {
198                 payloadObject.add("params", params);
199             }
200
201             String message = mapper.toJson(payloadObject);
202
203             commandLatch = new CountDownLatch(1);
204             commandResponse = null;
205             nextMessageId++;
206
207             sendMessage(message);
208             if (commandLatch.await(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
209                 logger.debug("callMethod returns: {}", commandResponse);
210                 if (commandResponse.has("result")) {
211                     return commandResponse.get("result");
212                 } else {
213                     JsonElement error = commandResponse.get("error");
214                     logger.debug("Error received from server: {}", error);
215                     return null;
216                 }
217             } else {
218                 logger.debug("Timeout during callMethod({}, {})", methodName, params);
219                 return null;
220             }
221         } catch (IOException | InterruptedException e) {
222             logger.debug("Error during callMethod({}, {}): {}", methodName, params, e.getMessage(), e);
223             return null;
224         }
225     }
226 }