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.samsungtv.internal.protocol;
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;
22 import java.util.UUID;
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;
35 import com.google.gson.Gson;
38 * The {@link RemoteControllerWebSocket} is responsible for sending key codes to the
39 * Samsung TV via the websocket protocol (for newer TV's).
41 * @author Arjan Mels - Initial contribution
42 * @author Arjan Mels - Moved websocket inner classes to standalone classes
45 public class RemoteControllerWebSocket extends RemoteController implements Listener {
47 private final Logger logger = LoggerFactory.getLogger(RemoteControllerWebSocket.class);
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";
53 // WebSocket helper classes
54 private final WebSocketRemote webSocketRemote;
55 private final WebSocketArt webSocketArt;
56 private final WebSocketV2 webSocketV2;
58 // JSON parser class. Also used by WebSocket handlers.
59 final Gson gson = new Gson();
61 // Callback class. Also used by WebSocket handlers.
62 final RemoteControllerWebsocketCallback callback;
64 // Websocket client class shared by WebSocket handlers.
65 final WebSocketClient client;
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.
70 String currentSourceApp = null;
72 // last app in the apps list: used to detect when status information is complete in WebSocketV2.
74 String lastApp = null;
76 // timeout for status information search
77 private static final long UPDATE_CURRENT_APP_TIMEOUT = 5000;
78 private long previousUpdateCurrentApp = 0;
80 // UUID used for data exchange via websockets
81 final UUID uuid = UUID.randomUUID();
83 // Description of Apps
90 App(String appId, String name, int type) {
97 public String toString() {
102 // Map of all available apps
103 Map<String, App> apps = new LinkedHashMap<>();
106 * Create and initialize remote controller instance.
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
115 public RemoteControllerWebSocket(String host, int port, String appName, String uniqueId,
116 RemoteControllerWebsocketCallback remoteControllerWebsocketCallback) throws RemoteControllerException {
117 super(host, port, appName, uniqueId);
119 this.callback = remoteControllerWebsocketCallback;
121 WebSocketFactory webSocketFactory = remoteControllerWebsocketCallback.getWebSocketFactory();
122 if (webSocketFactory == null) {
123 throw new RemoteControllerException("No WebSocketFactory available");
126 client = webSocketFactory.createWebSocketClient("samsungtv");
128 client.addLifeCycleListener(this);
130 webSocketRemote = new WebSocketRemote(this);
131 webSocketArt = new WebSocketArt(this);
132 webSocketV2 = new WebSocketV2(this);
136 public boolean isConnected() {
137 return webSocketRemote.isConnected();
141 public void openConnection() throws RemoteControllerException {
142 logger.trace("openConnection()");
144 if (!(client.isStarted() || client.isStarting())) {
145 logger.debug("RemoteControllerWebSocket start Client");
148 client.setMaxBinaryMessageBufferSize(1000000);
149 // websocket connect will be done in lifetime handler
151 } catch (Exception e) {
152 logger.warn("Cannot connect to websocket remote control interface: {}", e.getMessage(), e);
153 throw new RemoteControllerException(e);
159 private void connectWebSockets() {
160 logger.trace("connectWebSockets()");
162 String encodedAppName = Base64.getUrlEncoder().encodeToString(appName.getBytes());
166 if (SamsungTvConfiguration.PROTOCOL_SECUREWEBSOCKET
167 .equals(callback.getConfig(SamsungTvConfiguration.PROTOCOL))) {
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);
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);
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);
194 private void closeConnection() throws RemoteControllerException {
195 logger.debug("RemoteControllerWebSocket closeConnection");
198 webSocketRemote.close();
199 webSocketArt.close();
202 } catch (Exception e) {
203 throw new RemoteControllerException(e);
208 public void close() throws RemoteControllerException {
209 logger.debug("RemoteControllerWebSocket close");
214 * Retrieve app status for all apps. In the WebSocketv2 handler the currently running app will be determined
216 void updateCurrentApp() {
217 if (webSocketV2.isNotConnected()) {
218 logger.warn("Cannot retrieve current app webSocketV2 is not connected");
222 // update still running and not timed out
223 if (lastApp != null && System.currentTimeMillis() < previousUpdateCurrentApp + UPDATE_CURRENT_APP_TIMEOUT) {
228 previousUpdateCurrentApp = System.currentTimeMillis();
230 currentSourceApp = null;
232 // retrieve last app (don't merge with next loop as this might run asynchronously
233 for (App app : apps.values()) {
237 for (App app : apps.values()) {
238 webSocketV2.getAppStatus(app.appId);
243 * Send key code to Samsung TV.
245 * @param key Key code to send.
246 * @throws RemoteControllerException
249 public void sendKey(KeyCode key) throws RemoteControllerException {
253 public void sendKeyPress(KeyCode key) throws RemoteControllerException {
257 public void sendKey(KeyCode key, boolean press) throws RemoteControllerException {
258 logger.debug("Try to send command: {}", key);
260 if (!isConnected()) {
265 sendKeyData(key, press);
266 } catch (RemoteControllerException e) {
267 logger.debug("Couldn't send command", e);
268 logger.debug("Retry one time...");
273 sendKeyData(key, press);
278 * Send sequence of key codes to Samsung TV.
280 * @param keys List of key codes to send.
281 * @throws RemoteControllerException
284 public void sendKeys(List<KeyCode> keys) throws RemoteControllerException {
289 * Send sequence of key codes to Samsung TV.
291 * @param keys List of key codes to send.
292 * @param sleepInMs Sleep between key code sending in milliseconds.
293 * @throws RemoteControllerException
295 public void sendKeys(List<KeyCode> keys, int sleepInMs) throws RemoteControllerException {
296 logger.debug("Try to send sequence of commands: {}", keys);
298 if (!isConnected()) {
302 for (int i = 0; i < keys.size(); i++) {
303 KeyCode key = keys.get(i);
305 sendKeyData(key, false);
306 } catch (RemoteControllerException e) {
307 logger.debug("Couldn't send command", e);
308 logger.debug("Retry one time...");
313 sendKeyData(key, false);
316 if ((keys.size() - 1) != i) {
317 // Sleep a while between commands
319 Thread.sleep(sleepInMs);
320 } catch (InterruptedException e) {
321 Thread.currentThread().interrupt();
327 logger.debug("Command(s) successfully sent");
330 private void sendKeyData(KeyCode key, boolean press) throws RemoteControllerException {
331 webSocketRemote.sendKeyData(press, key.toString());
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;
341 webSocketRemote.sendSourceApp(appName, deepLink);
344 public void sendUrl(String url) {
345 String processedUrl = url.replace("/", "\\/");
346 webSocketRemote.sendSourceApp("org.tizen.browser", false, processedUrl);
349 public List<String> getAppList() {
350 ArrayList<String> appList = new ArrayList<>();
351 for (App app : apps.values()) {
352 appList.add(app.name);
358 public void lifeCycleStarted(@Nullable LifeCycle arg0) {
359 logger.trace("WebSocketClient started");
364 public void lifeCycleFailure(@Nullable LifeCycle arg0, @Nullable Throwable throwable) {
365 logger.warn("WebSocketClient failure: {}", throwable != null ? throwable.toString() : null);
369 public void lifeCycleStarting(@Nullable LifeCycle arg0) {
370 logger.trace("WebSocketClient starting");
374 public void lifeCycleStopped(@Nullable LifeCycle arg0) {
375 logger.trace("WebSocketClient stopped");
379 public void lifeCycleStopping(@Nullable LifeCycle arg0) {
380 logger.trace("WebSocketClient stopping");