/*******************************************************************************
 * MIT License
 *
 * Copyright (c) 2016, 2017 Anthony Law
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * Contributors:
 *      - Anthony Law (mob41) - Initial API Implementation
 *      - bwssytems
 *      - Christian Fischer (computerlyrik)
 *******************************************************************************/
package com.github.mob41.blapi;

import java.io.Closeable;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.xml.bind.DatatypeConverter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.mob41.blapi.mac.Mac;
import com.github.mob41.blapi.pkt.CmdPacket;
import com.github.mob41.blapi.pkt.CmdPayload;
import com.github.mob41.blapi.pkt.Packet;
import com.github.mob41.blapi.pkt.auth.AES;
import com.github.mob41.blapi.pkt.auth.AuthCmdPayload;
import com.github.mob41.blapi.pkt.dis.DiscoveryPacket;

/**
 * This is the base class of all Broadlink devices (e.g. SP1, RMPro)
 * 
 * @author Anthony
 *
 */
public abstract class BLDevice implements Closeable {

    /**
     * The specific logger for this class
     */
    protected static final Logger log = LoggerFactory.getLogger(BLDevice.class);

    /**
     * Initial key for encryption
     */
    public static final byte[] INITIAL_KEY = { 0x09, 0x76, 0x28, 0x34, 0x3f, (byte) 0xe9, (byte) 0x9e, 0x23, 0x76, 0x5c,
            0x15, 0x13, (byte) 0xac, (byte) 0xcf, (byte) 0x8b, 0x02 }; // 16-byte

    /**
     * Initial iv for encryption
     */
    public static final byte[] INITIAL_IV = { 0x56, 0x2e, 0x17, (byte) 0x99, 0x6d, 0x09, 0x3d, 0x28, (byte) 0xdd,
            (byte) 0xb3, (byte) 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58 }; // 16-short

    public static final int DEFAULT_BYTES_SIZE = 0x38; // 56-bytes

    // Devices type HEX

    public static final short DEV_SP1 = 0x0;

    public static final short DEV_SP2 = 0x2711;

    public static final short DEV_SP2_HONEYWELL_ALT1 = 0x2719;

    public static final short DEV_SP2_HONEYWELL_ALT2 = 0x7919;

    public static final short DEV_SP2_HONEYWELL_ALT3 = 0x271a;

    public static final short DEV_SP2_HONEYWELL_ALT4 = 0x791a;

    public static final short DEV_SPMINI = 0x2720;

    public static final short DEV_SP3 = 0x753e;

    public static final short DEV_SPMINI2 = 0x2728;

    public static final short DEV_SPMINI_OEM_ALT1 = 0x2733;

    public static final short DEV_SPMINI_OEM_ALT2 = 0x273e;

    public static final short DEV_SPMINI_PLUS = 0x2736;

    public static final short DEV_RM_2 = 0x2712;

    public static final short DEV_RM_MINI = 0x2737;

    public static final short DEV_RM_MINI_3 = 0x27c2;

    public static final short DEV_RM_PRO_PHICOMM = 0x273d;

    public static final short DEV_RM_2_HOME_PLUS = 0x2783;

    public static final short DEV_RM_2_2HOME_PLUS_GDT = 0x277c;

    public static final short DEV_RM_2_PRO_PLUS = 0x272a;

    public static final short DEV_RM_2_PRO_PLUS_2 = 0x2787;

    public static final short DEV_RM_2_PRO_PLUS_2_BL = 0x278b;

    public static final short DEV_RM_MINI_SHATE = 0x278f;

    public static final short DEV_A1 = 0x2714;

    public static final short DEV_MP1 = 0x4EB5;
    
    public static final short DEV_HYSEN = 0x4EAD;

    public static final short DEV_FLOUREON = 0xffffffad;

    //
    // Friendly device description
    //
    // Notice: Developers are not recommended to use device description as device identifiers.
    //         Instead, developers are advised to use Device Type Hex numbers.
    
    //Unknown
    
    public static final String DESC_UNKNOWN = "Unknown Device";
    
    //RM Series

    public static final String DESC_RM_2 = "RM 2";

    public static final String DESC_RM_MINI = "RM Mini";

    public static final String DESC_RM_MINI_3 = "RM Mini 3";

