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.io.BufferedWriter;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.InputStreamReader;
19 import java.io.OutputStreamWriter;
20 import java.io.Reader;
21 import java.io.StringWriter;
22 import java.io.Writer;
23 import java.net.InetSocketAddress;
24 import java.net.Socket;
25 import java.util.Arrays;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.samsungtv.internal.Utils;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
34 * The {@link RemoteControllerLegacy} is responsible for sending key codes to the
38 * href="http://sc0ty.pl/2012/02/samsung-tv-network-remote-control-protocol/">http://sc0ty.pl/2012/02/samsung-tv-
39 * network-remote-control-protocol/</a>
42 * @author Pauli Anttila - Initial contribution
43 * @author Arjan Mels - Renamed and reworked to use RemoteController base class, to allow different protocols
44 * @author Nick Waterton - moved Sendkeys to RemoteController, reworked sendkey, sendKeyData
47 public class RemoteControllerLegacy extends RemoteController {
49 private static final int CONNECTION_TIMEOUT = 500;
51 private final Logger logger = LoggerFactory.getLogger(RemoteControllerLegacy.class);
53 // Access granted response
54 private static final char[] ACCESS_GRANTED_RESP = new char[] { 0x64, 0x00, 0x01, 0x00 };
56 // User rejected your network remote controller response
57 private static final char[] ACCESS_DENIED_RESP = new char[] { 0x64, 0x00, 0x00, 0x00 };
59 // waiting for user to grant or deny access response
60 private static final char[] WAITING_USER_GRANT_RESP = new char[] { 0x0A, 0x00, 0x02, 0x00, 0x00, 0x00 };
62 // timeout or cancelled by user response
63 private static final char[] ACCESS_TIMEOUT_RESP = new char[] { 0x65, 0x00 };
65 private static final String APP_STRING = "iphone.iapp.samsung";
67 private @Nullable Socket socket;
68 private @Nullable InputStreamReader reader;
69 private @Nullable BufferedWriter writer;
72 * Create and initialize remote controller instance.
74 * @param host Host name of the Samsung TV.
75 * @param port TCP port of the remote controller protocol.
76 * @param appName Application name used to send key codes.
77 * @param uniqueId Unique Id used to send key codes.
79 public RemoteControllerLegacy(String host, int port, @Nullable String appName, @Nullable String uniqueId) {
80 super(host, port, appName, uniqueId);
84 * Open Connection to Samsung TV.
86 * @throws RemoteControllerException
88 public void openConnection() throws RemoteControllerException {
92 logger.debug("{}: Open connection to host '{}:{}'", host, host, port);
94 Socket localsocket = new Socket();
98 socket.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT);
100 throw new IOException("no Socket");
102 } catch (IOException e) {
103 logger.debug("{}: Cannot connect to Legacy Remote Controller: {}", host, e.getMessage());
104 throw new RemoteControllerException("Connection failed", e);
107 InputStream inputStream;
109 BufferedWriter localwriter = new BufferedWriter(new OutputStreamWriter(localsocket.getOutputStream()));
110 writer = localwriter;
111 inputStream = localsocket.getInputStream();
112 InputStreamReader localreader = new InputStreamReader(inputStream);
113 reader = localreader;
115 logger.debug("{}: Connection successfully opened...querying access", host);
116 writeInitialInfo(localwriter, localsocket);
117 readInitialInfo(localreader);
120 while ((i = inputStream.available()) > 0) {
123 } catch (IOException e) {
124 throw new RemoteControllerException(e);
128 private void writeInitialInfo(Writer writer, Socket socket) throws RemoteControllerException {
132 * offset value and description
133 * ------ ---------------------
134 * 0x00 0x00 - datagram type?
135 * 0x01 0x0013 - string length (little endian)
136 * 0x03 "iphone.iapp.samsung" - string content
137 * 0x16 0x0038 - payload size (little endian)
140 * Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
141 * encoded with base64 algorithm. Every string is preceded by
142 * 2-bytes field containing encoded string length.
144 * These three strings are as follow:
146 * remote control device IP, unique ID – value to distinguish
147 * controllers, name – it will be displayed as controller name.
152 writer.append((char) 0x00);
153 writeString(writer, APP_STRING);
154 writeString(writer, createRegistrationPayload(socket.getLocalAddress().getHostAddress()));
156 } catch (IOException e) {
157 throw new RemoteControllerException(e);
161 private void readInitialInfo(Reader reader) throws RemoteControllerException {
165 * offset value and description
166 * ------ ---------------------
167 * 0x00 don't know, it it always 0x00 or 0x02
168 * 0x01 0x000c - string length (little endian)
169 * 0x03 "iapp.samsung" - string content
170 * 0x0f 0x0006 - payload size (little endian)
178 char[] result = readCharArray(reader);
180 if (Arrays.equals(result, ACCESS_GRANTED_RESP)) {
181 logger.debug("{}: Access granted", host);
182 } else if (Arrays.equals(result, ACCESS_DENIED_RESP)) {
183 throw new RemoteControllerException("Access denied");
184 } else if (Arrays.equals(result, ACCESS_TIMEOUT_RESP)) {
185 throw new RemoteControllerException("Registration timed out");
186 } else if (Arrays.equals(result, WAITING_USER_GRANT_RESP)) {
187 throw new RemoteControllerException("Waiting for user to grant access");
189 throw new RemoteControllerException("Unknown response received for access query");
191 } catch (IOException e) {
192 throw new RemoteControllerException(e);
197 * Close connection to Samsung TV.
200 public void closeConnection() {
202 if (socket != null) {
205 } catch (IOException e) {
210 public void sendUrl(String command) {
211 logger.warn("{}: Remote control legacy: unsupported command: {}", host, command);
214 public void sendSourceApp(String command) {
215 logger.warn("{}: Remote control legacy: unsupported command: {}", host, command);
218 public void updateCurrentApp() {
221 public void getArtmodeStatus(String... optionalRequests) {
224 public boolean closeApp() {
228 public void getAppStatus(String id) {
231 public boolean noApps() {
235 private void logResult(String msg, Throwable cause) {
236 if (logger.isTraceEnabled()) {
237 logger.trace("{}: {}: ", host, msg, cause);
239 logger.debug("{}: {}: {}", host, msg, cause.getMessage());
244 * Send key code to Samsung TV.
246 * @param key Key code to send.
248 public void sendKey(Object key) {
249 if (!(key instanceof KeyCode)) {
250 logger.warn("{}: Remote control legacy: unsupported command: {}", host, key);
253 logger.trace("{}: Try to send command: {}", host, key);
254 for (int i = 0; i < 2; i++) {
257 if (sendKeyData((KeyCode) key)) {
258 logger.trace("{}: Command successfully sent", host);
261 } catch (RemoteControllerException e) {
262 logResult("Couldn't send command", e);
265 logger.debug("{}: Retry send command {} attempt {}...", host, key, i);
267 logger.warn("{}: Command Retrys failed", host);
270 public void sendKeyPress(KeyCode key, int duration) {
274 public boolean isConnected() {
275 return socket != null && !socket.isClosed() && socket.isConnected();
278 private String createRegistrationPayload(String ip) throws IOException {
280 * Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
281 * encoded with base64 algorithm. Every string is preceded by 2-bytes
282 * field containing encoded string length.
284 * These three strings are as follow:
286 * remote control device IP, unique ID – value to distinguish
287 * controllers, name – it will be displayed as controller name.
290 StringWriter w = new StringWriter();
291 w.append((char) 0x64);
292 w.append((char) 0x00);
293 writeBase64String(w, ip);
294 writeBase64String(w, uniqueId);
295 writeBase64String(w, appName);
300 private void writeString(Writer writer, String str) throws IOException {
301 int len = str.length();
302 byte low = (byte) (len & 0xFF);
303 byte high = (byte) ((len >> 8) & 0xFF);
305 writer.append((char) (low));
306 writer.append((char) (high));
310 private void writeBase64String(Writer writer, String str) throws IOException {
311 writeString(writer, Utils.b64encode(str));
314 private String readString(Reader reader) throws IOException {
315 return new String(readCharArray(reader));
318 private char[] readCharArray(Reader reader) throws IOException {
319 byte low = (byte) reader.read();
320 byte high = (byte) reader.read();
321 int len = (high << 8) + low;
324 char[] buffer = new char[len];
328 return new char[] {};
332 private boolean sendKeyData(KeyCode key) {
333 logger.debug("{}: Sending key code {}", host, key.getValue());
335 Writer localwriter = writer;
336 Reader localreader = reader;
337 if (localwriter == null || localreader == null) {
342 * offset value and description
343 * ------ ---------------------
345 * 0x01 0x0013 - string length (little endian)
346 * 0x03 "iphone.iapp.samsung" - string content
347 * 0x16 0x0011 - payload size (little endian)
353 localwriter.append((char) 0x00);
354 writeString(localwriter, APP_STRING);
355 writeString(localwriter, createKeyDataPayload(key));
359 * Read response. Response is pretty useless, because TV seems to
360 * send same response in both ok and error situation.
363 readString(localreader);
364 readCharArray(localreader);
365 } catch (IOException e) {
366 logResult("Couldn't send command", e);
372 private String createKeyDataPayload(KeyCode key) throws IOException {
377 * offset value and description
378 * ------ ---------------------
379 * 0x18 three 0x00 bytes
380 * 0x1b 0x000c - key code size (little endian)
381 * 0x1d key code encoded as base64 string
386 StringWriter writer = new StringWriter();
387 writer.append((char) 0x00);
388 writer.append((char) 0x00);
389 writer.append((char) 0x00);
390 writeBase64String(writer, key.getValue());
392 return writer.toString();
396 public void close() throws RemoteControllerException {