]> git.basschouten.com Git - openhab-addons.git/blob
ea207afc7a9e4c2d2b6fc1016cf8368b5f3d400c
[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
216             refreshJob = scheduler.schedule(runnable, INIT_POLLING_DELAY_S, TimeUnit.SECONDS);
217             logger.debug("Status collection '{}' will start in '{}' seconds.", getThing().getUID(),
218                     INIT_POLLING_DELAY_S);
219         } else if (tivoConfigData.doPollChanges()) {
220             // Run at intervals
221             refreshJob = scheduler.scheduleWithFixedDelay(runnable, INIT_POLLING_DELAY_S,
222                     tivoConfigData.getPollInterval(), TimeUnit.SECONDS);
223             logger.debug("Status polling '{}' will start in '{}' seconds.", getThing().getUID(), INIT_POLLING_DELAY_S);
224         } else {
225             // Just update the status now
226             tivoConnection.ifPresent(connection -> {
227                 try {
228                     connection.statusRefresh();
229                 } catch (InterruptedException e) {
230                     // TiVo handler disposed or openHAB exiting, do nothing
231                 }
232             });
233         }
234     }
235
236     /**
237      * {@link chChannelChange} performs channel changing operations.
238      *
239      * @param commandKeyword the TiVo command object.
240      * @param command the command parameter.
241      * @return TivoStatusData status of the command.
242      * @throws InterruptedException
243      */
244     private TivoStatusData chChannelChange(String commandKeyword, String command) throws InterruptedException {
245         int channel = -1;
246         int subChannel = -1;
247
248         TivoStatusData tmpStatus = tivoConnection.get().getServiceStatus();
249         try {
250             // Parse the channel number and if there is a decimal, the sub-channel number (OTA channels)
251             Matcher matcher = NUMERIC_PATTERN.matcher(command);
252             if (matcher.find()) {
253                 if (matcher.groupCount() >= 1) {
254                     channel = Integer.parseInt(matcher.group(1).trim());
255                 }
256                 if (matcher.groupCount() >= 2 && matcher.group(2) != null) {
257                     subChannel = Integer.parseInt(matcher.group(2).trim());
258                 }
259             } else {
260                 // The command string was not a number, throw exception to catch & log below
261                 throw new NumberFormatException();
262             }
263
264             String tmpCommand = commandKeyword + " " + channel + ((subChannel != -1) ? (" " + subChannel) : "");
265             logger.debug("chChannelChange '{}' sending command to tivo: '{}'", getThing().getUID(), tmpCommand);
266
267             // Attempt to execute the command on the tivo
268             tivoConnection.get().cmdTivoSend(tmpCommand);
269             TimeUnit.MILLISECONDS.sleep(tivoConfigData.getCmdWaitInterval() * 2);
270
271             tmpStatus = tivoConnection.get().getServiceStatus();
272
273             // Check to see if the command was successful
274             if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT && tmpStatus.isCmdOk()) {
275                 if (tmpStatus.getMsg().contains("CH_STATUS")) {
276                     return tmpStatus;
277                 }
278             } else if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT) {
279                 logger.warn("TiVo'{}' set channel command failed '{}' with msg '{}'", getThing().getUID(), tmpCommand,
280                         tmpStatus.getMsg());
281                 switch (tmpStatus.getMsg()) {
282                     case "CH_FAILED NO_LIVE":
283                         tmpStatus.setChannelNum(channel);
284                         tmpStatus.setSubChannelNum(subChannel);
285                         return tmpStatus;
286                     case "CH_FAILED RECORDING":
287                     case "CH_FAILED MISSING_CHANNEL":
288                     case "CH_FAILED MALFORMED_CHANNEL":
289                     case "CH_FAILED INVALID_CHANNEL":
290                         return tmpStatus;
291                     case "NO_STATUS_DATA_RETURNED":
292                         tmpStatus.setChannelNum(-1);
293                         tmpStatus.setSubChannelNum(-1);
294                         tmpStatus.setRecording(false);
295                         return tmpStatus;
296                 }
297             }
298
299         } catch (NumberFormatException e) {
300             logger.warn("TiVo'{}' unable to parse channel integer, value sent was: '{}'", getThing().getUID(),
301                     command.toString());
302         }
303         return tmpStatus;
304     }
305
306     /**
307      * {@link updateTivoStatus} populates the items with the status / channel information.
308      *
309      * @param tivoStatusData the {@link TivoStatusData}
310      */
311     public void updateTivoStatus(TivoStatusData oldStatusData, TivoStatusData newStatusData) {
312         if (newStatusData.getConnectionStatus() != ConnectionStatus.INIT) {
313             // Update Item Status
314             if (newStatusData.getPubToUI()) {
315                 if (oldStatusData.getConnectionStatus() == ConnectionStatus.INIT
316                         || !(oldStatusData.getMsg().contentEquals(newStatusData.getMsg()))) {
317                     updateState(CHANNEL_TIVO_STATUS, new StringType(newStatusData.getMsg()));
318                 }
319                 // If the cmd was successful, publish the channel numbers
320                 if (newStatusData.isCmdOk() && newStatusData.getChannelNum() != -1) {
321                     if (oldStatusData.getConnectionStatus() == ConnectionStatus.INIT
322                             || oldStatusData.getChannelNum() != newStatusData.getChannelNum()
323                             || oldStatusData.getSubChannelNum() != newStatusData.getSubChannelNum()) {
324                         if (newStatusData.getSubChannelNum() == -1) {
325                             updateState(CHANNEL_TIVO_CHANNEL_FORCE, new DecimalType(newStatusData.getChannelNum()));
326                             updateState(CHANNEL_TIVO_CHANNEL_SET, new DecimalType(newStatusData.getChannelNum()));
327                         } else {
328                             updateState(CHANNEL_TIVO_CHANNEL_FORCE, new DecimalType(
329                                     newStatusData.getChannelNum() + "." + newStatusData.getSubChannelNum()));
330                             updateState(CHANNEL_TIVO_CHANNEL_SET, new DecimalType(
331                                     newStatusData.getChannelNum() + "." + newStatusData.getSubChannelNum()));
332                         }
333                     }
334                     updateState(CHANNEL_TIVO_IS_RECORDING, newStatusData.isRecording() ? OnOffType.ON : OnOffType.OFF);
335                 }
336
337                 // Now set the pubToUI flag to false, as we have already published this status
338                 if (isLinked(CHANNEL_TIVO_STATUS) || isLinked(CHANNEL_TIVO_CHANNEL_FORCE)
339                         || isLinked(CHANNEL_TIVO_CHANNEL_SET)) {
340                     newStatusData.setPubToUI(false);
341                     tivoConnection.get().setServiceStatus(newStatusData);
342                 }
343             }
344
345             // Update Thing status
346             if (newStatusData.getConnectionStatus() != lastConnectionStatus) {
347                 switch (newStatusData.getConnectionStatus()) {
348                     case OFFLINE:
349                         this.setStatusOffline();
350                         break;
351                     case ONLINE:
352                         updateStatus(ThingStatus.ONLINE);
353                         break;
354                     case STANDBY:
355                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
356                                 "STANDBY MODE: Send command TIVO to Remote Control Button (IRCODE) item to wakeup.");
357                         break;
358                     case UNKNOWN:
359                         updateStatus(ThingStatus.OFFLINE);
360                         break;
361                     case INIT:
362                         break;
363                 }
364                 lastConnectionStatus = newStatusData.getConnectionStatus();
365             }
366         }
367     }
368 }