2 * Copyright (c) 2010-2021 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 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;
64 private final KodiClientSocketEventListener eventHandler;
66 public KodiClientSocket(KodiClientSocketEventListener eventHandler, URI uri, ScheduledExecutorService scheduler,
67 WebSocketClient webSocketClient) {
68 this.eventHandler = eventHandler;
70 this.scheduler = scheduler;
71 this.client = webSocketClient;
75 * Attempts to create a connection to the Kodi host and begin listening for updates over the async http web socket
79 public synchronized void open() throws IOException {
81 logger.warn("open: connection is already open");
83 KodiWebSocketListener socket = new KodiWebSocketListener();
84 ClientUpgradeRequest request = new ClientUpgradeRequest();
86 sessionFuture = client.connect(socket, uri, request);
90 * Close this connection to the Kodi instance
93 // if there is an old web socket then clean up and destroy
94 if (session != null) {
99 if (sessionFuture != null && !sessionFuture.isDone()) {
100 sessionFuture.cancel(true);
104 public boolean isConnected() {
105 if (session == null || !session.isOpen()) {
112 public class KodiWebSocketListener {
115 public void onConnect(Session wssession) {
116 logger.trace("Connected to server");
119 if (eventHandler != null) {
120 scheduler.submit(() -> {
122 eventHandler.onConnectionOpened();
123 } catch (Exception e) {
124 logger.debug("Error handling onConnectionOpened(): {}", e.getMessage(), e);
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();
141 logger.trace("Event received from server: {}", json);
142 if (eventHandler != null) {
143 scheduler.submit(() -> {
145 eventHandler.handleEvent(json);
146 } catch (Exception e) {
147 logger.debug("Error handling event {} player state change message: {}", json,
156 public void onClose(int statusCode, String reason) {
157 logger.trace("Closing a WebSocket due to {}", reason);
160 if (eventHandler != null) {
161 scheduler.submit(() -> {
163 eventHandler.onConnectionClosed();
164 } catch (Exception e) {
165 logger.debug("Error handling onConnectionClosed(): {}", e.getMessage(), e);
172 public void onError(Throwable error) {
173 logger.trace("Error occured: {}", error.getMessage());
174 onClose(0, error.getMessage());
178 private void sendMessage(String str) throws IOException {
180 logger.trace("send message: {}", str);
181 session.getRemote().sendString(str);
183 throw new IOException("Socket not initialized");
187 public JsonElement callMethod(String methodName) {
188 return callMethod(methodName, null);
191 public synchronized JsonElement callMethod(String methodName, JsonObject params) {
193 JsonObject payloadObject = new JsonObject();
194 payloadObject.addProperty("jsonrpc", "2.0");
195 payloadObject.addProperty("id", nextMessageId);
196 payloadObject.addProperty("method", methodName);
198 if (params != null) {
199 payloadObject.add("params", params);
202 String message = mapper.toJson(payloadObject);
204 commandLatch = new CountDownLatch(1);
205 commandResponse = null;
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");
214 JsonElement error = commandResponse.get("error");
215 logger.debug("Error received from server: {}", error);
219 logger.debug("Timeout during callMethod({}, {})", methodName, params);
222 } catch (IOException | InterruptedException e) {
223 logger.debug("Error during callMethod({}, {}): {}", methodName, params, e.getMessage(), e);