]> git.basschouten.com Git - openhab-addons.git/blob
7e5a1a62f8f99dd597152980927df5df7b360a10
[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 java.util.Optional;
16 import java.util.stream.Stream;
17
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22
23 import com.google.gson.JsonSyntaxException;
24
25 /**
26  * Websocket class to retrieve app status
27  *
28  * @author Arjan Mels - Initial contribution
29  * @author Nick Waterton - Updated to handle >2020 TV's
30  */
31 @NonNullByDefault
32 class WebSocketV2 extends WebSocketBase {
33     private final Logger logger = LoggerFactory.getLogger(WebSocketV2.class);
34
35     private String host = "";
36     private String className = "";
37     // temporary storage for source appId.
38     String currentSourceApp = "";
39
40     WebSocketV2(RemoteControllerWebSocket remoteControllerWebSocket) {
41         super(remoteControllerWebSocket);
42         this.host = remoteControllerWebSocket.host;
43         this.className = this.getClass().getSimpleName();
44     }
45
46     @SuppressWarnings("unused")
47     @NonNullByDefault({})
48     private static class JSONAcq {
49         String id;
50         boolean result;
51
52         static class Error {
53             String code;
54             String details;
55             String message;
56             String status;
57         }
58
59         Error error;
60
61         public String getId() {
62             return Optional.ofNullable(id).orElse("");
63         }
64
65         public boolean getResult() {
66             return Optional.ofNullable(result).orElse(false);
67         }
68
69         public String getErrorCode() {
70             return Optional.ofNullable(error).map(a -> a.code).orElse("");
71         }
72     }
73
74     @SuppressWarnings("unused")
75     @NonNullByDefault({})
76     private static class JSONMessage {
77         String event;
78         String id;
79
80         static class Result {
81             String id;
82             String name;
83             String running;
84             String visible;
85         }
86
87         static class Data {
88             String id;
89             String token;
90         }
91
92         static class Error {
93             String code;
94             String details;
95             String message;
96             String status;
97         }
98
99         Result result;
100         Data data;
101         Error error;
102
103         public String getEvent() {
104             return Optional.ofNullable(event).orElse("");
105         }
106
107         public String getName() {
108             return Optional.ofNullable(result).map(a -> a.name).orElse("");
109         }
110
111         public String getId() {
112             return Optional.ofNullable(result).map(a -> a.id).orElse("");
113         }
114
115         public String getVisible() {
116             return Optional.ofNullable(result).map(a -> a.visible).orElse("");
117         }
118     }
119
120     @Override
121     public void onWebSocketText(@Nullable String msgarg) {
122         if (msgarg == null) {
123             return;
124         }
125         String msg = msgarg.replace('\n', ' ');
126         super.onWebSocketText(msg);
127         try {
128             JSONAcq jsonAcq = this.remoteControllerWebSocket.gson.fromJson(msg, JSONAcq.class);
129             if (jsonAcq != null && !jsonAcq.getId().isBlank()) {
130                 if (jsonAcq.getResult()) {
131                     // 3 second delay as app does not report visible until then.
132                     remoteControllerWebSocket.getAppStatus(jsonAcq.getId());
133                 }
134                 if (!jsonAcq.getErrorCode().isBlank()) {
135                     if ("404".equals(jsonAcq.getErrorCode())) {
136                         // remove app from manual list if it's not installed using message id.
137                         removeApp(jsonAcq.getId());
138                     }
139                 }
140                 return;
141             }
142         } catch (JsonSyntaxException ignore) {
143             // ignore error
144         }
145         try {
146             JSONMessage jsonMsg = this.remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
147             if (jsonMsg == null) {
148                 return;
149             }
150             if (!jsonMsg.getId().isBlank()) {
151                 handleResult(jsonMsg);
152                 return;
153             }
154             if (jsonMsg.error != null) {
155                 logger.debug("{}: WebSocketV2 Error received: {}", host, msg);
156                 return;
157             }
158
159             switch (jsonMsg.getEvent()) {
160                 case "ms.channel.connect":
161                     logger.debug("{}: V2 channel connected. Token = {}", host, jsonMsg.data.token);
162
163                     // update is requested from ed.installedApp.get event: small risk that this websocket is not
164                     // yet connected
165                     // on >2020 TV's this doesn't work so samsungTvAppWatchService should kick in automatically
166                     break;
167                 case "ms.channel.clientConnect":
168                     logger.debug("{}: V2 client connected", host);
169                     break;
170                 case "ms.channel.clientDisconnect":
171                     logger.debug("{}: V2 client disconnected", host);
172                     break;
173                 default:
174                     logger.debug("{}: V2 Unknown event: {}", host, msg);
175             }
176         } catch (JsonSyntaxException e) {
177             logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
178         }
179     }
180
181     /**
182      * Handle results of getappstatus response, updates current running app channel
183      */
184     private synchronized void handleResult(JSONMessage jsonMsg) {
185         if (remoteControllerWebSocket.noApps()) {
186             updateApps(jsonMsg);
187         }
188         if (!jsonMsg.getName().isBlank() && "true".equals(jsonMsg.getVisible())) {
189             logger.debug("{}: Running app: {} = {}", host, jsonMsg.getId(), jsonMsg.getName());
190             currentSourceApp = jsonMsg.getId();
191             remoteControllerWebSocket.callback.currentAppUpdated(jsonMsg.getName());
192         }
193         if (currentSourceApp.equals(jsonMsg.getId()) && "false".equals(jsonMsg.getVisible())) {
194             currentSourceApp = "";
195             remoteControllerWebSocket.callback.currentAppUpdated("");
196         }
197     }
198
199     @NonNullByDefault({})
200     class JSONApp {
201         public JSONApp(String id, String method) {
202             this(id, method, null);
203         }
204
205         public JSONApp(String id, String method, @Nullable String metaTag) {
206             // use message id to identify app to remove
207             this.id = id;
208             this.method = method;
209             params.id = id;
210             // not working
211             params.metaTag = metaTag;
212         }
213
214         class Params {
215             String id;
216             String metaTag;
217         }
218
219         String method;
220         String id;
221         Params params = new Params();
222     }
223
224     /**
225      * update manApp.name if it's incorrect
226      */
227     void updateApps(JSONMessage jsonMsg) {
228         remoteControllerWebSocket.manApps.values().stream()
229                 .filter(a -> a.getAppId().equals(jsonMsg.getId()) && !a.getName().equals(jsonMsg.getName()))
230                 .peek(a -> logger.trace("{}: Updated app name {} to: {}", host, a.getName(), jsonMsg.getName()))
231                 .findFirst().ifPresent(a -> a.setName(jsonMsg.getName()));
232
233         updateApp(jsonMsg);
234     }
235
236     /**
237      * Fix app key, if it's the app id
238      */
239     @SuppressWarnings("null")
240     void updateApp(JSONMessage jsonMsg) {
241         if (remoteControllerWebSocket.manApps.containsKey(jsonMsg.getId())) {
242             int type = remoteControllerWebSocket.manApps.get(jsonMsg.getId()).getType();
243             remoteControllerWebSocket.manApps.put(jsonMsg.getName(),
244                     remoteControllerWebSocket.new App(jsonMsg.getId(), jsonMsg.getName(), type));
245             remoteControllerWebSocket.manApps.remove(jsonMsg.getId());
246             logger.trace("{}: Updated app id {} name to: {}", host, jsonMsg.getId(), jsonMsg.getName());
247             remoteControllerWebSocket.updateCount = 0;
248         }
249     }
250
251     /**
252      * Send get application status
253      *
254      * @param id appId of app to get status for
255      */
256     void getAppStatus(String id) {
257         if (!id.isEmpty()) {
258             boolean appType = getAppStream().filter(a -> a.getAppId().equals(id)).map(a -> a.getType() == 2).findFirst()
259                     .orElse(true);
260             // note apptype 4 always seems to return an error, so use default of 2 (true)
261             String apptype = (appType) ? "ms.application.get" : "ms.webapplication.get";
262             sendCommand(remoteControllerWebSocket.gson.toJson(new JSONApp(id, apptype)));
263         }
264     }
265
266     /**
267      * Closes current app if one is open
268      *
269      * @return false if no app was running, true if an app was closed
270      */
271     public boolean closeApp() {
272         return getAppStream().filter(a -> a.appId.equals(currentSourceApp))
273                 .peek(a -> logger.debug("{}: closing app: {}", host, a.getName()))
274                 .map(a -> closeApp(a.getAppId(), a.getType() == 2)).findFirst().orElse(false);
275     }
276
277     public boolean closeApp(String appId, boolean appType) {
278         String apptype = (appType) ? "ms.application.stop" : "ms.webapplication.stop";
279         sendCommand(remoteControllerWebSocket.gson.toJson(new JSONApp(appId, apptype)));
280         return true;
281     }
282
283     public void removeApp(String id) {
284         remoteControllerWebSocket.manApps.values().removeIf(app -> app.getAppId().equals(id));
285     }
286
287     public Stream<RemoteControllerWebSocket.App> getAppStream() {
288         return (remoteControllerWebSocket.noApps()) ? remoteControllerWebSocket.manApps.values().stream()
289                 : remoteControllerWebSocket.apps.values().stream();
290     }
291
292     /**
293      * Launches app by appId, closes current app if sent ""
294      * adds app if it's missing from manApps
295      *
296      * @param id AppId to launch
297      * @param type (2 or 4)
298      * @param metaTag optional url to launch (not working)
299      */
300     public void sendSourceApp(String id, boolean type, @Nullable String metaTag) {
301         if (!id.isBlank()) {
302             if (id.equals(currentSourceApp)) {
303                 logger.debug("{}: {} already running", host, id);
304                 return;
305             }
306             if ("org.tizen.browser".equals(id) && remoteControllerWebSocket.noApps()) {
307                 logger.warn("{}: using {} - you need a correct entry for \"Internet\" in the appslist file", host, id);
308             }
309             if (!getAppStream().anyMatch(a -> a.getAppId().equals(id))) {
310                 logger.debug("{}: Adding App : {}", host, id);
311                 remoteControllerWebSocket.manApps.put(id, remoteControllerWebSocket.new App(id, id, (type) ? 2 : 4));
312             }
313             String apptype = (type) ? "ms.application.start" : "ms.webapplication.start";
314             sendCommand(remoteControllerWebSocket.gson.toJson(new JSONApp(id, apptype, metaTag)));
315         } else {
316             if (!closeApp()) {
317                 remoteControllerWebSocket.sendKeyPress(KeyCode.KEY_EXIT, 2000);
318             }
319         }
320     }
321 }