org.openhab.binding.samsungtv.internal.protocol.RemoteController.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.samsungtv.internal.protocol.RemoteController.java

Source

/**
 * Copyright (c) 2015 openHAB UG (haftungsbeschraenkt) and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.binding.samsungtv.internal.protocol;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.net.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link RemoteController} is responsible for sending key codes to the
 * Samsung TV.
 * 
 * @see <a
 *      href="http://sc0ty.pl/2012/02/samsung-tv-network-remote-control-protocol/">http://sc0ty.pl/2012/02/samsung-tv-network-remote-control-protocol/</a>
 * 
 * 
 * @author Pauli Anttila - Initial contribution
 */
public class RemoteController {

    private Logger logger = LoggerFactory.getLogger(RemoteController.class);

    // Access granted response
    private final char[] ACCESS_GRANTED_RESP = new char[] { 0x64, 0x00, 0x01, 0x00 };

    // User rejected your network remote controller response
    private final char[] ACCESS_DENIED_RESP = new char[] { 0x64, 0x00, 0x00, 0x00 };

    // waiting for user to grant or deny access response
    private final char[] WAITING_USER_GRANT_RESP = new char[] { 0x0A, 0x00, 0x02, 0x00, 0x00, 0x00 };

    // timeout or cancelled by user response
    private final char[] ACCESS_TIMEOUT_RESP = new char[] { 0x65, 0x00 };

    private final String APP_STRING = "iphone.iapp.samsung";

    private final int TIMEOUT = 5000;

    private String host;
    private int port;
    private String appName;
    private String uniqueId;

    private Socket socket;
    private InputStreamReader reader;
    private BufferedWriter writer;

    /**
     * Create and initialize remote controller instance.
     * 
     * @param host Host name of the Samsung TV.
     * @param port TCP port of the remote controller protocol.
     * @param appName Application name used to send key codes.
     * @param uniqueId Unique Id used to send key codes.
     */
    public RemoteController(String host, int port, String appName, String uniqueId) {
        this.host = host;
        this.port = port;
        this.appName = appName != null ? appName : "";
        this.uniqueId = uniqueId != null ? uniqueId : "";
    }

    /**
     * Open Connection to Samsung TV.
     * 
     * @throws RemoteControllerException
     */
    public void openConnection() throws RemoteControllerException {
        logger.debug("Open connection to host '{}:{}'", host, port);

        socket = new Socket();
        try {
            socket.connect(new InetSocketAddress(host, port), TIMEOUT);
        } catch (Exception e) {
            throw new RemoteControllerException("Connection failed", e);
        }

        logger.debug("Connection successfully opened...quering access");

        try {
            /* @formatter:off
             * 
             * offset value and description
             * ------ ---------------------
             * 0x00   0x00 - datagram type?
             * 0x01   0x0013 - string length (little endian)
             * 0x03   "iphone.iapp.samsung" - string content 
             * 0x16   0x0038 - payload size (little endian) 
             * 0x18   payload
             * 
             * Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
             * encoded with base64 algorithm. Every string is preceded by
             * 2-bytes field containing encoded string length.
             * 
             * These three strings are as follow:
             * 
             * remote control device IP, unique ID  value to distinguish
             * controllers, name  it will be displayed as controller name.
             * 
             * @formatter:on
             */

            writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

            writer.append((char) 0x00);
            writeString(writer, APP_STRING);
            writeString(writer, createRegistrationPayload(socket.getLocalAddress().getHostAddress()));
            writer.flush();

            try {

                /* @formatter:off
                 * 
                 * offset value and description
                 * ------ ---------------------
                 * 0x00   don't know, it it always 0x00 or 0x02
                 * 0x01   0x000c - string length (little endian)
                 * 0x03   "iapp.samsung" - string content
                 * 0x0f   0x0006 - payload size (little endian)
                 * 0x11   payload
                 * 
                 * @formatter:on
                 */

                InputStream in = socket.getInputStream();
                reader = new InputStreamReader(in);

                reader.skip(1);
                readString(reader);
                char[] result = readCharArray(reader);

                if (Arrays.equals(result, ACCESS_GRANTED_RESP)) {
                    logger.debug("Access granted");

                } else if (Arrays.equals(result, ACCESS_DENIED_RESP)) {
                    throw new RemoteControllerException("Access denied");

                } else if (Arrays.equals(result, ACCESS_TIMEOUT_RESP)) {
                    throw new RemoteControllerException("Registration timed out");

                } else if (Arrays.equals(result, WAITING_USER_GRANT_RESP)) {
                    throw new RemoteControllerException("Waiting for user to grant access");

                } else {
                    throw new RemoteControllerException("Unknown response received for access query");
                }

                int i;
                while ((i = in.available()) > 0) {
                    in.skip(i);
                }

            } catch (IOException e) {
                throw new RemoteControllerException(e);
            }
        } catch (IOException e) {
            throw new RemoteControllerException(e);
        }
    }

    /**
     * Close connection to Samsung TV.
     * 
     * @throws RemoteControllerException
     */
    public void closeConnection() throws RemoteControllerException {
        try {
            socket.close();
        } catch (IOException e) {
            throw new RemoteControllerException(e);
        }
    }

