2 * Copyright (c) 2010-2024 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.AndroidTVTranslationProvider;
62 import org.openhab.binding.androidtv.internal.utils.AndroidTVPKI;
63 import org.openhab.core.OpenHAB;
64 import org.openhab.core.library.types.StringType;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.types.Command;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
71 * The {@link ShieldTVConnectionManager} is responsible for handling connections via the shieldtv protocol
73 * Significant portions reused from Lutron binding with permission from Bob A.
75 * @author Ben Rosenblum - Initial contribution
78 public class ShieldTVConnectionManager {
79 private static final int DEFAULT_RECONNECT_SECONDS = 60;
80 private static final int DEFAULT_HEARTBEAT_SECONDS = 5;
81 private static final long KEEPALIVE_TIMEOUT_SECONDS = 30;
82 private static final String DEFAULT_KEYSTORE_PASSWORD = "secret";
83 private static final int DEFAULT_PORT = 8987;
85 private final Logger logger = LoggerFactory.getLogger(ShieldTVConnectionManager.class);
87 private ScheduledExecutorService scheduler;
89 private final AndroidTVHandler handler;
90 private ShieldTVConfiguration config;
91 private final AndroidTVTranslationProvider translationProvider;
93 private @NonNullByDefault({}) SSLSocketFactory sslSocketFactory;
94 private @Nullable SSLSocket sslSocket;
95 private @Nullable BufferedWriter writer;
96 private @Nullable BufferedReader reader;
98 private @NonNullByDefault({}) SSLServerSocketFactory sslServerSocketFactory;
99 private @Nullable Socket shimServerSocket;
100 private @Nullable BufferedWriter shimWriter;
101 private @Nullable BufferedReader shimReader;
103 private @NonNullByDefault({}) ShieldTVMessageParser messageParser;
105 private final BlockingQueue<ShieldTVCommand> sendQueue = new LinkedBlockingQueue<>();
106 private final BlockingQueue<ShieldTVCommand> shimQueue = new LinkedBlockingQueue<>();
108 private @Nullable Future<?> asyncInitializeTask;
109 private @Nullable Future<?> shimAsyncInitializeTask;
111 private @Nullable Thread senderThread;
112 private @Nullable Thread readerThread;
113 private @Nullable Thread shimSenderThread;
114 private @Nullable Thread shimReaderThread;
116 private @Nullable ScheduledFuture<?> keepAliveJob;
117 private @Nullable ScheduledFuture<?> keepAliveReconnectJob;
118 private @Nullable ScheduledFuture<?> connectRetryJob;
119 private final Object keepAliveReconnectLock = new Object();
120 private final Object connectionLock = new Object();
121 private int periodicUpdate;
123 private @Nullable ScheduledFuture<?> deviceHealthJob;
124 private boolean isOnline = true;
126 private StringBuffer sbReader = new StringBuffer();
127 private StringBuffer sbShimReader = new StringBuffer();
128 private String lastMsg = "";
129 private String thisMsg = "";
130 private boolean inMessage = false;
131 private String msgType = "";
133 private boolean disposing = false;
134 private boolean isLoggedIn = false;
135 private String statusMessage = "";
137 private String hostName = "";
138 private String currentApp = "";
139 private String deviceId = "";
140 private String arch = "";
142 private AndroidTVPKI androidtvPKI = new AndroidTVPKI();
143 private byte[] encryptionKey;
145 private boolean appDBPopulated = false;
146 private Map<String, String> appNameDB = new HashMap<>();
147 private Map<String, String> appURLDB = new HashMap<>();
149 public ShieldTVConnectionManager(AndroidTVHandler handler, ShieldTVConfiguration config) {
150 messageParser = new ShieldTVMessageParser(this);
151 this.config = config;
152 this.handler = handler;
153 this.translationProvider = handler.getTranslationProvider();
154 this.scheduler = handler.getScheduler();
155 this.encryptionKey = androidtvPKI.generateEncryptionKey();
159 public void setHostName(String hostName) {
160 this.hostName = hostName;
161 handler.setThingProperty("deviceName", hostName);
164 public String getHostName() {
168 public String getThingID() {
169 return handler.getThingID();
172 public void setDeviceID(String deviceId) {
173 this.deviceId = deviceId;
174 handler.setThingProperty("deviceID", deviceId);
177 public String getDeviceID() {
181 public void setArch(String arch) {
183 handler.setThingProperty("architectures", arch);
186 public String getArch() {
190 public void setCurrentApp(String currentApp) {
191 this.currentApp = currentApp;
192 handler.updateChannelState(CHANNEL_APP, new StringType(currentApp));
194 if (this.appDBPopulated) {
198 if (appNameDB.get(currentApp) != null) {
199 appName = appNameDB.get(currentApp);
200 handler.updateChannelState(CHANNEL_APPNAME, new StringType(appName));
202 logger.info("Unknown Android App: {}", currentApp);
203 handler.updateChannelState(CHANNEL_APPNAME, new StringType(""));
206 if (appURLDB.get(currentApp) != null) {
207 appURL = appURLDB.get(currentApp);
208 handler.updateChannelState(CHANNEL_APPURL, new StringType(appURL));
210 handler.updateChannelState(CHANNEL_APPURL, new StringType(""));
215 public String getStatusMessage() {
216 return statusMessage;
219 private void setStatus(boolean isLoggedIn) {
221 setStatus(isLoggedIn, "online.online");
223 setStatus(isLoggedIn, "offline.unknown");
227 private void setStatus(boolean isLoggedIn, String statusMessage) {
228 String translatedMessage = translationProvider.getText(statusMessage);
229 if ((this.isLoggedIn != isLoggedIn) || (!this.statusMessage.equals(translatedMessage))) {
230 this.isLoggedIn = isLoggedIn;
231 this.statusMessage = translatedMessage;
232 handler.checkThingStatus();
236 public String getCurrentApp() {
240 private void sendPeriodicUpdate() {
241 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("080b120308cd08"))); // Get Hostname
242 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08f30712020805"))); // No Reply
243 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08f10712020800"))); // Get App DB
244 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08ec0712020806"))); // Get App
247 public void setLoggedIn(boolean isLoggedIn) {
248 if (!this.isLoggedIn && isLoggedIn) {
249 sendPeriodicUpdate();
252 if (this.isLoggedIn != isLoggedIn) {
253 setStatus(isLoggedIn);
257 public boolean getLoggedIn() {
261 private boolean servicePing() {
264 SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.port);
265 try (Socket socket = new Socket()) {
266 socket.connect(socketAddress, timeout);
268 } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
270 } catch (IOException ignored) {
271 // IOException is thrown by automatic close() of the socket.
272 // This should actually never return a value as we should return true above already
277 private void checkHealth() {
280 isOnline = servicePing();
284 logger.debug("{} - Device Health - Online: {} - Logged In: {}", handler.getThingID(), isOnline, isLoggedIn);
285 if (isOnline != this.isOnline) {
286 this.isOnline = isOnline;
288 logger.debug("{} - Device is back online. Attempting reconnection.", handler.getThingID());
294 public void setKeys(String privKey, String cert) {
296 androidtvPKI.setKeys(privKey, encryptionKey, cert);
297 androidtvPKI.saveKeyStore(config.keystorePassword, encryptionKey);
298 } catch (GeneralSecurityException e) {
299 logger.debug("General security exception", e);
300 } catch (IOException e) {
301 logger.debug("IO Exception", e);
302 } catch (Exception e) {
303 logger.debug("General Exception", e);
307 public void setAppDB(Map<String, String> appNameDB, Map<String, String> appURLDB) {
308 this.appNameDB = appNameDB;
309 this.appURLDB = appURLDB;
310 this.appDBPopulated = true;
311 logger.debug("{} - App DB Populated", handler.getThingID());
312 logger.trace("{} - Handler appNameDB: {} appURLDB: {}", handler.getThingID(), this.appNameDB, this.appURLDB);
313 handler.updateCDP(CHANNEL_APP, this.appNameDB);
316 private TrustManager[] defineNoOpTrustManager() {
317 return new TrustManager[] { new X509TrustManager() {
319 public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
320 logger.debug("Assuming client certificate is valid");
321 if (chain != null && logger.isTraceEnabled()) {
322 for (int cert = 0; cert < chain.length; cert++) {
323 logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
324 logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
325 logger.trace("Serial number: {}", chain[cert].getSerialNumber());
331 public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
332 logger.debug("Assuming server certificate is valid");
333 if (chain != null && logger.isTraceEnabled()) {
334 for (int cert = 0; cert < chain.length; cert++) {
335 logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
336 logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
337 logger.trace("Serial number: {}", chain[cert].getSerialNumber());
343 public X509Certificate @Nullable [] getAcceptedIssuers() {
349 private void initialize() {
350 SSLContext sslContext;
352 String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
353 File folder = new File(folderName);
355 if (!folder.exists()) {
356 logger.debug("Creating directory {}", folderName);
360 config.port = (config.port > 0) ? config.port : DEFAULT_PORT;
362 config.keystoreFileName = (!config.keystoreFileName.equals("")) ? config.keystoreFileName
363 : folderName + "/shieldtv." + ((config.shim) ? "shim." : "") + handler.getThing().getUID().getId()
365 config.keystorePassword = (!config.keystorePassword.equals("")) ? config.keystorePassword
366 : DEFAULT_KEYSTORE_PASSWORD;
368 androidtvPKI.setKeystoreFileName(config.keystoreFileName);
369 androidtvPKI.setAlias("nvidia");
371 deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, config.heartbeat, config.heartbeat,
375 File keystoreFile = new File(config.keystoreFileName);
377 if (!keystoreFile.exists() || config.shimNewKeys) {
378 androidtvPKI.generateNewKeyPair(encryptionKey);
379 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
381 androidtvPKI.loadFromKeyStore(config.keystorePassword, this.encryptionKey);
384 logger.trace("{} - Initializing SSL Context", handler.getThingID());
385 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
386 kmf.init(androidtvPKI.getKeyStore(config.keystorePassword, this.encryptionKey),
387 config.keystorePassword.toCharArray());
389 TrustManager[] trustManagers = defineNoOpTrustManager();
391 sslContext = SSLContext.getInstance("TLS");
392 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
394 sslSocketFactory = sslContext.getSocketFactory();
396 asyncInitializeTask = scheduler.submit(this::connect);
398 shimAsyncInitializeTask = scheduler.submit(this::shimInitialize);
400 } catch (NoSuchAlgorithmException | IOException e) {
401 setStatus(false, "offline.error-initalizing-keystore");
402 logger.debug("Error initializing keystore", e);
403 } catch (UnrecoverableKeyException e) {
404 setStatus(false, "offline.key-unrecoverable-with-supplied-password");
405 } catch (GeneralSecurityException e) {
406 logger.debug("General security exception", e);
407 } catch (Exception e) {
408 logger.debug("General exception", e);
412 public void connect() {
413 synchronized (connectionLock) {
416 logger.debug("{} - Opening ShieldTV SSL connection to {}:{}", handler.getThingID(),
417 config.ipAddress, config.port);
418 SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(config.ipAddress, config.port);
419 sslSocket.startHandshake();
420 writer = new BufferedWriter(
421 new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
422 reader = new BufferedReader(
423 new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.ISO_8859_1));
424 this.sslSocket = sslSocket;
425 } catch (UnknownHostException e) {
426 setStatus(false, "offline.unknown-host");
428 } catch (IllegalArgumentException e) {
429 // port out of valid range
430 setStatus(false, "offline.invalid-port-number");
432 } catch (InterruptedIOException e) {
433 logger.debug("Interrupted while establishing ShieldTV connection");
434 Thread.currentThread().interrupt();
436 } catch (IOException e) {
437 setStatus(false, "offline.error-opening-ssl-connection-check-log");
438 logger.info("{} - Error opening SSL connection to {}:{} {}", handler.getThingID(), config.ipAddress,
439 config.port, e.getMessage());
441 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
445 setStatus(false, "offline.initializing");
447 Thread readerThread = new Thread(this::readerThreadJob, "ShieldTV reader " + handler.getThingID());
448 readerThread.setDaemon(true);
449 readerThread.start();
450 this.readerThread = readerThread;
452 Thread senderThread = new Thread(this::senderThreadJob, "ShieldTV sender " + handler.getThingID());
453 senderThread.setDaemon(true);
454 senderThread.start();
455 this.senderThread = senderThread;
458 this.periodicUpdate = 20;
459 logger.debug("{} - Starting ShieldTV keepalive job with interval {}", handler.getThingID(),
461 keepAliveJob = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, config.heartbeat,
462 config.heartbeat, TimeUnit.SECONDS);
464 String login = ShieldTVRequest.encodeMessage(ShieldTVRequest.loginRequest());
465 sendCommand(new ShieldTVCommand(login));
468 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
473 public void shimInitialize() {
474 synchronized (connectionLock) {
475 AndroidTVPKI shimPKI = new AndroidTVPKI();
476 byte[] shimEncryptionKey = shimPKI.generateEncryptionKey();
477 SSLContext sslContext;
480 shimPKI.generateNewKeyPair(shimEncryptionKey);
481 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
482 kmf.init(shimPKI.getKeyStore(config.keystorePassword, shimEncryptionKey),
483 config.keystorePassword.toCharArray());
484 TrustManager[] trustManagers = defineNoOpTrustManager();
485 sslContext = SSLContext.getInstance("TLS");
486 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
487 this.sslServerSocketFactory = sslContext.getServerSocketFactory();
489 logger.debug("{} - Opening ShieldTV shim on port {}", handler.getThingID(), config.port);
490 ServerSocket sslServerSocket = this.sslServerSocketFactory.createServerSocket(config.port);
493 logger.debug("{} - Waiting for shim connection...", handler.getThingID());
494 Socket serverSocket = sslServerSocket.accept();
497 SSLSession session = ((SSLSocket) serverSocket).getSession();
498 Certificate[] cchain2 = session.getLocalCertificates();
499 for (int i = 0; i < cchain2.length; i++) {
500 logger.trace("Connection from: {}", ((X509Certificate) cchain2[i]).getSubjectX500Principal());
503 logger.trace("Peer host is {}", session.getPeerHost());
504 logger.trace("Cipher is {}", session.getCipherSuite());
505 logger.trace("Protocol is {}", session.getProtocol());
506 logger.trace("ID is {}", new BigInteger(session.getId()));
507 logger.trace("Session created in {}", session.getCreationTime());
508 logger.trace("Session accessed in {}", session.getLastAccessedTime());
510 shimWriter = new BufferedWriter(
511 new OutputStreamWriter(serverSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
512 shimReader = new BufferedReader(
513 new InputStreamReader(serverSocket.getInputStream(), StandardCharsets.ISO_8859_1));
514 this.shimServerSocket = serverSocket;
516 Thread readerThread = new Thread(this::shimReaderThreadJob,
517 "ShieldTV shim reader " + handler.getThingID());
518 readerThread.setDaemon(true);
519 readerThread.start();
520 this.shimReaderThread = readerThread;
522 Thread senderThread = new Thread(this::shimSenderThreadJob,
523 "ShieldTV shim sender" + handler.getThingID());
524 senderThread.setDaemon(true);
525 senderThread.start();
526 this.shimSenderThread = senderThread;
528 } catch (Exception e) {
529 logger.trace("Shim initalization exception", e);
535 private void scheduleConnectRetry(long waitSeconds) {
536 logger.trace("{} - Scheduling ShieldTV connection retry in {} seconds", handler.getThingID(), waitSeconds);
537 connectRetryJob = scheduler.schedule(this::connect, waitSeconds, TimeUnit.SECONDS);
541 * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and
544 * @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
545 * connect or reconnect, and true when calling from dispose.
547 private void disconnect(boolean interruptAll) {
548 synchronized (connectionLock) {
549 logger.debug("{} - Disconnecting ShieldTV", handler.getThingID());
551 this.isLoggedIn = false;
553 ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
554 if (connectRetryJob != null) {
555 connectRetryJob.cancel(true);
557 ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
558 if (keepAliveJob != null) {
559 keepAliveJob.cancel(true);
562 reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
564 Thread senderThread = this.senderThread;
565 if (senderThread != null && senderThread.isAlive()) {
566 senderThread.interrupt();
569 Thread readerThread = this.readerThread;
570 if (readerThread != null && readerThread.isAlive()) {
571 readerThread.interrupt();
574 Thread shimSenderThread = this.shimSenderThread;
575 if (shimSenderThread != null && shimSenderThread.isAlive()) {
576 shimSenderThread.interrupt();
579 Thread shimReaderThread = this.shimReaderThread;
580 if (shimReaderThread != null && shimReaderThread.isAlive()) {
581 shimReaderThread.interrupt();
584 SSLSocket sslSocket = this.sslSocket;
585 if (sslSocket != null) {
588 } catch (IOException e) {
589 logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
591 this.sslSocket = null;
593 BufferedReader reader = this.reader;
594 if (reader != null) {
597 } catch (IOException e) {
598 logger.debug("Error closing reader: {}", e.getMessage());
601 BufferedWriter writer = this.writer;
602 if (writer != null) {
605 } catch (IOException e) {
606 logger.debug("Error closing writer: {}", e.getMessage());
610 Socket shimServerSocket = this.shimServerSocket;
611 if (shimServerSocket != null) {
613 shimServerSocket.close();
614 } catch (IOException e) {
615 logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
617 this.shimServerSocket = null;
619 BufferedReader shimReader = this.shimReader;
620 if (shimReader != null) {
623 } catch (IOException e) {
624 logger.debug("Error closing shimReader: {}", e.getMessage());
627 BufferedWriter shimWriter = this.shimWriter;
628 if (shimWriter != null) {
631 } catch (IOException e) {
632 logger.debug("Error closing shimWriter: {}", e.getMessage());
638 private void reconnect() {
639 synchronized (connectionLock) {
640 if (!this.disposing) {
641 logger.debug("{} - Attempting to reconnect to the ShieldTV", handler.getThingID());
642 setStatus(false, "offline.reconnecting");
650 * Method executed by the message sender thread (senderThread)
652 private void senderThreadJob() {
653 logger.debug("{} - Command sender thread started", handler.getThingID());
655 while (!Thread.currentThread().isInterrupted() && writer != null) {
656 ShieldTVCommand command = sendQueue.take();
659 BufferedWriter writer = this.writer;
660 if (writer != null) {
661 logger.trace("{} - Raw ShieldTV command decodes as: {}", handler.getThingID(),
662 ShieldTVRequest.decodeMessage(command.toString()));
663 writer.write(command.toString());
666 } catch (InterruptedIOException e) {
667 logger.debug("Interrupted while sending to ShieldTV");
668 setStatus(false, "offline.interrupted");
669 break; // exit loop and terminate thread
670 } catch (IOException e) {
671 logger.warn("{} - Communication error, will try to reconnect ShieldTV. Error: {}",
672 handler.getThingID(), e.getMessage());
673 setStatus(false, "offline.communication-error-will-try-to-reconnect");
674 sendQueue.add(command); // Requeue command
675 this.isLoggedIn = false;
677 break; // reconnect() will start a new thread; terminate this one
679 if (config.delay > 0) {
680 Thread.sleep(config.delay); // introduce delay to throttle send rate
683 } catch (InterruptedException e) {
684 Thread.currentThread().interrupt();
686 logger.debug("{} - Command sender thread exiting", handler.getThingID());
690 private void shimSenderThreadJob() {
691 logger.debug("Shim sender thread started");
693 while (!Thread.currentThread().isInterrupted() && shimWriter != null) {
694 ShieldTVCommand command = shimQueue.take();
697 BufferedWriter writer = this.shimWriter;
698 if (writer != null) {
699 logger.trace("Shim received from shield: {}",
700 ShieldTVRequest.decodeMessage(command.toString()));
701 writer.write(command.toString());
704 } catch (InterruptedIOException e) {
705 logger.debug("Shim interrupted while sending.");
706 break; // exit loop and terminate thread
707 } catch (IOException e) {
708 logger.warn("Shim communication error. Error: {}", e.getMessage());
709 break; // reconnect() will start a new thread; terminate this one
712 } catch (InterruptedException e) {
713 Thread.currentThread().interrupt();
715 logger.debug("Command sender thread exiting");
719 private void flushReader() {
720 if (!inMessage && (sbReader.length() > 0)) {
721 sbReader.setLength(sbReader.length() - 2);
722 messageParser.handleMessage(sbReader.toString());
724 sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
726 sbReader.setLength(0);
727 sbReader.append(lastMsg);
729 sbReader.append(thisMsg);
733 private void finishReaderMessage() {
734 sbReader.append(thisMsg);
737 messageParser.handleMessage(sbReader.toString());
739 sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
741 sbReader.setLength(0);
744 private String fixMessage(String tempMsg) {
745 if (tempMsg.length() % 2 > 0) {
746 tempMsg = "0" + tempMsg;
752 * Method executed by the message reader thread (readerThread)
754 private void readerThreadJob() {
755 logger.debug("{} - Message reader thread started", handler.getThingID());
757 BufferedReader reader = this.reader;
758 while (!Thread.interrupted() && reader != null) {
759 thisMsg = fixMessage(Integer.toHexString(reader.read()));
760 if (HARD_DROP.equals(thisMsg)) {
761 // Shield has crashed the connection. Disconnect hard.
762 logger.debug("{} - readerThreadJob received ffffffff. Disconnecting hard.", handler.getThingID());
763 this.isLoggedIn = false;
767 if (DELIMITER_08.equals(lastMsg) && !inMessage) {
771 } else if (DELIMITER_18.equals(lastMsg) && thisMsg.equals(msgType) && inMessage) {
772 if (!msgType.startsWith(DELIMITER_0)) {
773 sbReader.append(thisMsg);
774 thisMsg = fixMessage(Integer.toHexString(reader.read()));
776 finishReaderMessage();
777 } else if (DELIMITER_00.equals(msgType) && (sbReader.toString().length() == 16)) {
778 // keepalive messages don't have delimiters but are always 18 in length
779 finishReaderMessage();
781 sbReader.append(thisMsg);
785 } catch (InterruptedIOException e) {
786 logger.debug("Interrupted while reading");
787 setStatus(false, "offline.interrupted");
788 } catch (IOException e) {
789 logger.debug("I/O error while reading from stream: {}", e.getMessage());
790 setStatus(false, "offline.io-error");
791 } catch (RuntimeException e) {
792 logger.warn("Runtime exception in reader thread", e);
793 setStatus(false, "offline.runtime-exception");
795 logger.debug("{} - Message reader thread exiting", handler.getThingID());
799 private void shimReaderThreadJob() {
800 logger.debug("Shim reader thread started");
801 String thisShimMsg = "";
802 int thisShimRawMsg = 0;
803 int payloadRemain = 0;
804 int payloadBlock = 0;
805 String thisShimMsgType = "";
806 boolean inShimMessage = false;
808 BufferedReader reader = this.shimReader;
809 while (!Thread.interrupted() && reader != null) {
810 thisShimRawMsg = reader.read();
811 thisShimMsg = fixMessage(Integer.toHexString(thisShimRawMsg));
812 if (HARD_DROP.equals(thisShimMsg)) {
816 if (!inShimMessage) {
817 // Beginning of payload
818 sbShimReader.setLength(0);
819 sbShimReader.append(thisShimMsg);
820 inShimMessage = true;
822 } else if ((payloadBlock == 1) && (DELIMITER_00.equals(thisShimMsg))) {
823 sbShimReader.append(thisShimMsg);
825 thisShimMsgType = thisShimMsg;
826 while (payloadRemain > 1) {
827 thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
828 sbShimReader.append(thisShimMsg);
834 } else if ((payloadBlock == 1)
835 && (thisShimMsg.startsWith(DELIMITER_F1) || thisShimMsg.startsWith(DELIMITER_F3))) {
836 sbShimReader.append(thisShimMsg);
838 thisShimMsgType = thisShimMsg;
839 while (payloadRemain > 1) {
840 thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
841 sbShimReader.append(thisShimMsg);
847 } else if (payloadBlock == 1) {
848 thisShimMsgType = thisShimMsg;
849 sbShimReader.append(thisShimMsg);
851 } else if (payloadBlock == 2) {
852 sbShimReader.append(thisShimMsg);
854 } else if (payloadBlock == 3) {
855 // Length of remainder of packet
856 payloadRemain = thisShimRawMsg;
857 sbShimReader.append(thisShimMsg);
859 } else if (payloadBlock == 4) {
860 sbShimReader.append(thisShimMsg);
861 logger.trace("PB4 SSR {} TSMT {} TSM {} PR {}", sbShimReader.toString(), thisShimMsgType,
862 thisShimMsg, payloadRemain);
863 if (DELIMITER_E9.equals(thisShimMsgType) || DELIMITER_F0.equals(thisShimMsgType)
864 || DELIMITER_EC.equals(thisShimMsgType)) {
865 payloadRemain = thisShimRawMsg + 1;
867 while (payloadRemain > 1) {
868 thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
869 sbShimReader.append(thisShimMsg);
877 if ((payloadBlock > 5) && (payloadRemain == 0)) {
878 logger.trace("Shim sending to shield: {}", sbShimReader.toString());
879 sendQueue.add(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbShimReader.toString())));
880 inShimMessage = false;
883 sbShimReader.setLength(0);
886 } catch (InterruptedIOException e) {
887 logger.debug("Interrupted while reading");
888 setStatus(false, "offline.interrupted");
889 } catch (IOException e) {
890 logger.debug("I/O error while reading from stream: {}", e.getMessage());
891 setStatus(false, "offline.io-error");
892 } catch (RuntimeException e) {
893 logger.warn("Runtime exception in reader thread", e);
894 setStatus(false, "offline.runtime-exception");
896 logger.debug("Message reader thread exiting");
900 private void sendKeepAlive() {
901 logger.trace("{} - Sending ShieldTV keepalive query", handler.getThingID());
902 String keepalive = ShieldTVRequest.encodeMessage(ShieldTVRequest.keepAlive());
903 sendCommand(new ShieldTVCommand(keepalive));
905 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08ec0712020806"))); // Get App
906 if (this.periodicUpdate <= 1) {
907 sendPeriodicUpdate();
908 this.periodicUpdate = 20;
913 reconnectTaskSchedule();
917 * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should
919 * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
921 private void reconnectTaskSchedule() {
922 synchronized (keepAliveReconnectLock) {
923 keepAliveReconnectJob = scheduler.schedule(this::keepAliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
929 * Cancels the reconnect task keepAliveReconnectJob.
931 private void reconnectTaskCancel(boolean interrupt) {
932 synchronized (keepAliveReconnectLock) {
933 ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
934 if (keepAliveReconnectJob != null) {
935 logger.trace("{} - Canceling ShieldTV scheduled reconnect job.", handler.getThingID());
936 keepAliveReconnectJob.cancel(interrupt);
937 this.keepAliveReconnectJob = null;
943 * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
944 * validMessageReceived() which in turn calls reconnectTaskCancel().
946 private void keepAliveTimeoutExpired() {
947 logger.debug("{} - ShieldTV keepalive response timeout expired. Initiating reconnect.", handler.getThingID());
951 public void validMessageReceived() {
952 reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
955 public void sendCommand(ShieldTVCommand command) {
956 if ((!config.shim) && (!command.isEmpty())) {
957 sendQueue.add(command);
961 public void sendShim(ShieldTVCommand command) {
962 if (!command.isEmpty()) {
963 shimQueue.add(command);
967 public void handleCommand(ChannelUID channelUID, Command command) {
968 logger.debug("{} - Command received: {}", handler.getThingID(), channelUID.getId());
970 if (CHANNEL_KEYPRESS.equals(channelUID.getId())) {
971 if (command instanceof StringType) {
972 switch (command.toString()) {
974 sendCommand(new ShieldTVCommand(
975 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
976 sendCommand(new ShieldTVCommand(
977 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
980 sendCommand(new ShieldTVCommand(
981 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
982 sendCommand(new ShieldTVCommand(
983 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
986 sendCommand(new ShieldTVCommand(
987 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
988 sendCommand(new ShieldTVCommand(
989 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
992 sendCommand(new ShieldTVCommand(
993 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
994 sendCommand(new ShieldTVCommand(
995 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
998 sendCommand(new ShieldTVCommand(
999 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1000 sendCommand(new ShieldTVCommand(
1001 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1004 sendCommand(new ShieldTVCommand(
1005 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1006 sendCommand(new ShieldTVCommand(
1007 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1010 sendCommand(new ShieldTVCommand(
1011 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1012 sendCommand(new ShieldTVCommand(
1013 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1016 sendCommand(new ShieldTVCommand(
1017 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1018 sendCommand(new ShieldTVCommand(
1019 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1021 case "KEY_PLAYPAUSE":
1022 sendCommand(new ShieldTVCommand(
1023 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1024 sendCommand(new ShieldTVCommand(
1025 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1028 sendCommand(new ShieldTVCommand(
1029 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1030 sendCommand(new ShieldTVCommand(
1031 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1034 sendCommand(new ShieldTVCommand(
1035 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1036 sendCommand(new ShieldTVCommand(
1037 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1039 case "KEY_UP_PRESS":
1040 sendCommand(new ShieldTVCommand(
1041 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
1043 case "KEY_DOWN_PRESS":
1044 sendCommand(new ShieldTVCommand(
1045 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
1047 case "KEY_RIGHT_PRESS":
1048 sendCommand(new ShieldTVCommand(
1049 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
1051 case "KEY_LEFT_PRESS":
1052 sendCommand(new ShieldTVCommand(
1053 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
1055 case "KEY_ENTER_PRESS":
1056 sendCommand(new ShieldTVCommand(
1057 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1059 case "KEY_HOME_PRESS":
1060 sendCommand(new ShieldTVCommand(
1061 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1063 case "KEY_BACK_PRESS":
1064 sendCommand(new ShieldTVCommand(
1065 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1067 case "KEY_MENU_PRESS":
1068 sendCommand(new ShieldTVCommand(
1069 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1071 case "KEY_PLAYPAUSE_PRESS":
1072 sendCommand(new ShieldTVCommand(
1073 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1075 case "KEY_REWIND_PRESS":
1076 sendCommand(new ShieldTVCommand(
1077 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1079 case "KEY_FORWARD_PRESS":
1080 sendCommand(new ShieldTVCommand(
1081 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1083 case "KEY_UP_RELEASE":
1084 sendCommand(new ShieldTVCommand(
1085 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
1087 case "KEY_DOWN_RELEASE":
1088 sendCommand(new ShieldTVCommand(
1089 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
1091 case "KEY_RIGHT_RELEASE":
1092 sendCommand(new ShieldTVCommand(
1093 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
1095 case "KEY_LEFT_RELEASE":
1096 sendCommand(new ShieldTVCommand(
1097 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
1099 case "KEY_ENTER_RELEASE":
1100 sendCommand(new ShieldTVCommand(
1101 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1103 case "KEY_HOME_RELEASE":
1104 sendCommand(new ShieldTVCommand(
1105 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1107 case "KEY_BACK_RELEASE":
1108 sendCommand(new ShieldTVCommand(
1109 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1111 case "KEY_MENU_RELEASE":
1112 sendCommand(new ShieldTVCommand(
1113 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1115 case "KEY_PLAYPAUSE_RELEASE":
1116 sendCommand(new ShieldTVCommand(
1117 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1119 case "KEY_REWIND_RELEASE":
1120 sendCommand(new ShieldTVCommand(
1121 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1123 case "KEY_FORWARD_RELEASE":
1124 sendCommand(new ShieldTVCommand(
1125 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1128 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401e")));
1131 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e4010")));
1134 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401f")));
1137 sendCommand(new ShieldTVCommand(
1138 ShieldTVRequest.encodeMessage("08f007120c08031208080110031a020102")));
1141 sendCommand(new ShieldTVCommand(
1142 ShieldTVRequest.encodeMessage("08f007120c08031208080110011a020102")));
1145 sendCommand(new ShieldTVCommand(
1146 ShieldTVRequest.encodeMessage("08f007120c08031208080110021a020102")));
1149 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e9071209081410012001320138")));
1152 if (command.toString().length() == 5) {
1153 // Account for KEY_(ASCII Character)
1154 String keyPress = "08ec07120708011201"
1155 + ShieldTVRequest.decodeMessage(new String("" + command.toString().charAt(4))) + "1801";
1156 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage(keyPress)));
1158 logger.trace("Unknown Keypress: {}", command.toString());
1161 } else if (CHANNEL_PINCODE.equals(channelUID.getId())) {
1162 if (command instanceof StringType) {
1164 // Do PIN for shieldtv protocol
1165 logger.debug("{} - ShieldTV PIN Process Started", handler.getThingID());
1166 String pin = ShieldTVRequest.pinRequest(command.toString());
1167 String message = ShieldTVRequest.encodeMessage(pin);
1168 sendCommand(new ShieldTVCommand(message));
1171 } else if (CHANNEL_DEBUG.equals(channelUID.getId())) {
1172 if (command instanceof StringType) {
1173 if (command.toString().startsWith("RAW", 9)) {
1174 String newCommand = command.toString().substring(13);
1175 String message = ShieldTVRequest.encodeMessage(newCommand);
1176 if (logger.isTraceEnabled()) {
1177 logger.trace("Raw Message Decodes as: {}", ShieldTVRequest.decodeMessage(message));
1179 sendCommand(new ShieldTVCommand(message));
1180 } else if (command.toString().startsWith("MSG", 9)) {
1181 String newCommand = command.toString().substring(13);
1182 messageParser.handleMessage(newCommand);
1185 } else if (CHANNEL_APP.equals(channelUID.getId())) {
1186 if (command instanceof StringType) {
1187 String message = ShieldTVRequest.encodeMessage(ShieldTVRequest.startApp(command.toString()));
1188 sendCommand(new ShieldTVCommand(message));
1190 } else if (CHANNEL_KEYBOARD.equals(channelUID.getId())) {
1191 if (command instanceof StringType) {
1192 String entry = ShieldTVRequest.keyboardEntry(command.toString());
1193 logger.trace("Keyboard Entry {}", entry);
1194 String message = ShieldTVRequest.encodeMessage(entry);
1195 sendCommand(new ShieldTVCommand(message));
1196 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e9071209081410012001320138")));
1201 public void dispose() {
1202 this.disposing = true;
1204 Future<?> asyncInitializeTask = this.asyncInitializeTask;
1205 if (asyncInitializeTask != null) {
1206 asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1208 Future<?> shimAsyncInitializeTask = this.shimAsyncInitializeTask;
1209 if (shimAsyncInitializeTask != null) {
1210 shimAsyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1212 ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
1213 if (deviceHealthJob != null) {
1214 deviceHealthJob.cancel(true);