    public static final String DESC_RM_PRO_PHICOMM = "RM Pro";

    public static final String DESC_RM_2_HOME_PLUS = "RM 2 Home Plus";

    public static final String DESC_RM_2_2HOME_PLUS_GDT = "RM 2 Home Plus GDT";

    public static final String DESC_RM_2_PRO_PLUS = "RM 2 Pro Plus";

    public static final String DESC_RM_2_PRO_PLUS_2 = "RM 2 Pro Plus 2";

    public static final String DESC_RM_2_PRO_PLUS_2_BL = "RM 2 Pro Plus 2 BL";

    public static final String DESC_RM_MINI_SHATE = "RM Mini SHATE";
    
    //A Series

    public static final String DESC_A1 = "Environmental Sensor";
    
    //MP Series

    public static final String DESC_MP1 = "Power Strip";
    
    //SP Series

    public static final String DESC_SP1 = "Smart Plug V1";

    public static final String DESC_SP2 = "Smart Plug V2";

    public static final String DESC_SP2_HONEYWELL_ALT1 = "Smart Plug Honeywell Alt 1";

    public static final String DESC_SP2_HONEYWELL_ALT2 = "Smart Plug Honeywell Alt 2";

    public static final String DESC_SP2_HONEYWELL_ALT3 = "Smart Plug Honeywell Alt 3";

    public static final String DESC_SP2_HONEYWELL_ALT4 = "Smart Plug Honeywell Alt 4";

    public static final String DESC_SPMINI = "Smart Plug Mini";

    public static final String DESC_SP3 = "Smart Plug V3";

    public static final String DESC_SPMINI2 = "Smart Plug Mini V2";

    public static final String DESC_SPMINI_OEM_ALT1 = "Smart Plug OEM Alt 1";

    public static final String DESC_SPMINI_OEM_ALT2 = "Smart Plug OEM Alt 2";

    public static final String DESC_SPMINI_PLUS = "Smart Plug Mini Plus";

    public static final String DESC_HYSEN = "Hysen Thermostat";

    public static final String DESC_FLOUREON = "Floureon Thermostat";
    /**
     * The destination port for discovery broadcasting (from __init__.py)
     */
    public static final int DISCOVERY_DEST_PORT = 80;

    /**
     * The discovery receive buffer size (from __init__.py)
     */
    public static final int DISCOVERY_RECEIVE_BUFFER_SIZE = 0x40; // 64-bytes

    /**
     * Default discovery timeout (10 seconds)
     */
    public static final int DEFAULT_TIMEOUT = 10000; // 10 seconds (10000 ms)

    /**
     * Packet count that is sent by this instance of BLDevice. This is for
     * {@link #sendCmdPkt(CmdPayload) sendCmdPkt} method.
     */
    private int pktCount;

    /**
     * Encryption key. Initialization value is {@link #INITIAL_KEY INITIAL_KEY}.
     * This is for {@link #sendCmdPkt(CmdPayload) sendCmdPkt} method.
     */
    private byte[] key;

    /**
     * Encryption iv. Initialization value is {@link #INITIAL_IV INITIAL_IV}.
     * This is for {@link #sendCmdPkt(CmdPayload) sendCmdPkt} method.
     */
    private byte[] iv;

    /**
     * Device/Client ID. Initialization value is <code>{0,0,0,0}</code>. And it
     * is changed after the {@link #auth() auth} method, that Broadlink devices
     * will provide a id for this client/device. This is for
     * {@link #sendCmdPkt(CmdPayload) sendCmdPkt} method.
     */
    private byte[] id;

    /**
     * Device type received from discovering devices, or those
     * <code>BLDevice.DEV_*</code> constants
     */
    private final short deviceType;
    
    /**
     * A friendly description of this device
     */
    private final String deviceDesc;

    /**
     * Specific datagram socket for this instance, to reuse address.
     */
    private DatagramSocket sock;

    /**
     * Target device host
     */
    private String host;

    /**
     * Target device MAC, using {@link com.github.mob41.blapi.mac.Mac}
     * implementation to handle MAC addresses
     */
    private Mac mac;
    
    /**
     * AES decryption object
     */
    private AES aes = null;
    
