]> git.basschouten.com Git - openhab-addons.git/blob
5abfb938fc0cdda2221a87b51c7008b423adf161
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.isEmpty()) {
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         lastConnectionStatus = ConnectionStatus.UNKNOWN;
119         this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
120                 "Power on device or check network configuration/connection.");
121     }
122
123     private void sendCommand(String commandKeyword, String commandParameter, TivoStatusData currentStatus)
124             throws InterruptedException {
125         if (tivoConnection.isEmpty()) {
126             return;
127         }
128
129         TivoStatusData deviceStatus = tivoConnection.get().getServiceStatus();
130         TivoStatusData commandResult = null;
131         logger.debug("handleCommand '{}' - {} found!", getThing().getUID(), commandKeyword);
132         // Re-write command keyword if we are in STANDBY, as only IRCODE TIVO will wake the unit from
133         // standby mode, otherwise just execute the commands
134         if (deviceStatus.getConnectionStatus() == ConnectionStatus.STANDBY && commandKeyword.contentEquals("TELEPORT")
135                 && commandParameter.contentEquals("TIVO")) {
136             String command = "IRCODE " + commandParameter;
137             logger.debug("TiVo '{}' TELEPORT re-mapped to IRCODE as we are in standby: '{}'", getThing().getUID(),
138                     command);
139             commandResult = tivoConnection.get().cmdTivoSend(command);
140         } else if (commandKeyword.contentEquals("FORCECH") || commandKeyword.contentEquals("SETCH")) {
141             commandResult = chChannelChange(commandKeyword, commandParameter);
142         } else {
143             commandResult = tivoConnection.get().cmdTivoSend(commandKeyword + " " + commandParameter);
144         }
145
146         // Post processing
147         if (commandResult != null && commandParameter.contentEquals("STANDBY")) {
148             // Force thing state into STANDBY as this command does not return a status when executed
149             commandResult.setConnectionStatus(ConnectionStatus.STANDBY);
150         }
151
152         // Push status updates
153         if (commandResult != null && commandResult.isCmdOk()) {
154             updateTivoStatus(currentStatus, commandResult);
155         }
156
157         if (!tivoConfigData.isKeepConnActive()) {
158             // disconnect once command is complete
159             tivoConnection.get().connTivoDisconnect();
160         }
161     }
162
163     @Override
164     public void initialize() {
165         logger.debug("Initializing a TiVo '{}' with config options", getThing().getUID());
166
167         tivoConfigData = getConfigAs(TivoConfigData.class);
168
169         tivoConfigData.setCfgIdentifier(getThing().getUID().getAsString());
170         tivoConnection = Optional.of(new TivoStatusProvider(tivoConfigData, this));
171
172         updateStatus(ThingStatus.UNKNOWN);
173         lastConnectionStatus = ConnectionStatus.UNKNOWN;
174         logger.debug("Initializing a TiVo handler for thing '{}' - finished!", getThing().getUID());
175
176         startPollStatus();
177     }
178
179     @Override
180     public void dispose() {
181         logger.debug("Disposing of a TiVo handler for thing '{}'", getThing().getUID());
182
183         ScheduledFuture<?> refreshJob = this.refreshJob;
184         if (refreshJob != null) {
185             refreshJob.cancel(false);
186             this.refreshJob = null;
187         }
188
189         if (tivoConnection.isPresent()) {
190             try {
191                 tivoConnection.get().connTivoDisconnect();
192             } catch (InterruptedException e) {
193                 // TiVo handler disposed or openHAB exiting, do nothing
194             }
195             tivoConnection = Optional.empty();
196         }
197     }
198
199     /**
200      * {@link startPollStatus} scheduled job to poll for changes in state.
201      */
202     private void startPollStatus() {
203         Runnable runnable = () -> {
204             logger.debug("startPollStatus '{}' @ rate of '{}' seconds", getThing().getUID(),
205                     tivoConfigData.getPollInterval());
206             tivoConnection.ifPresent(connection -> {
207                 try {
208                     connection.statusRefresh();
209                 } catch (InterruptedException e) {
210                     // TiVo handler disposed or openHAB exiting, do nothing
211                 }
212             });
213         };
214
215         if (tivoConfigData.isKeepConnActive()) {
216             // Run once every 12 hours to keep the connection from going stale
217             refreshJob = scheduler.scheduleWithFixedDelay(runnable, INIT_POLLING_DELAY_S, POLLING_DELAY_12HR_S,
218                     TimeUnit.SECONDS);
219             logger.debug("Status collection '{}' will start in '{}' seconds.", getThing().getUID(),
220                     INIT_POLLING_DELAY_S);
221         } else if (tivoConfigData.doPollChanges()) {
222             // Run at intervals
223             refreshJob = scheduler.scheduleWithFixedDelay(runnable, INIT_POLLING_DELAY_S,
224                     tivoConfigData.getPollInterval(), TimeUnit.SECONDS);
225             logger.debug("Status polling '{}' will start in '{}' seconds.", getThing().getUID(), INIT_POLLING_DELAY_S);
226         } else {
227             // Just update the status now
228             tivoConnection.ifPresent(connection -> {
229                 try {
230                     connection.statusRefresh();
231                 } catch (InterruptedException e) {
232                     // TiVo handler disposed or openHAB exiting, do nothing
233                 }
234             });
235         }
236     }
237
238     /**
239      * {@link chChannelChange} performs channel changing operations.
240      *
241      * @param commandKeyword the TiVo command object.
242      * @param command the command parameter.
243      * @return TivoStatusData status of the command.
244      * @throws InterruptedException
245      */
246     private TivoStatusData chChannelChange(String commandKeyword, String command) throws InterruptedException {
247         int channel = -1;
248         int subChannel = -1;
249
250         TivoStatusData tmpStatus = tivoConnection.get().getServiceStatus();
251         try {
252             // Parse the channel number and if there is a decimal, the sub-channel number (OTA channels)
253             Matcher matcher = NUMERIC_PATTERN.matcher(command);
254             if (matcher.find()) {
255                 if (matcher.groupCount() >= 1) {
256                     channel = Integer.parseInt(matcher.group(1).trim());
257                 }
258                 if (matcher.groupCount() >= 2 && matcher.group(2) != null) {
259                     subChannel = Integer.parseInt(matcher.group(2).trim());
260                 }
261             } else {
262                 // The command string was not a number, throw exception to catch & log below
263                 throw new NumberFormatException();
264             }
265
266             String tmpCommand = commandKeyword + " " + channel + ((subChannel != -1) ? (" " + subChannel) : "");
267             logger.debug("chChannelChange '{}' sending command to tivo: '{}'", getThing().getUID(), tmpCommand);
268
269             // Attempt to execute the command on the tivo
270             tivoConnection.get().cmdTivoSend(tmpCommand);
271             TimeUnit.MILLISECONDS.sleep(tivoConfigData.getCmdWaitInterval() * 2);
272
273             tmpStatus = tivoConnection.get().getServiceStatus();
274
275             // Check to see if the command was successful
276             if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT && tmpStatus.isCmdOk()) {
277                 if (tmpStatus.getMsg().contains("CH_STATUS")) {
278                     return tmpStatus;
279                 }
280             } else if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT) {
281                 logger.warn("TiVo'{}' set channel command failed '{}' with msg '{}'", getThing().getUID(), tmpCommand,
282                         tmpStatus.getMsg());
283                 switch (tmpStatus.getMsg()) {
284                     case "CH_FAILED NO_LIVE":
285                         tmpStatus.setChannelNum(channel);
286                         tmpStatus.setSubChannelNum(subChannel);
287                         return tmpStatus;
288                     case "CH_FAILED RECORDING":
289                     case "CH_FAILED MISSING_CHANNEL":
290                     case "CH_FAILED MALFORMED_CHANNEL":
291                     case "CH_FAILED INVALID_CHANNEL":
292                         return tmpStatus;
293                     case "NO_STATUS_DATA_RETURNED":
294                         tmpStatus.setChannelNum(-1);
295                         tmpStatus.setSubChannelNum(-1);
296                         tmpStatus.setRecording(false);
297                         return tmpStatus;
298                 }
299             }
300
301         } catch (NumberFormatException e) {
302             logger.warn("TiVo'{}' unable to parse channel integer, value sent was: '{}'", getThing().getUID(), command);
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 }