]> git.basschouten.com Git - openhab-addons.git/blob
c52937c9ac600f4726a9e2c138402c6b399e3fb1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.androidtv.internal.protocol.shieldtv;
14
15 import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
16 import static org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConstants.*;
17
18 import java.io.BufferedReader;
19 import java.io.BufferedWriter;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.InputStreamReader;
23 import java.io.InterruptedIOException;
24 import java.io.OutputStreamWriter;
25 import java.math.BigInteger;
26 import java.net.ConnectException;
27 import java.net.InetSocketAddress;
28 import java.net.NoRouteToHostException;
29 import java.net.ServerSocket;
30 import java.net.Socket;
31 import java.net.SocketAddress;
32 import java.net.SocketTimeoutException;
33 import java.net.UnknownHostException;
34 import java.nio.charset.StandardCharsets;
35 import java.security.GeneralSecurityException;
36 import java.security.NoSuchAlgorithmException;
37 import java.security.UnrecoverableKeyException;
38 import java.security.cert.Certificate;
39 import java.security.cert.X509Certificate;
40 import java.util.HashMap;
41 import java.util.Map;
42 import java.util.concurrent.BlockingQueue;
43 import java.util.concurrent.Future;
44 import java.util.concurrent.LinkedBlockingQueue;
45 import java.util.concurrent.ScheduledExecutorService;
46 import java.util.concurrent.ScheduledFuture;
47 import java.util.concurrent.TimeUnit;
48
49 import javax.net.ssl.KeyManagerFactory;
50 import javax.net.ssl.SSLContext;
51 import javax.net.ssl.SSLServerSocketFactory;
52 import javax.net.ssl.SSLSession;
53 import javax.net.ssl.SSLSocket;
54 import javax.net.ssl.SSLSocketFactory;
55 import javax.net.ssl.TrustManager;
56 import javax.net.ssl.X509TrustManager;
57
58 import org.eclipse.jdt.annotation.NonNullByDefault;
59 import org.eclipse.jdt.annotation.Nullable;
60 import org.openhab.binding.androidtv.internal.AndroidTVHandler;
61 import org.openhab.binding.androidtv.internal.AndroidTVTranslationProvider;
62 import org.openhab.binding.androidtv.internal.utils.AndroidTVPKI;
63 import org.openhab.core.OpenHAB;
64 import org.openhab.core.library.types.StringType;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.types.Command;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 /**
71  * The {@link ShieldTVConnectionManager} is responsible for handling connections via the shieldtv protocol
72  *
73  * Significant portions reused from Lutron binding with permission from Bob A.
74  *
75  * @author Ben Rosenblum - Initial contribution
76  */
77 @NonNullByDefault
78 public class ShieldTVConnectionManager {
79     private static final int DEFAULT_RECONNECT_SECONDS = 60;
80     private static final int DEFAULT_HEARTBEAT_SECONDS = 5;
81     private static final long KEEPALIVE_TIMEOUT_SECONDS = 30;
82     private static final String DEFAULT_KEYSTORE_PASSWORD = "secret";
83     private static final int DEFAULT_PORT = 8987;
84
85     private final Logger logger = LoggerFactory.getLogger(ShieldTVConnectionManager.class);
86
87     private ScheduledExecutorService scheduler;
88
89     private final AndroidTVHandler handler;
90     private ShieldTVConfiguration config;
91     private final AndroidTVTranslationProvider translationProvider;
92
93     private @NonNullByDefault({}) SSLSocketFactory sslSocketFactory;
94     private @Nullable SSLSocket sslSocket;
95     private @Nullable BufferedWriter writer;
96     private @Nullable BufferedReader reader;
97
98     private @NonNullByDefault({}) SSLServerSocketFactory sslServerSocketFactory;
99     private @Nullable Socket shimServerSocket;
100     private @Nullable BufferedWriter shimWriter;
101     private @Nullable BufferedReader shimReader;
102
103     private @NonNullByDefault({}) ShieldTVMessageParser messageParser;
104
105     private final BlockingQueue<ShieldTVCommand> sendQueue = new LinkedBlockingQueue<>();
106     private final BlockingQueue<ShieldTVCommand> shimQueue = new LinkedBlockingQueue<>();
107
108     private @Nullable Future<?> asyncInitializeTask;
109     private @Nullable Future<?> shimAsyncInitializeTask;
110
111     private @Nullable Thread senderThread;
112     private @Nullable Thread readerThread;
113     private @Nullable Thread shimSenderThread;
114     private @Nullable Thread shimReaderThread;
115
116     private @Nullable ScheduledFuture<?> keepAliveJob;
117     private @Nullable ScheduledFuture<?> keepAliveReconnectJob;
118     private @Nullable ScheduledFuture<?> connectRetryJob;
119     private final Object keepAliveReconnectLock = new Object();
120     private final Object connectionLock = new Object();
121     private int periodicUpdate;
122
123     private @Nullable ScheduledFuture<?> deviceHealthJob;
124     private boolean isOnline = true;
125
126     private StringBuffer sbReader = new StringBuffer();
127     private StringBuffer sbShimReader = new StringBuffer();
128     private String lastMsg = "";
129     private String thisMsg = "";
130     private boolean inMessage = false;
131     private String msgType = "";
132
133     private boolean disposing = false;
134     private boolean isLoggedIn = false;
135     private String statusMessage = "";
136
137     private String hostName = "";
138     private String currentApp = "";
139     private String deviceId = "";
140     private String arch = "";
141
142     private AndroidTVPKI androidtvPKI = new AndroidTVPKI();
143     private byte[] encryptionKey;
144
145     private boolean appDBPopulated = false;
146     private Map<String, String> appNameDB = new HashMap<>();
147     private Map<String, String> appURLDB = new HashMap<>();
148
149     public ShieldTVConnectionManager(AndroidTVHandler handler, ShieldTVConfiguration config) {
150         messageParser = new ShieldTVMessageParser(this);
151         this.config = config;
152         this.handler = handler;
153         this.translationProvider = handler.getTranslationProvider();
154         this.scheduler = handler.getScheduler();
155         this.encryptionKey = androidtvPKI.generateEncryptionKey();
156         initialize();
157     }
158
159     public void setHostName(String hostName) {
160         this.hostName = hostName;
161         handler.setThingProperty("deviceName", hostName);
162     }
163
164     public String getHostName() {
165         return hostName;
166     }
167
168     public String getThingID() {
169         return handler.getThingID();
170     }
171
172     public void setDeviceID(String deviceId) {
173         this.deviceId = deviceId;
174         handler.setThingProperty("deviceID", deviceId);
175     }
176
177     public String getDeviceID() {
178         return deviceId;
179     }
180
181     public void setArch(String arch) {
182         this.arch = arch;
183         handler.setThingProperty("architectures", arch);
184     }
185
186     public String getArch() {
187         return arch;
188     }
189
190     public void setCurrentApp(String currentApp) {
191         this.currentApp = currentApp;
192         handler.updateChannelState(CHANNEL_APP, new StringType(currentApp));
193
194         if (this.appDBPopulated) {
195             String appName = "";
196             String appURL = "";
197
198             if (appNameDB.get(currentApp) != null) {
199                 appName = appNameDB.get(currentApp);
200                 handler.updateChannelState(CHANNEL_APPNAME, new StringType(appName));
201             } else {
202                 logger.info("Unknown Android App: {}", currentApp);
203                 handler.updateChannelState(CHANNEL_APPNAME, new StringType(""));
204             }
205
206             if (appURLDB.get(currentApp) != null) {
207                 appURL = appURLDB.get(currentApp);
208                 handler.updateChannelState(CHANNEL_APPURL, new StringType(appURL));
209             } else {
210                 handler.updateChannelState(CHANNEL_APPURL, new StringType(""));
211             }
212         }
213     }
214
215     public String getStatusMessage() {
216         return statusMessage;
217     }
218
219     private void setStatus(boolean isLoggedIn) {
220         if (isLoggedIn) {
221             setStatus(isLoggedIn, "online.online");
222         } else {
223             setStatus(isLoggedIn, "offline.unknown");
224         }
225     }
226
227     private void setStatus(boolean isLoggedIn, String statusMessage) {
228         String translatedMessage = translationProvider.getText(statusMessage);
229         if ((this.isLoggedIn != isLoggedIn) || (!this.statusMessage.equals(translatedMessage))) {
230             this.isLoggedIn = isLoggedIn;
231             this.statusMessage = translatedMessage;
232             handler.checkThingStatus();
233         }
234     }
235
236     public String getCurrentApp() {
237         return currentApp;
238     }
239
240     private void sendPeriodicUpdate() {
241         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("080b120308cd08"))); // Get Hostname
242         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08f30712020805"))); // No Reply
243         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08f10712020800"))); // Get App DB
244         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08ec0712020806"))); // Get App
245     }
246
247     public void setLoggedIn(boolean isLoggedIn) {
248         if (!this.isLoggedIn && isLoggedIn) {
249             sendPeriodicUpdate();
250         }
251
252         if (this.isLoggedIn != isLoggedIn) {
253             setStatus(isLoggedIn);
254         }
255     }
256
257     public boolean getLoggedIn() {
258         return isLoggedIn;
259     }
260
261     private boolean servicePing() {
262         int timeout = 500;
263
264         SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.port);
265         try (Socket socket = new Socket()) {
266             socket.connect(socketAddress, timeout);
267             return true;
268         } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
269             return false;
270         } catch (IOException ignored) {
271             // IOException is thrown by automatic close() of the socket.
272             // This should actually never return a value as we should return true above already
273             return true;
274         }
275     }
276
277     private void checkHealth() {
278         boolean isOnline;
279         if (!isLoggedIn) {
280             isOnline = servicePing();
281         } else {
282             isOnline = true;
283         }
284         logger.debug("{} - Device Health - Online: {} - Logged In: {}", handler.getThingID(), isOnline, isLoggedIn);
285         if (isOnline != this.isOnline) {
286             this.isOnline = isOnline;
287             if (isOnline) {
288                 logger.debug("{} - Device is back online.  Attempting reconnection.", handler.getThingID());
289                 reconnect();
290             }
291         }
292     }
293
294     public void setKeys(String privKey, String cert) {
295         try {
296             androidtvPKI.setKeys(privKey, encryptionKey, cert);
297             androidtvPKI.saveKeyStore(config.keystorePassword, encryptionKey);
298         } catch (GeneralSecurityException e) {
299             logger.debug("General security exception", e);
300         } catch (IOException e) {
301             logger.debug("IO Exception", e);
302         } catch (Exception e) {
303             logger.debug("General Exception", e);
304         }
305     }
306
307     public void setAppDB(Map<String, String> appNameDB, Map<String, String> appURLDB) {
308         this.appNameDB = appNameDB;
309         this.appURLDB = appURLDB;
310         this.appDBPopulated = true;
311         logger.debug("{} - App DB Populated", handler.getThingID());
312         logger.trace("{} - Handler appNameDB: {} appURLDB: {}", handler.getThingID(), this.appNameDB, this.appURLDB);
313         handler.updateCDP(CHANNEL_APP, this.appNameDB);
314     }
315
316     private TrustManager[] defineNoOpTrustManager() {
317         return new TrustManager[] { new X509TrustManager() {
318             @Override
319             public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
320                 logger.debug("Assuming client certificate is valid");
321                 if (chain != null && logger.isTraceEnabled()) {
322                     for (int cert = 0; cert < chain.length; cert++) {
323                         logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
324                         logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
325                         logger.trace("Serial number: {}", chain[cert].getSerialNumber());
326                     }
327                 }
328             }
329
330             @Override
331             public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
332                 logger.debug("Assuming server certificate is valid");
333                 if (chain != null && logger.isTraceEnabled()) {
334                     for (int cert = 0; cert < chain.length; cert++) {
335                         logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
336                         logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
337                         logger.trace("Serial number: {}", chain[cert].getSerialNumber());
338                     }
339                 }
340             }
341
342             @Override
343             public X509Certificate @Nullable [] getAcceptedIssuers() {
344                 return null;
345             }
346         } };
347     }
348
349     private void initialize() {
350         SSLContext sslContext;
351
352         String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
353         File folder = new File(folderName);
354
355         if (!folder.exists()) {
356             logger.debug("Creating directory {}", folderName);
357             folder.mkdirs();
358         }
359
360         config.port = (config.port > 0) ? config.port : DEFAULT_PORT;
361         config.reconnect = (config.reconnect > 0) ? config.reconnect : DEFAULT_RECONNECT_SECONDS;
362         config.heartbeat = (config.heartbeat > 0) ? config.heartbeat : DEFAULT_HEARTBEAT_SECONDS;
363         config.delay = (config.delay < 0) ? 0 : config.delay;
364         config.shim = (config.shim) ? true : false;
365         config.shimNewKeys = (config.shimNewKeys) ? true : false;
366
367         config.keystoreFileName = (!config.keystoreFileName.equals("")) ? config.keystoreFileName
368                 : folderName + "/shieldtv." + ((config.shim) ? "shim." : "") + handler.getThing().getUID().getId()
369                         + ".keystore";
370         config.keystorePassword = (!config.keystorePassword.equals("")) ? config.keystorePassword
371                 : DEFAULT_KEYSTORE_PASSWORD;
372
373         androidtvPKI.setKeystoreFileName(config.keystoreFileName);
374         androidtvPKI.setAlias("nvidia");
375
376         deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, config.heartbeat, config.heartbeat,
377                 TimeUnit.SECONDS);
378
379         try {
380             File keystoreFile = new File(config.keystoreFileName);
381
382             if (!keystoreFile.exists() || config.shimNewKeys) {
383                 androidtvPKI.generateNewKeyPair(encryptionKey);
384                 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
385             } else {
386                 androidtvPKI.loadFromKeyStore(config.keystorePassword, this.encryptionKey);
387             }
388
389             logger.trace("{} - Initializing SSL Context", handler.getThingID());
390             KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
391             kmf.init(androidtvPKI.getKeyStore(config.keystorePassword, this.encryptionKey),
392                     config.keystorePassword.toCharArray());
393
394             TrustManager[] trustManagers = defineNoOpTrustManager();
395
396             sslContext = SSLContext.getInstance("TLS");
397             sslContext.init(kmf.getKeyManagers(), trustManagers, null);
398
399             sslSocketFactory = sslContext.getSocketFactory();
400             if (!config.shim) {
401                 asyncInitializeTask = scheduler.submit(this::connect);
402             } else {
403                 shimAsyncInitializeTask = scheduler.submit(this::shimInitialize);
404             }
405         } catch (NoSuchAlgorithmException | IOException e) {
406             setStatus(false, "offline.error-initalizing-keystore");
407             logger.debug("Error initializing keystore", e);
408         } catch (UnrecoverableKeyException e) {
409             setStatus(false, "offline.key-unrecoverable-with-supplied-password");
410         } catch (GeneralSecurityException e) {
411             logger.debug("General security exception", e);
412         } catch (Exception e) {
413             logger.debug("General exception", e);
414         }
415     }
416
417     public void connect() {
418         synchronized (connectionLock) {
419             if (isOnline) {
420                 try {
421                     logger.debug("{} - Opening ShieldTV SSL connection to {}:{}", handler.getThingID(),
422                             config.ipAddress, config.port);
423                     SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(config.ipAddress, config.port);
424                     sslSocket.startHandshake();
425                     writer = new BufferedWriter(
426                             new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
427                     reader = new BufferedReader(
428                             new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.ISO_8859_1));
429                     this.sslSocket = sslSocket;
430                 } catch (UnknownHostException e) {
431                     setStatus(false, "offline.unknown-host");
432                     return;
433                 } catch (IllegalArgumentException e) {
434                     // port out of valid range
435                     setStatus(false, "offline.invalid-port-number");
436                     return;
437                 } catch (InterruptedIOException e) {
438                     logger.debug("Interrupted while establishing ShieldTV connection");
439                     Thread.currentThread().interrupt();
440                     return;
441                 } catch (IOException e) {
442                     setStatus(false, "offline.error-opening-ssl-connection-check-log");
443                     logger.info("{} - Error opening SSL connection to {}:{} {}", handler.getThingID(), config.ipAddress,
444                             config.port, e.getMessage());
445                     disconnect(false);
446                     scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
447                     return;
448                 }
449
450                 setStatus(false, "offline.initializing");
451
452                 Thread readerThread = new Thread(this::readerThreadJob, "ShieldTV reader " + handler.getThingID());
453                 readerThread.setDaemon(true);
454                 readerThread.start();
455                 this.readerThread = readerThread;
456
457                 Thread senderThread = new Thread(this::senderThreadJob, "ShieldTV sender " + handler.getThingID());
458                 senderThread.setDaemon(true);
459                 senderThread.start();
460                 this.senderThread = senderThread;
461
462                 if (!config.shim) {
463                     this.periodicUpdate = 20;
464                     logger.debug("{} - Starting ShieldTV keepalive job with interval {}", handler.getThingID(),
465                             config.heartbeat);
466                     keepAliveJob = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, config.heartbeat,
467                             config.heartbeat, TimeUnit.SECONDS);
468
469                     String login = ShieldTVRequest.encodeMessage(ShieldTVRequest.loginRequest());
470                     sendCommand(new ShieldTVCommand(login));
471                 }
472             } else {
473                 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
474             }
475         }
476     }
477
478     public void shimInitialize() {
479         synchronized (connectionLock) {
480             AndroidTVPKI shimPKI = new AndroidTVPKI();
481             byte[] shimEncryptionKey = shimPKI.generateEncryptionKey();
482             SSLContext sslContext;
483
484             try {
485                 shimPKI.generateNewKeyPair(shimEncryptionKey);
486                 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
487                 kmf.init(shimPKI.getKeyStore(config.keystorePassword, shimEncryptionKey),
488                         config.keystorePassword.toCharArray());
489                 TrustManager[] trustManagers = defineNoOpTrustManager();
490                 sslContext = SSLContext.getInstance("TLS");
491                 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
492                 this.sslServerSocketFactory = sslContext.getServerSocketFactory();
493
494                 logger.debug("{} - Opening ShieldTV shim on port {}", handler.getThingID(), config.port);
495                 ServerSocket sslServerSocket = this.sslServerSocketFactory.createServerSocket(config.port);
496
497                 while (true) {
498                     logger.debug("{} - Waiting for shim connection...", handler.getThingID());
499                     Socket serverSocket = sslServerSocket.accept();
500                     disconnect(false);
501                     connect();
502                     SSLSession session = ((SSLSocket) serverSocket).getSession();
503                     Certificate[] cchain2 = session.getLocalCertificates();
504                     for (int i = 0; i < cchain2.length; i++) {
505                         logger.trace("Connection from: {}", ((X509Certificate) cchain2[i]).getSubjectX500Principal());
506                     }
507
508                     logger.trace("Peer host is {}", session.getPeerHost());
509                     logger.trace("Cipher is {}", session.getCipherSuite());
510                     logger.trace("Protocol is {}", session.getProtocol());
511                     logger.trace("ID is {}", new BigInteger(session.getId()));
512                     logger.trace("Session created in {}", session.getCreationTime());
513                     logger.trace("Session accessed in {}", session.getLastAccessedTime());
514
515                     shimWriter = new BufferedWriter(
516                             new OutputStreamWriter(serverSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
517                     shimReader = new BufferedReader(
518                             new InputStreamReader(serverSocket.getInputStream(), StandardCharsets.ISO_8859_1));
519                     this.shimServerSocket = serverSocket;
520
521                     Thread readerThread = new Thread(this::shimReaderThreadJob,
522                             "ShieldTV shim reader " + handler.getThingID());
523                     readerThread.setDaemon(true);
524                     readerThread.start();
525                     this.shimReaderThread = readerThread;
526
527                     Thread senderThread = new Thread(this::shimSenderThreadJob,
528                             "ShieldTV shim sender" + handler.getThingID());
529                     senderThread.setDaemon(true);
530                     senderThread.start();
531                     this.shimSenderThread = senderThread;
532                 }
533             } catch (Exception e) {
534                 logger.trace("Shim initalization exception", e);
535                 return;
536             }
537         }
538     }
539
540     private void scheduleConnectRetry(long waitSeconds) {
541         logger.trace("{} - Scheduling ShieldTV connection retry in {} seconds", handler.getThingID(), waitSeconds);
542         connectRetryJob = scheduler.schedule(this::connect, waitSeconds, TimeUnit.SECONDS);
543     }
544
545     /**
546      * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and
547      * clean up.
548      *
549      * @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
550      *            connect or reconnect, and true when calling from dispose.
551      */
552     private void disconnect(boolean interruptAll) {
553         synchronized (connectionLock) {
554             logger.debug("{} - Disconnecting ShieldTV", handler.getThingID());
555
556             this.isLoggedIn = false;
557
558             ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
559             if (connectRetryJob != null) {
560                 connectRetryJob.cancel(true);
561             }
562             ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
563             if (keepAliveJob != null) {
564                 keepAliveJob.cancel(true);
565             }
566
567             reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
568
569             Thread senderThread = this.senderThread;
570             if (senderThread != null && senderThread.isAlive()) {
571                 senderThread.interrupt();
572             }
573
574             Thread readerThread = this.readerThread;
575             if (readerThread != null && readerThread.isAlive()) {
576                 readerThread.interrupt();
577             }
578
579             Thread shimSenderThread = this.shimSenderThread;
580             if (shimSenderThread != null && shimSenderThread.isAlive()) {
581                 shimSenderThread.interrupt();
582             }
583
584             Thread shimReaderThread = this.shimReaderThread;
585             if (shimReaderThread != null && shimReaderThread.isAlive()) {
586                 shimReaderThread.interrupt();
587             }
588
589             SSLSocket sslSocket = this.sslSocket;
590             if (sslSocket != null) {
591                 try {
592                     sslSocket.close();
593                 } catch (IOException e) {
594                     logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
595                 }
596                 this.sslSocket = null;
597             }
598             BufferedReader reader = this.reader;
599             if (reader != null) {
600                 try {
601                     reader.close();
602                 } catch (IOException e) {
603                     logger.debug("Error closing reader: {}", e.getMessage());
604                 }
605             }
606             BufferedWriter writer = this.writer;
607             if (writer != null) {
608                 try {
609                     writer.close();
610                 } catch (IOException e) {
611                     logger.debug("Error closing writer: {}", e.getMessage());
612                 }
613             }
614
615             Socket shimServerSocket = this.shimServerSocket;
616             if (shimServerSocket != null) {
617                 try {
618                     shimServerSocket.close();
619                 } catch (IOException e) {
620                     logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
621                 }
622                 this.shimServerSocket = null;
623             }
624             BufferedReader shimReader = this.shimReader;
625             if (shimReader != null) {
626                 try {
627                     shimReader.close();
628                 } catch (IOException e) {
629                     logger.debug("Error closing shimReader: {}", e.getMessage());
630                 }
631             }
632             BufferedWriter shimWriter = this.shimWriter;
633             if (shimWriter != null) {
634                 try {
635                     shimWriter.close();
636                 } catch (IOException e) {
637                     logger.debug("Error closing shimWriter: {}", e.getMessage());
638                 }
639             }
640         }
641     }
642
643     private void reconnect() {
644         synchronized (connectionLock) {
645             if (!this.disposing) {
646                 logger.debug("{} - Attempting to reconnect to the ShieldTV", handler.getThingID());
647                 setStatus(false, "offline.reconnecting");
648                 disconnect(false);
649                 connect();
650             }
651         }
652     }
653
654     /**
655      * Method executed by the message sender thread (senderThread)
656      */
657     private void senderThreadJob() {
658         logger.debug("{} - Command sender thread started", handler.getThingID());
659         try {
660             while (!Thread.currentThread().isInterrupted() && writer != null) {
661                 ShieldTVCommand command = sendQueue.take();
662
663                 try {
664                     BufferedWriter writer = this.writer;
665                     if (writer != null) {
666                         logger.trace("{} - Raw ShieldTV command decodes as: {}", handler.getThingID(),
667                                 ShieldTVRequest.decodeMessage(command.toString()));
668                         writer.write(command.toString());
669                         writer.flush();
670                     }
671                 } catch (InterruptedIOException e) {
672                     logger.debug("Interrupted while sending to ShieldTV");
673                     setStatus(false, "offline.interrupted");
674                     break; // exit loop and terminate thread
675                 } catch (IOException e) {
676                     logger.warn("{} - Communication error, will try to reconnect ShieldTV. Error: {}",
677                             handler.getThingID(), e.getMessage());
678                     setStatus(false, "offline.communication-error-will-try-to-reconnect");
679                     sendQueue.add(command); // Requeue command
680                     this.isLoggedIn = false;
681                     reconnect();
682                     break; // reconnect() will start a new thread; terminate this one
683                 }
684                 if (config.delay > 0) {
685                     Thread.sleep(config.delay); // introduce delay to throttle send rate
686                 }
687             }
688         } catch (InterruptedException e) {
689             Thread.currentThread().interrupt();
690         } finally {
691             logger.debug("{} - Command sender thread exiting", handler.getThingID());
692         }
693     }
694
695     private void shimSenderThreadJob() {
696         logger.debug("Shim sender thread started");
697         try {
698             while (!Thread.currentThread().isInterrupted() && shimWriter != null) {
699                 ShieldTVCommand command = shimQueue.take();
700
701                 try {
702                     BufferedWriter writer = this.shimWriter;
703                     if (writer != null) {
704                         logger.trace("Shim received from shield: {}",
705                                 ShieldTVRequest.decodeMessage(command.toString()));
706                         writer.write(command.toString());
707                         writer.flush();
708                     }
709                 } catch (InterruptedIOException e) {
710                     logger.debug("Shim interrupted while sending.");
711                     break; // exit loop and terminate thread
712                 } catch (IOException e) {
713                     logger.warn("Shim communication error. Error: {}", e.getMessage());
714                     break; // reconnect() will start a new thread; terminate this one
715                 }
716             }
717         } catch (InterruptedException e) {
718             Thread.currentThread().interrupt();
719         } finally {
720             logger.debug("Command sender thread exiting");
721         }
722     }
723
724     private void flushReader() {
725         if (!inMessage && (sbReader.length() > 0)) {
726             sbReader.setLength(sbReader.length() - 2);
727             messageParser.handleMessage(sbReader.toString());
728             if (config.shim) {
729                 sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
730             }
731             sbReader.setLength(0);
732             sbReader.append(lastMsg);
733         }
734         sbReader.append(thisMsg);
735         lastMsg = thisMsg;
736     }
737
738     private void finishReaderMessage() {
739         sbReader.append(thisMsg);
740         lastMsg = "";
741         inMessage = false;
742         messageParser.handleMessage(sbReader.toString());
743         if (config.shim) {
744             sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
745         }
746         sbReader.setLength(0);
747     }
748
749     private String fixMessage(String tempMsg) {
750         if (tempMsg.length() % 2 > 0) {
751             tempMsg = "0" + tempMsg;
752         }
753         return tempMsg;
754     }
755
756     /**
757      * Method executed by the message reader thread (readerThread)
758      */
759     private void readerThreadJob() {
760         logger.debug("{} - Message reader thread started", handler.getThingID());
761         try {
762             BufferedReader reader = this.reader;
763             while (!Thread.interrupted() && reader != null) {
764                 thisMsg = fixMessage(Integer.toHexString(reader.read()));
765                 if (HARD_DROP.equals(thisMsg)) {
766                     // Shield has crashed the connection. Disconnect hard.
767                     logger.debug("{} - readerThreadJob received ffffffff.  Disconnecting hard.", handler.getThingID());
768                     this.isLoggedIn = false;
769                     reconnect();
770                     break;
771                 }
772                 if (DELIMITER_08.equals(lastMsg) && !inMessage) {
773                     flushReader();
774                     inMessage = true;
775                     msgType = thisMsg;
776                 } else if (DELIMITER_18.equals(lastMsg) && thisMsg.equals(msgType) && inMessage) {
777                     if (!msgType.startsWith(DELIMITER_0)) {
778                         sbReader.append(thisMsg);
779                         thisMsg = fixMessage(Integer.toHexString(reader.read()));
780                     }
781                     finishReaderMessage();
782                 } else if (DELIMITER_00.equals(msgType) && (sbReader.toString().length() == 16)) {
783                     // keepalive messages don't have delimiters but are always 18 in length
784                     finishReaderMessage();
785                 } else {
786                     sbReader.append(thisMsg);
787                     lastMsg = thisMsg;
788                 }
789             }
790         } catch (InterruptedIOException e) {
791             logger.debug("Interrupted while reading");
792             setStatus(false, "offline.interrupted");
793         } catch (IOException e) {
794             logger.debug("I/O error while reading from stream: {}", e.getMessage());
795             setStatus(false, "offline.io-error");
796         } catch (RuntimeException e) {
797             logger.warn("Runtime exception in reader thread", e);
798             setStatus(false, "offline.runtime-exception");
799         } finally {
800             logger.debug("{} - Message reader thread exiting", handler.getThingID());
801         }
802     }
803
804     private void shimReaderThreadJob() {
805         logger.debug("Shim reader thread started");
806         String thisShimMsg = "";
807         int thisShimRawMsg = 0;
808         int payloadRemain = 0;
809         int payloadBlock = 0;
810         String thisShimMsgType = "";
811         boolean inShimMessage = false;
812         try {
813             BufferedReader reader = this.shimReader;
814             while (!Thread.interrupted() && reader != null) {
815                 thisShimRawMsg = reader.read();
816                 thisShimMsg = fixMessage(Integer.toHexString(thisShimRawMsg));
817                 if (HARD_DROP.equals(thisShimMsg)) {
818                     disconnect(false);
819                     break;
820                 }
821                 if (!inShimMessage) {
822                     // Beginning of payload
823                     sbShimReader.setLength(0);
824                     sbShimReader.append(thisShimMsg);
825                     inShimMessage = true;
826                     payloadBlock++;
827                 } else if ((payloadBlock == 1) && (DELIMITER_00.equals(thisShimMsg))) {
828                     sbShimReader.append(thisShimMsg);
829                     payloadRemain = 8;
830                     thisShimMsgType = thisShimMsg;
831                     while (payloadRemain > 1) {
832                         thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
833                         sbShimReader.append(thisShimMsg);
834                         payloadRemain--;
835                         payloadBlock++;
836                     }
837                     payloadRemain--;
838                     payloadBlock++;
839                 } else if ((payloadBlock == 1)
840                         && (thisShimMsg.startsWith(DELIMITER_F1) || thisShimMsg.startsWith(DELIMITER_F3))) {
841                     sbShimReader.append(thisShimMsg);
842                     payloadRemain = 6;
843                     thisShimMsgType = thisShimMsg;
844                     while (payloadRemain > 1) {
845                         thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
846                         sbShimReader.append(thisShimMsg);
847                         payloadRemain--;
848                         payloadBlock++;
849                     }
850                     payloadRemain--;
851                     payloadBlock++;
852                 } else if (payloadBlock == 1) {
853                     thisShimMsgType = thisShimMsg;
854                     sbShimReader.append(thisShimMsg);
855                     payloadBlock++;
856                 } else if (payloadBlock == 2) {
857                     sbShimReader.append(thisShimMsg);
858                     payloadBlock++;
859                 } else if (payloadBlock == 3) {
860                     // Length of remainder of packet
861                     payloadRemain = thisShimRawMsg;
862                     sbShimReader.append(thisShimMsg);
863                     payloadBlock++;
864                 } else if (payloadBlock == 4) {
865                     sbShimReader.append(thisShimMsg);
866                     logger.trace("PB4 SSR {} TSMT {} TSM {} PR {}", sbShimReader.toString(), thisShimMsgType,
867                             thisShimMsg, payloadRemain);
868                     if (DELIMITER_E9.equals(thisShimMsgType) || DELIMITER_F0.equals(thisShimMsgType)
869                             || DELIMITER_EC.equals(thisShimMsgType)) {
870                         payloadRemain = thisShimRawMsg + 1;
871                     }
872                     while (payloadRemain > 1) {
873                         thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
874                         sbShimReader.append(thisShimMsg);
875                         payloadRemain--;
876                         payloadBlock++;
877                     }
878                     payloadRemain--;
879                     payloadBlock++;
880                 }
881
882                 if ((payloadBlock > 5) && (payloadRemain == 0)) {
883                     logger.trace("Shim sending to shield: {}", sbShimReader.toString());
884                     sendQueue.add(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbShimReader.toString())));
885                     inShimMessage = false;
886                     payloadBlock = 0;
887                     payloadRemain = 0;
888                     sbShimReader.setLength(0);
889                 }
890             }
891         } catch (InterruptedIOException e) {
892             logger.debug("Interrupted while reading");
893             setStatus(false, "offline.interrupted");
894         } catch (IOException e) {
895             logger.debug("I/O error while reading from stream: {}", e.getMessage());
896             setStatus(false, "offline.io-error");
897         } catch (RuntimeException e) {
898             logger.warn("Runtime exception in reader thread", e);
899             setStatus(false, "offline.runtime-exception");
900         } finally {
901             logger.debug("Message reader thread exiting");
902         }
903     }
904
905     private void sendKeepAlive() {
906         logger.trace("{} - Sending ShieldTV keepalive query", handler.getThingID());
907         String keepalive = ShieldTVRequest.encodeMessage(ShieldTVRequest.keepAlive());
908         sendCommand(new ShieldTVCommand(keepalive));
909         if (isLoggedIn) {
910             sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08ec0712020806"))); // Get App
911             if (this.periodicUpdate <= 1) {
912                 sendPeriodicUpdate();
913                 this.periodicUpdate = 20;
914             } else {
915                 periodicUpdate--;
916             }
917         }
918         reconnectTaskSchedule();
919     }
920
921     /**
922      * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should
923      * be
924      * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
925      */
926     private void reconnectTaskSchedule() {
927         synchronized (keepAliveReconnectLock) {
928             keepAliveReconnectJob = scheduler.schedule(this::keepAliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
929                     TimeUnit.SECONDS);
930         }
931     }
932
933     /**
934      * Cancels the reconnect task keepAliveReconnectJob.
935      */
936     private void reconnectTaskCancel(boolean interrupt) {
937         synchronized (keepAliveReconnectLock) {
938             ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
939             if (keepAliveReconnectJob != null) {
940                 logger.trace("{} - Canceling ShieldTV scheduled reconnect job.", handler.getThingID());
941                 keepAliveReconnectJob.cancel(interrupt);
942                 this.keepAliveReconnectJob = null;
943             }
944         }
945     }
946
947     /**
948      * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
949      * validMessageReceived() which in turn calls reconnectTaskCancel().
950      */
951     private void keepAliveTimeoutExpired() {
952         logger.debug("{} - ShieldTV keepalive response timeout expired. Initiating reconnect.", handler.getThingID());
953         reconnect();
954     }
955
956     public void validMessageReceived() {
957         reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
958     }
959
960     public void sendCommand(ShieldTVCommand command) {
961         if ((!config.shim) && (!command.isEmpty())) {
962             sendQueue.add(command);
963         }
964     }
965
966     public void sendShim(ShieldTVCommand command) {
967         if (!command.isEmpty()) {
968             shimQueue.add(command);
969         }
970     }
971
972     public void handleCommand(ChannelUID channelUID, Command command) {
973         logger.debug("{} - Command received: {}", handler.getThingID(), channelUID.getId());
974
975         if (CHANNEL_KEYPRESS.equals(channelUID.getId())) {
976             if (command instanceof StringType) {
977                 switch (command.toString()) {
978                     case "KEY_UP":
979                         sendCommand(new ShieldTVCommand(
980                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
981                         sendCommand(new ShieldTVCommand(
982                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
983                         break;
984                     case "KEY_DOWN":
985                         sendCommand(new ShieldTVCommand(
986                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
987                         sendCommand(new ShieldTVCommand(
988                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
989                         break;
990                     case "KEY_RIGHT":
991                         sendCommand(new ShieldTVCommand(
992                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
993                         sendCommand(new ShieldTVCommand(
994                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
995                         break;
996                     case "KEY_LEFT":
997                         sendCommand(new ShieldTVCommand(
998                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
999                         sendCommand(new ShieldTVCommand(
1000                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
1001                         break;
1002                     case "KEY_ENTER":
1003                         sendCommand(new ShieldTVCommand(
1004                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1005                         sendCommand(new ShieldTVCommand(
1006                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1007                         break;
1008                     case "KEY_HOME":
1009                         sendCommand(new ShieldTVCommand(
1010                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1011                         sendCommand(new ShieldTVCommand(
1012                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1013                         break;
1014                     case "KEY_BACK":
1015                         sendCommand(new ShieldTVCommand(
1016                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1017                         sendCommand(new ShieldTVCommand(
1018                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1019                         break;
1020                     case "KEY_MENU":
1021                         sendCommand(new ShieldTVCommand(
1022                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1023                         sendCommand(new ShieldTVCommand(
1024                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1025                         break;
1026                     case "KEY_PLAYPAUSE":
1027                         sendCommand(new ShieldTVCommand(
1028                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1029                         sendCommand(new ShieldTVCommand(
1030                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1031                         break;
1032                     case "KEY_REWIND":
1033                         sendCommand(new ShieldTVCommand(
1034                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1035                         sendCommand(new ShieldTVCommand(
1036                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1037                         break;
1038                     case "KEY_FORWARD":
1039                         sendCommand(new ShieldTVCommand(
1040                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1041                         sendCommand(new ShieldTVCommand(
1042                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1043                         break;
1044                     case "KEY_UP_PRESS":
1045                         sendCommand(new ShieldTVCommand(
1046                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
1047                         break;
1048                     case "KEY_DOWN_PRESS":
1049                         sendCommand(new ShieldTVCommand(
1050                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
1051                         break;
1052                     case "KEY_RIGHT_PRESS":
1053                         sendCommand(new ShieldTVCommand(
1054                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
1055                         break;
1056                     case "KEY_LEFT_PRESS":
1057                         sendCommand(new ShieldTVCommand(
1058                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
1059                         break;
1060                     case "KEY_ENTER_PRESS":
1061                         sendCommand(new ShieldTVCommand(
1062                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1063                         break;
1064                     case "KEY_HOME_PRESS":
1065                         sendCommand(new ShieldTVCommand(
1066                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1067                         break;
1068                     case "KEY_BACK_PRESS":
1069                         sendCommand(new ShieldTVCommand(
1070                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1071                         break;
1072                     case "KEY_MENU_PRESS":
1073                         sendCommand(new ShieldTVCommand(
1074                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1075                         break;
1076                     case "KEY_PLAYPAUSE_PRESS":
1077                         sendCommand(new ShieldTVCommand(
1078                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1079                         break;
1080                     case "KEY_REWIND_PRESS":
1081                         sendCommand(new ShieldTVCommand(
1082                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1083                         break;
1084                     case "KEY_FORWARD_PRESS":
1085                         sendCommand(new ShieldTVCommand(
1086                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1087                         break;
1088                     case "KEY_UP_RELEASE":
1089                         sendCommand(new ShieldTVCommand(
1090                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
1091                         break;
1092                     case "KEY_DOWN_RELEASE":
1093                         sendCommand(new ShieldTVCommand(
1094                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
1095                         break;
1096                     case "KEY_RIGHT_RELEASE":
1097                         sendCommand(new ShieldTVCommand(
1098                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
1099                         break;
1100                     case "KEY_LEFT_RELEASE":
1101                         sendCommand(new ShieldTVCommand(
1102                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
1103                         break;
1104                     case "KEY_ENTER_RELEASE":
1105                         sendCommand(new ShieldTVCommand(
1106                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1107                         break;
1108                     case "KEY_HOME_RELEASE":
1109                         sendCommand(new ShieldTVCommand(
1110                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1111                         break;
1112                     case "KEY_BACK_RELEASE":
1113                         sendCommand(new ShieldTVCommand(
1114                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1115                         break;
1116                     case "KEY_MENU_RELEASE":
1117                         sendCommand(new ShieldTVCommand(
1118                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1119                         break;
1120                     case "KEY_PLAYPAUSE_RELEASE":
1121                         sendCommand(new ShieldTVCommand(
1122                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1123                         break;
1124                     case "KEY_REWIND_RELEASE":
1125                         sendCommand(new ShieldTVCommand(
1126                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1127                         break;
1128                     case "KEY_FORWARD_RELEASE":
1129                         sendCommand(new ShieldTVCommand(
1130                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1131                         break;
1132                     case "KEY_POWER":
1133                         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401e")));
1134                         break;
1135                     case "KEY_POWERON":
1136                         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e4010")));
1137                         break;
1138                     case "KEY_GOOGLE":
1139                         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401f")));
1140                         break;
1141                     case "KEY_VOLUP":
1142                         sendCommand(new ShieldTVCommand(
1143                                 ShieldTVRequest.encodeMessage("08f007120c08031208080110031a020102")));
1144                         break;
1145                     case "KEY_VOLDOWN":
1146                         sendCommand(new ShieldTVCommand(
1147                                 ShieldTVRequest.encodeMessage("08f007120c08031208080110011a020102")));
1148                         break;
1149                     case "KEY_MUTE":
1150                         sendCommand(new ShieldTVCommand(
1151                                 ShieldTVRequest.encodeMessage("08f007120c08031208080110021a020102")));
1152                         break;
1153                     case "KEY_SUBMIT":
1154                         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e9071209081410012001320138")));
1155                         break;
1156                 }
1157                 if (command.toString().length() == 5) {
1158                     // Account for KEY_(ASCII Character)
1159                     String keyPress = "08ec07120708011201"
1160                             + ShieldTVRequest.decodeMessage(new String("" + command.toString().charAt(4))) + "1801";
1161                     sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage(keyPress)));
1162                 } else {
1163                     logger.trace("Unknown Keypress: {}", command.toString());
1164                 }
1165             }
1166         } else if (CHANNEL_PINCODE.equals(channelUID.getId())) {
1167             if (command instanceof StringType) {
1168                 if (!isLoggedIn) {
1169                     // Do PIN for shieldtv protocol
1170                     logger.debug("{} - ShieldTV PIN Process Started", handler.getThingID());
1171                     String pin = ShieldTVRequest.pinRequest(command.toString());
1172                     String message = ShieldTVRequest.encodeMessage(pin);
1173                     sendCommand(new ShieldTVCommand(message));
1174                 }
1175             }
1176         } else if (CHANNEL_DEBUG.equals(channelUID.getId())) {
1177             if (command instanceof StringType) {
1178                 if (command.toString().startsWith("RAW", 9)) {
1179                     String newCommand = command.toString().substring(13);
1180                     String message = ShieldTVRequest.encodeMessage(newCommand);
1181                     if (logger.isTraceEnabled()) {
1182                         logger.trace("Raw Message Decodes as: {}", ShieldTVRequest.decodeMessage(message));
1183                     }
1184                     sendCommand(new ShieldTVCommand(message));
1185                 } else if (command.toString().startsWith("MSG", 9)) {
1186                     String newCommand = command.toString().substring(13);
1187                     messageParser.handleMessage(newCommand);
1188                 }
1189             }
1190         } else if (CHANNEL_APP.equals(channelUID.getId())) {
1191             if (command instanceof StringType) {
1192                 String message = ShieldTVRequest.encodeMessage(ShieldTVRequest.startApp(command.toString()));
1193                 sendCommand(new ShieldTVCommand(message));
1194             }
1195         } else if (CHANNEL_KEYBOARD.equals(channelUID.getId())) {
1196             if (command instanceof StringType) {
1197                 String entry = ShieldTVRequest.keyboardEntry(command.toString());
1198                 logger.trace("Keyboard Entry {}", entry);
1199                 String message = ShieldTVRequest.encodeMessage(entry);
1200                 sendCommand(new ShieldTVCommand(message));
1201                 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e9071209081410012001320138")));
1202             }
1203         }
1204     }
1205
1206     public void dispose() {
1207         this.disposing = true;
1208
1209         Future<?> asyncInitializeTask = this.asyncInitializeTask;
1210         if (asyncInitializeTask != null) {
1211             asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1212         }
1213         Future<?> shimAsyncInitializeTask = this.shimAsyncInitializeTask;
1214         if (shimAsyncInitializeTask != null) {
1215             shimAsyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1216         }
1217         ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
1218         if (deviceHealthJob != null) {
1219             deviceHealthJob.cancel(true);
1220         }
1221         disconnect(true);
1222     }
1223 }