    /**
     * flag to denote this object alreay authorized.
     */
    private boolean alreadyAuthorized;
    
    /**
     * Constructs a <code>BLDevice</code>, with a device type (constants),
     * hostname and MAC address
     * 
     * @param deviceType
     *            Device type constants (<code>BLDevice.DEV_*</code>)
     * @param deviceDesc
     *            Friendly device description
     * @param host
     *            Hostname of target Broadlink device
     * @param mac
     *            MAC address of target Broadlink device
     * @throws IOException
     *             Problems on constructing a datagram socket
     */
    protected BLDevice(short deviceType, String deviceDesc, String host, Mac mac) throws IOException {
        key = INITIAL_KEY;
        iv = INITIAL_IV;
        id = new byte[] { 0, 0, 0, 0 };

        pktCount = new Random().nextInt(0xffff);

        this.deviceType = deviceType;
        this.deviceDesc = deviceDesc;
        
        this.host = host;
        this.mac = mac;

        sock = new DatagramSocket();
        sock.setReuseAddress(true);
        sock.setBroadcast(true);
        aes = new AES(iv, key);
        alreadyAuthorized = false;
    }

    /**
     * Releases the resources of this <code>BLDevice</code>
     */
    @Override
    public void close() {
        sock.close();
    }

    /**
     * Returns the device type of this Broadlink device
     * 
     * @return The device type in <code>short</code>
     */
    public short getDeviceType() {
        return deviceType;
    }

    /**
     * Returns this Broadlink device's hostname / IP address
     * 
     * @return The hostname / IP address in String
     */
    public String getHost() {
        return host;
    }

    /**
     * Returns this Broadlink device's MAC address
     * 
     * @return The MAC address in BLApi's <code>Mac</code> implementation
     */
    public Mac getMac() {
        return mac;
    }

    public AES getAes() {
		return aes;
	}

	/**
     * Returns a friendly description of this BLDevice
     * @return a String
     */
    public String getDeviceDescription() {
        return deviceDesc;
    }

    /**
     * Compatibility with previous code
     * @return Boolean whether this method is success or not
     * @throws IOException If I/O goes wrong
     */
    public boolean auth() throws IOException {
    	return auth(false);
    }

    /**
     * Authenticates with the broadlink device, before any other control
     * commands
     * @param reauth Setting this to true forces to perform re-auth with the device. Defaults not to perform re-auth.
     * @return Boolean whether this method is success or not
     * @throws IOException
     *             If I/O goes wrong
     */
    public boolean auth(boolean reauth) throws IOException {
        log.debug("auth Authentication method starts");
        if(alreadyAuthorized && !reauth) {
        	log.debug("auth Already Authorized.");
        	return true;
        }

        AuthCmdPayload sendPayload = new AuthCmdPayload();
        log.debug("auth Sending CmdPacket with AuthCmdPayload: cmd=" + Integer.toHexString(sendPayload.getCommand())
                    + " len=" + sendPayload.getPayload().getData().length);

        log.debug("auth AuthPayload initial bytes to send: {}", DatatypeConverter.printHexBinary(sendPayload.getPayload().getData()));

        DatagramPacket recvPack = sendCmdPkt(10000, 2048, sendPayload);

        byte[] data = recvPack.getData();
        
        if(data.length <= 0) {
            log.error("auth Received 0 bytes on initial request.");
            alreadyAuthorized = false;
            return false;
        }

        log.debug("auth recv encrypted data bytes (" + data.length +") after initial req: {}", DatatypeConverter.printHexBinary(data));

        byte[] payload = null;
        try {
            log.debug("auth Decrypting encrypted data");

            payload = decryptFromDeviceMessage(data);

            log.debug("auth Decrypted. len=" + payload.length);

        } catch (Exception e) {
            log.error("auth Received datagram decryption error. Aborting method", e);
            alreadyAuthorized = false;
            return false;
        }

        log.debug("auth Packet received payload bytes: " + DatatypeConverter.printHexBinary(payload));

        key = subbytes(payload, 0x04, 0x14);

        log.debug("auth Packet received key bytes: " + DatatypeConverter.printHexBinary(key));

        if (key.length % 16 != 0) {
            log.error("auth Received key len is not a multiple of 16! Aborting");
            alreadyAuthorized = false;
            return false;
        }

        // recreate AES object with new key
        aes = new AES(iv, key);

        id = subbytes(payload, 0x00, 0x04);

        log.debug("auth Packet received id bytes: " + DatatypeConverter.printHexBinary(id) + " with ID len=" + id.length);

        log.debug("auth End of authentication method");
        alreadyAuthorized = true;

        return true;
    }

