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;
26 import java.util.Base64;
27 import java.util.List;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * The {@link RemoteControllerLegacy} is responsible for sending key codes to the
39 * href="http://sc0ty.pl/2012/02/samsung-tv-network-remote-control-protocol/">http://sc0ty.pl/2012/02/samsung-tv-
40 * network-remote-control-protocol/</a>
43 * @author Pauli Anttila - Initial contribution
44 * @author Arjan Mels - Renamed and reworked to use RemoteController base class, to allow different protocols
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
89 public void openConnection() throws RemoteControllerException {
90 logger.debug("Open connection to host '{}:{}'", host, port);
92 Socket localsocket = new Socket();
95 socket.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT);
96 } catch (IOException e) {
97 logger.debug("Cannot connect to Legacy Remote Controller: {}", e.getMessage());
98 throw new RemoteControllerException("Connection failed", e);
101 InputStream inputStream;
103 BufferedWriter localwriter = new BufferedWriter(new OutputStreamWriter(localsocket.getOutputStream()));
104 writer = localwriter;
105 inputStream = localsocket.getInputStream();
106 InputStreamReader localreader = new InputStreamReader(inputStream);
107 reader = localreader;
109 logger.debug("Connection successfully opened...querying access");
110 writeInitialInfo(localwriter, localsocket);
111 readInitialInfo(localreader);
114 while ((i = inputStream.available()) > 0) {
117 } catch (IOException e) {
118 throw new RemoteControllerException(e);
122 private void writeInitialInfo(Writer writer, Socket socket) throws RemoteControllerException {
126 * offset value and description
127 * ------ ---------------------
128 * 0x00 0x00 - datagram type?
129 * 0x01 0x0013 - string length (little endian)
130 * 0x03 "iphone.iapp.samsung" - string content
131 * 0x16 0x0038 - payload size (little endian)
134 * Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
135 * encoded with base64 algorithm. Every string is preceded by
136 * 2-bytes field containing encoded string length.
138 * These three strings are as follow:
140 * remote control device IP, unique ID – value to distinguish
141 * controllers, name – it will be displayed as controller name.
146 writer.append((char) 0x00);
147 writeString(writer, APP_STRING);
148 writeString(writer, createRegistrationPayload(socket.getLocalAddress().getHostAddress()));
150 } catch (IOException e) {
151 throw new RemoteControllerException(e);
155 private void readInitialInfo(Reader reader) throws RemoteControllerException {
159 * offset value and description
160 * ------ ---------------------
161 * 0x00 don't know, it it always 0x00 or 0x02
162 * 0x01 0x000c - string length (little endian)
163 * 0x03 "iapp.samsung" - string content
164 * 0x0f 0x0006 - payload size (little endian)
172 char[] result = readCharArray(reader);
174 if (Arrays.equals(result, ACCESS_GRANTED_RESP)) {
175 logger.debug("Access granted");
176 } else if (Arrays.equals(result, ACCESS_DENIED_RESP)) {
177 throw new RemoteControllerException("Access denied");
178 } else if (Arrays.equals(result, ACCESS_TIMEOUT_RESP)) {
179 throw new RemoteControllerException("Registration timed out");
180 } else if (Arrays.equals(result, WAITING_USER_GRANT_RESP)) {
181 throw new RemoteControllerException("Waiting for user to grant access");
183 throw new RemoteControllerException("Unknown response received for access query");
185 } catch (IOException e) {
186 throw new RemoteControllerException(e);
191 * Close connection to Samsung TV.
193 * @throws RemoteControllerException
195 public void closeConnection() throws RemoteControllerException {
197 if (socket != null) {
200 } catch (IOException e) {
201 throw new RemoteControllerException(e);
206 * Send key code to Samsung TV.
208 * @param key Key code to send.
209 * @throws RemoteControllerException
212 public void sendKey(KeyCode key) throws RemoteControllerException {
213 logger.debug("Try to send command: {}", key);
215 if (!isConnected()) {
221 } catch (RemoteControllerException e) {
222 logger.debug("Couldn't send command", e);
223 logger.debug("Retry one time...");
231 logger.debug("Command successfully sent");
235 * Send sequence of key codes to Samsung TV.
237 * @param keys List of key codes to send.
238 * @throws RemoteControllerException
241 public void sendKeys(List<KeyCode> keys) throws RemoteControllerException {
246 * Send sequence of key codes to Samsung TV.
248 * @param keys List of key codes to send.
249 * @param sleepInMs Sleep between key code sending in milliseconds.
250 * @throws RemoteControllerException
252 public void sendKeys(List<KeyCode> keys, int sleepInMs) throws RemoteControllerException {
253 logger.debug("Try to send sequence of commands: {}", keys);
255 if (!isConnected()) {
259 for (int i = 0; i < keys.size(); i++) {
260 KeyCode key = keys.get(i);
263 } catch (RemoteControllerException e) {
264 logger.debug("Couldn't send command", e);
265 logger.debug("Retry one time...");
273 if ((keys.size() - 1) != i) {
274 // Sleep a while between commands
276 Thread.sleep(sleepInMs);
277 } catch (InterruptedException e) {
283 logger.debug("Command(s) successfully sent");
287 public boolean isConnected() {
288 return socket != null && !socket.isClosed() && socket.isConnected();
291 private String createRegistrationPayload(String ip) throws IOException {
293 * Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
294 * encoded with base64 algorithm. Every string is preceded by 2-bytes
295 * field containing encoded string length.
297 * These three strings are as follow:
299 * remote control device IP, unique ID – value to distinguish
300 * controllers, name – it will be displayed as controller name.
303 StringWriter w = new StringWriter();
304 w.append((char) 0x64);
305 w.append((char) 0x00);
306 writeBase64String(w, ip);
307 writeBase64String(w, uniqueId);
308 writeBase64String(w, appName);
313 private void writeString(Writer writer, String str) throws IOException {
314 int len = str.length();
315 byte low = (byte) (len & 0xFF);
316 byte high = (byte) ((len >> 8) & 0xFF);
318 writer.append((char) (low));
319 writer.append((char) (high));
323 private void writeBase64String(Writer writer, String str) throws IOException {
324 String tmp = Base64.getEncoder().encodeToString(str.getBytes());
325 writeString(writer, tmp);
328 private String readString(Reader reader) throws IOException {
329 char[] buf = readCharArray(reader);
330 return new String(buf);
333 private char[] readCharArray(Reader reader) throws IOException {
334 byte low = (byte) reader.read();
335 byte high = (byte) reader.read();
336 int len = (high << 8) + low;
339 char[] buffer = new char[len];
343 return new char[] {};
347 private void sendKeyData(KeyCode key) throws RemoteControllerException {
348 logger.debug("Sending key code {}", key.getValue());
350 Writer localwriter = writer;
351 Reader localreader = reader;
352 if (localwriter == null || localreader == null) {
357 * offset value and description
358 * ------ ---------------------
360 * 0x01 0x0013 - string length (little endian)
361 * 0x03 "iphone.iapp.samsung" - string content
362 * 0x16 0x0011 - payload size (little endian)
368 localwriter.append((char) 0x00);
369 writeString(localwriter, APP_STRING);
370 writeString(localwriter, createKeyDataPayload(key));
374 * Read response. Response is pretty useless, because TV seems to
375 * send same response in both ok and error situation.
378 readString(localreader);
379 readCharArray(localreader);
380 } catch (IOException e) {
381 throw new RemoteControllerException(e);
385 private String createKeyDataPayload(KeyCode key) throws IOException {
390 * offset value and description
391 * ------ ---------------------
392 * 0x18 three 0x00 bytes
393 * 0x1b 0x000c - key code size (little endian)
394 * 0x1d key code encoded as base64 string
399 StringWriter writer = new StringWriter();
400 writer.append((char) 0x00);
401 writer.append((char) 0x00);
402 writer.append((char) 0x00);
403 writeBase64String(writer, key.getValue());
405 return writer.toString();
409 public void close() throws RemoteControllerException {