]> git.basschouten.com Git - openhab-addons.git/blob
f7c06c262d8124fa95c37a891b6e2218fd456bb3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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 static org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration.*;
16
17 import java.util.Arrays;
18 import java.util.Optional;
19
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;
25
26 import com.google.gson.Gson;
27 import com.google.gson.JsonElement;
28 import com.google.gson.JsonSyntaxException;
29
30 /**
31  * Websocket class for remote control
32  *
33  * @author Arjan Mels - Initial contribution
34  * @author Nick Waterton - changes to sendKey(), some refactoring
35  */
36 @NonNullByDefault
37 class WebSocketRemote extends WebSocketBase {
38     private final Logger logger = LoggerFactory.getLogger(WebSocketRemote.class);
39
40     private static Gson gson = new Gson();
41
42     private String host = "";
43     private String className = "";
44     private boolean mouseEnabled = false;
45
46     @SuppressWarnings("unused")
47     @NonNullByDefault({})
48     public static class JSONMessage {
49         String event;
50
51         static class App {
52             String appId;
53             String name;
54             int app_type;
55
56             public String getAppId() {
57                 return Optional.ofNullable(appId).orElse("");
58             }
59
60             public String getName() {
61                 return Optional.ofNullable(name).orElse("");
62             }
63
64             public int getAppType() {
65                 return Optional.ofNullable(app_type).orElse(2);
66             }
67         }
68
69         static class Data {
70             String update_type;
71             App[] data;
72             String id;
73             String token;
74         }
75
76         // data is sometimes a json object, sometimes a string or number
77         JsonElement data;
78         Data newData;
79
80         static class Params {
81             String params;
82
83             static class Data {
84                 String appId;
85             }
86
87             Data data;
88         }
89
90         Params params;
91
92         public String getEvent() {
93             return Optional.ofNullable(event).orElse("");
94         }
95
96         public Data getData() {
97             return Optional.ofNullable(data).map(a -> gson.fromJson(a, Data.class)).orElse(new Data());
98         }
99
100         public String getDataAsString() {
101             return Optional.ofNullable(data).map(a -> a.toString()).orElse("");
102         }
103
104         public App[] getAppData() {
105             return Optional.ofNullable(getData()).map(a -> a.data).orElse(new App[0]);
106         }
107
108         public String getToken() {
109             return Optional.ofNullable(getData()).map(a -> a.token).orElse("");
110         }
111
112         public String getUpdateType() {
113             return Optional.ofNullable(getData()).map(a -> a.update_type).orElse("");
114         }
115
116         public String getAppId() {
117             return Optional.ofNullable(params).map(a -> a.data).map(a -> a.appId).orElse("");
118         }
119     }
120
121     /**
122      * @param remoteControllerWebSocket
123      */
124     WebSocketRemote(RemoteControllerWebSocket remoteControllerWebSocket) {
125         super(remoteControllerWebSocket);
126         this.host = remoteControllerWebSocket.host;
127         this.className = this.getClass().getSimpleName();
128     }
129
130     @Override
131     public void onWebSocketError(@Nullable Throwable error) {
132         super.onWebSocketError(error);
133     }
134
135     @Override
136     public void onWebSocketText(@Nullable String msgarg) {
137         if (msgarg == null) {
138             return;
139         }
140         String msg = msgarg.replace('\n', ' ');
141         super.onWebSocketText(msg);
142         try {
143             JSONMessage jsonMsg = remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
144             if (jsonMsg == null) {
145                 return;
146             }
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
153                         try {
154                             this.remoteControllerWebSocket.openConnection();
155                         } catch (RemoteControllerException e) {
156                             logger.warn("{}: {}: Error ({})", host, className, e.getMessage());
157                         }
158                     }
159                     getApps();
160                     break;
161                 case "ms.channel.clientConnect":
162                     logger.debug("{}: Another Remote client has connected", host);
163                     break;
164                 case "ms.channel.clientDisconnect":
165                     logger.debug("{}: Other Remote client has disconnected", host);
166                     break;
167                 case "ms.channel.timeOut":
168                     logger.warn("{}: Remote Control Channel Timeout, SendKey/power commands are not available", host);
169                     break;
170                 case "ms.channel.unauthorized":
171                     logger.warn("{}: Remote Control is not authorized, please allow access on your TV", host);
172                     break;
173                 case "ms.remote.imeStart":
174                     // Keyboard input start enable
175                     break;
176                 case "ms.remote.imeDone":
177                     // keyboard input enabled
178                     break;
179                 case "ms.remote.imeUpdate":
180                     // keyboard text selected (base64 format) is in data.toString()
181                     // retrieve with getDataAsString()
182                     break;
183                 case "ms.remote.imeEnd":
184                     // keyboard selection completed
185                     break;
186                 case "ms.remote.touchEnable":
187                     logger.debug("{}: Mouse commands enabled", host);
188                     mouseEnabled = true;
189                     break;
190                 case "ms.remote.touchDisable":
191                     logger.debug("{}: Mouse commands disabled", host);
192                     mouseEnabled = false;
193                     break;
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();
199                     }
200                     break;
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("");
206                     }
207                     break;
208                 case "ed.edenApp.get":
209                     break;
210                 case "ed.installedApp.get":
211                     handleInstalledApps(jsonMsg);
212                     break;
213                 default:
214                     logger.debug("{}: WebSocketRemote Unknown event: {}", host, msg);
215             }
216         } catch (JsonSyntaxException e) {
217             logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
218         }
219     }
220
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();
227     }
228
229     void getApps() {
230         sendCommand(remoteControllerWebSocket.gson.toJson(new JSONSourceApp("ed.installedApp.get")));
231     }
232
233     @NonNullByDefault({})
234     static class JSONSourceApp {
235         public JSONSourceApp(String event) {
236             this(event, "");
237         }
238
239         public JSONSourceApp(String event, String appId) {
240             params.event = event;
241             if (!appId.isBlank()) {
242                 params.data.appId = appId;
243             }
244         }
245
246         public JSONSourceApp(String appName, boolean deepLink) {
247             this(appName, deepLink, null);
248         }
249
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;
254         }
255
256         static class Params {
257             static class Data {
258                 String appId;
259                 String action_type;
260                 String metaTag;
261             }
262
263             String event = "ed.apps.launch";
264             String to = "host";
265             Data data = new Data();
266         }
267
268         String method = "ms.channel.emit";
269         Params params = new Params();
270     }
271
272     public void sendSourceApp(String appName, boolean deepLink, @Nullable String metaTag) {
273         sendCommand(remoteControllerWebSocket.gson.toJson(new JSONSourceApp(appName, deepLink, metaTag)));
274     }
275
276     @NonNullByDefault({})
277     class JSONRemoteControl {
278         public JSONRemoteControl(String action, String value) {
279             switch (action) {
280                 case "Move":
281                     params.Cmd = action;
282                     // {"x": x, "y": y, "Time": str(duration)}
283                     params.Position = remoteControllerWebSocket.gson.fromJson(value, location.class);
284                     params.TypeOfRemote = "ProcessMouseDevice";
285                     break;
286                 case "MouseClick":
287                     params.Cmd = value;
288                     params.TypeOfRemote = "ProcessMouseDevice";
289                     break;
290                 case "Click":
291                 case "Press":
292                 case "Release":
293                     params.Cmd = action;
294                     params.DataOfCmd = value;
295                     params.Option = "false";
296                     params.TypeOfRemote = "SendRemoteKey";
297                     break;
298                 case "End":
299                     params.TypeOfRemote = "SendInputEnd";
300                     break;
301                 case "Text":
302                     params.Cmd = Utils.b64encode(value);
303                     params.DataOfCmd = "base64";
304                     params.TypeOfRemote = "SendInputString";
305                     break;
306             }
307         }
308
309         class location {
310             int x;
311             int y;
312             String Time;
313         }
314
315         class Params {
316             String Cmd;
317             String DataOfCmd;
318             location Position;
319             String Option;
320             String TypeOfRemote;
321         }
322
323         String method = "ms.remote.control";
324         Params params = new Params();
325     }
326
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);
330             return;
331         }
332         sendCommand(remoteControllerWebSocket.gson.toJson(new JSONRemoteControl(action, key)));
333     }
334 }