    /**
     * Sends a command packet from localhost to Broadlink device, with buffer
     * size 1024 bytes, 10 seconds timeout<br>
     * <br>
     * Before any commands to be sent to the device, {@link #auth() auth} must
     * be ran first in order to authenticate with the device and gain a device
     * ID, encryption key and IV.
     * 
     * @param cmdPayload
     *            Command data to be sent
     * @return {@link DatagramPacket} containing the byte data and sender host
     *         information.
     * @throws IOException
     *             Problems when sending the packet
     */
    public DatagramPacket sendCmdPkt(CmdPayload cmdPayload) throws IOException {
        return sendCmdPkt(10000, cmdPayload);
    }

    /**
     * Sends a command packet from localhost to Broadlink device, with default
     * buffer size 1024 bytes<br>
     * <br>
     * Before any commands to be sent to the device, {@link #auth() auth} must
     * be ran first in order to authenticate with the device and gain a device
     * ID, encryption key and IV.
     * 
     * @param timeout
     *            Socket read timeout
     * @param cmdPayload
     *            Command data to be sent
     * @return {@link DatagramPacket} containing the byte data and sender host
     *         information.
     * @throws IOException
     *             Problems when sending the packet
     */
    public DatagramPacket sendCmdPkt(int timeout, CmdPayload cmdPayload) throws IOException {
        return sendCmdPkt(InetAddress.getLocalHost(), 0, timeout, 1024, cmdPayload);
    }

    /**
     * Sends a command packet from localhost to Broadlink device<br>
     * <br>
     * Before any commands to be sent to the device, {@link #auth() auth} must
     * be ran first in order to authenticate with the device and gain a device
     * ID, encryption key and IV.
     * 
     * @param timeout
     *            Socket read timeout
     * @param bufSize
     *            Receive datagram buffer size
     * @param cmdPayload
     *            Command data to be sent
     * @return {@link DatagramPacket} containing the byte data and sender host
     *         information.
     * @throws IOException
     *             Problems when sending the packet
     */
    public DatagramPacket sendCmdPkt(int timeout, int bufSize, CmdPayload cmdPayload) throws IOException {
        return sendCmdPkt(InetAddress.getLocalHost(), 0, timeout, bufSize, cmdPayload);
    }

    /**
     * Binds to a specific IP address and sends a command packet to Broadlink
     * device<br>
     * <br>
     * Before any commands to be sent to the device, {@link #auth() auth} must
     * be ran first in order to authenticate with the device and gain a device
     * ID, encryption key and IV.
     * 
     * @param sourceIpAddr
     *            Bind the socket to this IP address
     * @param sourcePort
     *            Bind the socket to this port
     * @param timeout
     *            Socket read timeout
     * @param bufSize
     *            Receive datagram buffer size
     * @param cmdPayload
     *            Command data to be sent
     * @return {@link DatagramPacket} containing the byte data and sender host
     *         information.
     * @throws IOException
     *             Problems when sending the packet
     */
    public DatagramPacket sendCmdPkt(InetAddress sourceIpAddr, int sourcePort, int timeout, int bufSize,
            CmdPayload cmdPayload) throws IOException {
        CmdPacket cmdPkt = new CmdPacket(mac, pktCount++, id, aes, cmdPayload);
        log.debug("sendCmdPkt - Send Command Packet bytes: {}", DatatypeConverter.printHexBinary(cmdPkt.getData()));
        return sendPkt(sock, cmdPkt, InetAddress.getByName(host), 80, timeout, bufSize);
    }

