]> git.basschouten.com Git - openhab-addons.git/blob
e9c5eac8f7afbd4dbcf2521f6cd31fe4577d0688
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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 JsonParser parser = new JsonParser();
64     private final Gson mapper;
65
66     private static int ping = 0;
67
68     private final SonyAudioClientSocketEventListener eventHandler;
69
70     public SonyAudioClientSocket(SonyAudioClientSocketEventListener eventHandler, URI uri,
71             ScheduledExecutorService scheduler) {
72         mapper = new GsonBuilder().disableHtmlEscaping().create();
73         this.eventHandler = eventHandler;
74         this.uri = uri;
75         this.scheduler = scheduler;
76     }
77
78     public synchronized void open(WebSocketClient webSocketClient) {
79         try {
80             if (isConnected()) {
81                 logger.warn("connect: connection is already open");
82             }
83
84             SonyAudioWebSocketListener socket = new SonyAudioWebSocketListener();
85             ClientUpgradeRequest request = new ClientUpgradeRequest();
86
87             try {
88                 webSocketClient.connect(socket, uri, request).get(1, TimeUnit.SECONDS);
89             } catch (TimeoutException e) {
90                 logger.debug("Could not establish websocket within a second.");
91             }
92         } catch (Exception e) {
93             logger.debug("Exception then trying to start the websocket {}", e.getMessage(), e);
94         }
95     }
96
97     public void close() {
98         logger.debug("Closing socket {}", uri);
99         // if there is an old web socket then clean up and destroy
100         if (session != null) {
101             try {
102                 session.close();
103             } catch (Exception e) {
104                 logger.debug("Exception during closing the websocket session {}", e.getMessage(), e);
105             }
106             session = null;
107         }
108     }
109
110     public boolean isConnected() {
111         if (session == null || !session.isOpen()) {
112             connected = false;
113             return connected;
114         }
115
116         RemoteEndpoint remote = session.getRemote();
117
118         ByteBuffer payload = ByteBuffer.allocate(4).putInt(ping++);
119         try {
120             remote.sendPing(payload);
121         } catch (IOException e) {
122             logger.warn("Connection to {} lost: {}", uri, e.getMessage());
123             connected = false;
124         }
125
126         return connected;
127     }
128
129     @WebSocket
130     public class SonyAudioWebSocketListener {
131
132         @OnWebSocketConnect
133         public void onConnect(Session wssession) {
134             logger.debug("Connected to server");
135             session = wssession;
136             connected = true;
137             if (eventHandler != null) {
138                 scheduler.submit(() -> {
139                     try {
140                         eventHandler.onConnectionOpened(uri);
141                     } catch (Exception e) {
142                         logger.error("Error handling onConnectionOpened() {}", e.getMessage(), e);
143                     }
144                 });
145             }
146         }
147
148         @OnWebSocketMessage
149         public void onMessage(String message) {
150             logger.debug("Message received from server: {}", message);
151             try {
152                 final JsonObject json = parser.parse(message).getAsJsonObject();
153                 if (json.has("id")) {
154                     logger.debug("Response received from server: {}", json);
155                     int messageId = json.get("id").getAsInt();
156                     if (messageId == nextMessageId - 1) {
157                         commandResponse = json;
158                         commandLatch.countDown();
159                     }
160                 } else {
161                     logger.debug("Event received from server: {}", json);
162                     try {
163                         if (eventHandler != null) {
164                             scheduler.submit(() -> {
165                                 try {
166                                     eventHandler.handleEvent(json);
167                                 } catch (Exception e) {
168                                     logger.error("Error handling event {} player state change message: {}", json,
169                                             e.getMessage(), e);
170                                 }
171                             });
172                         }
173                     } catch (Exception e) {
174                         logger.error("Error handling player state change message", e);
175                     }
176                 }
177             } catch (JsonParseException e) {
178                 logger.debug("Not valid JSON message: {}", e.getMessage(), e);
179             }
180         }
181
182         @OnWebSocketClose
183         public void onClose(int statusCode, String reason) {
184             session = null;
185             connected = false;
186             logger.debug("Closing a WebSocket due to {}", reason);
187             scheduler.submit(() -> {
188                 try {
189                     eventHandler.onConnectionClosed();
190                 } catch (Exception e) {
191                     logger.error("Error handling onConnectionClosed()", e);
192                 }
193             });
194         }
195
196         @OnWebSocketError
197         public void onError(Throwable error) {
198             onClose(0, error.getMessage());
199         }
200     }
201
202     private void sendMessage(String str) throws IOException {
203         if (isConnected()) {
204             logger.debug("send message fo {}: {}", uri.toString(), str);
205             session.getRemote().sendString(str);
206         } else {
207             String stack = "";
208             stack += "Printing stack trace:\n";
209             StackTraceElement[] elements = Thread.currentThread().getStackTrace();
210             for (int i = 1; i < elements.length; i++) {
211                 StackTraceElement s = elements[i];
212                 stack += "\tat " + s.getClassName() + "." + s.getMethodName() + "(" + s.getFileName() + ":"
213                         + s.getLineNumber() + ")\n";
214             }
215             logger.error("Socket not initialized, trying to send {} {}", str, stack);
216             throw new IOException("Socket not initialized");
217         }
218     }
219
220     public synchronized JsonElement callMethod(SonyAudioMethod method) throws IOException {
221         try {
222             method.id = nextMessageId;
223             String message = mapper.toJson(method);
224             logger.debug("callMethod send {}", message);
225
226             commandLatch = new CountDownLatch(1);
227             commandResponse = null;
228             nextMessageId++;
229
230             sendMessage(message);
231             if (commandLatch.await(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
232                 logger.debug("callMethod {} returns {}", uri.toString(), commandResponse.toString());
233                 return commandResponse.get("result");
234             } else {
235                 logger.debug("Timeout during callMethod({}, {})", method.method, message);
236                 throw new IOException("Timeout during callMethod");
237             }
238         } catch (InterruptedException e) {
239             throw new IOException("Timeout in callMethod");
240         }
241     }
242
243     public URI getURI() {
244         return uri;
245     }
246 }