2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.kodi.internal.protocol;
15 import java.io.IOException;
17 import java.util.concurrent.CountDownLatch;
18 import java.util.concurrent.Future;
19 import java.util.concurrent.ScheduledExecutorService;
20 import java.util.concurrent.TimeUnit;
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;
33 import com.google.gson.Gson;
34 import com.google.gson.JsonElement;
35 import com.google.gson.JsonObject;
36 import com.google.gson.JsonParser;
39 * KodiClientSocket implements the low level communication to Kodi through
40 * websocket. Usually this communication is done through port 9090
42 * @author Paul Frank - Initial contribution
44 public class KodiClientSocket {
46 private final Logger logger = LoggerFactory.getLogger(KodiClientSocket.class);
48 private final ScheduledExecutorService scheduler;
49 private static final int REQUEST_TIMEOUT_MS = 60000;
51 private CountDownLatch commandLatch = null;
52 private JsonObject commandResponse = null;
53 private int nextMessageId = 1;
55 private boolean connected = false;
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;
63 private final KodiClientSocketEventListener eventHandler;
65 public KodiClientSocket(KodiClientSocketEventListener eventHandler, URI uri, ScheduledExecutorService scheduler,
66 WebSocketClient webSocketClient) {
67 this.eventHandler = eventHandler;
69 this.scheduler = scheduler;
70 this.client = webSocketClient;
74 * Attempts to create a connection to the Kodi host and begin listening for updates over the async http web socket
78 public synchronized void open() throws IOException {
80 logger.warn("open: connection is already open");
82 KodiWebSocketListener socket = new KodiWebSocketListener();
83 ClientUpgradeRequest request = new ClientUpgradeRequest();
85 sessionFuture = client.connect(socket, uri, request);
89 * Close this connection to the Kodi instance
92 // if there is an old web socket then clean up and destroy
93 if (session != null) {
98 if (sessionFuture != null && !sessionFuture.isDone()) {
99 sessionFuture.cancel(true);
103 public boolean isConnected() {
104 if (session == null || !session.isOpen()) {
111 public class KodiWebSocketListener {
114 public void onConnect(Session wssession) {
115 logger.trace("Connected to server");
118 if (eventHandler != null) {
119 scheduler.submit(() -> {
121 eventHandler.onConnectionOpened();
122 } catch (Exception e) {
123 logger.debug("Error handling onConnectionOpened(): {}", e.getMessage(), e);
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();
140 logger.trace("Event received from server: {}", json);
141 if (eventHandler != null) {
142 scheduler.submit(() -> {
144 eventHandler.handleEvent(json);
145 } catch (Exception e) {
146 logger.debug("Error handling event {} player state change message: {}", json,
155 public void onClose(int statusCode, String reason) {
156 logger.trace("Closing a WebSocket due to {}", reason);
159 if (eventHandler != null) {
160 scheduler.submit(() -> {
162 eventHandler.onConnectionClosed();
163 } catch (Exception e) {
164 logger.debug("Error handling onConnectionClosed(): {}", e.getMessage(), e);
171 public void onError(Throwable error) {
172 logger.trace("Error occured: {}", error.getMessage());
173 onClose(0, error.getMessage());
177 private void sendMessage(String str) throws IOException {
179 logger.trace("send message: {}", str);
180 session.getRemote().sendString(str);
182 throw new IOException("Socket not initialized");
186 public JsonElement callMethod(String methodName) {
187 return callMethod(methodName, null);
190 public synchronized JsonElement callMethod(String methodName, JsonObject params) {
192 JsonObject payloadObject = new JsonObject();
193 payloadObject.addProperty("jsonrpc", "2.0");
194 payloadObject.addProperty("id", nextMessageId);
195 payloadObject.addProperty("method", methodName);
197 if (params != null) {
198 payloadObject.add("params", params);
201 String message = mapper.toJson(payloadObject);
203 commandLatch = new CountDownLatch(1);
204 commandResponse = null;
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");
213 JsonElement error = commandResponse.get("error");
214 logger.debug("Error received from server: {}", error);
218 logger.debug("Timeout during callMethod({}, {})", methodName, params);
221 } catch (IOException | InterruptedException e) {
222 logger.debug("Error during callMethod({}, {}): {}", methodName, params, e.getMessage(), e);