    /**
     * Creates a Broadlink device client
     * 
     * @param deviceType
     *            Device type constant (<code>BLDevice.DEV_*</code>)
     * @param host
     *            Target Broadlink device hostname
     * @param mac
     *            Target Broadlink device MAC address
     * @return A BLDevice client
     * @throws IOException
     *             Problems when constucting a datagram socket
     */
    public static BLDevice createInstance(short deviceType, String host, Mac mac) throws IOException {
        String desc = BLDevice.getDescOfType(deviceType);
        switch (deviceType) {
        case DEV_SP1:
            return new SP1Device(host, mac);
        case DEV_SP2:
        case DEV_SP2_HONEYWELL_ALT1:
        case DEV_SP2_HONEYWELL_ALT2:
        case DEV_SP2_HONEYWELL_ALT3:
        case DEV_SP2_HONEYWELL_ALT4:
        case DEV_SPMINI:
        case DEV_SP3:
        case DEV_SPMINI2:
        case DEV_SPMINI_OEM_ALT1:
        case DEV_SPMINI_OEM_ALT2:
        case DEV_SPMINI_PLUS:
            return new SP2Device(deviceType, desc, host, mac);
        case DEV_RM_2:
        case DEV_RM_MINI:
        case DEV_RM_MINI_3:
            return new RM2Device(deviceType, desc, host, mac);
        case DEV_RM_PRO_PHICOMM:
        case DEV_RM_2_HOME_PLUS:
        case DEV_RM_2_2HOME_PLUS_GDT:
        case DEV_RM_2_PRO_PLUS:
        case DEV_RM_2_PRO_PLUS_2:
        case DEV_RM_2_PRO_PLUS_2_BL:
        case DEV_RM_MINI_SHATE:
            return new RM2Device(deviceType, desc, host, mac);
        case DEV_A1:
            return new A1Device(host, mac);
        case DEV_MP1:
            return new MP1Device(host, mac);
        case DEV_FLOUREON:
            return new FloureonDevice(host, mac);
        case DEV_HYSEN:
            return new HysenDevice(host, mac);
        }
        return null;
    }

    /**
     * Discover Broadlink devices in the local network, with
     * {@link #DEFAULT_TIMEOUT default timeout}
     * 
     * @return An array of <code>BLDevice</code> in the network
     * @throws IOException
     *             Problems when discovering
     */
    public static BLDevice[] discoverDevices() throws IOException {
        return discoverDevices(DEFAULT_TIMEOUT);
    }

    /**
     * Discover Broadlink devices in the local network
     * 
     * @param timeout
     *            Socket read timeout
     * @return An array of <code>BLDevice</code> in the network
     * @throws IOException
     *             Problems when discovering
     */
    public static BLDevice[] discoverDevices(int timeout) throws IOException {
        return discoverDevices(InetAddress.getLocalHost(), 0, timeout);
    }

