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.sonyaudio.internal.protocol;
15 import java.io.IOException;
17 import java.nio.ByteBuffer;
18 import java.util.concurrent.CountDownLatch;
19 import java.util.concurrent.ScheduledExecutorService;
20 import java.util.concurrent.TimeUnit;
21 import java.util.concurrent.TimeoutException;
23 import org.eclipse.jetty.websocket.api.RemoteEndpoint;
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.ClientUpgradeRequest;
31 import org.eclipse.jetty.websocket.client.WebSocketClient;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import com.google.gson.Gson;
36 import com.google.gson.GsonBuilder;
37 import com.google.gson.JsonElement;
38 import com.google.gson.JsonObject;
39 import com.google.gson.JsonParseException;
40 import com.google.gson.JsonParser;
43 * The {@link SonyAudioConnection} is responsible for communicating with SONY audio products
46 * @author David Ã…berg - Initial contribution
48 public class SonyAudioClientSocket {
49 private final Logger logger = LoggerFactory.getLogger(SonyAudioClientSocket.class);
51 private final ScheduledExecutorService scheduler;
52 private static final int REQUEST_TIMEOUT_MS = 60000;
54 private CountDownLatch commandLatch = null;
55 private JsonObject commandResponse = null;
56 private int nextMessageId = 1;
58 private boolean connected = false;
60 private final URI uri;
61 private Session session;
63 private final Gson mapper;
65 private static int ping = 0;
67 private final SonyAudioClientSocketEventListener eventHandler;
69 public SonyAudioClientSocket(SonyAudioClientSocketEventListener eventHandler, URI uri,
70 ScheduledExecutorService scheduler) {
71 mapper = new GsonBuilder().disableHtmlEscaping().create();
72 this.eventHandler = eventHandler;
74 this.scheduler = scheduler;
77 public synchronized void open(WebSocketClient webSocketClient) {
80 logger.warn("connect: connection is already open");
83 SonyAudioWebSocketListener socket = new SonyAudioWebSocketListener();
84 ClientUpgradeRequest request = new ClientUpgradeRequest();
87 webSocketClient.connect(socket, uri, request).get(1, TimeUnit.SECONDS);
88 } catch (TimeoutException e) {
89 logger.debug("Could not establish websocket within a second.");
91 } catch (Exception e) {
92 logger.debug("Exception then trying to start the websocket {}", e.getMessage(), e);
97 logger.debug("Closing socket {}", uri);
98 // if there is an old web socket then clean up and destroy
99 if (session != null) {
102 } catch (Exception e) {
103 logger.debug("Exception during closing the websocket session {}", e.getMessage(), e);
109 public boolean isConnected() {
110 if (session == null || !session.isOpen()) {
115 RemoteEndpoint remote = session.getRemote();
117 ByteBuffer payload = ByteBuffer.allocate(4).putInt(ping++);
119 remote.sendPing(payload);
120 } catch (IOException e) {
121 logger.warn("Connection to {} lost: {}", uri, e.getMessage());
129 public class SonyAudioWebSocketListener {
132 public void onConnect(Session wssession) {
133 logger.debug("Connected to server");
136 if (eventHandler != null) {
137 scheduler.submit(() -> {
139 eventHandler.onConnectionOpened(uri);
140 } catch (Exception e) {
141 logger.error("Error handling onConnectionOpened() {}", e.getMessage(), e);
148 public void onMessage(String message) {
149 logger.debug("Message received from server: {}", message);
151 final JsonObject json = JsonParser.parseString(message).getAsJsonObject();
152 if (json.has("id")) {
153 logger.debug("Response received from server: {}", json);
154 int messageId = json.get("id").getAsInt();
155 if (messageId == nextMessageId - 1) {
156 commandResponse = json;
157 commandLatch.countDown();
160 logger.debug("Event received from server: {}", json);
162 if (eventHandler != null) {
163 scheduler.submit(() -> {
165 eventHandler.handleEvent(json);
166 } catch (Exception e) {
167 logger.error("Error handling event {} player state change message: {}", json,
172 } catch (Exception e) {
173 logger.error("Error handling player state change message", e);
176 } catch (JsonParseException e) {
177 logger.debug("Not valid JSON message: {}", e.getMessage(), e);
182 public void onClose(int statusCode, String reason) {
185 logger.debug("Closing a WebSocket due to {}", reason);
186 scheduler.submit(() -> {
188 eventHandler.onConnectionClosed();
189 } catch (Exception e) {
190 logger.error("Error handling onConnectionClosed()", e);
196 public void onError(Throwable error) {
197 onClose(0, error.getMessage());
201 private void sendMessage(String str) throws IOException {
203 logger.debug("send message fo {}: {}", uri.toString(), str);
204 session.getRemote().sendString(str);
207 stack += "Printing stack trace:\n";
208 StackTraceElement[] elements = Thread.currentThread().getStackTrace();
209 for (int i = 1; i < elements.length; i++) {
210 StackTraceElement s = elements[i];
211 stack += "\tat " + s.getClassName() + "." + s.getMethodName() + "(" + s.getFileName() + ":"
212 + s.getLineNumber() + ")\n";
214 logger.error("Socket not initialized, trying to send {} {}", str, stack);
215 throw new IOException("Socket not initialized");
219 public synchronized JsonElement callMethod(SonyAudioMethod method) throws IOException {
221 method.id = nextMessageId;
222 String message = mapper.toJson(method);
223 logger.debug("callMethod send {}", message);
225 commandLatch = new CountDownLatch(1);
226 commandResponse = null;
229 sendMessage(message);
230 if (commandLatch.await(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
231 logger.debug("callMethod {} returns {}", uri.toString(), commandResponse.toString());
232 return commandResponse.get("result");
234 logger.debug("Timeout during callMethod({}, {})", method.method, message);
235 throw new IOException("Timeout during callMethod");
237 } catch (InterruptedException e) {
238 throw new IOException("Timeout in callMethod");
242 public URI getURI() {