]> git.basschouten.com Git - openhab-addons.git/blob
3d18cc7dbbc9b991bee8811803d61e3fabe7b0b6
[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.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;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 /**
35  * The {@link RemoteControllerLegacy} is responsible for sending key codes to the
36  * Samsung TV.
37  *
38  * @see <a
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>
41  *
42  *
43  * @author Pauli Anttila - Initial contribution
44  * @author Arjan Mels - Renamed and reworked to use RemoteController base class, to allow different protocols
45  */
46 @NonNullByDefault
47 public class RemoteControllerLegacy extends RemoteController {
48
49     private static final int CONNECTION_TIMEOUT = 500;
50
51     private final Logger logger = LoggerFactory.getLogger(RemoteControllerLegacy.class);
52
53     // Access granted response
54     private static final char[] ACCESS_GRANTED_RESP = new char[] { 0x64, 0x00, 0x01, 0x00 };
55
56     // User rejected your network remote controller response
57     private static final char[] ACCESS_DENIED_RESP = new char[] { 0x64, 0x00, 0x00, 0x00 };
58
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 };
61
62     // timeout or cancelled by user response
63     private static final char[] ACCESS_TIMEOUT_RESP = new char[] { 0x65, 0x00 };
64
65     private static final String APP_STRING = "iphone.iapp.samsung";
66
67     private @Nullable Socket socket;
68     private @Nullable InputStreamReader reader;
69     private @Nullable BufferedWriter writer;
70
71     /**
72      * Create and initialize remote controller instance.
73      *
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.
78      */
79     public RemoteControllerLegacy(String host, int port, @Nullable String appName, @Nullable String uniqueId) {
80         super(host, port, appName, uniqueId);
81     }
82
83     /**
84      * Open Connection to Samsung TV.
85      *
86      * @throws RemoteControllerException
87      */
88     @Override
89     public void openConnection() throws RemoteControllerException {
90         logger.debug("Open connection to host '{}:{}'", host, port);
91
92         Socket localsocket = new Socket();
93         socket = localsocket;
94         try {
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);
99         }
100
101         InputStream inputStream;
102         try {
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;
108
109             logger.debug("Connection successfully opened...querying access");
110             writeInitialInfo(localwriter, localsocket);
111             readInitialInfo(localreader);
112
113             int i;
114             while ((i = inputStream.available()) > 0) {
115                 inputStream.skip(i);
116             }
117         } catch (IOException e) {
118             throw new RemoteControllerException(e);
119         }
120     }
121
122     private void writeInitialInfo(Writer writer, Socket socket) throws RemoteControllerException {
123         try {
124             /* @formatter:off
125             *
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)
132             * 0x18   payload
133             *
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.
137             *
138             * These three strings are as follow:
139             *
140             * remote control device IP, unique ID – value to distinguish
141             * controllers, name – it will be displayed as controller name.
142             *
143             * @formatter:on
144             */
145
146             writer.append((char) 0x00);
147             writeString(writer, APP_STRING);
148             writeString(writer, createRegistrationPayload(socket.getLocalAddress().getHostAddress()));
149             writer.flush();
150         } catch (IOException e) {
151             throw new RemoteControllerException(e);
152         }
153     }
154
155     private void readInitialInfo(Reader reader) throws RemoteControllerException {
156         try {
157             /* @formatter:off
158             *
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)
165             * 0x11   payload
166             *
167             * @formatter:on
168             */
169
170             reader.skip(1);
171             readString(reader);
172             char[] result = readCharArray(reader);
173
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");
182             } else {
183                 throw new RemoteControllerException("Unknown response received for access query");
184             }
185         } catch (IOException e) {
186             throw new RemoteControllerException(e);
187         }
188     }
189
190     /**
191      * Close connection to Samsung TV.
192      *
193      * @throws RemoteControllerException
194      */
195     public void closeConnection() throws RemoteControllerException {
196         try {
197             if (socket != null) {
198                 socket.close();
199             }
200         } catch (IOException e) {
201             throw new RemoteControllerException(e);
202         }
203     }
204
205     /**
206      * Send key code to Samsung TV.
207      *
208      * @param key Key code to send.
209      * @throws RemoteControllerException
210      */
211     @Override
212     public void sendKey(KeyCode key) throws RemoteControllerException {
213         logger.debug("Try to send command: {}", key);
214
215         if (!isConnected()) {
216             openConnection();
217         }
218
219         try {
220             sendKeyData(key);
221         } catch (RemoteControllerException e) {
222             logger.debug("Couldn't send command", e);
223             logger.debug("Retry one time...");
224
225             closeConnection();
226             openConnection();
227
228             sendKeyData(key);
229         }
230
231         logger.debug("Command successfully sent");
232     }
233
234     /**
235      * Send sequence of key codes to Samsung TV.
236      *
237      * @param keys List of key codes to send.
238      * @throws RemoteControllerException
239      */
240     @Override
241     public void sendKeys(List<KeyCode> keys) throws RemoteControllerException {
242         sendKeys(keys, 300);
243     }
244
245     /**
246      * Send sequence of key codes to Samsung TV.
247      *
248      * @param keys List of key codes to send.
249      * @param sleepInMs Sleep between key code sending in milliseconds.
250      * @throws RemoteControllerException
251      */
252     public void sendKeys(List<KeyCode> keys, int sleepInMs) throws RemoteControllerException {
253         logger.debug("Try to send sequence of commands: {}", keys);
254
255         if (!isConnected()) {
256             openConnection();
257         }
258
259         for (int i = 0; i < keys.size(); i++) {
260             KeyCode key = keys.get(i);
261             try {
262                 sendKeyData(key);
263             } catch (RemoteControllerException e) {
264                 logger.debug("Couldn't send command", e);
265                 logger.debug("Retry one time...");
266
267                 closeConnection();
268                 openConnection();
269
270                 sendKeyData(key);
271             }
272
273             if ((keys.size() - 1) != i) {
274                 // Sleep a while between commands
275                 try {
276                     Thread.sleep(sleepInMs);
277                 } catch (InterruptedException e) {
278                     return;
279                 }
280             }
281         }
282
283         logger.debug("Command(s) successfully sent");
284     }
285
286     @Override
287     public boolean isConnected() {
288         return socket != null && !socket.isClosed() && socket != null && socket.isConnected();
289     }
290
291     private String createRegistrationPayload(String ip) throws IOException {
292         /*
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.
296          *
297          * These three strings are as follow:
298          *
299          * remote control device IP, unique ID – value to distinguish
300          * controllers, name – it will be displayed as controller name.
301          */
302
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);
309         w.flush();
310         return w.toString();
311     }
312
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);
317
318         writer.append((char) (low));
319         writer.append((char) (high));
320         writer.append(str);
321     }
322
323     private void writeBase64String(Writer writer, String str) throws IOException {
324         String tmp = Base64.getEncoder().encodeToString(str.getBytes());
325         writeString(writer, tmp);
326     }
327
328     private String readString(Reader reader) throws IOException {
329         char[] buf = readCharArray(reader);
330         return new String(buf);
331     }
332
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;
337
338         if (len > 0) {
339             char[] buffer = new char[len];
340             reader.read(buffer);
341             return buffer;
342         } else {
343             return new char[] {};
344         }
345     }
346
347     private void sendKeyData(KeyCode key) throws RemoteControllerException {
348         logger.debug("Sending key code {}", key.getValue());
349
350         Writer localwriter = writer;
351         Reader localreader = reader;
352         if (localwriter == null || localreader == null) {
353             return;
354         }
355         /* @formatter:off
356          *
357          * offset value and description
358          * ------ ---------------------
359          * 0x00   always 0x00
360          * 0x01   0x0013 - string length (little endian)
361          * 0x03   "iphone.iapp.samsung" - string content
362          * 0x16   0x0011 - payload size (little endian)
363          * 0x18   payload
364          *
365          * @formatter:on
366          */
367         try {
368             localwriter.append((char) 0x00);
369             writeString(localwriter, APP_STRING);
370             writeString(localwriter, createKeyDataPayload(key));
371             localwriter.flush();
372
373             /*
374              * Read response. Response is pretty useless, because TV seems to
375              * send same response in both ok and error situation.
376              */
377             localreader.skip(1);
378             readString(localreader);
379             readCharArray(localreader);
380         } catch (IOException e) {
381             throw new RemoteControllerException(e);
382         }
383     }
384
385     private String createKeyDataPayload(KeyCode key) throws IOException {
386         /* @formatter:off
387         *
388         * Payload:
389         *
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
395         *
396         * @formatter:on
397         */
398
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());
404         writer.flush();
405         return writer.toString();
406     }
407
408     @Override
409     public void close() throws RemoteControllerException {
410         if (isConnected()) {
411             closeConnection();
412         }
413     }
414 }