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.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;
361 config.reconnect = (config.reconnect > 0) ? config.reconnect : DEFAULT_RECONNECT_SECONDS;
362 config.heartbeat = (config.heartbeat > 0) ? config.heartbeat : DEFAULT_HEARTBEAT_SECONDS;
363 config.delay = (config.delay < 0) ? 0 : config.delay;
364 config.shim = (config.shim) ? true : false;
365 config.shimNewKeys = (config.shimNewKeys) ? true : false;
367 config.keystoreFileName = (!config.keystoreFileName.equals("")) ? config.keystoreFileName
368 : folderName + "/shieldtv." + ((config.shim) ? "shim." : "") + handler.getThing().getUID().getId()
370 config.keystorePassword = (!config.keystorePassword.equals("")) ? config.keystorePassword
371 : DEFAULT_KEYSTORE_PASSWORD;
373 androidtvPKI.setKeystoreFileName(config.keystoreFileName);
374 androidtvPKI.setAlias("nvidia");
376 deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, config.heartbeat, config.heartbeat,
380 File keystoreFile = new File(config.keystoreFileName);
382 if (!keystoreFile.exists() || config.shimNewKeys) {
383 androidtvPKI.generateNewKeyPair(encryptionKey);
384 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
386 androidtvPKI.loadFromKeyStore(config.keystorePassword, this.encryptionKey);
389 logger.trace("{} - Initializing SSL Context", handler.getThingID());
390 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
391 kmf.init(androidtvPKI.getKeyStore(config.keystorePassword, this.encryptionKey),
392 config.keystorePassword.toCharArray());
394 TrustManager[] trustManagers = defineNoOpTrustManager();
396 sslContext = SSLContext.getInstance("TLS");
397 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
399 sslSocketFactory = sslContext.getSocketFactory();
401 asyncInitializeTask = scheduler.submit(this::connect);
403 shimAsyncInitializeTask = scheduler.submit(this::shimInitialize);
405 } catch (NoSuchAlgorithmException | IOException e) {
406 setStatus(false, "offline.error-initalizing-keystore");
407 logger.debug("Error initializing keystore", e);
408 } catch (UnrecoverableKeyException e) {
409 setStatus(false, "offline.key-unrecoverable-with-supplied-password");
410 } catch (GeneralSecurityException e) {
411 logger.debug("General security exception", e);
412 } catch (Exception e) {
413 logger.debug("General exception", e);
417 public void connect() {
418 synchronized (connectionLock) {
421 logger.debug("{} - Opening ShieldTV SSL connection to {}:{}", handler.getThingID(),
422 config.ipAddress, config.port);
423 SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(config.ipAddress, config.port);
424 sslSocket.startHandshake();
425 writer = new BufferedWriter(
426 new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
427 reader = new BufferedReader(
428 new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.ISO_8859_1));
429 this.sslSocket = sslSocket;
430 } catch (UnknownHostException e) {
431 setStatus(false, "offline.unknown-host");
433 } catch (IllegalArgumentException e) {
434 // port out of valid range
435 setStatus(false, "offline.invalid-port-number");
437 } catch (InterruptedIOException e) {
438 logger.debug("Interrupted while establishing ShieldTV connection");
439 Thread.currentThread().interrupt();
441 } catch (IOException e) {
442 setStatus(false, "offline.error-opening-ssl-connection-check-log");
443 logger.info("{} - Error opening SSL connection to {}:{} {}", handler.getThingID(), config.ipAddress,
444 config.port, e.getMessage());
446 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
450 setStatus(false, "offline.initializing");
452 Thread readerThread = new Thread(this::readerThreadJob, "ShieldTV reader " + handler.getThingID());
453 readerThread.setDaemon(true);
454 readerThread.start();
455 this.readerThread = readerThread;
457 Thread senderThread = new Thread(this::senderThreadJob, "ShieldTV sender " + handler.getThingID());
458 senderThread.setDaemon(true);
459 senderThread.start();
460 this.senderThread = senderThread;
463 this.periodicUpdate = 20;
464 logger.debug("{} - Starting ShieldTV keepalive job with interval {}", handler.getThingID(),
466 keepAliveJob = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, config.heartbeat,
467 config.heartbeat, TimeUnit.SECONDS);
469 String login = ShieldTVRequest.encodeMessage(ShieldTVRequest.loginRequest());
470 sendCommand(new ShieldTVCommand(login));
473 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
478 public void shimInitialize() {
479 synchronized (connectionLock) {
480 AndroidTVPKI shimPKI = new AndroidTVPKI();
481 byte[] shimEncryptionKey = shimPKI.generateEncryptionKey();
482 SSLContext sslContext;
485 shimPKI.generateNewKeyPair(shimEncryptionKey);
486 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
487 kmf.init(shimPKI.getKeyStore(config.keystorePassword, shimEncryptionKey),
488 config.keystorePassword.toCharArray());
489 TrustManager[] trustManagers = defineNoOpTrustManager();
490 sslContext = SSLContext.getInstance("TLS");
491 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
492 this.sslServerSocketFactory = sslContext.getServerSocketFactory();
494 logger.debug("{} - Opening ShieldTV shim on port {}", handler.getThingID(), config.port);
495 ServerSocket sslServerSocket = this.sslServerSocketFactory.createServerSocket(config.port);
498 logger.debug("{} - Waiting for shim connection...", handler.getThingID());
499 Socket serverSocket = sslServerSocket.accept();
502 SSLSession session = ((SSLSocket) serverSocket).getSession();
503 Certificate[] cchain2 = session.getLocalCertificates();
504 for (int i = 0; i < cchain2.length; i++) {
505 logger.trace("Connection from: {}", ((X509Certificate) cchain2[i]).getSubjectX500Principal());
508 logger.trace("Peer host is {}", session.getPeerHost());
509 logger.trace("Cipher is {}", session.getCipherSuite());
510 logger.trace("Protocol is {}", session.getProtocol());
511 logger.trace("ID is {}", new BigInteger(session.getId()));
512 logger.trace("Session created in {}", session.getCreationTime());
513 logger.trace("Session accessed in {}", session.getLastAccessedTime());
515 shimWriter = new BufferedWriter(
516 new OutputStreamWriter(serverSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
517 shimReader = new BufferedReader(
518 new InputStreamReader(serverSocket.getInputStream(), StandardCharsets.ISO_8859_1));
519 this.shimServerSocket = serverSocket;
521 Thread readerThread = new Thread(this::shimReaderThreadJob,
522 "ShieldTV shim reader " + handler.getThingID());
523 readerThread.setDaemon(true);
524 readerThread.start();
525 this.shimReaderThread = readerThread;
527 Thread senderThread = new Thread(this::shimSenderThreadJob,
528 "ShieldTV shim sender" + handler.getThingID());
529 senderThread.setDaemon(true);
530 senderThread.start();
531 this.shimSenderThread = senderThread;
533 } catch (Exception e) {
534 logger.trace("Shim initalization exception", e);
540 private void scheduleConnectRetry(long waitSeconds) {
541 logger.trace("{} - Scheduling ShieldTV connection retry in {} seconds", handler.getThingID(), waitSeconds);
542 connectRetryJob = scheduler.schedule(this::connect, waitSeconds, TimeUnit.SECONDS);
546 * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and
549 * @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
550 * connect or reconnect, and true when calling from dispose.
552 private void disconnect(boolean interruptAll) {
553 synchronized (connectionLock) {
554 logger.debug("{} - Disconnecting ShieldTV", handler.getThingID());
556 this.isLoggedIn = false;
558 ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
559 if (connectRetryJob != null) {
560 connectRetryJob.cancel(true);
562 ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
563 if (keepAliveJob != null) {
564 keepAliveJob.cancel(true);
567 reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
569 Thread senderThread = this.senderThread;
570 if (senderThread != null && senderThread.isAlive()) {
571 senderThread.interrupt();
574 Thread readerThread = this.readerThread;
575 if (readerThread != null && readerThread.isAlive()) {
576 readerThread.interrupt();
579 Thread shimSenderThread = this.shimSenderThread;
580 if (shimSenderThread != null && shimSenderThread.isAlive()) {
581 shimSenderThread.interrupt();
584 Thread shimReaderThread = this.shimReaderThread;
585 if (shimReaderThread != null && shimReaderThread.isAlive()) {
586 shimReaderThread.interrupt();
589 SSLSocket sslSocket = this.sslSocket;
590 if (sslSocket != null) {
593 } catch (IOException e) {
594 logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
596 this.sslSocket = null;
598 BufferedReader reader = this.reader;
599 if (reader != null) {
602 } catch (IOException e) {
603 logger.debug("Error closing reader: {}", e.getMessage());
606 BufferedWriter writer = this.writer;
607 if (writer != null) {
610 } catch (IOException e) {
611 logger.debug("Error closing writer: {}", e.getMessage());
615 Socket shimServerSocket = this.shimServerSocket;
616 if (shimServerSocket != null) {
618 shimServerSocket.close();
619 } catch (IOException e) {
620 logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
622 this.shimServerSocket = null;
624 BufferedReader shimReader = this.shimReader;
625 if (shimReader != null) {
628 } catch (IOException e) {
629 logger.debug("Error closing shimReader: {}", e.getMessage());
632 BufferedWriter shimWriter = this.shimWriter;
633 if (shimWriter != null) {
636 } catch (IOException e) {
637 logger.debug("Error closing shimWriter: {}", e.getMessage());
643 private void reconnect() {
644 synchronized (connectionLock) {
645 if (!this.disposing) {
646 logger.debug("{} - Attempting to reconnect to the ShieldTV", handler.getThingID());
647 setStatus(false, "offline.reconnecting");
655 * Method executed by the message sender thread (senderThread)
657 private void senderThreadJob() {
658 logger.debug("{} - Command sender thread started", handler.getThingID());
660 while (!Thread.currentThread().isInterrupted() && writer != null) {
661 ShieldTVCommand command = sendQueue.take();
664 BufferedWriter writer = this.writer;
665 if (writer != null) {
666 logger.trace("{} - Raw ShieldTV command decodes as: {}", handler.getThingID(),
667 ShieldTVRequest.decodeMessage(command.toString()));
668 writer.write(command.toString());
671 } catch (InterruptedIOException e) {
672 logger.debug("Interrupted while sending to ShieldTV");
673 setStatus(false, "offline.interrupted");
674 break; // exit loop and terminate thread
675 } catch (IOException e) {
676 logger.warn("{} - Communication error, will try to reconnect ShieldTV. Error: {}",
677 handler.getThingID(), e.getMessage());
678 setStatus(false, "offline.communication-error-will-try-to-reconnect");
679 sendQueue.add(command); // Requeue command
680 this.isLoggedIn = false;
682 break; // reconnect() will start a new thread; terminate this one
684 if (config.delay > 0) {
685 Thread.sleep(config.delay); // introduce delay to throttle send rate
688 } catch (InterruptedException e) {
689 Thread.currentThread().interrupt();
691 logger.debug("{} - Command sender thread exiting", handler.getThingID());
695 private void shimSenderThreadJob() {
696 logger.debug("Shim sender thread started");
698 while (!Thread.currentThread().isInterrupted() && shimWriter != null) {
699 ShieldTVCommand command = shimQueue.take();
702 BufferedWriter writer = this.shimWriter;
703 if (writer != null) {
704 logger.trace("Shim received from shield: {}",
705 ShieldTVRequest.decodeMessage(command.toString()));
706 writer.write(command.toString());
709 } catch (InterruptedIOException e) {
710 logger.debug("Shim interrupted while sending.");
711 break; // exit loop and terminate thread
712 } catch (IOException e) {
713 logger.warn("Shim communication error. Error: {}", e.getMessage());
714 break; // reconnect() will start a new thread; terminate this one
717 } catch (InterruptedException e) {
718 Thread.currentThread().interrupt();
720 logger.debug("Command sender thread exiting");
724 private void flushReader() {
725 if (!inMessage && (sbReader.length() > 0)) {
726 sbReader.setLength(sbReader.length() - 2);
727 messageParser.handleMessage(sbReader.toString());
729 sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
731 sbReader.setLength(0);
732 sbReader.append(lastMsg);
734 sbReader.append(thisMsg);
738 private void finishReaderMessage() {
739 sbReader.append(thisMsg);
742 messageParser.handleMessage(sbReader.toString());
744 sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
746 sbReader.setLength(0);
749 private String fixMessage(String tempMsg) {
750 if (tempMsg.length() % 2 > 0) {
751 tempMsg = "0" + tempMsg;
757 * Method executed by the message reader thread (readerThread)
759 private void readerThreadJob() {
760 logger.debug("{} - Message reader thread started", handler.getThingID());
762 BufferedReader reader = this.reader;
763 while (!Thread.interrupted() && reader != null) {
764 thisMsg = fixMessage(Integer.toHexString(reader.read()));
765 if (HARD_DROP.equals(thisMsg)) {
766 // Shield has crashed the connection. Disconnect hard.
767 logger.debug("{} - readerThreadJob received ffffffff. Disconnecting hard.", handler.getThingID());
768 this.isLoggedIn = false;
772 if (DELIMITER_08.equals(lastMsg) && !inMessage) {
776 } else if (DELIMITER_18.equals(lastMsg) && thisMsg.equals(msgType) && inMessage) {
777 if (!msgType.startsWith(DELIMITER_0)) {
778 sbReader.append(thisMsg);
779 thisMsg = fixMessage(Integer.toHexString(reader.read()));
781 finishReaderMessage();
782 } else if (DELIMITER_00.equals(msgType) && (sbReader.toString().length() == 16)) {
783 // keepalive messages don't have delimiters but are always 18 in length
784 finishReaderMessage();
786 sbReader.append(thisMsg);
790 } catch (InterruptedIOException e) {
791 logger.debug("Interrupted while reading");
792 setStatus(false, "offline.interrupted");
793 } catch (IOException e) {
794 logger.debug("I/O error while reading from stream: {}", e.getMessage());
795 setStatus(false, "offline.io-error");
796 } catch (RuntimeException e) {
797 logger.warn("Runtime exception in reader thread", e);
798 setStatus(false, "offline.runtime-exception");
800 logger.debug("{} - Message reader thread exiting", handler.getThingID());
804 private void shimReaderThreadJob() {
805 logger.debug("Shim reader thread started");
806 String thisShimMsg = "";
807 int thisShimRawMsg = 0;
808 int payloadRemain = 0;
809 int payloadBlock = 0;
810 String thisShimMsgType = "";
811 boolean inShimMessage = false;
813 BufferedReader reader = this.shimReader;
814 while (!Thread.interrupted() && reader != null) {
815 thisShimRawMsg = reader.read();
816 thisShimMsg = fixMessage(Integer.toHexString(thisShimRawMsg));
817 if (HARD_DROP.equals(thisShimMsg)) {
821 if (!inShimMessage) {
822 // Beginning of payload
823 sbShimReader.setLength(0);
824 sbShimReader.append(thisShimMsg);
825 inShimMessage = true;
827 } else if ((payloadBlock == 1) && (DELIMITER_00.equals(thisShimMsg))) {
828 sbShimReader.append(thisShimMsg);
830 thisShimMsgType = thisShimMsg;
831 while (payloadRemain > 1) {
832 thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
833 sbShimReader.append(thisShimMsg);
839 } else if ((payloadBlock == 1)
840 && (thisShimMsg.startsWith(DELIMITER_F1) || thisShimMsg.startsWith(DELIMITER_F3))) {
841 sbShimReader.append(thisShimMsg);
843 thisShimMsgType = thisShimMsg;
844 while (payloadRemain > 1) {
845 thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
846 sbShimReader.append(thisShimMsg);
852 } else if (payloadBlock == 1) {
853 thisShimMsgType = thisShimMsg;
854 sbShimReader.append(thisShimMsg);
856 } else if (payloadBlock == 2) {
857 sbShimReader.append(thisShimMsg);
859 } else if (payloadBlock == 3) {
860 // Length of remainder of packet
861 payloadRemain = thisShimRawMsg;
862 sbShimReader.append(thisShimMsg);
864 } else if (payloadBlock == 4) {
865 sbShimReader.append(thisShimMsg);
866 logger.trace("PB4 SSR {} TSMT {} TSM {} PR {}", sbShimReader.toString(), thisShimMsgType,
867 thisShimMsg, payloadRemain);
868 if (DELIMITER_E9.equals(thisShimMsgType) || DELIMITER_F0.equals(thisShimMsgType)
869 || DELIMITER_EC.equals(thisShimMsgType)) {
870 payloadRemain = thisShimRawMsg + 1;
872 while (payloadRemain > 1) {
873 thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
874 sbShimReader.append(thisShimMsg);
882 if ((payloadBlock > 5) && (payloadRemain == 0)) {
883 logger.trace("Shim sending to shield: {}", sbShimReader.toString());
884 sendQueue.add(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbShimReader.toString())));
885 inShimMessage = false;
888 sbShimReader.setLength(0);
891 } catch (InterruptedIOException e) {
892 logger.debug("Interrupted while reading");
893 setStatus(false, "offline.interrupted");
894 } catch (IOException e) {
895 logger.debug("I/O error while reading from stream: {}", e.getMessage());
896 setStatus(false, "offline.io-error");
897 } catch (RuntimeException e) {
898 logger.warn("Runtime exception in reader thread", e);
899 setStatus(false, "offline.runtime-exception");
901 logger.debug("Message reader thread exiting");
905 private void sendKeepAlive() {
906 logger.trace("{} - Sending ShieldTV keepalive query", handler.getThingID());
907 String keepalive = ShieldTVRequest.encodeMessage(ShieldTVRequest.keepAlive());
908 sendCommand(new ShieldTVCommand(keepalive));
910 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08ec0712020806"))); // Get App
911 if (this.periodicUpdate <= 1) {
912 sendPeriodicUpdate();
913 this.periodicUpdate = 20;
918 reconnectTaskSchedule();
922 * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should
924 * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
926 private void reconnectTaskSchedule() {
927 synchronized (keepAliveReconnectLock) {
928 keepAliveReconnectJob = scheduler.schedule(this::keepAliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
934 * Cancels the reconnect task keepAliveReconnectJob.
936 private void reconnectTaskCancel(boolean interrupt) {
937 synchronized (keepAliveReconnectLock) {
938 ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
939 if (keepAliveReconnectJob != null) {
940 logger.trace("{} - Canceling ShieldTV scheduled reconnect job.", handler.getThingID());
941 keepAliveReconnectJob.cancel(interrupt);
942 this.keepAliveReconnectJob = null;
948 * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
949 * validMessageReceived() which in turn calls reconnectTaskCancel().
951 private void keepAliveTimeoutExpired() {
952 logger.debug("{} - ShieldTV keepalive response timeout expired. Initiating reconnect.", handler.getThingID());
956 public void validMessageReceived() {
957 reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
960 public void sendCommand(ShieldTVCommand command) {
961 if ((!config.shim) && (!command.isEmpty())) {
962 sendQueue.add(command);
966 public void sendShim(ShieldTVCommand command) {
967 if (!command.isEmpty()) {
968 shimQueue.add(command);
972 public void handleCommand(ChannelUID channelUID, Command command) {
973 logger.debug("{} - Command received: {}", handler.getThingID(), channelUID.getId());
975 if (CHANNEL_KEYPRESS.equals(channelUID.getId())) {
976 if (command instanceof StringType) {
977 switch (command.toString()) {
979 sendCommand(new ShieldTVCommand(
980 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
981 sendCommand(new ShieldTVCommand(
982 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
985 sendCommand(new ShieldTVCommand(
986 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
987 sendCommand(new ShieldTVCommand(
988 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
991 sendCommand(new ShieldTVCommand(
992 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
993 sendCommand(new ShieldTVCommand(
994 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
997 sendCommand(new ShieldTVCommand(
998 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
999 sendCommand(new ShieldTVCommand(
1000 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
1003 sendCommand(new ShieldTVCommand(
1004 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1005 sendCommand(new ShieldTVCommand(
1006 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1009 sendCommand(new ShieldTVCommand(
1010 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1011 sendCommand(new ShieldTVCommand(
1012 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1015 sendCommand(new ShieldTVCommand(
1016 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1017 sendCommand(new ShieldTVCommand(
1018 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1021 sendCommand(new ShieldTVCommand(
1022 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1023 sendCommand(new ShieldTVCommand(
1024 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1026 case "KEY_PLAYPAUSE":
1027 sendCommand(new ShieldTVCommand(
1028 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1029 sendCommand(new ShieldTVCommand(
1030 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1033 sendCommand(new ShieldTVCommand(
1034 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1035 sendCommand(new ShieldTVCommand(
1036 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1039 sendCommand(new ShieldTVCommand(
1040 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1041 sendCommand(new ShieldTVCommand(
1042 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1044 case "KEY_UP_PRESS":
1045 sendCommand(new ShieldTVCommand(
1046 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
1048 case "KEY_DOWN_PRESS":
1049 sendCommand(new ShieldTVCommand(
1050 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
1052 case "KEY_RIGHT_PRESS":
1053 sendCommand(new ShieldTVCommand(
1054 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
1056 case "KEY_LEFT_PRESS":
1057 sendCommand(new ShieldTVCommand(
1058 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
1060 case "KEY_ENTER_PRESS":
1061 sendCommand(new ShieldTVCommand(
1062 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1064 case "KEY_HOME_PRESS":
1065 sendCommand(new ShieldTVCommand(
1066 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1068 case "KEY_BACK_PRESS":
1069 sendCommand(new ShieldTVCommand(
1070 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1072 case "KEY_MENU_PRESS":
1073 sendCommand(new ShieldTVCommand(
1074 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1076 case "KEY_PLAYPAUSE_PRESS":
1077 sendCommand(new ShieldTVCommand(
1078 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1080 case "KEY_REWIND_PRESS":
1081 sendCommand(new ShieldTVCommand(
1082 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1084 case "KEY_FORWARD_PRESS":
1085 sendCommand(new ShieldTVCommand(
1086 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1088 case "KEY_UP_RELEASE":
1089 sendCommand(new ShieldTVCommand(
1090 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
1092 case "KEY_DOWN_RELEASE":
1093 sendCommand(new ShieldTVCommand(
1094 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
1096 case "KEY_RIGHT_RELEASE":
1097 sendCommand(new ShieldTVCommand(
1098 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
1100 case "KEY_LEFT_RELEASE":
1101 sendCommand(new ShieldTVCommand(
1102 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
1104 case "KEY_ENTER_RELEASE":
1105 sendCommand(new ShieldTVCommand(
1106 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1108 case "KEY_HOME_RELEASE":
1109 sendCommand(new ShieldTVCommand(
1110 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1112 case "KEY_BACK_RELEASE":
1113 sendCommand(new ShieldTVCommand(
1114 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1116 case "KEY_MENU_RELEASE":
1117 sendCommand(new ShieldTVCommand(
1118 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1120 case "KEY_PLAYPAUSE_RELEASE":
1121 sendCommand(new ShieldTVCommand(
1122 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1124 case "KEY_REWIND_RELEASE":
1125 sendCommand(new ShieldTVCommand(
1126 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1128 case "KEY_FORWARD_RELEASE":
1129 sendCommand(new ShieldTVCommand(
1130 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1133 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401e")));
1136 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e4010")));
1139 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401f")));
1142 sendCommand(new ShieldTVCommand(
1143 ShieldTVRequest.encodeMessage("08f007120c08031208080110031a020102")));
1146 sendCommand(new ShieldTVCommand(
1147 ShieldTVRequest.encodeMessage("08f007120c08031208080110011a020102")));
1150 sendCommand(new ShieldTVCommand(
1151 ShieldTVRequest.encodeMessage("08f007120c08031208080110021a020102")));
1154 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e9071209081410012001320138")));
1157 if (command.toString().length() == 5) {
1158 // Account for KEY_(ASCII Character)
1159 String keyPress = "08ec07120708011201"
1160 + ShieldTVRequest.decodeMessage(new String("" + command.toString().charAt(4))) + "1801";
1161 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage(keyPress)));
1163 logger.trace("Unknown Keypress: {}", command.toString());
1166 } else if (CHANNEL_PINCODE.equals(channelUID.getId())) {
1167 if (command instanceof StringType) {
1169 // Do PIN for shieldtv protocol
1170 logger.debug("{} - ShieldTV PIN Process Started", handler.getThingID());
1171 String pin = ShieldTVRequest.pinRequest(command.toString());
1172 String message = ShieldTVRequest.encodeMessage(pin);
1173 sendCommand(new ShieldTVCommand(message));
1176 } else if (CHANNEL_DEBUG.equals(channelUID.getId())) {
1177 if (command instanceof StringType) {
1178 if (command.toString().startsWith("RAW", 9)) {
1179 String newCommand = command.toString().substring(13);
1180 String message = ShieldTVRequest.encodeMessage(newCommand);
1181 if (logger.isTraceEnabled()) {
1182 logger.trace("Raw Message Decodes as: {}", ShieldTVRequest.decodeMessage(message));
1184 sendCommand(new ShieldTVCommand(message));
1185 } else if (command.toString().startsWith("MSG", 9)) {
1186 String newCommand = command.toString().substring(13);
1187 messageParser.handleMessage(newCommand);
1190 } else if (CHANNEL_APP.equals(channelUID.getId())) {
1191 if (command instanceof StringType) {
1192 String message = ShieldTVRequest.encodeMessage(ShieldTVRequest.startApp(command.toString()));
1193 sendCommand(new ShieldTVCommand(message));
1195 } else if (CHANNEL_KEYBOARD.equals(channelUID.getId())) {
1196 if (command instanceof StringType) {
1197 String entry = ShieldTVRequest.keyboardEntry(command.toString());
1198 logger.trace("Keyboard Entry {}", entry);
1199 String message = ShieldTVRequest.encodeMessage(entry);
1200 sendCommand(new ShieldTVCommand(message));
1201 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e9071209081410012001320138")));
1206 public void dispose() {
1207 this.disposing = true;
1209 Future<?> asyncInitializeTask = this.asyncInitializeTask;
1210 if (asyncInitializeTask != null) {
1211 asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1213 Future<?> shimAsyncInitializeTask = this.shimAsyncInitializeTask;
1214 if (shimAsyncInitializeTask != null) {
1215 shimAsyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1217 ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
1218 if (deviceHealthJob != null) {
1219 deviceHealthJob.cancel(true);