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