    /**
     * Discover Broadlink devices in the network, binded with a specific IP
     * address
     * 
     * @param sourceIpAddr
     *            The IP address to be binded
     * @param sourcePort
     *            The port to be binded
     * @param timeout
     *            Socket read timeout
     * @return An array of <code>BLDevice</code> in the network
     * @throws IOException
     *             Problems when discovering
     */
    public static BLDevice[] discoverDevices(InetAddress sourceIpAddr, int sourcePort, int timeout) throws IOException {
        boolean debug = log.isDebugEnabled();

        if (debug)
            log.debug("Discovering devices");

        List<BLDevice> devices = new ArrayList<BLDevice>(50);

        if (debug)
            log.debug("Constructing DiscoveryPacket");

        DiscoveryPacket dpkt = new DiscoveryPacket(sourceIpAddr, sourcePort);

        DatagramSocket sock = new DatagramSocket(sourcePort, sourceIpAddr);

        sock.setBroadcast(true);
        sock.setReuseAddress(true);

        byte[] sendBytes = dpkt.getData();
        DatagramPacket sendpack = new DatagramPacket(sendBytes, sendBytes.length,
                InetAddress.getByName("255.255.255.255"), DISCOVERY_DEST_PORT);

        if (debug)
            log.debug("Sending broadcast");

        sock.send(sendpack);

        byte[] receBytes = new byte[DISCOVERY_RECEIVE_BUFFER_SIZE];

        DatagramPacket recePacket = new DatagramPacket(receBytes, 0, receBytes.length);
        if (timeout == 0) {
            if (debug)
                log.debug("No timeout was set. Blocking thread until received");
            log.debug("Waiting for datagrams");

            sock.receive(recePacket);

            if (debug)
                log.debug("Received. Closing socket");

            sock.close();

            String host = recePacket.getAddress().getHostAddress();
            Mac mac = new Mac(subbytes(receBytes, 0x3a, 0x40));
            short deviceType = (short) (receBytes[0x34] | receBytes[0x35] << 8);

            if (debug)
                log.debug("Info: host=" + host + " mac=" + mac.getMacString() + " deviceType=0x"
                        + Integer.toHexString(deviceType));
            log.debug("Creating BLDevice instance");

            BLDevice inst = createInstance(deviceType, host, mac);

            if (inst != null) {
                if (debug)
                    log.debug("Adding to found devices list");

                devices.add(inst);
            } else if (debug) {
                log.debug("Cannot create instance, returned null, not adding to found devices list");
            }
        } else {
            if (debug)
                log.debug("A timeout of " + timeout + " ms was set. Running loop");

            long startTime = System.currentTimeMillis();
            long elapsed;
            while ((elapsed = System.currentTimeMillis() - startTime) < timeout) {
                if (debug)
                    log.debug("Elapsed: " + elapsed + " ms");
                log.debug("Socket timeout: timeout-elapsed=" + (timeout - elapsed));

                sock.setSoTimeout((int) (timeout - elapsed));

                try {
                    if (debug)
                        log.debug("Waiting for datagrams");

                    sock.receive(recePacket);
                } catch (SocketTimeoutException e) {
                    if (debug)
                        log.debug("Socket timed out for " + (timeout - elapsed) + " ms", e);

                    break;
                }

                if (debug)
                    log.debug("Received datagram");

                String host = recePacket.getAddress().getHostAddress();
                Mac mac = new Mac(reverseBytes(subbytes(receBytes, 0x3a, 0x40)));
                short deviceType = (short) (receBytes[0x34] | receBytes[0x35] << 8);

                if (debug)
                    log.debug("Info: host=" + host + " mac=" + mac.getMacString() + " deviceType=0x"
                            + Integer.toHexString(deviceType));
                log.debug("Creating BLDevice instance");

                BLDevice inst = createInstance(deviceType, host, mac);

                if (inst != null) {
                    if (debug)
                        log.debug("Adding to found devices list");

                    devices.add(inst);
                } else if (debug) {
                    log.debug("Cannot create instance, returned null, not adding to found devices list");
                }
            }
        }

        if (debug)
            log.debug("Converting list to array: " + devices.size());

        BLDevice[] out = new BLDevice[devices.size()];

        for (int i = 0; i < out.length; i++) {
            out[i] = devices.get(i);
        }

        if (debug)
            log.debug("End of device discovery: " + out.length);
        
        sock.close();

        return out;
    }
    
