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 java.util.Optional;
16 import java.util.stream.Stream;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
23 import com.google.gson.JsonSyntaxException;
26 * Websocket class to retrieve app status
28 * @author Arjan Mels - Initial contribution
29 * @author Nick Waterton - Updated to handle >2020 TV's
32 class WebSocketV2 extends WebSocketBase {
33 private final Logger logger = LoggerFactory.getLogger(WebSocketV2.class);
35 private String host = "";
36 private String className = "";
37 // temporary storage for source appId.
38 String currentSourceApp = "";
40 WebSocketV2(RemoteControllerWebSocket remoteControllerWebSocket) {
41 super(remoteControllerWebSocket);
42 this.host = remoteControllerWebSocket.host;
43 this.className = this.getClass().getSimpleName();
46 @SuppressWarnings("unused")
48 private static class JSONAcq {
61 public String getId() {
62 return Optional.ofNullable(id).orElse("");
65 public boolean getResult() {
66 return Optional.ofNullable(result).orElse(false);
69 public String getErrorCode() {
70 return Optional.ofNullable(error).map(a -> a.code).orElse("");
74 @SuppressWarnings("unused")
76 private static class JSONMessage {
103 public String getEvent() {
104 return Optional.ofNullable(event).orElse("");
107 public String getName() {
108 return Optional.ofNullable(result).map(a -> a.name).orElse("");
111 public String getId() {
112 return Optional.ofNullable(result).map(a -> a.id).orElse("");
115 public String getVisible() {
116 return Optional.ofNullable(result).map(a -> a.visible).orElse("");
121 public void onWebSocketText(@Nullable String msgarg) {
122 if (msgarg == null) {
125 String msg = msgarg.replace('\n', ' ');
126 super.onWebSocketText(msg);
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());
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());
142 } catch (JsonSyntaxException ignore) {
146 JSONMessage jsonMsg = this.remoteControllerWebSocket.gson.fromJson(msg, JSONMessage.class);
147 if (jsonMsg == null) {
150 if (!jsonMsg.getId().isBlank()) {
151 handleResult(jsonMsg);
154 if (jsonMsg.error != null) {
155 logger.debug("{}: WebSocketV2 Error received: {}", host, msg);
159 switch (jsonMsg.getEvent()) {
160 case "ms.channel.connect":
161 logger.debug("{}: V2 channel connected. Token = {}", host, jsonMsg.data.token);
163 // update is requested from ed.installedApp.get event: small risk that this websocket is not
165 // on >2020 TV's this doesn't work so samsungTvAppWatchService should kick in automatically
167 case "ms.channel.clientConnect":
168 logger.debug("{}: V2 client connected", host);
170 case "ms.channel.clientDisconnect":
171 logger.debug("{}: V2 client disconnected", host);
174 logger.debug("{}: V2 Unknown event: {}", host, msg);
176 } catch (JsonSyntaxException e) {
177 logger.warn("{}: {}: Error ({}) in message: {}", host, className, e.getMessage(), msg);
182 * Handle results of getappstatus response, updates current running app channel
184 private synchronized void handleResult(JSONMessage jsonMsg) {
185 if (remoteControllerWebSocket.noApps()) {
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());
193 if (currentSourceApp.equals(jsonMsg.getId()) && "false".equals(jsonMsg.getVisible())) {
194 currentSourceApp = "";
195 remoteControllerWebSocket.callback.currentAppUpdated("");
199 @NonNullByDefault({})
201 public JSONApp(String id, String method) {
202 this(id, method, null);
205 public JSONApp(String id, String method, @Nullable String metaTag) {
206 // use message id to identify app to remove
208 this.method = method;
211 params.metaTag = metaTag;
221 Params params = new Params();
225 * update manApp.name if it's incorrect
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()));
237 * Fix app key, if it's the app id
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;
252 * Send get application status
254 * @param id appId of app to get status for
256 void getAppStatus(String id) {
258 boolean appType = getAppStream().filter(a -> a.getAppId().equals(id)).map(a -> a.getType() == 2).findFirst()
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)));
267 * Closes current app if one is open
269 * @return false if no app was running, true if an app was closed
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);
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)));
283 public void removeApp(String id) {
284 remoteControllerWebSocket.manApps.values().removeIf(app -> app.getAppId().equals(id));
287 public Stream<RemoteControllerWebSocket.App> getAppStream() {
288 return (remoteControllerWebSocket.noApps()) ? remoteControllerWebSocket.manApps.values().stream()
289 : remoteControllerWebSocket.apps.values().stream();
293 * Launches app by appId, closes current app if sent ""
294 * adds app if it's missing from manApps
296 * @param id AppId to launch
297 * @param type (2 or 4)
298 * @param metaTag optional url to launch (not working)
300 public void sendSourceApp(String id, boolean type, @Nullable String metaTag) {
302 if (id.equals(currentSourceApp)) {
303 logger.debug("{}: {} already running", host, id);
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);
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));
313 String apptype = (type) ? "ms.application.start" : "ms.webapplication.start";
314 sendCommand(remoteControllerWebSocket.gson.toJson(new JSONApp(id, apptype, metaTag)));
317 remoteControllerWebSocket.sendKeyPress(KeyCode.KEY_EXIT, 2000);