]> git.basschouten.com Git - openhab-addons.git/blob
0ebcfef62cc6a4a063714add9dd4277e9fe8abcc
[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.samsungtv.internal.protocol;
14
15 import java.net.URI;
16 import java.net.URISyntaxException;
17 import java.util.ArrayList;
18 import java.util.Base64;
19 import java.util.LinkedHashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.UUID;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.util.StringUtil;
27 import org.eclipse.jetty.util.component.LifeCycle;
28 import org.eclipse.jetty.util.component.LifeCycle.Listener;
29 import org.eclipse.jetty.websocket.client.WebSocketClient;
30 import org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration;
31 import org.openhab.core.io.net.http.WebSocketFactory;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import com.google.gson.Gson;
36
37 /**
38  * The {@link RemoteControllerWebSocket} is responsible for sending key codes to the
39  * Samsung TV via the websocket protocol (for newer TV's).
40  *
41  * @author Arjan Mels - Initial contribution
42  * @author Arjan Mels - Moved websocket inner classes to standalone classes
43  */
44 @NonNullByDefault
45 public class RemoteControllerWebSocket extends RemoteController implements Listener {
46
47     private final Logger logger = LoggerFactory.getLogger(RemoteControllerWebSocket.class);
48
49     private static final String WS_ENDPOINT_REMOTE_CONTROL = "/api/v2/channels/samsung.remote.control";
50     private static final String WS_ENDPOINT_ART = "/api/v2/channels/com.samsung.art-app";
51     private static final String WS_ENDPOINT_V2 = "/api/v2";
52
53     // WebSocket helper classes
54     private final WebSocketRemote webSocketRemote;
55     private final WebSocketArt webSocketArt;
56     private final WebSocketV2 webSocketV2;
57
58     // JSON parser class. Also used by WebSocket handlers.
59     final Gson gson = new Gson();
60
61     // Callback class. Also used by WebSocket handlers.
62     final RemoteControllerWebsocketCallback callback;
63
64     // Websocket client class shared by WebSocket handlers.
65     final WebSocketClient client;
66
67     // temporary storage for source app. Will be used as value for the sourceApp channel when information is complete.
68     // Also used by Websocket handlers.
69     @Nullable
70     String currentSourceApp = null;
71
72     // last app in the apps list: used to detect when status information is complete in WebSocketV2.
73     @Nullable
74     String lastApp = null;
75
76     // timeout for status information search
77     private static final long UPDATE_CURRENT_APP_TIMEOUT = 5000;
78     private long previousUpdateCurrentApp = 0;
79
80     // UUID used for data exchange via websockets
81     final UUID uuid = UUID.randomUUID();
82
83     // Description of Apps
84     @NonNullByDefault()
85     class App {
86         String appId;
87         String name;
88         int type;
89
90         App(String appId, String name, int type) {
91             this.appId = appId;
92             this.name = name;
93             this.type = type;
94         }
95
96         @Override
97         public String toString() {
98             return this.name;
99         }
100     }
101
102     // Map of all available apps
103     Map<String, App> apps = new LinkedHashMap<>();
104
105     /**
106      * Create and initialize remote controller instance.
107      *
108      * @param host Host name of the Samsung TV.
109      * @param port TCP port of the remote controller protocol.
110      * @param appName Application name used to send key codes.
111      * @param uniqueId Unique Id used to send key codes.
112      * @param remoteControllerWebsocketCallback callback
113      * @throws RemoteControllerException
114      */
115     public RemoteControllerWebSocket(String host, int port, String appName, String uniqueId,
116             RemoteControllerWebsocketCallback remoteControllerWebsocketCallback) throws RemoteControllerException {
117         super(host, port, appName, uniqueId);
118
119         this.callback = remoteControllerWebsocketCallback;
120
121         WebSocketFactory webSocketFactory = remoteControllerWebsocketCallback.getWebSocketFactory();
122         if (webSocketFactory == null) {
123             throw new RemoteControllerException("No WebSocketFactory available");
124         }
125
126         client = webSocketFactory.createWebSocketClient("samsungtv");
127
128         client.addLifeCycleListener(this);
129
130         webSocketRemote = new WebSocketRemote(this);
131         webSocketArt = new WebSocketArt(this);
132         webSocketV2 = new WebSocketV2(this);
133     }
134
135     @Override
136     public boolean isConnected() {
137         return webSocketRemote.isConnected();
138     }
139
140     @Override
141     public void openConnection() throws RemoteControllerException {
142         logger.trace("openConnection()");
143
144         if (!(client.isStarted() || client.isStarting())) {
145             logger.debug("RemoteControllerWebSocket start Client");
146             try {
147                 client.start();
148                 client.setMaxBinaryMessageBufferSize(1000000);
149                 // websocket connect will be done in lifetime handler
150                 return;
151             } catch (Exception e) {
152                 logger.warn("Cannot connect to websocket remote control interface: {}", e.getMessage(), e);
153                 throw new RemoteControllerException(e);
154             }
155         }
156         connectWebSockets();
157     }
158
159     private void connectWebSockets() {
160         logger.trace("connectWebSockets()");
161
162         String encodedAppName = Base64.getUrlEncoder().encodeToString(appName.getBytes());
163
164         String protocol;
165
166         if (SamsungTvConfiguration.PROTOCOL_SECUREWEBSOCKET
167                 .equals(callback.getConfig(SamsungTvConfiguration.PROTOCOL))) {
168             protocol = "wss";
169         } else {
170             protocol = "ws";
171         }
172
173         try {
174             String token = (String) callback.getConfig(SamsungTvConfiguration.WEBSOCKET_TOKEN);
175             webSocketRemote.connect(new URI(protocol, null, host, port, WS_ENDPOINT_REMOTE_CONTROL,
176                     "name=" + encodedAppName + (StringUtil.isNotBlank(token) ? "&token=" + token : ""), null));
177         } catch (RemoteControllerException | URISyntaxException e) {
178             logger.warn("Problem connecting to remote websocket", e);
179         }
180
181         try {
182             webSocketArt.connect(new URI(protocol, null, host, port, WS_ENDPOINT_ART, "name=" + encodedAppName, null));
183         } catch (RemoteControllerException | URISyntaxException e) {
184             logger.warn("Problem connecting to artmode websocket", e);
185         }
186
187         try {
188             webSocketV2.connect(new URI(protocol, null, host, port, WS_ENDPOINT_V2, "name=" + encodedAppName, null));
189         } catch (RemoteControllerException | URISyntaxException e) {
190             logger.warn("Problem connecting to V2 websocket", e);
191         }
192     }
193
194     private void closeConnection() throws RemoteControllerException {
195         logger.debug("RemoteControllerWebSocket closeConnection");
196
197         try {
198             webSocketRemote.close();
199             webSocketArt.close();
200             webSocketV2.close();
201             client.stop();
202         } catch (Exception e) {
203             throw new RemoteControllerException(e);
204         }
205     }
206
207     @Override
208     public void close() throws RemoteControllerException {
209         logger.debug("RemoteControllerWebSocket close");
210         closeConnection();
211     }
212
213     /**
214      * Retrieve app status for all apps. In the WebSocketv2 handler the currently running app will be determined
215      */
216     void updateCurrentApp() {
217         if (webSocketV2.isNotConnected()) {
218             logger.warn("Cannot retrieve current app webSocketV2 is not connected");
219             return;
220         }
221
222         // update still running and not timed out
223         if (lastApp != null && System.currentTimeMillis() < previousUpdateCurrentApp + UPDATE_CURRENT_APP_TIMEOUT) {
224             return;
225         }
226
227         lastApp = null;
228         previousUpdateCurrentApp = System.currentTimeMillis();
229
230         currentSourceApp = null;
231
232         // retrieve last app (don't merge with next loop as this might run asynchronously
233         for (App app : apps.values()) {
234             lastApp = app.appId;
235         }
236
237         for (App app : apps.values()) {
238             webSocketV2.getAppStatus(app.appId);
239         }
240     }
241
242     /**
243      * Send key code to Samsung TV.
244      *
245      * @param key Key code to send.
246      * @throws RemoteControllerException
247      */
248     @Override
249     public void sendKey(KeyCode key) throws RemoteControllerException {
250         sendKey(key, false);
251     }
252
253     public void sendKeyPress(KeyCode key) throws RemoteControllerException {
254         sendKey(key, true);
255     }
256
257     public void sendKey(KeyCode key, boolean press) throws RemoteControllerException {
258         logger.debug("Try to send command: {}", key);
259
260         if (!isConnected()) {
261             openConnection();
262         }
263
264         try {
265             sendKeyData(key, press);
266         } catch (RemoteControllerException e) {
267             logger.debug("Couldn't send command", e);
268             logger.debug("Retry one time...");
269
270             closeConnection();
271             openConnection();
272
273             sendKeyData(key, press);
274         }
275     }
276
277     /**
278      * Send sequence of key codes to Samsung TV.
279      *
280      * @param keys List of key codes to send.
281      * @throws RemoteControllerException
282      */
283     @Override
284     public void sendKeys(List<KeyCode> keys) throws RemoteControllerException {
285         sendKeys(keys, 300);
286     }
287
288     /**
289      * Send sequence of key codes to Samsung TV.
290      *
291      * @param keys List of key codes to send.
292      * @param sleepInMs Sleep between key code sending in milliseconds.
293      * @throws RemoteControllerException
294      */
295     public void sendKeys(List<KeyCode> keys, int sleepInMs) throws RemoteControllerException {
296         logger.debug("Try to send sequence of commands: {}", keys);
297
298         if (!isConnected()) {
299             openConnection();
300         }
301
302         for (int i = 0; i < keys.size(); i++) {
303             KeyCode key = keys.get(i);
304             try {
305                 sendKeyData(key, false);
306             } catch (RemoteControllerException e) {
307                 logger.debug("Couldn't send command", e);
308                 logger.debug("Retry one time...");
309
310                 closeConnection();
311                 openConnection();
312
313                 sendKeyData(key, false);
314             }
315
316             if ((keys.size() - 1) != i) {
317                 // Sleep a while between commands
318                 try {
319                     Thread.sleep(sleepInMs);
320                 } catch (InterruptedException e) {
321                     Thread.currentThread().interrupt();
322                     return;
323                 }
324             }
325         }
326
327         logger.debug("Command(s) successfully sent");
328     }
329
330     private void sendKeyData(KeyCode key, boolean press) throws RemoteControllerException {
331         webSocketRemote.sendKeyData(press, key.toString());
332     }
333
334     public void sendSourceApp(String app) {
335         String appName = app;
336         App appVal = apps.get(app);
337         boolean deepLink = false;
338         appName = appVal.appId;
339         deepLink = appVal.type == 2;
340
341         webSocketRemote.sendSourceApp(appName, deepLink);
342     }
343
344     public void sendUrl(String url) {
345         String processedUrl = url.replace("/", "\\/");
346         webSocketRemote.sendSourceApp("org.tizen.browser", false, processedUrl);
347     }
348
349     public List<String> getAppList() {
350         ArrayList<String> appList = new ArrayList<>();
351         for (App app : apps.values()) {
352             appList.add(app.name);
353         }
354         return appList;
355     }
356
357     @Override
358     public void lifeCycleStarted(@Nullable LifeCycle arg0) {
359         logger.trace("WebSocketClient started");
360         connectWebSockets();
361     }
362
363     @Override
364     public void lifeCycleFailure(@Nullable LifeCycle arg0, @Nullable Throwable throwable) {
365         logger.warn("WebSocketClient failure: {}", throwable != null ? throwable.toString() : null);
366     }
367
368     @Override
369     public void lifeCycleStarting(@Nullable LifeCycle arg0) {
370         logger.trace("WebSocketClient starting");
371     }
372
373     @Override
374     public void lifeCycleStopped(@Nullable LifeCycle arg0) {
375         logger.trace("WebSocketClient stopped");
376     }
377
378     @Override
379     public void lifeCycleStopping(@Nullable LifeCycle arg0) {
380         logger.trace("WebSocketClient stopping");
381     }
382 }