2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.tivo.internal.handler;
15 import static org.openhab.binding.tivo.internal.TiVoBindingConstants.*;
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;
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;
43 * The {@link TiVoHandler} is the BaseThingHandler responsible for handling commands that are
44 * sent to one of the Tivo's channels.
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
52 public class TiVoHandler extends BaseThingHandler {
53 private static final Pattern NUMERIC_PATTERN = Pattern.compile("(\\d+)\\.?(\\d+)?");
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;
62 * Instantiates a new TiVo handler.
64 * @param thing the thing
66 public TiVoHandler(Thing thing) {
68 logger.debug("TiVoHandler '{}' - creating", getThing().getUID());
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);
76 if (!isInitialized() || !tivoConnection.isPresent()) {
77 logger.debug("handleCommand '{}' device is not initialized yet, command '{}' will be ignored.",
78 getThing().getUID(), channelUID + " " + command);
82 TivoStatusData currentStatus = tivoConnection.get().getServiceStatus();
83 String commandKeyword = "";
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(),
93 switch (channelUID.getId()) {
94 case CHANNEL_TIVO_CHANNEL_FORCE:
95 commandKeyword = "FORCECH";
97 case CHANNEL_TIVO_CHANNEL_SET:
98 commandKeyword = "SETCH";
100 case CHANNEL_TIVO_TELEPORT:
101 commandKeyword = "TELEPORT";
103 case CHANNEL_TIVO_IRCMD:
104 commandKeyword = "IRCODE";
106 case CHANNEL_TIVO_KBDCMD:
107 commandKeyword = "KEYBOARD";
111 sendCommand(commandKeyword, commandParameter, currentStatus);
112 } catch (InterruptedException e) {
113 // TiVo handler disposed or openHAB exiting, do nothing
117 public void setStatusOffline() {
118 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
119 "Power on device or check network configuration/connection.");
122 private void sendCommand(String commandKeyword, String commandParameter, TivoStatusData currentStatus)
123 throws InterruptedException {
124 if (!tivoConnection.isPresent()) {
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(),
138 commandResult = tivoConnection.get().cmdTivoSend(command);
139 } else if (commandKeyword.contentEquals("FORCECH") || commandKeyword.contentEquals("SETCH")) {
140 commandResult = chChannelChange(commandKeyword, commandParameter);
142 commandResult = tivoConnection.get().cmdTivoSend(commandKeyword + " " + commandParameter);
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);
151 // Push status updates
152 if (commandResult != null && commandResult.isCmdOk()) {
153 updateTivoStatus(currentStatus, commandResult);
156 if (!tivoConfigData.isKeepConnActive()) {
157 // disconnect once command is complete
158 tivoConnection.get().connTivoDisconnect();
163 public void initialize() {
164 logger.debug("Initializing a TiVo '{}' with config options", getThing().getUID());
166 tivoConfigData = getConfigAs(TivoConfigData.class);
168 tivoConfigData.setCfgIdentifier(getThing().getUID().getAsString());
169 tivoConnection = Optional.of(new TivoStatusProvider(tivoConfigData, this));
171 updateStatus(ThingStatus.UNKNOWN);
172 lastConnectionStatus = ConnectionStatus.UNKNOWN;
173 logger.debug("Initializing a TiVo handler for thing '{}' - finished!", getThing().getUID());
179 public void dispose() {
180 logger.debug("Disposing of a TiVo handler for thing '{}'", getThing().getUID());
182 ScheduledFuture<?> refreshJob = this.refreshJob;
183 if (refreshJob != null) {
184 refreshJob.cancel(false);
185 this.refreshJob = null;
188 if (tivoConnection.isPresent()) {
190 tivoConnection.get().connTivoDisconnect();
191 } catch (InterruptedException e) {
192 // TiVo handler disposed or openHAB exiting, do nothing
194 tivoConnection = Optional.empty();
199 * {@link startPollStatus} scheduled job to poll for changes in state.
201 private void startPollStatus() {
202 Runnable runnable = () -> {
203 logger.debug("startPollStatus '{}' @ rate of '{}' seconds", getThing().getUID(),
204 tivoConfigData.getPollInterval());
205 tivoConnection.ifPresent(connection -> {
207 connection.statusRefresh();
208 } catch (InterruptedException e) {
209 // TiVo handler disposed or openHAB exiting, do nothing
214 if (tivoConfigData.isKeepConnActive()) {
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()) {
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);
225 // Just update the status now
226 tivoConnection.ifPresent(connection -> {
228 connection.statusRefresh();
229 } catch (InterruptedException e) {
230 // TiVo handler disposed or openHAB exiting, do nothing
237 * {@link chChannelChange} performs channel changing operations.
239 * @param commandKeyword the TiVo command object.
240 * @param command the command parameter.
241 * @return TivoStatusData status of the command.
242 * @throws InterruptedException
244 private TivoStatusData chChannelChange(String commandKeyword, String command) throws InterruptedException {
248 TivoStatusData tmpStatus = tivoConnection.get().getServiceStatus();
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());
256 if (matcher.groupCount() >= 2 && matcher.group(2) != null) {
257 subChannel = Integer.parseInt(matcher.group(2).trim());
260 // The command string was not a number, throw exception to catch & log below
261 throw new NumberFormatException();
264 String tmpCommand = commandKeyword + " " + channel + ((subChannel != -1) ? (" " + subChannel) : "");
265 logger.debug("chChannelChange '{}' sending command to tivo: '{}'", getThing().getUID(), tmpCommand);
267 // Attempt to execute the command on the tivo
268 tivoConnection.get().cmdTivoSend(tmpCommand);
269 TimeUnit.MILLISECONDS.sleep(tivoConfigData.getCmdWaitInterval() * 2);
271 tmpStatus = tivoConnection.get().getServiceStatus();
273 // Check to see if the command was successful
274 if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT && tmpStatus.isCmdOk()) {
275 if (tmpStatus.getMsg().contains("CH_STATUS")) {
278 } else if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT) {
279 logger.warn("TiVo'{}' set channel command failed '{}' with msg '{}'", getThing().getUID(), tmpCommand,
281 switch (tmpStatus.getMsg()) {
282 case "CH_FAILED NO_LIVE":
283 tmpStatus.setChannelNum(channel);
284 tmpStatus.setSubChannelNum(subChannel);
286 case "CH_FAILED RECORDING":
287 case "CH_FAILED MISSING_CHANNEL":
288 case "CH_FAILED MALFORMED_CHANNEL":
289 case "CH_FAILED INVALID_CHANNEL":
291 case "NO_STATUS_DATA_RETURNED":
292 tmpStatus.setChannelNum(-1);
293 tmpStatus.setSubChannelNum(-1);
294 tmpStatus.setRecording(false);
299 } catch (NumberFormatException e) {
300 logger.warn("TiVo'{}' unable to parse channel integer, value sent was: '{}'", getThing().getUID(),
307 * {@link updateTivoStatus} populates the items with the status / channel information.
309 * @param tivoStatusData the {@link TivoStatusData}
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()));
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()));
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()));
334 updateState(CHANNEL_TIVO_IS_RECORDING, newStatusData.isRecording() ? OnOffType.ON : OnOffType.OFF);
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);
345 // Update Thing status
346 if (newStatusData.getConnectionStatus() != lastConnectionStatus) {
347 switch (newStatusData.getConnectionStatus()) {
349 this.setStatusOffline();
352 updateStatus(ThingStatus.ONLINE);
355 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
356 "STANDBY MODE: Send command TIVO to Remote Control Button (IRCODE) item to wakeup.");
359 updateStatus(ThingStatus.OFFLINE);
364 lastConnectionStatus = newStatusData.getConnectionStatus();