]> git.basschouten.com Git - openhab-addons.git/blob
d53717bd6f50f9818624cfdf23f24b3b3fcf3458
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.tivo.internal.service;
14
15 import static org.openhab.binding.tivo.internal.TiVoBindingConstants.CONFIG_SOCKET_TIMEOUT_MS;
16
17 import java.io.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.InputStreamReader;
21 import java.io.PrintStream;
22 import java.net.Socket;
23 import java.net.SocketTimeoutException;
24 import java.net.UnknownHostException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.tivo.internal.handler.TiVoHandler;
32 import org.openhab.binding.tivo.internal.service.TivoStatusData.ConnectionStatus;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 /**
37  * TivoStatusProvider class to maintain a connection out to the Tivo, monitor and process status messages returned..
38  *
39  * @author Jayson Kubilis - Initial contribution
40  * @author Andrew Black - Updates / compilation corrections
41  * @author Michael Lobstein - Updated for OH3
42  */
43
44 @NonNullByDefault
45 public class TivoStatusProvider {
46     private static final Pattern TIVO_STATUS_PATTERN = Pattern.compile("^CH_STATUS (\\d{4}) (?:(\\d{4}))?");
47     private static final int TIMEOUT_SEC = 3000;
48
49     private final Logger logger = LoggerFactory.getLogger(TivoStatusProvider.class);
50     private @Nullable Socket tivoSocket = null;
51     private @Nullable PrintStream streamWriter = null;
52     private @Nullable StreamReader streamReader = null;
53     private @Nullable TiVoHandler tivoHandler = null;
54     private TivoStatusData tivoStatusData = new TivoStatusData();
55     private TivoConfigData tivoConfigData = new TivoConfigData();
56     private final String thingUid;
57
58     /**
59      * Instantiates a new TivoConfigStatusProvider.
60      *
61      * @param tivoConfigData {@link TivoConfigData} configuration data for the specific thing.
62      * @param tivoStatusData {@link TivoStatusData} status data for the specific thing.
63      * @param tivoHandler {@link TivoHandler} parent handler object for the TivoConfigStatusProvider.
64      *
65      */
66
67     public TivoStatusProvider(TivoConfigData tivoConfigData, TiVoHandler tivoHandler) {
68         this.tivoStatusData = new TivoStatusData(false, -1, -1, false, "INITIALISING", false, ConnectionStatus.UNKNOWN);
69         this.tivoConfigData = tivoConfigData;
70         this.tivoHandler = tivoHandler;
71         this.thingUid = tivoHandler.getThing().getUID().getAsString();
72     }
73
74     /**
75      * {@link statusRefresh} initiates a connection to the TiVo. When a new connection is made and the TiVo is online,
76      * the current channel is always returned. The connection is then closed (allows the socket to be used by other
77      * devices).
78      *
79      * @return {@link TivoStatusData} object
80      * @throws InterruptedException
81      */
82     public void statusRefresh() throws InterruptedException {
83         if (tivoStatusData.getConnectionStatus() != ConnectionStatus.INIT) {
84             logger.debug(" statusRefresh '{}' - EXISTING status data - '{}'", tivoConfigData.getCfgIdentifier(),
85                     tivoStatusData.toString());
86         }
87         connTivoConnect();
88         doNappTime();
89         if (!tivoConfigData.isKeepConnActive()) {
90             connTivoDisconnect();
91         }
92     }
93
94     /**
95      * {@link cmdTivoSend} sends a command to the Tivo.
96      *
97      * @param tivoCommand the complete command string (KEYWORD + PARAMETERS e.g. SETCH 102) to send.
98      * @return {@link TivoStatusData} status data object, contains the result of the command.
99      * @throws InterruptedException
100      */
101     public @Nullable TivoStatusData cmdTivoSend(String tivoCommand) throws InterruptedException {
102         boolean connected = connTivoConnect();
103         PrintStream streamWriter = this.streamWriter;
104
105         if (!connected || streamWriter == null) {
106             return new TivoStatusData(false, -1, -1, false, "CONNECTION FAILED", false, ConnectionStatus.OFFLINE);
107         }
108         logger.debug("TiVo '{}' - sending command: '{}'", tivoConfigData.getCfgIdentifier(), tivoCommand);
109         int repeatCount = 1;
110         // Handle special keyboard "repeat" commands
111         if (tivoCommand.contains("*")) {
112             repeatCount = Integer.parseInt(tivoCommand.substring(tivoCommand.indexOf("*") + 1));
113             tivoCommand = tivoCommand.substring(0, tivoCommand.indexOf("*"));
114             logger.debug("TiVo '{}' - repeating command: '{}' for '{}' times", tivoConfigData.getCfgIdentifier(),
115                     tivoCommand, repeatCount);
116         }
117         for (int i = 1; i <= repeatCount; i++) {
118             // Send the command
119             streamWriter.println(tivoCommand.toString() + "\r");
120             if (streamWriter.checkError()) {
121                 logger.debug("TiVo '{}' - called cmdTivoSend and encountered an IO error",
122                         tivoConfigData.getCfgIdentifier());
123                 tivoStatusData = new TivoStatusData(false, -1, -1, false, "CONNECTION FAILED", false,
124                         ConnectionStatus.OFFLINE);
125                 connTivoReconnect();
126             }
127         }
128         return tivoStatusData;
129     }
130
131     /**
132      * {@link statusParse} processes the {@link TivoStatusData} status message returned from the TiVo.
133      *
134      * For channel status messages form 'CH_STATUS channel reason' or 'CH_STATUS channel sub-channel reason' calls
135      * {@link getParsedChannel} and returns the channel number (if a match is found in a valid formatted message).
136      *
137      * @param rawStatus string representing the message text returned by the TiVo
138      * @return TivoStatusData object conditionally populated based upon the raw status message
139      */
140     private TivoStatusData statusParse(String rawStatus) {
141         logger.debug(" statusParse '{}' - running on string '{}'", tivoConfigData.getCfgIdentifier(), rawStatus);
142
143         if (rawStatus.contentEquals("COMMAND_TIMEOUT")) {
144             // Ignore COMMAND_TIMEOUT, they occur a few seconds after each successful command, just return existing
145             // status again
146             return this.tivoStatusData;
147         } else {
148             switch (rawStatus) {
149                 case "":
150                     return new TivoStatusData(false, -1, -1, false, "NO_STATUS_DATA_RETURNED", false,
151                             tivoStatusData.getConnectionStatus());
152                 case "LIVETV_READY":
153                     return new TivoStatusData(true, -1, -1, false, "LIVETV_READY", true, ConnectionStatus.ONLINE);
154                 case "CH_FAILED NO_LIVE":
155                     return new TivoStatusData(false, -1, -1, false, "CH_FAILED NO_LIVE", true,
156                             ConnectionStatus.STANDBY);
157                 case "CH_FAILED RECORDING":
158                 case "CH_FAILED MISSING_CHANNEL":
159                 case "CH_FAILED MALFORMED_CHANNEL":
160                 case "CH_FAILED INVALID_CHANNEL":
161                     return new TivoStatusData(false, -1, -1, false, rawStatus, true, ConnectionStatus.ONLINE);
162                 case "INVALID_COMMAND":
163                     return new TivoStatusData(false, -1, -1, false, "INVALID_COMMAND", false, ConnectionStatus.ONLINE);
164                 case "CONNECTION_RETRIES_EXHAUSTED":
165                     return new TivoStatusData(false, -1, -1, false, "CONNECTION_RETRIES_EXHAUSTED", true,
166                             ConnectionStatus.OFFLINE);
167             }
168         }
169
170         // Only other documented status is in the form 'CH_STATUS channel reason' or
171         // 'CH_STATUS channel sub-channel reason'
172         Matcher matcher = TIVO_STATUS_PATTERN.matcher(rawStatus);
173         int chNum = -1; // -1 used globally to indicate channel number error
174         int subChNum = -1;
175         boolean isRecording = false;
176
177         if (matcher.find()) {
178             logger.debug(" statusParse '{}' - groups '{}' with group count of '{}'", tivoConfigData.getCfgIdentifier(),
179                     matcher.group(), matcher.groupCount());
180             if (matcher.groupCount() == 1 || matcher.groupCount() == 2) {
181                 chNum = Integer.parseInt(matcher.group(1).trim());
182                 logger.debug(" statusParse '{}' - parsed channel '{}'", tivoConfigData.getCfgIdentifier(), chNum);
183             }
184             if (matcher.groupCount() == 2) {
185                 subChNum = Integer.parseInt(matcher.group(2).trim());
186                 logger.debug(" statusParse '{}' - parsed sub-channel '{}'", tivoConfigData.getCfgIdentifier(),
187                         subChNum);
188             }
189
190             if (rawStatus.contains("RECORDING")) {
191                 isRecording = true;
192             }
193
194             rawStatus = rawStatus.replace(" REMOTE", "");
195             rawStatus = rawStatus.replace(" LOCAL", "");
196             return new TivoStatusData(true, chNum, subChNum, isRecording, rawStatus, true, ConnectionStatus.ONLINE);
197         }
198         logger.warn(" TiVo '{}' - Unhandled/unexpected status message: '{}'", tivoConfigData.getCfgIdentifier(),
199                 rawStatus);
200         return new TivoStatusData(false, -1, -1, false, rawStatus, false, tivoStatusData.getConnectionStatus());
201     }
202
203     /**
204      * {@link connIsConnected} returns the connection state of the Socket, streamWriter and streamReader objects.
205      *
206      * @return true = connection exists and all objects look OK, false = connection does not exist or a problem has
207      *         occurred
208      *
209      */
210     private boolean connIsConnected() {
211         Socket tivoSocket = this.tivoSocket;
212         PrintStream streamWriter = this.streamWriter;
213
214         if (tivoSocket == null) {
215             logger.debug(" connIsConnected '{}' - FALSE: tivoSocket=null", tivoConfigData.getCfgIdentifier());
216             return false;
217         } else if (!tivoSocket.isConnected()) {
218             logger.debug(" connIsConnected '{}' - FALSE: tivoSocket.isConnected=false",
219                     tivoConfigData.getCfgIdentifier());
220             return false;
221         } else if (tivoSocket.isClosed()) {
222             logger.debug(" connIsConnected '{}' - FALSE: tivoSocket.isClosed=true", tivoConfigData.getCfgIdentifier());
223             return false;
224         } else if (streamWriter == null) {
225             logger.debug(" connIsConnected '{}' - FALSE: tivoIOSendCommand=null", tivoConfigData.getCfgIdentifier());
226             return false;
227         } else if (streamWriter.checkError()) {
228             logger.debug(" connIsConnected '{}' - FALSE: tivoIOSendCommand.checkError()=true",
229                     tivoConfigData.getCfgIdentifier());
230             return false;
231         } else if (streamReader == null) {
232             logger.debug(" connIsConnected '{}' - FALSE: streamReader=null", tivoConfigData.getCfgIdentifier());
233             return false;
234         }
235         return true;
236     }
237
238     /**
239      * {@link connTivoConnect} manages the creation / retry process of the socket connection.
240      *
241      * @return true = connected, false = not connected
242      * @throws InterruptedException
243      */
244     public boolean connTivoConnect() throws InterruptedException {
245         for (int iL = 1; iL <= tivoConfigData.getNumRetry(); iL++) {
246             logger.debug(" connTivoConnect '{}' - starting connection process '{}' of '{}'.",
247                     tivoConfigData.getCfgIdentifier(), iL, tivoConfigData.getNumRetry());
248
249             // Sort out the socket connection
250             if (connSocketConnect()) {
251                 logger.debug(" connTivoConnect '{}' - Socket created / connection made.",
252                         tivoConfigData.getCfgIdentifier());
253                 StreamReader streamReader = this.streamReader;
254                 if (streamReader != null && streamReader.isAlive()) {
255                     return true;
256                 }
257             } else {
258                 logger.debug(" connTivoConnect '{}' - Socket creation failed.", tivoConfigData.getCfgIdentifier());
259                 TiVoHandler tivoHandler = this.tivoHandler;
260                 if (tivoHandler != null) {
261                     tivoHandler.setStatusOffline();
262                 }
263             }
264             // Sleep and retry
265             doNappTime();
266         }
267         return false;
268     }
269
270     /**
271      * {@link connTivoReconnect} disconnect and reconnect the socket connection to the TiVo.
272      *
273      * @return boolean true = connection succeeded, false = connection failed
274      * @throws InterruptedException
275      */
276     public boolean connTivoReconnect() throws InterruptedException {
277         connTivoDisconnect();
278         doNappTime();
279         return connTivoConnect();
280     }
281
282     /**
283      * {@link connTivoDisconnect} cleanly closes the socket connection and dependent objects
284      *
285      */
286     public void connTivoDisconnect() throws InterruptedException {
287         TiVoHandler tivoHandler = this.tivoHandler;
288         StreamReader streamReader = this.streamReader;
289         PrintStream streamWriter = this.streamWriter;
290         Socket tivoSocket = this.tivoSocket;
291
292         logger.debug(" connTivoSocket '{}' - requested to disconnect/cleanup connection objects",
293                 tivoConfigData.getCfgIdentifier());
294
295         // if isCfgKeepConnOpen = false, don't set status to OFFLINE since the socket is closed after each command
296         if (tivoHandler != null && tivoConfigData.isKeepConnActive()) {
297             tivoHandler.setStatusOffline();
298         }
299
300         if (streamWriter != null) {
301             streamWriter.close();
302             this.streamWriter = null;
303         }
304
305         try {
306             if (tivoSocket != null) {
307                 tivoSocket.close();
308                 this.tivoSocket = null;
309             }
310         } catch (IOException e) {
311             logger.debug(" TiVo '{}' - I/O exception while disconnecting: '{}'.  Connection closed.",
312                     tivoConfigData.getCfgIdentifier(), e.getMessage());
313         }
314
315         if (streamReader != null) {
316             streamReader.interrupt();
317             streamReader.join(TIMEOUT_SEC);
318             this.streamReader = null;
319         }
320     }
321
322     /**
323      * {@link connSocketConnect} opens a Socket connection to the TiVo. Creates a {@link StreamReader} (Input)
324      * thread to read the responses from the TiVo and a PrintStream (Output) {@link cmdTivoSend}
325      * to send commands to the device.
326      *
327      * @param pConnect true = make a new connection , false = close existing connection
328      * @return boolean true = connection succeeded, false = connection failed
329      * @throws InterruptedException
330      */
331     private synchronized boolean connSocketConnect() throws InterruptedException {
332         logger.debug(" connSocketConnect '{}' - attempting connection to host '{}', port '{}'",
333                 tivoConfigData.getCfgIdentifier(), tivoConfigData.getHost(), tivoConfigData.getTcpPort());
334
335         if (connIsConnected()) {
336             logger.debug(" connSocketConnect '{}' - already connected to host '{}', port '{}'",
337                     tivoConfigData.getCfgIdentifier(), tivoConfigData.getHost(), tivoConfigData.getTcpPort());
338             return true;
339         } else {
340             // something is wrong, so force a disconnect/clean up so we can try again
341             connTivoDisconnect();
342         }
343
344         try {
345             Socket tivoSocket = new Socket(tivoConfigData.getHost(), tivoConfigData.getTcpPort());
346             tivoSocket.setKeepAlive(true);
347             tivoSocket.setSoTimeout(CONFIG_SOCKET_TIMEOUT_MS);
348             tivoSocket.setReuseAddress(true);
349
350             if (tivoSocket.isConnected() && !tivoSocket.isClosed()) {
351                 if (streamWriter == null) {
352                     streamWriter = new PrintStream(tivoSocket.getOutputStream(), false);
353                 }
354                 if (this.streamReader == null) {
355                     StreamReader streamReader = new StreamReader(tivoSocket.getInputStream());
356                     streamReader.start();
357                     this.streamReader = streamReader;
358                 }
359                 this.tivoSocket = tivoSocket;
360             } else {
361                 logger.debug(" connSocketConnect '{}' - socket creation failed to host '{}', port '{}'",
362                         tivoConfigData.getCfgIdentifier(), tivoConfigData.getHost(), tivoConfigData.getTcpPort());
363                 return false;
364             }
365
366             return true;
367
368         } catch (UnknownHostException e) {
369             logger.debug(" TiVo '{}' - while connecting, unexpected host error: '{}'",
370                     tivoConfigData.getCfgIdentifier(), e.getMessage());
371         } catch (IOException e) {
372             if (tivoStatusData.getConnectionStatus() != ConnectionStatus.OFFLINE) {
373                 logger.debug(" TiVo '{}' - I/O exception while connecting: '{}'", tivoConfigData.getCfgIdentifier(),
374                         e.getMessage());
375             }
376         }
377         return false;
378     }
379
380     /**
381      * {@link doNappTime} sleeps for the period specified by the getCmdWaitInterval parameter. Primarily used to allow
382      * the TiVo time to process responses after a command is issued.
383      *
384      * @throws InterruptedException
385      */
386     public void doNappTime() throws InterruptedException {
387         TimeUnit.MILLISECONDS.sleep(tivoConfigData.getCmdWaitInterval());
388     }
389
390     public TivoStatusData getServiceStatus() {
391         return tivoStatusData;
392     }
393
394     public void setServiceStatus(TivoStatusData tivoStatusData) {
395         this.tivoStatusData = tivoStatusData;
396     }
397
398     /**
399      * {@link StreamReader} data stream reader that reads the status data returned from the TiVo.
400      *
401      */
402     public class StreamReader extends Thread {
403         private @Nullable BufferedReader bufferedReader = null;
404
405         /**
406          * {@link StreamReader} construct a data stream reader that reads the status data returned from the TiVo via a
407          * BufferedReader.
408          *
409          * @param inputStream socket input stream.
410          * @throws IOException
411          */
412         public StreamReader(InputStream inputStream) {
413             this.setName("OH-binding-" + thingUid + "-" + tivoConfigData.getHost() + ":" + tivoConfigData.getTcpPort());
414             this.bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
415             this.setDaemon(true);
416         }
417
418         @Override
419         public void run() {
420             try {
421                 logger.debug("streamReader {} is running. ", tivoConfigData.getCfgIdentifier());
422                 while (!Thread.currentThread().isInterrupted()) {
423                     String receivedData = null;
424                     BufferedReader reader = bufferedReader;
425                     if (reader == null) {
426                         throw new IOException("streamReader failed: input stream is null");
427                     }
428
429                     try {
430                         receivedData = reader.readLine();
431                     } catch (SocketTimeoutException e) {
432                         // Do nothing. Just allow the thread to check if it has to stop.
433                     }
434
435                     if (receivedData != null) {
436                         logger.debug("TiVo {} data received: {}", tivoConfigData.getCfgIdentifier(), receivedData);
437                         TivoStatusData commandResult = statusParse(receivedData);
438                         TiVoHandler handler = tivoHandler;
439                         if (handler != null) {
440                             handler.updateTivoStatus(tivoStatusData, commandResult);
441                         }
442                         tivoStatusData = commandResult;
443                     }
444                 }
445
446             } catch (IOException e) {
447                 closeBufferedReader();
448                 logger.debug("TiVo {} is disconnected. ", tivoConfigData.getCfgIdentifier(), e);
449             }
450             closeBufferedReader();
451             logger.debug("streamReader {} is stopped. ", tivoConfigData.getCfgIdentifier());
452         }
453
454         private void closeBufferedReader() {
455             BufferedReader reader = bufferedReader;
456             if (reader != null) {
457                 try {
458                     reader.close();
459                     this.bufferedReader = null;
460                 } catch (IOException e) {
461                     logger.debug("Error closing bufferedReader: {}", e.getMessage(), e);
462                 }
463             }
464         }
465     }
466 }