]> git.basschouten.com Git - openhab-addons.git/blob
a2495643d2892952ec6ab6edeb471b962fcb3feb
[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.sonyaudio.internal.protocol;
14
15 import java.io.IOException;
16 import java.net.URI;
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;
22
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;
34
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;
41
42 /**
43  * The {@link SonyAudioConnection} is responsible for communicating with SONY audio products
44  * handlers.
45  *
46  * @author David Ã…berg - Initial contribution
47  */
48 public class SonyAudioClientSocket {
49     private final Logger logger = LoggerFactory.getLogger(SonyAudioClientSocket.class);
50
51     private final ScheduledExecutorService scheduler;
52     private static final int REQUEST_TIMEOUT_MS = 60000;
53
54     private CountDownLatch commandLatch = null;
55     private JsonObject commandResponse = null;
56     private int nextMessageId = 1;
57
58     private boolean connected = false;
59
60     private final URI uri;
61     private Session session;
62
63     private final Gson mapper;
64
65     private static int ping = 0;
66
67     private final SonyAudioClientSocketEventListener eventHandler;
68
69     public SonyAudioClientSocket(SonyAudioClientSocketEventListener eventHandler, URI uri,
70             ScheduledExecutorService scheduler) {
71         mapper = new GsonBuilder().disableHtmlEscaping().create();
72         this.eventHandler = eventHandler;
73         this.uri = uri;
74         this.scheduler = scheduler;
75     }
76
77     public synchronized void open(WebSocketClient webSocketClient) {
78         try {
79             if (isConnected()) {
80                 logger.warn("connect: connection is already open");
81             }
82
83             SonyAudioWebSocketListener socket = new SonyAudioWebSocketListener();
84             ClientUpgradeRequest request = new ClientUpgradeRequest();
85
86             try {
87                 webSocketClient.connect(socket, uri, request).get(1, TimeUnit.SECONDS);
88             } catch (TimeoutException e) {
89                 logger.debug("Could not establish websocket within a second.");
90             }
91         } catch (Exception e) {
92             logger.debug("Exception then trying to start the websocket {}", e.getMessage(), e);
93         }
94     }
95
96     public void close() {
97         logger.debug("Closing socket {}", uri);
98         // if there is an old web socket then clean up and destroy
99         if (session != null) {
100             try {
101                 session.close();
102             } catch (Exception e) {
103                 logger.debug("Exception during closing the websocket session {}", e.getMessage(), e);
104             }
105             session = null;
106         }
107     }
108
109     public boolean isConnected() {
110         if (session == null || !session.isOpen()) {
111             connected = false;
112             return connected;
113         }
114
115         RemoteEndpoint remote = session.getRemote();
116
117         ByteBuffer payload = ByteBuffer.allocate(4).putInt(ping++);
118         try {
119             remote.sendPing(payload);
120         } catch (IOException e) {
121             logger.warn("Connection to {} lost: {}", uri, e.getMessage());
122             connected = false;
123         }
124
125         return connected;
126     }
127
128     @WebSocket
129     public class SonyAudioWebSocketListener {
130
131         @OnWebSocketConnect
132         public void onConnect(Session wssession) {
133             logger.debug("Connected to server");
134             session = wssession;
135             connected = true;
136             if (eventHandler != null) {
137                 scheduler.submit(() -> {
138                     try {
139                         eventHandler.onConnectionOpened(uri);
140                     } catch (Exception e) {
141                         logger.error("Error handling onConnectionOpened() {}", e.getMessage(), e);
142                     }
143                 });
144             }
145         }
146
147         @OnWebSocketMessage
148         public void onMessage(String message) {
149             logger.debug("Message received from server: {}", message);
150             try {
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();
158                     }
159                 } else {
160                     logger.debug("Event received from server: {}", json);
161                     try {
162                         if (eventHandler != null) {
163                             scheduler.submit(() -> {
164                                 try {
165                                     eventHandler.handleEvent(json);
166                                 } catch (Exception e) {
167                                     logger.error("Error handling event {} player state change message: {}", json,
168                                             e.getMessage(), e);
169                                 }
170                             });
171                         }
172                     } catch (Exception e) {
173                         logger.error("Error handling player state change message", e);
174                     }
175                 }
176             } catch (JsonParseException e) {
177                 logger.debug("Not valid JSON message: {}", e.getMessage(), e);
178             }
179         }
180
181         @OnWebSocketClose
182         public void onClose(int statusCode, String reason) {
183             session = null;
184             connected = false;
185             logger.debug("Closing a WebSocket due to {}", reason);
186             scheduler.submit(() -> {
187                 try {
188                     eventHandler.onConnectionClosed();
189                 } catch (Exception e) {
190                     logger.error("Error handling onConnectionClosed()", e);
191                 }
192             });
193         }
194
195         @OnWebSocketError
196         public void onError(Throwable error) {
197             onClose(0, error.getMessage());
198         }
199     }
200
201     private void sendMessage(String str) throws IOException {
202         if (isConnected()) {
203             logger.debug("send message fo {}: {}", uri.toString(), str);
204             session.getRemote().sendString(str);
205         } else {
206             String stack = "";
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";
213             }
214             logger.error("Socket not initialized, trying to send {} {}", str, stack);
215             throw new IOException("Socket not initialized");
216         }
217     }
218
219     public synchronized JsonElement callMethod(SonyAudioMethod method) throws IOException {
220         try {
221             method.id = nextMessageId;
222             String message = mapper.toJson(method);
223             logger.debug("callMethod send {}", message);
224
225             commandLatch = new CountDownLatch(1);
226             commandResponse = null;
227             nextMessageId++;
228
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");
233             } else {
234                 logger.debug("Timeout during callMethod({}, {})", method.method, message);
235                 throw new IOException("Timeout during callMethod");
236             }
237         } catch (InterruptedException e) {
238             throw new IOException("Timeout in callMethod");
239         }
240     }
241
242     public URI getURI() {
243         return uri;
244     }
245 }