    public static String getDescOfType(short devType){
        switch (devType) {
        
        //
        // RM Series
        //
        
        case BLDevice.DEV_RM_2:
            return DESC_RM_2;
        case BLDevice.DEV_RM_MINI:
            return DESC_RM_MINI;
        case BLDevice.DEV_RM_MINI_3:
            return DESC_RM_MINI_3;
        case BLDevice.DEV_RM_PRO_PHICOMM:
            return DESC_RM_PRO_PHICOMM;
        case BLDevice.DEV_RM_2_HOME_PLUS:
            return DESC_RM_2_HOME_PLUS;
        case BLDevice.DEV_RM_2_2HOME_PLUS_GDT:
            return DESC_RM_2_2HOME_PLUS_GDT;
        case BLDevice.DEV_RM_2_PRO_PLUS:
            return DESC_RM_2_PRO_PLUS;
        case BLDevice.DEV_RM_2_PRO_PLUS_2:
            return DESC_RM_2_PRO_PLUS_2;
        case BLDevice.DEV_RM_2_PRO_PLUS_2_BL:
            return DESC_RM_2_PRO_PLUS_2_BL;
        case BLDevice.DEV_RM_MINI_SHATE:
            return DESC_RM_MINI_SHATE;
        
        //
        // SP2 Series
        //

        case BLDevice.DEV_SP2:
            return DESC_SP2;
        case BLDevice.DEV_SP2_HONEYWELL_ALT1:
            return DESC_SP2_HONEYWELL_ALT1;
        case BLDevice.DEV_SP2_HONEYWELL_ALT2:
            return DESC_SP2_HONEYWELL_ALT2;
        case BLDevice.DEV_SP2_HONEYWELL_ALT3:
            return DESC_SP2_HONEYWELL_ALT3;
        case BLDevice.DEV_SP2_HONEYWELL_ALT4:
            return DESC_SP2_HONEYWELL_ALT4;
        case BLDevice.DEV_SP3:
            return DESC_SP3;
        case BLDevice.DEV_SPMINI:
            return DESC_SPMINI;
        case BLDevice.DEV_SPMINI2:
            return DESC_SPMINI2;
        case BLDevice.DEV_SPMINI_OEM_ALT1:
            return DESC_SPMINI_OEM_ALT1;
        case BLDevice.DEV_SPMINI_OEM_ALT2:
            return DESC_SPMINI_OEM_ALT2;
        case BLDevice.DEV_SPMINI_PLUS:
            return DESC_SPMINI_PLUS;

        case BLDevice.DEV_SP1:
        	return BLDevice.DESC_SP1;
        case BLDevice.DEV_MP1:
        	return BLDevice.DESC_MP1;
        case BLDevice.DEV_A1:
        	return BLDevice.DESC_A1;
        case BLDevice.DEV_HYSEN:
            return BLDevice.DESC_HYSEN;
        case BLDevice.DEV_FLOUREON:
            return BLDevice.DESC_FLOUREON;
        //
        // Unregonized
        //
        default:
            return DESC_UNKNOWN;
        }
    }

    /**
     * Misc: Reverse the byte array
     * 
     * @param data
     *            Original data
     * @return Result byte array
     */
    public static byte[] reverseBytes(byte[] data) {
        byte[] out = new byte[data.length];

        for (int i = 0; i < out.length; i++) {
            out[i] = data[data.length - 1 - i];
        }

        return out;
    }

    /**
     * Misc: Pull bytes out from end of array until a non null is detected
     * 
     * @param data
     *            Original data
     * @param offset
     *            Starting offset
     * @return Result byte array
     */
    public static byte[] removeNullsFromEnd(byte[] data, int offset) {
    	int new_length = 0;
        for (int i = data.length - 1; i >= offset; i--) {
            if (data[i] != 0x00) { // null
            	new_length = i + 1;
                break;
            }
        }

        byte[] out = new byte[new_length];

        for (int x = offset; x < new_length; x++) {
            out[x - offset] = data[x];
        }

        return out;
    }

    /**
     * Misc: Pull bytes out from an array until a NULL (0) is detected
     * 
     * @param data
     *            Original data
     * @param offset
     *            Starting offset
     * @return Result byte array
     */
    public static byte[] subbytesTillNull(byte[] data, int offset) {
    	int new_length = 0;
        for (int i = offset; i < data.length; i++) {
            if (data[i] == 0x00) { // null
            	new_length = i;
                break;
            }
        }

        byte[] out = new byte[new_length];

        for (int x = offset; x < new_length; x++) {
            out[x - offset] = data[x];
        }

        return out;
    }

    /**
     * Get Payload without header and padded for decryption.
     * 
     * @param data the encrypted data message from the device and includes the header
     * @return Payload bytes without the header and padded to modulo 16
     */
    public byte[] getRawPayloadBytesPadded(byte[] data) {
        byte[] encData = subbytes(data, BLDevice.DEFAULT_BYTES_SIZE, data.length);
        byte[] newBytes = null;
        if(encData.length > 0) {
          int numpad = 16 - (encData.length % 16);

          newBytes = new byte[encData.length+numpad];
          for(int i = 0; i < newBytes.length; i++) {
        	  if(i < encData.length)
        		  newBytes[i] = encData[i];
        	  else
        		  newBytes[i] = 0x00;
          }
        }
        return newBytes;
    }
    
