]> git.basschouten.com Git - openhab-addons.git/blob
d014d0ec91f280654561c1f4473e21e438dc1f2d
[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
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;
32
33 /**
34  * The {@link RemoteControllerLegacy} is responsible for sending key codes to the
35  * Samsung TV.
36  *
37  * @see <a
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>
40  *
41  *
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
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     public void openConnection() throws RemoteControllerException {
89         if (isConnected()) {
90             return;
91         }
92         logger.debug("{}: Open connection to host '{}:{}'", host, host, port);
93
94         Socket localsocket = new Socket();
95         socket = localsocket;
96         try {
97             if (socket != null) {
98                 socket.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT);
99             } else {
100                 throw new IOException("no Socket");
101             }
102         } catch (IOException e) {
103             logger.debug("{}: Cannot connect to Legacy Remote Controller: {}", host, e.getMessage());
104             throw new RemoteControllerException("Connection failed", e);
105         }
106
107         InputStream inputStream;
108         try {
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;
114
115             logger.debug("{}: Connection successfully opened...querying access", host);
116             writeInitialInfo(localwriter, localsocket);
117             readInitialInfo(localreader);
118
119             int i;
120             while ((i = inputStream.available()) > 0) {
121                 inputStream.skip(i);
122             }
123         } catch (IOException e) {
124             throw new RemoteControllerException(e);
125         }
126     }
127
128     private void writeInitialInfo(Writer writer, Socket socket) throws RemoteControllerException {
129         try {
130             /* @formatter:off
131             *
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)
138             * 0x18   payload
139             *
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.
143             *
144             * These three strings are as follow:
145             *
146             * remote control device IP, unique ID – value to distinguish
147             * controllers, name – it will be displayed as controller name.
148             *
149             * @formatter:on
150             */
151
152             writer.append((char) 0x00);
153             writeString(writer, APP_STRING);
154             writeString(writer, createRegistrationPayload(socket.getLocalAddress().getHostAddress()));
155             writer.flush();
156         } catch (IOException e) {
157             throw new RemoteControllerException(e);
158         }
159     }
160
161     private void readInitialInfo(Reader reader) throws RemoteControllerException {
162         try {
163             /* @formatter:off
164             *
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)
171             * 0x11   payload
172             *
173             * @formatter:on
174             */
175
176             reader.skip(1);
177             readString(reader);
178             char[] result = readCharArray(reader);
179
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");
188             } else {
189                 throw new RemoteControllerException("Unknown response received for access query");
190             }
191         } catch (IOException e) {
192             throw new RemoteControllerException(e);
193         }
194     }
195
196     /**
197      * Close connection to Samsung TV.
198      *
199      */
200     public void closeConnection() {
201         try {
202             if (socket != null) {
203                 socket.close();
204             }
205         } catch (IOException e) {
206             // ignore error
207         }
208     }
209
210     public void sendUrl(String command) {
211         logger.warn("{}: Remote control legacy: unsupported command: {}", host, command);
212     }
213
214     public void sendSourceApp(String command) {
215         logger.warn("{}: Remote control legacy: unsupported command: {}", host, command);
216     }
217
218     public void updateCurrentApp() {
219     }
220
221     public void getArtmodeStatus(String... optionalRequests) {
222     }
223
224     public boolean closeApp() {
225         return false;
226     }
227
228     public void getAppStatus(String id) {
229     }
230
231     public boolean noApps() {
232         return false;
233     }
234
235     private void logResult(String msg, Throwable cause) {
236         if (logger.isTraceEnabled()) {
237             logger.trace("{}: {}: ", host, msg, cause);
238         } else {
239             logger.debug("{}: {}: {}", host, msg, cause.getMessage());
240         }
241     }
242
243     /**
244      * Send key code to Samsung TV.
245      *
246      * @param key Key code to send.
247      */
248     public void sendKey(Object key) {
249         if (!(key instanceof KeyCode)) {
250             logger.warn("{}: Remote control legacy: unsupported command: {}", host, key);
251             return;
252         }
253         logger.trace("{}: Try to send command: {}", host, key);
254         for (int i = 0; i < 2; i++) {
255             try {
256                 openConnection();
257                 if (sendKeyData((KeyCode) key)) {
258                     logger.trace("{}: Command successfully sent", host);
259                     return;
260                 }
261             } catch (RemoteControllerException e) {
262                 logResult("Couldn't send command", e);
263             }
264             closeConnection();
265             logger.debug("{}: Retry send command {} attempt {}...", host, key, i);
266         }
267         logger.warn("{}: Command Retrys failed", host);
268     }
269
270     public void sendKeyPress(KeyCode key, int duration) {
271         sendKey(key);
272     }
273
274     public boolean isConnected() {
275         return socket != null && !socket.isClosed() && socket.isConnected();
276     }
277
278     private String createRegistrationPayload(String ip) throws IOException {
279         /*
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.
283          *
284          * These three strings are as follow:
285          *
286          * remote control device IP, unique ID – value to distinguish
287          * controllers, name – it will be displayed as controller name.
288          */
289
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);
296         w.flush();
297         return w.toString();
298     }
299
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);
304
305         writer.append((char) (low));
306         writer.append((char) (high));
307         writer.append(str);
308     }
309
310     private void writeBase64String(Writer writer, String str) throws IOException {
311         writeString(writer, Utils.b64encode(str));
312     }
313
314     private String readString(Reader reader) throws IOException {
315         return new String(readCharArray(reader));
316     }
317
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;
322
323         if (len > 0) {
324             char[] buffer = new char[len];
325             reader.read(buffer);
326             return buffer;
327         } else {
328             return new char[] {};
329         }
330     }
331
332     private boolean sendKeyData(KeyCode key) {
333         logger.debug("{}: Sending key code {}", host, key.getValue());
334
335         Writer localwriter = writer;
336         Reader localreader = reader;
337         if (localwriter == null || localreader == null) {
338             return false;
339         }
340         /* @formatter:off
341          *
342          * offset value and description
343          * ------ ---------------------
344          * 0x00   always 0x00
345          * 0x01   0x0013 - string length (little endian)
346          * 0x03   "iphone.iapp.samsung" - string content
347          * 0x16   0x0011 - payload size (little endian)
348          * 0x18   payload
349          *
350          * @formatter:on
351          */
352         try {
353             localwriter.append((char) 0x00);
354             writeString(localwriter, APP_STRING);
355             writeString(localwriter, createKeyDataPayload(key));
356             localwriter.flush();
357
358             /*
359              * Read response. Response is pretty useless, because TV seems to
360              * send same response in both ok and error situation.
361              */
362             localreader.skip(1);
363             readString(localreader);
364             readCharArray(localreader);
365         } catch (IOException e) {
366             logResult("Couldn't send command", e);
367             return false;
368         }
369         return true;
370     }
371
372     private String createKeyDataPayload(KeyCode key) throws IOException {
373         /* @formatter:off
374         *
375         * Payload:
376         *
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
382         *
383         * @formatter:on
384         */
385
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());
391         writer.flush();
392         return writer.toString();
393     }
394
395     @Override
396     public void close() throws RemoteControllerException {
397         if (isConnected()) {
398             closeConnection();
399         }
400     }
401 }