]> git.basschouten.com Git - openhab-addons.git/blob
b831649333bf29b0340ec00c52ca941d3aa2b6ad
[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
88         // this will close the connection and re-open every 12 hours
89         if (tivoConfigData.isKeepConnActive()) {
90             connTivoDisconnect();
91             doNappTime();
92         }
93
94         connTivoConnect();
95         doNappTime();
96         if (!tivoConfigData.isKeepConnActive()) {
97             connTivoDisconnect();
98         }
99     }
100
101     /**
102      * {@link cmdTivoSend} sends a command to the Tivo.
103      *
104      * @param tivoCommand the complete command string (KEYWORD + PARAMETERS e.g. SETCH 102) to send.
105      * @return {@link TivoStatusData} status data object, contains the result of the command.
106      * @throws InterruptedException
107      */
108     public @Nullable TivoStatusData cmdTivoSend(String tivoCommand) throws InterruptedException {
109         boolean connected = connTivoConnect();
110         PrintStream streamWriter = this.streamWriter;
111
112         if (!connected || streamWriter == null) {
113             return new TivoStatusData(false, -1, -1, false, "CONNECTION FAILED", false, ConnectionStatus.OFFLINE);
114         }
115         logger.debug("TiVo '{}' - sending command: '{}'", tivoConfigData.getCfgIdentifier(), tivoCommand);
116         int repeatCount = 1;
117         // Handle special keyboard "repeat" commands
118         if (tivoCommand.contains("*")) {
119             repeatCount = Integer.parseInt(tivoCommand.substring(tivoCommand.indexOf("*") + 1));
120             tivoCommand = tivoCommand.substring(0, tivoCommand.indexOf("*"));
121             logger.debug("TiVo '{}' - repeating command: '{}' for '{}' times", tivoConfigData.getCfgIdentifier(),
122                     tivoCommand, repeatCount);
123         }
124         for (int i = 1; i <= repeatCount; i++) {
125             // Send the command
126             streamWriter.println(tivoCommand.toString() + "\r");
127             if (streamWriter.checkError()) {
128                 logger.debug("TiVo '{}' - called cmdTivoSend and encountered an IO error",
129                         tivoConfigData.getCfgIdentifier());
130                 tivoStatusData = new TivoStatusData(false, -1, -1, false, "CONNECTION FAILED", false,
131                         ConnectionStatus.OFFLINE);
132                 connTivoReconnect();
133             }
134         }
135         return tivoStatusData;
136     }
137
138     /**
139      * {@link statusParse} processes the {@link TivoStatusData} status message returned from the TiVo.
140      *
141      * For channel status messages form 'CH_STATUS channel reason' or 'CH_STATUS channel sub-channel reason' calls
142      * {@link getParsedChannel} and returns the channel number (if a match is found in a valid formatted message).
143      *
144      * @param rawStatus string representing the message text returned by the TiVo
145      * @return TivoStatusData object conditionally populated based upon the raw status message
146      */
147     private TivoStatusData statusParse(String rawStatus) {
148         logger.debug(" statusParse '{}' - running on string '{}'", tivoConfigData.getCfgIdentifier(), rawStatus);
149
150         if (rawStatus.contentEquals("COMMAND_TIMEOUT")) {
151             // If connection status was UNKNOWN, COMMAND_TIMEOUT indicates the Tivo is alive, so update the status
152             if (this.tivoStatusData.getConnectionStatus() == ConnectionStatus.UNKNOWN) {
153                 return new TivoStatusData(false, -1, -1, false, "COMMAND_TIMEOUT", false, ConnectionStatus.ONLINE);
154             }
155             // Otherwise ignore COMMAND_TIMEOUT, they occur a few seconds after each successful command, just return
156             // existing status again
157             return this.tivoStatusData;
158         } else {
159             switch (rawStatus) {
160                 case "":
161                     return new TivoStatusData(false, -1, -1, false, "NO_STATUS_DATA_RETURNED", false,
162                             tivoStatusData.getConnectionStatus());
163                 case "LIVETV_READY":
164                     return new TivoStatusData(true, -1, -1, false, "LIVETV_READY", true, ConnectionStatus.ONLINE);
165                 case "CH_FAILED NO_LIVE":
166                     return new TivoStatusData(false, -1, -1, false, "CH_FAILED NO_LIVE", true,
167                             ConnectionStatus.STANDBY);
168                 case "CH_FAILED RECORDING":
169                 case "CH_FAILED MISSING_CHANNEL":
170                 case "CH_FAILED MALFORMED_CHANNEL":
171                 case "CH_FAILED INVALID_CHANNEL":
172                     return new TivoStatusData(false, -1, -1, false, rawStatus, true, ConnectionStatus.ONLINE);
173                 case "INVALID_COMMAND":
174                     return new TivoStatusData(false, -1, -1, false, "INVALID_COMMAND", false, ConnectionStatus.ONLINE);
175                 case "CONNECTION_RETRIES_EXHAUSTED":
176                     return new TivoStatusData(false, -1, -1, false, "CONNECTION_RETRIES_EXHAUSTED", true,
177                             ConnectionStatus.OFFLINE);
178             }
179         }
180
181         // Only other documented status is in the form 'CH_STATUS channel reason' or
182         // 'CH_STATUS channel sub-channel reason'
183         Matcher matcher = TIVO_STATUS_PATTERN.matcher(rawStatus);
184         int chNum = -1; // -1 used globally to indicate channel number error
185         int subChNum = -1;
186         boolean isRecording = false;
187
188         if (matcher.find()) {
189             logger.debug(" statusParse '{}' - groups '{}' with group count of '{}'", tivoConfigData.getCfgIdentifier(),
190                     matcher.group(), matcher.groupCount());
191             if (matcher.groupCount() == 1 || matcher.groupCount() == 2) {
192                 chNum = Integer.parseInt(matcher.group(1).trim());
193                 logger.debug(" statusParse '{}' - parsed channel '{}'", tivoConfigData.getCfgIdentifier(), chNum);
194             }
195             if (matcher.groupCount() == 2 && matcher.group(2) != null) {
196                 subChNum = Integer.parseInt(matcher.group(2).trim());
197                 logger.debug(" statusParse '{}' - parsed sub-channel '{}'", tivoConfigData.getCfgIdentifier(),
198                         subChNum);
199             }
200
201             if (rawStatus.contains("RECORDING")) {
202                 isRecording = true;
203             }
204
205             rawStatus = rawStatus.replace(" REMOTE", "");
206             rawStatus = rawStatus.replace(" LOCAL", "");
207             return new TivoStatusData(true, chNum, subChNum, isRecording, rawStatus, true, ConnectionStatus.ONLINE);
208         }
209         logger.warn(" TiVo '{}' - Unhandled/unexpected status message: '{}'", tivoConfigData.getCfgIdentifier(),
210                 rawStatus);
211         return new TivoStatusData(false, -1, -1, false, rawStatus, false, tivoStatusData.getConnectionStatus());
212     }
213
214     /**
215      * {@link connIsConnected} returns the connection state of the Socket, streamWriter and streamReader objects.
216      *
217      * @return true = connection exists and all objects look OK, false = connection does not exist or a problem has
218      *         occurred
219      *
220      */
221     private boolean connIsConnected() {
222         Socket tivoSocket = this.tivoSocket;
223         PrintStream streamWriter = this.streamWriter;
224
225         if (tivoSocket == null) {
226             logger.debug(" connIsConnected '{}' - FALSE: tivoSocket=null", tivoConfigData.getCfgIdentifier());
227             return false;
228         } else if (!tivoSocket.isConnected()) {
229             logger.debug(" connIsConnected '{}' - FALSE: tivoSocket.isConnected=false",
230                     tivoConfigData.getCfgIdentifier());
231             return false;
232         } else if (tivoSocket.isClosed()) {
233             logger.debug(" connIsConnected '{}' - FALSE: tivoSocket.isClosed=true", tivoConfigData.getCfgIdentifier());
234             return false;
235         } else if (streamWriter == null) {
236             logger.debug(" connIsConnected '{}' - FALSE: tivoIOSendCommand=null", tivoConfigData.getCfgIdentifier());
237             return false;
238         } else if (streamWriter.checkError()) {
239             logger.debug(" connIsConnected '{}' - FALSE: tivoIOSendCommand.checkError()=true",
240                     tivoConfigData.getCfgIdentifier());
241             return false;
242         } else if (streamReader == null) {
243             logger.debug(" connIsConnected '{}' - FALSE: streamReader=null", tivoConfigData.getCfgIdentifier());
244             return false;
245         }
246         return true;
247     }
248
249     /**
250      * {@link connTivoConnect} manages the creation / retry process of the socket connection.
251      *
252      * @return true = connected, false = not connected
253      * @throws InterruptedException
254      */
255     public boolean connTivoConnect() throws InterruptedException {
256         for (int iL = 1; iL <= tivoConfigData.getNumRetry(); iL++) {
257             logger.debug(" connTivoConnect '{}' - starting connection process '{}' of '{}'.",
258                     tivoConfigData.getCfgIdentifier(), iL, tivoConfigData.getNumRetry());
259
260             // Sort out the socket connection
261             if (connSocketConnect()) {
262                 logger.debug(" connTivoConnect '{}' - Socket created / connection made.",
263                         tivoConfigData.getCfgIdentifier());
264                 StreamReader streamReader = this.streamReader;
265                 if (streamReader != null && streamReader.isAlive()) {
266                     // Send a newline to poke the Tivo to verify it responds with INVALID_COMMAND/COMMAND_TIMEOUT
267                     streamWriter.println("\r");
268                     return true;
269                 }
270             } else {
271                 logger.debug(" connTivoConnect '{}' - Socket creation failed.", tivoConfigData.getCfgIdentifier());
272                 TiVoHandler tivoHandler = this.tivoHandler;
273                 if (tivoHandler != null) {
274                     tivoHandler.setStatusOffline();
275                 }
276             }
277             // Sleep and retry
278             doNappTime();
279         }
280         return false;
281     }
282
283     /**
284      * {@link connTivoReconnect} disconnect and reconnect the socket connection to the TiVo.
285      *
286      * @return boolean true = connection succeeded, false = connection failed
287      * @throws InterruptedException
288      */
289     public boolean connTivoReconnect() throws InterruptedException {
290         connTivoDisconnect();
291         doNappTime();
292         return connTivoConnect();
293     }
294
295     /**
296      * {@link connTivoDisconnect} cleanly closes the socket connection and dependent objects
297      *
298      */
299     public void connTivoDisconnect() throws InterruptedException {
300         TiVoHandler tivoHandler = this.tivoHandler;
301         StreamReader streamReader = this.streamReader;
302         PrintStream streamWriter = this.streamWriter;
303         Socket tivoSocket = this.tivoSocket;
304
305         logger.debug(" connTivoSocket '{}' - requested to disconnect/cleanup connection objects",
306                 tivoConfigData.getCfgIdentifier());
307
308         // if isCfgKeepConnOpen = false, don't set status to OFFLINE since the socket is closed after each command
309         if (tivoHandler != null && tivoConfigData.isKeepConnActive()) {
310             tivoHandler.setStatusOffline();
311         }
312
313         if (streamWriter != null) {
314             streamWriter.close();
315             this.streamWriter = null;
316         }
317
318         try {
319             if (tivoSocket != null) {
320                 tivoSocket.close();
321                 this.tivoSocket = null;
322             }
323         } catch (IOException e) {
324             logger.debug(" TiVo '{}' - I/O exception while disconnecting: '{}'.  Connection closed.",
325                     tivoConfigData.getCfgIdentifier(), e.getMessage());
326         }
327
328         if (streamReader != null) {
329             streamReader.interrupt();
330             streamReader.join(TIMEOUT_SEC);
331             this.streamReader = null;
332         }
333     }
334
335     /**
336      * {@link connSocketConnect} opens a Socket connection to the TiVo. Creates a {@link StreamReader} (Input)
337      * thread to read the responses from the TiVo and a PrintStream (Output) {@link cmdTivoSend}
338      * to send commands to the device.
339      *
340      * @param pConnect true = make a new connection , false = close existing connection
341      * @return boolean true = connection succeeded, false = connection failed
342      * @throws InterruptedException
343      */
344     private synchronized boolean connSocketConnect() throws InterruptedException {
345         logger.debug(" connSocketConnect '{}' - attempting connection to host '{}', port '{}'",
346                 tivoConfigData.getCfgIdentifier(), tivoConfigData.getHost(), tivoConfigData.getTcpPort());
347
348         if (connIsConnected()) {
349             logger.debug(" connSocketConnect '{}' - already connected to host '{}', port '{}'",
350                     tivoConfigData.getCfgIdentifier(), tivoConfigData.getHost(), tivoConfigData.getTcpPort());
351             return true;
352         } else {
353             // something is wrong, so force a disconnect/clean up so we can try again
354             connTivoDisconnect();
355         }
356
357         try {
358             Socket tivoSocket = new Socket(tivoConfigData.getHost(), tivoConfigData.getTcpPort());
359             tivoSocket.setKeepAlive(true);
360             tivoSocket.setSoTimeout(CONFIG_SOCKET_TIMEOUT_MS);
361             tivoSocket.setReuseAddress(true);
362
363             if (tivoSocket.isConnected() && !tivoSocket.isClosed()) {
364                 if (streamWriter == null) {
365                     streamWriter = new PrintStream(tivoSocket.getOutputStream(), false);
366                 }
367                 if (this.streamReader == null) {
368                     StreamReader streamReader = new StreamReader(tivoSocket.getInputStream());
369                     streamReader.start();
370                     this.streamReader = streamReader;
371                 }
372                 this.tivoSocket = tivoSocket;
373             } else {
374                 logger.debug(" connSocketConnect '{}' - socket creation failed to host '{}', port '{}'",
375                         tivoConfigData.getCfgIdentifier(), tivoConfigData.getHost(), tivoConfigData.getTcpPort());
376                 return false;
377             }
378
379             return true;
380
381         } catch (UnknownHostException e) {
382             logger.debug(" TiVo '{}' - while connecting, unexpected host error: '{}'",
383                     tivoConfigData.getCfgIdentifier(), e.getMessage());
384         } catch (IOException e) {
385             if (tivoStatusData.getConnectionStatus() != ConnectionStatus.OFFLINE) {
386                 logger.debug(" TiVo '{}' - I/O exception while connecting: '{}'", tivoConfigData.getCfgIdentifier(),
387                         e.getMessage());
388             }
389         }
390         return false;
391     }
392
393     /**
394      * {@link doNappTime} sleeps for the period specified by the getCmdWaitInterval parameter. Primarily used to allow
395      * the TiVo time to process responses after a command is issued.
396      *
397      * @throws InterruptedException
398      */
399     public void doNappTime() throws InterruptedException {
400         TimeUnit.MILLISECONDS.sleep(tivoConfigData.getCmdWaitInterval());
401     }
402
403     public TivoStatusData getServiceStatus() {
404         return tivoStatusData;
405     }
406
407     public void setServiceStatus(TivoStatusData tivoStatusData) {
408         this.tivoStatusData = tivoStatusData;
409     }
410
411     /**
412      * {@link StreamReader} data stream reader that reads the status data returned from the TiVo.
413      *
414      */
415     public class StreamReader extends Thread {
416         private @Nullable BufferedReader bufferedReader = null;
417
418         /**
419          * {@link StreamReader} construct a data stream reader that reads the status data returned from the TiVo via a
420          * BufferedReader.
421          *
422          * @param inputStream socket input stream.
423          * @throws IOException
424          */
425         public StreamReader(InputStream inputStream) {
426             this.setName("OH-binding-" + thingUid + "-" + tivoConfigData.getHost() + ":" + tivoConfigData.getTcpPort());
427             this.bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
428             this.setDaemon(true);
429         }
430
431         @Override
432         public void run() {
433             try {
434                 logger.debug("streamReader {} is running. ", tivoConfigData.getCfgIdentifier());
435                 while (!Thread.currentThread().isInterrupted()) {
436                     String receivedData = null;
437                     BufferedReader reader = bufferedReader;
438                     if (reader == null) {
439                         throw new IOException("streamReader failed: input stream is null");
440                     }
441
442                     try {
443                         receivedData = reader.readLine();
444                     } catch (SocketTimeoutException e) {
445                         // Do nothing. Just allow the thread to check if it has to stop.
446                     }
447
448                     if (receivedData != null) {
449                         logger.debug("TiVo {} data received: {}", tivoConfigData.getCfgIdentifier(), receivedData);
450                         TivoStatusData commandResult = statusParse(receivedData);
451                         TiVoHandler handler = tivoHandler;
452                         if (handler != null) {
453                             handler.updateTivoStatus(tivoStatusData, commandResult);
454                         }
455                         tivoStatusData = commandResult;
456                     }
457                 }
458
459             } catch (IOException e) {
460                 closeBufferedReader();
461                 logger.debug("TiVo {} is disconnected. ", tivoConfigData.getCfgIdentifier(), e);
462             }
463             closeBufferedReader();
464             logger.debug("streamReader {} is stopped. ", tivoConfigData.getCfgIdentifier());
465         }
466
467         private void closeBufferedReader() {
468             BufferedReader reader = bufferedReader;
469             if (reader != null) {
470                 try {
471                     reader.close();
472                     this.bufferedReader = null;
473                 } catch (IOException e) {
474                     logger.debug("Error closing bufferedReader: {}", e.getMessage(), e);
475                 }
476             }
477         }
478     }
479 }