    protected byte[] decryptFromDeviceMessage(byte[] encData) throws Exception {
    	byte[] encPL = getRawPayloadBytesPadded(encData);
        byte[] pl = aes.decrypt(encPL);
        
    	return pl;
    }
    /**
     * Picks bytes from start-set to the end-set in a bytes array
     * 
     * @param data
     *            The bytes array to be used
     * @param start
     *            The starting position to be picked
     * @param end
     *            The ending position to be picked
     * @return The bytes array picked with length (<code>end - start</code>)
     */
    public static byte[] subbytes(byte[] data, int start, int end) {
        byte[] out = new byte[end - start];

        int outi = 0;
        for (int i = start; i < end; i++, outi++) {
            out[outi] = data[i];
        }

        return out;
    }

    /**
     * Sends a compiled packet to a destination host and port, and receives a
     * datagram from the source port specified.
     * 
     * @param pkt
     *            The compiled packet to be sent
     * @param sourceIpAddr
     *            Source IP address to be binded for receiving datagrams
     * @param sourcePort
     *            Source Port to be bineded for receiving datagrams
     * @param destIpAddr
     *            Destination IP address
     * @param destPort
     *            Destination Port
     * @param timeout
     *            Socket timeout. 0 will disable the timeout
     * @param bufSize
     *            Receiving datagram's buffer size
     * @return The received datagram
     * @throws IOException
     *             Thrown if socket timed out, cannot bind source IP and source
     *             port, no permission, etc.
     */
    public static DatagramPacket sendPkt(Packet pkt, InetAddress sourceIpAddr, int sourcePort, InetAddress destIpAddr,
            int destPort, int timeout, int bufSize) throws IOException {
    	log.debug("sendPkt - call with create socket for: " + sourceIpAddr.getHostAddress() + " and port " + sourcePort);
        DatagramSocket sock = new DatagramSocket(sourcePort, sourceIpAddr);

        sock.setBroadcast(true);
        sock.setReuseAddress(true);

        DatagramPacket recePkt = sendPkt(sock, pkt, destIpAddr, destPort, timeout, bufSize);
        sock.close();

        return recePkt;
    }

    /**
     * Sends a compiled packet to a destination host and port, and receives a
     * datagram from the source port specified.
     * 
     * @param sock
     *            Uses an external socket
     * @param pkt
     *            The compiled packet to be sent
     * @param destIpAddr
     *            Destination IP address
     * @param destPort
     *            Destination Port
     * @param timeout
     *            Socket timeout. 0 will disable the timeout
     * @param bufSize
     *            Receiving datagram's buffer size
     * @return The received datagram
     * @throws IOException
     *             Thrown if socket timed out, cannot bind source IP and source
     *             port, no permission, etc.
     */
    public static DatagramPacket sendPkt(DatagramSocket sock, Packet pkt, InetAddress destIpAddr, int destPort, int timeout, int bufSize) throws IOException {
    	
    	String boundHost = null;
    	if(sock.getInetAddress() == null)
    		boundHost = "0.0.0.0";
    	else
    		boundHost = sock.getInetAddress().getHostAddress();
    	log.debug("sendPkt - call with given sock for " + boundHost + " and port " + sock.getPort());

        byte[] data = pkt.getData();
        DatagramPacket sendpack = new DatagramPacket(data, data.length, destIpAddr, destPort);
        log.debug("snedPkt - data for length: " + data.length + " to: " + sendpack.getAddress().getHostAddress() + " for port: " + sendpack.getPort());

        byte[] rece = new byte[bufSize];
        DatagramPacket recepack = new DatagramPacket(rece, 0, rece.length);

        long startTime = System.currentTimeMillis();
        long elapsed;
        while ((elapsed = System.currentTimeMillis() - startTime) < timeout) {
            try {
                sock.send(sendpack);
                sock.setSoTimeout(1000);
                sock.receive(recepack);
                break;
            } catch (SocketTimeoutException e) {
                if (elapsed > timeout) {
                    break;
                }

                continue;
            }
        }

        log.debug("sendPkt - recv data bytes (" + recepack.getData().length +") after initial req: {}", DatatypeConverter.printHexBinary(recepack.getData()));
        recepack.setData(removeNullsFromEnd(recepack.getData(), 0));
        return recepack;
    }

    public static byte[] chgLen(byte[] data, int newLen) {
        byte[] newBytes = new byte[newLen];
        for (int i = 0; i < data.length; i++) {
            newBytes[i] = data[i];
        }
        return newBytes;
    }
}