]> git.basschouten.com Git - openhab-addons.git/blob
4546be62fa01040574ab6b2695dd1a3d648e6947
[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.handler;
14
15 import static org.openhab.binding.tivo.internal.TiVoBindingConstants.*;
16
17 import java.util.Optional;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.tivo.internal.service.TivoConfigData;
26 import org.openhab.binding.tivo.internal.service.TivoStatusData;
27 import org.openhab.binding.tivo.internal.service.TivoStatusData.ConnectionStatus;
28 import org.openhab.binding.tivo.internal.service.TivoStatusProvider;
29 import org.openhab.core.library.types.DecimalType;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.StringType;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.thing.binding.BaseThingHandler;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.types.RefreshType;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * The {@link TiVoHandler} is the BaseThingHandler responsible for handling commands that are
44  * sent to one of the Tivo's channels.
45  *
46  * @author Jayson Kubilis (DigitalBytes) - Initial contribution
47  * @author Andrew Black (AndyXMB) - Updates / compilation corrections. Addition of channel scanning functionality.
48  * @author Michael Lobstein - Updated for OH3
49  */
50
51 @NonNullByDefault
52 public class TiVoHandler extends BaseThingHandler {
53     private static final Pattern NUMERIC_PATTERN = Pattern.compile("(\\d+)\\.?(\\d+)?");
54
55     private final Logger logger = LoggerFactory.getLogger(TiVoHandler.class);
56     private TivoConfigData tivoConfigData = new TivoConfigData();
57     private ConnectionStatus lastConnectionStatus = ConnectionStatus.UNKNOWN;
58     private Optional<TivoStatusProvider> tivoConnection = Optional.empty();
59     private @Nullable ScheduledFuture<?> refreshJob;
60
61     /**
62      * Instantiates a new TiVo handler.
63      *
64      * @param thing the thing
65      */
66     public TiVoHandler(Thing thing) {
67         super(thing);
68         logger.debug("TiVoHandler '{}' - creating", getThing().getUID());
69     }
70
71     @Override
72     public void handleCommand(ChannelUID channelUID, Command command) {
73         // Handles the commands from the various TiVo channel objects
74         logger.debug("handleCommand '{}', parameter: {}", channelUID, command);
75
76         if (!isInitialized() || !tivoConnection.isPresent()) {
77             logger.debug("handleCommand '{}' device is not initialized yet, command '{}' will be ignored.",
78                     getThing().getUID(), channelUID + " " + command);
79             return;
80         }
81
82         TivoStatusData currentStatus = tivoConnection.get().getServiceStatus();
83         String commandKeyword = "";
84
85         String commandParameter = command.toString().toUpperCase();
86         if (command instanceof RefreshType) {
87             // Future enhancement, if we can come up with a sensible set of actions when a REFRESH is issued
88             logger.debug("TiVo '{}' skipping REFRESH command for channel: '{}'.", getThing().getUID(),
89                     channelUID.getId());
90             return;
91         }
92
93         switch (channelUID.getId()) {
94             case CHANNEL_TIVO_CHANNEL_FORCE:
95                 commandKeyword = "FORCECH";
96                 break;
97             case CHANNEL_TIVO_CHANNEL_SET:
98                 commandKeyword = "SETCH";
99                 break;
100             case CHANNEL_TIVO_TELEPORT:
101                 commandKeyword = "TELEPORT";
102                 break;
103             case CHANNEL_TIVO_IRCMD:
104                 commandKeyword = "IRCODE";
105                 break;
106             case CHANNEL_TIVO_KBDCMD:
107                 commandKeyword = "KEYBOARD";
108                 break;
109         }
110         try {
111             sendCommand(commandKeyword, commandParameter, currentStatus);
112         } catch (InterruptedException e) {
113             // TiVo handler disposed or openHAB exiting, do nothing
114         }
115     }
116
117     public void setStatusOffline() {
118         this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
119                 "Power on device or check network configuration/connection.");
120     }
121
122     private void sendCommand(String commandKeyword, String commandParameter, TivoStatusData currentStatus)
123             throws InterruptedException {
124         if (!tivoConnection.isPresent()) {
125             return;
126         }
127
128         TivoStatusData deviceStatus = tivoConnection.get().getServiceStatus();
129         TivoStatusData commandResult = null;
130         logger.debug("handleCommand '{}' - {} found!", getThing().getUID(), commandKeyword);
131         // Re-write command keyword if we are in STANDBY, as only IRCODE TIVO will wake the unit from
132         // standby mode, otherwise just execute the commands
133         if (deviceStatus.getConnectionStatus() == ConnectionStatus.STANDBY && commandKeyword.contentEquals("TELEPORT")
134                 && commandParameter.contentEquals("TIVO")) {
135             String command = "IRCODE " + commandParameter;
136             logger.debug("TiVo '{}' TELEPORT re-mapped to IRCODE as we are in standby: '{}'", getThing().getUID(),
137                     command);
138             commandResult = tivoConnection.get().cmdTivoSend(command);
139         } else if (commandKeyword.contentEquals("FORCECH") || commandKeyword.contentEquals("SETCH")) {
140             commandResult = chChannelChange(commandKeyword, commandParameter);
141         } else {
142             commandResult = tivoConnection.get().cmdTivoSend(commandKeyword + " " + commandParameter);
143         }
144
145         // Post processing
146         if (commandResult != null && commandParameter.contentEquals("STANDBY")) {
147             // Force thing state into STANDBY as this command does not return a status when executed
148             commandResult.setConnectionStatus(ConnectionStatus.STANDBY);
149         }
150
151         // Push status updates
152         if (commandResult != null && commandResult.isCmdOk()) {
153             updateTivoStatus(currentStatus, commandResult);
154         }
155
156         if (!tivoConfigData.isKeepConnActive()) {
157             // disconnect once command is complete
158             tivoConnection.get().connTivoDisconnect();
159         }
160     }
161
162     @Override
163     public void initialize() {
164         logger.debug("Initializing a TiVo '{}' with config options", getThing().getUID());
165
166         tivoConfigData = getConfigAs(TivoConfigData.class);
167
168         tivoConfigData.setCfgIdentifier(getThing().getUID().getAsString());
169         tivoConnection = Optional.of(new TivoStatusProvider(tivoConfigData, this));
170
171         updateStatus(ThingStatus.UNKNOWN);
172         lastConnectionStatus = ConnectionStatus.UNKNOWN;
173         logger.debug("Initializing a TiVo handler for thing '{}' - finished!", getThing().getUID());
174
175         startPollStatus();
176     }
177
178     @Override
179     public void dispose() {
180         logger.debug("Disposing of a TiVo handler for thing '{}'", getThing().getUID());
181
182         ScheduledFuture<?> refreshJob = this.refreshJob;
183         if (refreshJob != null) {
184             refreshJob.cancel(false);
185             this.refreshJob = null;
186         }
187
188         if (tivoConnection.isPresent()) {
189             try {
190                 tivoConnection.get().connTivoDisconnect();
191             } catch (InterruptedException e) {
192                 // TiVo handler disposed or openHAB exiting, do nothing
193             }
194             tivoConnection = Optional.empty();
195         }
196     }
197
198     /**
199      * {@link startPollStatus} scheduled job to poll for changes in state.
200      */
201     private void startPollStatus() {
202         Runnable runnable = () -> {
203             logger.debug("startPollStatus '{}' @ rate of '{}' seconds", getThing().getUID(),
204                     tivoConfigData.getPollInterval());
205             tivoConnection.ifPresent(connection -> {
206                 try {
207                     connection.statusRefresh();
208                 } catch (InterruptedException e) {
209                     // TiVo handler disposed or openHAB exiting, do nothing
210                 }
211             });
212         };
213
214         if (tivoConfigData.isKeepConnActive()) {
215             // Run once every 12 hours to keep the connection from going stale
216             refreshJob = scheduler.scheduleWithFixedDelay(runnable, INIT_POLLING_DELAY_S, POLLING_DELAY_12HR_S,
217                     TimeUnit.SECONDS);
218             logger.debug("Status collection '{}' will start in '{}' seconds.", getThing().getUID(),
219                     INIT_POLLING_DELAY_S);
220         } else if (tivoConfigData.doPollChanges()) {
221             // Run at intervals
222             refreshJob = scheduler.scheduleWithFixedDelay(runnable, INIT_POLLING_DELAY_S,
223                     tivoConfigData.getPollInterval(), TimeUnit.SECONDS);
224             logger.debug("Status polling '{}' will start in '{}' seconds.", getThing().getUID(), INIT_POLLING_DELAY_S);
225         } else {
226             // Just update the status now
227             tivoConnection.ifPresent(connection -> {
228                 try {
229                     connection.statusRefresh();
230                 } catch (InterruptedException e) {
231                     // TiVo handler disposed or openHAB exiting, do nothing
232                 }
233             });
234         }
235     }
236
237     /**
238      * {@link chChannelChange} performs channel changing operations.
239      *
240      * @param commandKeyword the TiVo command object.
241      * @param command the command parameter.
242      * @return TivoStatusData status of the command.
243      * @throws InterruptedException
244      */
245     private TivoStatusData chChannelChange(String commandKeyword, String command) throws InterruptedException {
246         int channel = -1;
247         int subChannel = -1;
248
249         TivoStatusData tmpStatus = tivoConnection.get().getServiceStatus();
250         try {
251             // Parse the channel number and if there is a decimal, the sub-channel number (OTA channels)
252             Matcher matcher = NUMERIC_PATTERN.matcher(command);
253             if (matcher.find()) {
254                 if (matcher.groupCount() >= 1) {
255                     channel = Integer.parseInt(matcher.group(1).trim());
256                 }
257                 if (matcher.groupCount() >= 2 && matcher.group(2) != null) {
258                     subChannel = Integer.parseInt(matcher.group(2).trim());
259                 }
260             } else {
261                 // The command string was not a number, throw exception to catch & log below
262                 throw new NumberFormatException();
263             }
264
265             String tmpCommand = commandKeyword + " " + channel + ((subChannel != -1) ? (" " + subChannel) : "");
266             logger.debug("chChannelChange '{}' sending command to tivo: '{}'", getThing().getUID(), tmpCommand);
267
268             // Attempt to execute the command on the tivo
269             tivoConnection.get().cmdTivoSend(tmpCommand);
270             TimeUnit.MILLISECONDS.sleep(tivoConfigData.getCmdWaitInterval() * 2);
271
272             tmpStatus = tivoConnection.get().getServiceStatus();
273
274             // Check to see if the command was successful
275             if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT && tmpStatus.isCmdOk()) {
276                 if (tmpStatus.getMsg().contains("CH_STATUS")) {
277                     return tmpStatus;
278                 }
279             } else if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT) {
280                 logger.warn("TiVo'{}' set channel command failed '{}' with msg '{}'", getThing().getUID(), tmpCommand,
281                         tmpStatus.getMsg());
282                 switch (tmpStatus.getMsg()) {
283                     case "CH_FAILED NO_LIVE":
284                         tmpStatus.setChannelNum(channel);
285                         tmpStatus.setSubChannelNum(subChannel);
286                         return tmpStatus;
287                     case "CH_FAILED RECORDING":
288                     case "CH_FAILED MISSING_CHANNEL":
289                     case "CH_FAILED MALFORMED_CHANNEL":
290                     case "CH_FAILED INVALID_CHANNEL":
291                         return tmpStatus;
292                     case "NO_STATUS_DATA_RETURNED":
293                         tmpStatus.setChannelNum(-1);
294                         tmpStatus.setSubChannelNum(-1);
295                         tmpStatus.setRecording(false);
296                         return tmpStatus;
297                 }
298             }
299
300         } catch (NumberFormatException e) {
301             logger.warn("TiVo'{}' unable to parse channel integer, value sent was: '{}'", getThing().getUID(),
302                     command.toString());
303         }
304         return tmpStatus;
305     }
306
307     /**
308      * {@link updateTivoStatus} populates the items with the status / channel information.
309      *
310      * @param tivoStatusData the {@link TivoStatusData}
311      */
312     public void updateTivoStatus(TivoStatusData oldStatusData, TivoStatusData newStatusData) {
313         if (newStatusData.getConnectionStatus() != ConnectionStatus.INIT) {
314             // Update Item Status
315             if (newStatusData.getPubToUI()) {
316                 if (oldStatusData.getConnectionStatus() == ConnectionStatus.INIT
317                         || !(oldStatusData.getMsg().contentEquals(newStatusData.getMsg()))) {
318                     updateState(CHANNEL_TIVO_STATUS, new StringType(newStatusData.getMsg()));
319                 }
320                 // If the cmd was successful, publish the channel numbers
321                 if (newStatusData.isCmdOk() && newStatusData.getChannelNum() != -1) {
322                     if (oldStatusData.getConnectionStatus() == ConnectionStatus.INIT
323                             || oldStatusData.getChannelNum() != newStatusData.getChannelNum()
324                             || oldStatusData.getSubChannelNum() != newStatusData.getSubChannelNum()) {
325                         if (newStatusData.getSubChannelNum() == -1) {
326                             updateState(CHANNEL_TIVO_CHANNEL_FORCE, new DecimalType(newStatusData.getChannelNum()));
327                             updateState(CHANNEL_TIVO_CHANNEL_SET, new DecimalType(newStatusData.getChannelNum()));
328                         } else {
329                             updateState(CHANNEL_TIVO_CHANNEL_FORCE, new DecimalType(
330                                     newStatusData.getChannelNum() + "." + newStatusData.getSubChannelNum()));
331                             updateState(CHANNEL_TIVO_CHANNEL_SET, new DecimalType(
332                                     newStatusData.getChannelNum() + "." + newStatusData.getSubChannelNum()));
333                         }
334                     }
335                     updateState(CHANNEL_TIVO_IS_RECORDING, newStatusData.isRecording() ? OnOffType.ON : OnOffType.OFF);
336                 }
337
338                 // Now set the pubToUI flag to false, as we have already published this status
339                 if (isLinked(CHANNEL_TIVO_STATUS) || isLinked(CHANNEL_TIVO_CHANNEL_FORCE)
340                         || isLinked(CHANNEL_TIVO_CHANNEL_SET)) {
341                     newStatusData.setPubToUI(false);
342                     tivoConnection.get().setServiceStatus(newStatusData);
343                 }
344             }
345
346             // Update Thing status
347             if (newStatusData.getConnectionStatus() != lastConnectionStatus) {
348                 switch (newStatusData.getConnectionStatus()) {
349                     case OFFLINE:
350                         this.setStatusOffline();
351                         break;
352                     case ONLINE:
353                         updateStatus(ThingStatus.ONLINE);
354                         break;
355                     case STANDBY:
356                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
357                                 "STANDBY MODE: Send command TIVO to Remote Control Button (IRCODE) item to wakeup.");
358                         break;
359                     case UNKNOWN:
360                         updateStatus(ThingStatus.OFFLINE);
361                         break;
362                     case INIT:
363                         break;
364                 }
365                 lastConnectionStatus = newStatusData.getConnectionStatus();
366             }
367         }
368     }
369 }