2 * Copyright (c) 2010-2024 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;
15 import static org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration.*;
17 import java.util.Arrays;
18 import java.util.Optional;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.samsungtv.internal.Utils;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
26 import com.google.gson.Gson;
27 import com.google.gson.JsonElement;
28 import com.google.gson.JsonSyntaxException;
31 * Websocket class for remote control
33 * @author Arjan Mels - Initial contribution
34 * @author Nick Waterton - changes to sendKey(), some refactoring
37 class WebSocketRemote extends WebSocketBase {
38 private final Logger logger = LoggerFactory.getLogger(WebSocketRemote.class);
40 private static Gson gson = new Gson();
42 private String host = "";
43 private String className = "";
44 private boolean mouseEnabled = false;
46 @SuppressWarnings("unused")
48 public static class JSONMessage {
56 public String getAppId() {
57 return Optional.ofNullable(appId).orElse("");
60 public String getName() {
61 return Optional.ofNullable(name).orElse("");
64 public int getAppType() {
65 return Optional.ofNullable(app_type).orElse(2);
76 // data is sometimes a json object, sometimes a string or number
92 public String getEvent() {
93 return Optional.ofNullable(event).orElse("");
96 public Data getData() {
97 return Optional.ofNullable(data).map(a -> gson.fromJson(a, Data.class)).orElse(new Data());
100 public String getDataAsString() {
101 return Optional.ofNullable(data).map(a -> a.toString()).orElse("");
104 public App[] getAppData() {
105 return Optional.ofNullable(getData()).map(a -> a.data).orElse(new App[0]);
108 public String getToken() {
109 return Optional.ofNullable(getData()).map(a -> a.token).orElse("");
112 public String getUpdateType() {
113 return Optional.ofNullable(getData()).map(a -> a.update_type).orElse("");
116 public String getAppId() {
117 return Optional.ofNullable(params).map(a -> a.data).map(a -> a.appId).orElse("");
122 * @param remoteControllerWebSocket
124 WebSocketRemote(RemoteControllerWebSocket remoteControllerWebSocket) {
125 super(remoteControllerWebSocket);
126 this.host = remoteControllerWebSocket.host;
127 this.className = this.getClass().getSimpleName();
131 public void onWebSocketError(@Nullable Throwable error) {
132 super.onWebSocketError(error);
136 public void onWebSocketText(@Nullable String msgarg) {
137 if (msgarg == null) {
140 String msg = msgarg.replace('\n', ' ');
141 super.onWebSocketText(msg);
143 JSONMessage jsonMsg = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
144 if (jsonMsg == null) {
147 switch (jsonMsg.getEvent()) {
148 case "ms.channel.connect":
149 logger.debug("{}: Remote channel connected. Token = {}", host, jsonMsg.getToken());
150 if (!jsonMsg.getToken().isBlank()) {
151 this.remoteControllerWebSocket.callback.putConfig(WEBSOCKET_TOKEN, jsonMsg.getToken());
152 // try opening additional websockets
154 this.remoteControllerWebSocket.openConnection();
155 } catch (RemoteControllerException e) {
156 logger.warn("{}: {}: Error ({})", host, className, e.getMessage());
161 case "ms.channel.clientConnect":
162 logger.debug("{}: Another Remote client has connected", host);
164 case "ms.channel.clientDisconnect":
165 logger.debug("{}: Other Remote client has disconnected", host);
167 case "ms.channel.timeOut":
168 logger.warn("{}: Remote Control Channel Timeout, SendKey/power commands are not available", host);
170 case "ms.channel.unauthorized":
171 logger.warn("{}: Remote Control is not authorized, please allow access on your TV", host);
173 case "ms.remote.imeStart":
174 // Keyboard input start enable
176 case "ms.remote.imeDone":
177 // keyboard input enabled
179 case "ms.remote.imeUpdate":
180 // keyboard text selected (base64 format) is in data.toString()
181 // retrieve with getDataAsString()
183 case "ms.remote.imeEnd":
184 // keyboard selection completed
186 case "ms.remote.touchEnable":
187 logger.debug("{}: Mouse commands enabled", host);
190 case "ms.remote.touchDisable":
191 logger.debug("{}: Mouse commands disabled", host);
192 mouseEnabled = false;
194 // note: the following 3 do not work on >2020 TV's
195 case "ed.edenTV.update":
196 logger.debug("{}: edenTV update: {}", host, jsonMsg.getUpdateType());
197 if ("ed.edenApp.update".equals(jsonMsg.getUpdateType())) {
198 remoteControllerWebSocket.updateCurrentApp();
201 case "ed.apps.launch":
202 logger.debug("{}: App launch: {}", host,
203 "200".equals(jsonMsg.getDataAsString()) ? "successfull" : "failed");
204 if ("200".equals(jsonMsg.getDataAsString())) {
205 remoteControllerWebSocket.getAppStatus("");
208 case "ed.edenApp.get":
210 case "ed.installedApp.get":
211 handleInstalledApps(jsonMsg);
214 logger.debug("{}: WebSocketRemote Unknown event: {}", host, msg);
216 } catch (JsonSyntaxException e) {
217 logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
221 private void handleInstalledApps(JSONMessage jsonMsg) {
222 remoteControllerWebSocket.apps.clear();
223 Arrays.stream(jsonMsg.getAppData()).forEach(a -> remoteControllerWebSocket.apps.put(a.getName(),
224 remoteControllerWebSocket.new App(a.getAppId(), a.getName(), a.getAppType())));
225 remoteControllerWebSocket.updateCurrentApp();
226 remoteControllerWebSocket.listApps();
230 sendCommand(remoteControllerWebSocket.gson.toJson(new JSONSourceApp("ed.installedApp.get")));
233 @NonNullByDefault({})
234 static class JSONSourceApp {
235 public JSONSourceApp(String event) {
239 public JSONSourceApp(String event, String appId) {
240 params.event = event;
241 if (!appId.isBlank()) {
242 params.data.appId = appId;
246 public JSONSourceApp(String appName, boolean deepLink) {
247 this(appName, deepLink, null);
250 public JSONSourceApp(String appName, boolean deepLink, String metaTag) {
251 params.data.appId = appName;
252 params.data.action_type = deepLink ? "DEEP_LINK" : "NATIVE_LAUNCH";
253 params.data.metaTag = metaTag;
256 static class Params {
263 String event = "ed.apps.launch";
265 Data data = new Data();
268 String method = "ms.channel.emit";
269 Params params = new Params();
272 public void sendSourceApp(String appName, boolean deepLink, @Nullable String metaTag) {
273 sendCommand(remoteControllerWebSocket.gson.toJson(new JSONSourceApp(appName, deepLink, metaTag)));
276 @NonNullByDefault({})
277 class JSONRemoteControl {
278 public JSONRemoteControl(String action, String value) {
282 // {"x": x, "y": y, "Time": str(duration)}
283 params.Position = remoteControllerWebSocket.gson.fromJson(value, location.class);
284 params.TypeOfRemote = "ProcessMouseDevice";
288 params.TypeOfRemote = "ProcessMouseDevice";
294 params.DataOfCmd = value;
295 params.Option = "false";
296 params.TypeOfRemote = "SendRemoteKey";
299 params.TypeOfRemote = "SendInputEnd";
302 params.Cmd = Utils.b64encode(value);
303 params.DataOfCmd = "base64";
304 params.TypeOfRemote = "SendInputString";
323 String method = "ms.remote.control";
324 Params params = new Params();
327 void sendKeyData(String action, String key) {
328 if (!mouseEnabled && ("Move".equals(action) || "MouseClick".equals(action))) {
329 logger.warn("{}: Mouse actions are not enabled for this app", host);
332 sendCommand(remoteControllerWebSocket.gson.toJson(new JSONRemoteControl(action, key)));