2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.androidtv.internal.protocol.shieldtv;
15 import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
16 import static org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConstants.*;
18 import java.io.BufferedReader;
19 import java.io.BufferedWriter;
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;
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;
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;
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;
70 * The {@link ShieldTVConnectionManager} is responsible for handling connections via the shieldtv protocol
72 * Significant portions reused from Lutron binding with permission from Bob A.
74 * @author Ben Rosenblum - Initial contribution
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;
84 private final Logger logger = LoggerFactory.getLogger(ShieldTVConnectionManager.class);
86 private ScheduledExecutorService scheduler;
88 private final AndroidTVHandler handler;
89 private ShieldTVConfiguration config;
91 private @NonNullByDefault({}) SSLSocketFactory sslSocketFactory;
92 private @Nullable SSLSocket sslSocket;
93 private @Nullable BufferedWriter writer;
94 private @Nullable BufferedReader reader;
96 private @NonNullByDefault({}) SSLServerSocketFactory sslServerSocketFactory;
97 private @Nullable Socket shimServerSocket;
98 private @Nullable BufferedWriter shimWriter;
99 private @Nullable BufferedReader shimReader;
101 private @NonNullByDefault({}) ShieldTVMessageParser messageParser;
103 private final BlockingQueue<ShieldTVCommand> sendQueue = new LinkedBlockingQueue<>();
104 private final BlockingQueue<ShieldTVCommand> shimQueue = new LinkedBlockingQueue<>();
106 private @Nullable Future<?> asyncInitializeTask;
107 private @Nullable Future<?> shimAsyncInitializeTask;
109 private @Nullable Thread senderThread;
110 private @Nullable Thread readerThread;
111 private @Nullable Thread shimSenderThread;
112 private @Nullable Thread shimReaderThread;
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;
121 private @Nullable ScheduledFuture<?> deviceHealthJob;
122 private boolean isOnline = true;
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 = "";
131 private boolean disposing = false;
132 private boolean isLoggedIn = false;
133 private String statusMessage = "";
135 private String hostName = "";
136 private String currentApp = "";
137 private String deviceId = "";
138 private String arch = "";
140 private AndroidTVPKI androidtvPKI = new AndroidTVPKI();
141 private byte[] encryptionKey;
143 private boolean appDBPopulated = false;
144 private Map<String, String> appNameDB = new HashMap<>();
145 private Map<String, String> appURLDB = new HashMap<>();
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();
156 public void setHostName(String hostName) {
157 this.hostName = hostName;
158 handler.setThingProperty("deviceName", hostName);
161 public String getHostName() {
165 public String getThingID() {
166 return handler.getThingID();
169 public void setDeviceID(String deviceId) {
170 this.deviceId = deviceId;
171 handler.setThingProperty("deviceID", deviceId);
174 public String getDeviceID() {
178 public void setArch(String arch) {
180 handler.setThingProperty("architectures", arch);
183 public String getArch() {
187 public void setCurrentApp(String currentApp) {
188 this.currentApp = currentApp;
189 handler.updateChannelState(CHANNEL_APP, new StringType(currentApp));
191 if (this.appDBPopulated) {
195 if (appNameDB.get(currentApp) != null) {
196 appName = appNameDB.get(currentApp);
197 handler.updateChannelState(CHANNEL_APPNAME, new StringType(appName));
199 logger.info("Unknown Android App: {}", currentApp);
200 handler.updateChannelState(CHANNEL_APPNAME, new StringType(""));
203 if (appURLDB.get(currentApp) != null) {
204 appURL = appURLDB.get(currentApp);
205 handler.updateChannelState(CHANNEL_APPURL, new StringType(appURL));
207 handler.updateChannelState(CHANNEL_APPURL, new StringType(""));
212 public String getStatusMessage() {
213 return statusMessage;
216 private void setStatus(boolean isLoggedIn) {
218 setStatus(isLoggedIn, "ONLINE");
220 setStatus(isLoggedIn, "UNKNOWN");
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();
232 public String getCurrentApp() {
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
243 public void setLoggedIn(boolean isLoggedIn) {
244 if (!this.isLoggedIn && isLoggedIn) {
245 sendPeriodicUpdate();
248 if (this.isLoggedIn != isLoggedIn) {
249 setStatus(isLoggedIn);
253 public boolean getLoggedIn() {
257 private boolean servicePing() {
260 SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.port);
261 try (Socket socket = new Socket()) {
262 socket.connect(socketAddress, timeout);
264 } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
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
273 private void checkHealth() {
276 isOnline = servicePing();
280 logger.debug("{} - Device Health - Online: {} - Logged In: {}", handler.getThingID(), isOnline, isLoggedIn);
281 if (isOnline != this.isOnline) {
282 this.isOnline = isOnline;
284 logger.debug("{} - Device is back online. Attempting reconnection.", handler.getThingID());
290 public void setKeys(String privKey, String cert) {
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);
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);
312 private TrustManager[] defineNoOpTrustManager() {
313 return new TrustManager[] { new X509TrustManager() {
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());
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());
339 public X509Certificate @Nullable [] getAcceptedIssuers() {
345 private void initialize() {
346 SSLContext sslContext;
348 String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
349 File folder = new File(folderName);
351 if (!folder.exists()) {
352 logger.debug("Creating directory {}", folderName);
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;
363 config.keystoreFileName = (!config.keystoreFileName.equals("")) ? config.keystoreFileName
364 : folderName + "/shieldtv." + ((config.shim) ? "shim." : "") + handler.getThing().getUID().getId()
366 config.keystorePassword = (!config.keystorePassword.equals("")) ? config.keystorePassword
367 : DEFAULT_KEYSTORE_PASSWORD;
369 androidtvPKI.setKeystoreFileName(config.keystoreFileName);
370 androidtvPKI.setAlias("nvidia");
372 deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, config.heartbeat, config.heartbeat,
376 File keystoreFile = new File(config.keystoreFileName);
378 if (!keystoreFile.exists() || config.shimNewKeys) {
379 androidtvPKI.generateNewKeyPair(encryptionKey);
380 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
382 androidtvPKI.loadFromKeyStore(config.keystorePassword, this.encryptionKey);
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());
390 TrustManager[] trustManagers = defineNoOpTrustManager();
392 sslContext = SSLContext.getInstance("TLS");
393 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
395 sslSocketFactory = sslContext.getSocketFactory();
397 asyncInitializeTask = scheduler.submit(this::connect);
399 shimAsyncInitializeTask = scheduler.submit(this::shimInitialize);
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);
413 public void connect() {
414 synchronized (connectionLock) {
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");
429 } catch (IllegalArgumentException e) {
430 // port out of valid range
431 setStatus(false, "Invalid port number");
433 } catch (InterruptedIOException e) {
434 logger.debug("Interrupted while establishing ShieldTV connection");
435 Thread.currentThread().interrupt();
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());
442 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
446 setStatus(false, "Initializing");
448 Thread readerThread = new Thread(this::readerThreadJob, "ShieldTV reader " + handler.getThingID());
449 readerThread.setDaemon(true);
450 readerThread.start();
451 this.readerThread = readerThread;
453 Thread senderThread = new Thread(this::senderThreadJob, "ShieldTV sender " + handler.getThingID());
454 senderThread.setDaemon(true);
455 senderThread.start();
456 this.senderThread = senderThread;
459 this.periodicUpdate = 20;
460 logger.debug("{} - Starting ShieldTV keepalive job with interval {}", handler.getThingID(),
462 keepAliveJob = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, config.heartbeat,
463 config.heartbeat, TimeUnit.SECONDS);
465 String login = ShieldTVRequest.encodeMessage(ShieldTVRequest.loginRequest());
466 sendCommand(new ShieldTVCommand(login));
469 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
474 public void shimInitialize() {
475 synchronized (connectionLock) {
476 AndroidTVPKI shimPKI = new AndroidTVPKI();
477 byte[] shimEncryptionKey = shimPKI.generateEncryptionKey();
478 SSLContext sslContext;
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();
490 logger.debug("{} - Opening ShieldTV shim on port {}", handler.getThingID(), config.port);
491 ServerSocket sslServerSocket = this.sslServerSocketFactory.createServerSocket(config.port);
494 logger.debug("{} - Waiting for shim connection...", handler.getThingID());
495 Socket serverSocket = sslServerSocket.accept();
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());
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());
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;
517 Thread readerThread = new Thread(this::shimReaderThreadJob,
518 "ShieldTV shim reader " + handler.getThingID());
519 readerThread.setDaemon(true);
520 readerThread.start();
521 this.shimReaderThread = readerThread;
523 Thread senderThread = new Thread(this::shimSenderThreadJob,
524 "ShieldTV shim sender" + handler.getThingID());
525 senderThread.setDaemon(true);
526 senderThread.start();
527 this.shimSenderThread = senderThread;
529 } catch (Exception e) {
530 logger.trace("Shim initalization exception", e);
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);
542 * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and
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.
548 private void disconnect(boolean interruptAll) {
549 synchronized (connectionLock) {
550 logger.debug("{} - Disconnecting ShieldTV", handler.getThingID());
552 this.isLoggedIn = false;
554 ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
555 if (connectRetryJob != null) {
556 connectRetryJob.cancel(true);
558 ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
559 if (keepAliveJob != null) {
560 keepAliveJob.cancel(true);
563 reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
565 Thread senderThread = this.senderThread;
566 if (senderThread != null && senderThread.isAlive()) {
567 senderThread.interrupt();
570 Thread readerThread = this.readerThread;
571 if (readerThread != null && readerThread.isAlive()) {
572 readerThread.interrupt();
575 Thread shimSenderThread = this.shimSenderThread;
576 if (shimSenderThread != null && shimSenderThread.isAlive()) {
577 shimSenderThread.interrupt();
580 Thread shimReaderThread = this.shimReaderThread;
581 if (shimReaderThread != null && shimReaderThread.isAlive()) {
582 shimReaderThread.interrupt();
585 SSLSocket sslSocket = this.sslSocket;
586 if (sslSocket != null) {
589 } catch (IOException e) {
590 logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
592 this.sslSocket = null;
594 BufferedReader reader = this.reader;
595 if (reader != null) {
598 } catch (IOException e) {
599 logger.debug("Error closing reader: {}", e.getMessage());
602 BufferedWriter writer = this.writer;
603 if (writer != null) {
606 } catch (IOException e) {
607 logger.debug("Error closing writer: {}", e.getMessage());
611 Socket shimServerSocket = this.shimServerSocket;
612 if (shimServerSocket != null) {
614 shimServerSocket.close();
615 } catch (IOException e) {
616 logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
618 this.shimServerSocket = null;
620 BufferedReader shimReader = this.shimReader;
621 if (shimReader != null) {
624 } catch (IOException e) {
625 logger.debug("Error closing shimReader: {}", e.getMessage());
628 BufferedWriter shimWriter = this.shimWriter;
629 if (shimWriter != null) {
632 } catch (IOException e) {
633 logger.debug("Error closing shimWriter: {}", e.getMessage());
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");
651 * Method executed by the message sender thread (senderThread)
653 private void senderThreadJob() {
654 logger.debug("{} - Command sender thread started", handler.getThingID());
656 while (!Thread.currentThread().isInterrupted() && writer != null) {
657 ShieldTVCommand command = sendQueue.take();
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());
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;
678 break; // reconnect() will start a new thread; terminate this one
680 if (config.delay > 0) {
681 Thread.sleep(config.delay); // introduce delay to throttle send rate
684 } catch (InterruptedException e) {
685 Thread.currentThread().interrupt();
687 logger.debug("{} - Command sender thread exiting", handler.getThingID());
691 private void shimSenderThreadJob() {
692 logger.debug("Shim sender thread started");
694 while (!Thread.currentThread().isInterrupted() && shimWriter != null) {
695 ShieldTVCommand command = shimQueue.take();
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());
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
713 } catch (InterruptedException e) {
714 Thread.currentThread().interrupt();
716 logger.debug("Command sender thread exiting");
720 private void flushReader() {
721 if (!inMessage && (sbReader.length() > 0)) {
722 sbReader.setLength(sbReader.length() - 2);
723 messageParser.handleMessage(sbReader.toString());
725 sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
727 sbReader.setLength(0);
728 sbReader.append(lastMsg);
730 sbReader.append(thisMsg);
734 private void finishReaderMessage() {
735 sbReader.append(thisMsg);
738 messageParser.handleMessage(sbReader.toString());
740 sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
742 sbReader.setLength(0);
745 private String fixMessage(String tempMsg) {
746 if (tempMsg.length() % 2 > 0) {
747 tempMsg = "0" + tempMsg;
753 * Method executed by the message reader thread (readerThread)
755 private void readerThreadJob() {
756 logger.debug("{} - Message reader thread started", handler.getThingID());
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;
768 if (DELIMITER_08.equals(lastMsg) && !inMessage) {
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()));
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();
782 sbReader.append(thisMsg);
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");
796 logger.debug("{} - Message reader thread exiting", handler.getThingID());
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;
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)) {
817 if (!inShimMessage) {
818 // Beginning of payload
819 sbShimReader.setLength(0);
820 sbShimReader.append(thisShimMsg);
821 inShimMessage = true;
823 } else if ((payloadBlock == 1) && (DELIMITER_00.equals(thisShimMsg))) {
824 sbShimReader.append(thisShimMsg);
826 thisShimMsgType = thisShimMsg;
827 while (payloadRemain > 1) {
828 thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
829 sbShimReader.append(thisShimMsg);
835 } else if ((payloadBlock == 1)
836 && (thisShimMsg.startsWith(DELIMITER_F1) || thisShimMsg.startsWith(DELIMITER_F3))) {
837 sbShimReader.append(thisShimMsg);
839 thisShimMsgType = thisShimMsg;
840 while (payloadRemain > 1) {
841 thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
842 sbShimReader.append(thisShimMsg);
848 } else if (payloadBlock == 1) {
849 thisShimMsgType = thisShimMsg;
850 sbShimReader.append(thisShimMsg);
852 } else if (payloadBlock == 2) {
853 sbShimReader.append(thisShimMsg);
855 } else if (payloadBlock == 3) {
856 // Length of remainder of packet
857 payloadRemain = thisShimRawMsg;
858 sbShimReader.append(thisShimMsg);
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;
868 while (payloadRemain > 1) {
869 thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
870 sbShimReader.append(thisShimMsg);
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;
884 sbShimReader.setLength(0);
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");
897 logger.debug("Message reader thread exiting");
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));
906 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08ec0712020806"))); // Get App
907 if (this.periodicUpdate <= 1) {
908 sendPeriodicUpdate();
909 this.periodicUpdate = 20;
914 reconnectTaskSchedule();
918 * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should
920 * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
922 private void reconnectTaskSchedule() {
923 synchronized (keepAliveReconnectLock) {
924 keepAliveReconnectJob = scheduler.schedule(this::keepAliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
930 * Cancels the reconnect task keepAliveReconnectJob.
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;
944 * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
945 * validMessageReceived() which in turn calls reconnectTaskCancel().
947 private void keepAliveTimeoutExpired() {
948 logger.debug("{} - ShieldTV keepalive response timeout expired. Initiating reconnect.", handler.getThingID());
952 public void validMessageReceived() {
953 reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
956 public void sendCommand(ShieldTVCommand command) {
957 if ((!config.shim) && (!command.isEmpty())) {
958 sendQueue.add(command);
962 public void sendShim(ShieldTVCommand command) {
963 if (!command.isEmpty()) {
964 shimQueue.add(command);
968 public void handleCommand(ChannelUID channelUID, Command command) {
969 logger.debug("{} - Command received: {}", handler.getThingID(), channelUID.getId());
971 if (CHANNEL_KEYPRESS.equals(channelUID.getId())) {
972 if (command instanceof StringType) {
973 switch (command.toString()) {
975 sendCommand(new ShieldTVCommand(
976 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
977 sendCommand(new ShieldTVCommand(
978 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
981 sendCommand(new ShieldTVCommand(
982 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
983 sendCommand(new ShieldTVCommand(
984 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
987 sendCommand(new ShieldTVCommand(
988 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
989 sendCommand(new ShieldTVCommand(
990 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
993 sendCommand(new ShieldTVCommand(
994 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
995 sendCommand(new ShieldTVCommand(
996 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
999 sendCommand(new ShieldTVCommand(
1000 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1001 sendCommand(new ShieldTVCommand(
1002 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1005 sendCommand(new ShieldTVCommand(
1006 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1007 sendCommand(new ShieldTVCommand(
1008 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1011 sendCommand(new ShieldTVCommand(
1012 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1013 sendCommand(new ShieldTVCommand(
1014 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1017 sendCommand(new ShieldTVCommand(
1018 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1019 sendCommand(new ShieldTVCommand(
1020 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1022 case "KEY_PLAYPAUSE":
1023 sendCommand(new ShieldTVCommand(
1024 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1025 sendCommand(new ShieldTVCommand(
1026 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1029 sendCommand(new ShieldTVCommand(
1030 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1031 sendCommand(new ShieldTVCommand(
1032 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1035 sendCommand(new ShieldTVCommand(
1036 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1037 sendCommand(new ShieldTVCommand(
1038 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1040 case "KEY_UP_PRESS":
1041 sendCommand(new ShieldTVCommand(
1042 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
1044 case "KEY_DOWN_PRESS":
1045 sendCommand(new ShieldTVCommand(
1046 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
1048 case "KEY_RIGHT_PRESS":
1049 sendCommand(new ShieldTVCommand(
1050 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
1052 case "KEY_LEFT_PRESS":
1053 sendCommand(new ShieldTVCommand(
1054 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
1056 case "KEY_ENTER_PRESS":
1057 sendCommand(new ShieldTVCommand(
1058 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1060 case "KEY_HOME_PRESS":
1061 sendCommand(new ShieldTVCommand(
1062 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1064 case "KEY_BACK_PRESS":
1065 sendCommand(new ShieldTVCommand(
1066 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1068 case "KEY_MENU_PRESS":
1069 sendCommand(new ShieldTVCommand(
1070 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1072 case "KEY_PLAYPAUSE_PRESS":
1073 sendCommand(new ShieldTVCommand(
1074 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1076 case "KEY_REWIND_PRESS":
1077 sendCommand(new ShieldTVCommand(
1078 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1080 case "KEY_FORWARD_PRESS":
1081 sendCommand(new ShieldTVCommand(
1082 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1084 case "KEY_UP_RELEASE":
1085 sendCommand(new ShieldTVCommand(
1086 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
1088 case "KEY_DOWN_RELEASE":
1089 sendCommand(new ShieldTVCommand(
1090 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
1092 case "KEY_RIGHT_RELEASE":
1093 sendCommand(new ShieldTVCommand(
1094 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
1096 case "KEY_LEFT_RELEASE":
1097 sendCommand(new ShieldTVCommand(
1098 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
1100 case "KEY_ENTER_RELEASE":
1101 sendCommand(new ShieldTVCommand(
1102 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1104 case "KEY_HOME_RELEASE":
1105 sendCommand(new ShieldTVCommand(
1106 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1108 case "KEY_BACK_RELEASE":
1109 sendCommand(new ShieldTVCommand(
1110 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1112 case "KEY_MENU_RELEASE":
1113 sendCommand(new ShieldTVCommand(
1114 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1116 case "KEY_PLAYPAUSE_RELEASE":
1117 sendCommand(new ShieldTVCommand(
1118 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1120 case "KEY_REWIND_RELEASE":
1121 sendCommand(new ShieldTVCommand(
1122 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1124 case "KEY_FORWARD_RELEASE":
1125 sendCommand(new ShieldTVCommand(
1126 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1129 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401e")));
1132 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e4010")));
1135 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401f")));
1138 sendCommand(new ShieldTVCommand(
1139 ShieldTVRequest.encodeMessage("08f007120c08031208080110031a020102")));
1142 sendCommand(new ShieldTVCommand(
1143 ShieldTVRequest.encodeMessage("08f007120c08031208080110011a020102")));
1146 sendCommand(new ShieldTVCommand(
1147 ShieldTVRequest.encodeMessage("08f007120c08031208080110021a020102")));
1150 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e9071209081410012001320138")));
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)));
1159 logger.trace("Unknown Keypress: {}", command.toString());
1162 } else if (CHANNEL_PINCODE.equals(channelUID.getId())) {
1163 if (command instanceof StringType) {
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));
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));
1180 sendCommand(new ShieldTVCommand(message));
1181 } else if (command.toString().startsWith("MSG", 9)) {
1182 String newCommand = command.toString().substring(13);
1183 messageParser.handleMessage(newCommand);
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));
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")));
1202 public void dispose() {
1203 this.disposing = true;
1205 Future<?> asyncInitializeTask = this.asyncInitializeTask;
1206 if (asyncInitializeTask != null) {
1207 asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1209 Future<?> shimAsyncInitializeTask = this.shimAsyncInitializeTask;
1210 if (shimAsyncInitializeTask != null) {
1211 shimAsyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1213 ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
1214 if (deviceHealthJob != null) {
1215 deviceHealthJob.cancel(true);