    /**
     * Send key code to Samsung TV.
     * 
     * @param key Key code to send.
     * @throws RemoteControllerException
     */
    public void sendKey(KeyCode key) throws RemoteControllerException {
        logger.debug("Try to send command: {}", key);

        if (!isConnected()) {
            openConnection();
        }

        try {
            sendKeyData(key);
        } catch (RemoteControllerException e) {
            logger.debug("Couldn't send command", e);
            logger.debug("Retry one time...");

            closeConnection();
            openConnection();

            sendKeyData(key);
        }

        logger.debug("Command successfully sent");
    }

    /**
     * Send sequence of key codes to Samsung TV.
     * 
     * @param keys List of key codes to send.
     * @throws RemoteControllerException
     */
    public void sendKeys(List<KeyCode> keys) throws RemoteControllerException {
        sendKeys(keys, 300);
    }

    /**
     * Send sequence of key codes to Samsung TV.
     * 
     * @param keys List of key codes to send.
     * @param sleepInMs Sleep between key code sending in milliseconds.
     * @throws RemoteControllerException
     */
    public void sendKeys(List<KeyCode> keys, int sleepInMs) throws RemoteControllerException {
        logger.debug("Try to send sequnce of commands: {}", keys);

        if (!isConnected()) {
            openConnection();
        }

        for (int i = 0; i < keys.size(); i++) {
            KeyCode key = keys.get(i);
            try {
                sendKeyData(key);
            } catch (RemoteControllerException e) {
                logger.debug("Couldn't send command", e);
                logger.debug("Retry one time...");

                closeConnection();
                openConnection();

                sendKeyData(key);
            }

            if ((keys.size() - 1) != i) {
                // Sleep a while between commands
                try {
                    Thread.sleep(sleepInMs);
                } catch (InterruptedException e) {
                    return;
                }
            }
        }

        logger.debug("Command(s) successfully sent");
    }

    private boolean isConnected() {
        if (socket == null || socket.isClosed() || !socket.isConnected()) {
            return false;
        } else {
            return true;
        }
    }

    private String createRegistrationPayload(String ip) throws IOException {
        /*
         * Payload starts with 2 bytes: 0x64 and 0x00, then comes 3 strings
         * encoded with base64 algorithm. Every string is preceded by 2-bytes
         * field containing encoded string length.
         * 
         * These three strings are as follow:
         * 
         * remote control device IP, unique ID  value to distinguish
         * controllers, name  it will be displayed as controller name.
         */

        StringWriter w = new StringWriter();
        w.append((char) 0x64);
        w.append((char) 0x00);
        writeBase64String(w, ip);
        writeBase64String(w, uniqueId);
        writeBase64String(w, appName);
        w.flush();
        return w.toString();
    }

    private void writeString(Writer writer, String str) throws IOException {
        int len = str.length();
        byte low = (byte) (len & 0xFF);
        byte high = (byte) ((len >> 8) & 0xFF);

        writer.append((char) (low));
        writer.append((char) (high));
        writer.append(str);
    }

    private void writeBase64String(Writer writer, String str) throws IOException {
        String tmp = new String(Base64.encodeBase64(str.getBytes()));
        writeString(writer, tmp);
    }

    private String readString(Reader reader) throws IOException {
        char[] buf = readCharArray(reader);
        return new String(buf);
    }

    private char[] readCharArray(Reader reader) throws IOException {
        byte low = (byte) reader.read();
        byte high = (byte) reader.read();
        int len = (high << 8) + low;

        char[] buffer = new char[len];
        reader.read(buffer);
        return buffer;
    }

    private void sendKeyData(KeyCode key) throws RemoteControllerException {
        logger.debug("Sending key code " + key.getValue());

        /* @formatter:off
         * 
         * offset value and description
         * ------ ---------------------
         * 0x00   always 0x00
         * 0x01   0x0013 - string length (little endian)
         * 0x03   "iphone.iapp.samsung" - string content
         * 0x16   0x0011 - payload size (little endian)
         * 0x18   payload
         * 
         * @formatter:on
         */
        try {
            writer.append((char) 0x00);
            writeString(writer, APP_STRING);
            writeString(writer, createKeyDataPayload(key));
            writer.flush();

            /*
             * Read response. Response is pretty useless, because TV seems to
             * send same response in both ok and error situation.
             */
            reader.skip(1);
            readString(reader);
            readCharArray(reader);
        } catch (IOException e) {
            throw new RemoteControllerException(e);
        }
    }

    private String createKeyDataPayload(KeyCode key) throws IOException {
        /* @formatter:off
         * 
         * Payload:
         * 
         * offset value and description
         * ------ ---------------------
         * 0x18   three 0x00 bytes
         * 0x1b   0x000c - key code size (little endian)
         * 0x1d   key code encoded as base64 string
         * 
         * @formatter:on
         */

        StringWriter writer = new StringWriter();
        writer.append((char) 0x00);
        writer.append((char) 0x00);
        writer.append((char) 0x00);
        writeBase64String(writer, key.getValue());
        writer.flush();
        return writer.toString();
    }
}