2 * Copyright (c) 2010-2023 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 lastConnectionStatus = ConnectionStatus.UNKNOWN;
119 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
120 "Power on device or check network configuration/connection.");
123 private void sendCommand(String commandKeyword, String commandParameter, TivoStatusData currentStatus)
124 throws InterruptedException {
125 if (!tivoConnection.isPresent()) {
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(),
139 commandResult = tivoConnection.get().cmdTivoSend(command);
140 } else if (commandKeyword.contentEquals("FORCECH") || commandKeyword.contentEquals("SETCH")) {
141 commandResult = chChannelChange(commandKeyword, commandParameter);
143 commandResult = tivoConnection.get().cmdTivoSend(commandKeyword + " " + commandParameter);
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);
152 // Push status updates
153 if (commandResult != null && commandResult.isCmdOk()) {
154 updateTivoStatus(currentStatus, commandResult);
157 if (!tivoConfigData.isKeepConnActive()) {
158 // disconnect once command is complete
159 tivoConnection.get().connTivoDisconnect();
164 public void initialize() {
165 logger.debug("Initializing a TiVo '{}' with config options", getThing().getUID());
167 tivoConfigData = getConfigAs(TivoConfigData.class);
169 tivoConfigData.setCfgIdentifier(getThing().getUID().getAsString());
170 tivoConnection = Optional.of(new TivoStatusProvider(tivoConfigData, this));
172 updateStatus(ThingStatus.UNKNOWN);
173 lastConnectionStatus = ConnectionStatus.UNKNOWN;
174 logger.debug("Initializing a TiVo handler for thing '{}' - finished!", getThing().getUID());
180 public void dispose() {
181 logger.debug("Disposing of a TiVo handler for thing '{}'", getThing().getUID());
183 ScheduledFuture<?> refreshJob = this.refreshJob;
184 if (refreshJob != null) {
185 refreshJob.cancel(false);
186 this.refreshJob = null;
189 if (tivoConnection.isPresent()) {
191 tivoConnection.get().connTivoDisconnect();
192 } catch (InterruptedException e) {
193 // TiVo handler disposed or openHAB exiting, do nothing
195 tivoConnection = Optional.empty();
200 * {@link startPollStatus} scheduled job to poll for changes in state.
202 private void startPollStatus() {
203 Runnable runnable = () -> {
204 logger.debug("startPollStatus '{}' @ rate of '{}' seconds", getThing().getUID(),
205 tivoConfigData.getPollInterval());
206 tivoConnection.ifPresent(connection -> {
208 connection.statusRefresh();
209 } catch (InterruptedException e) {
210 // TiVo handler disposed or openHAB exiting, do nothing
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,
219 logger.debug("Status collection '{}' will start in '{}' seconds.", getThing().getUID(),
220 INIT_POLLING_DELAY_S);
221 } else if (tivoConfigData.doPollChanges()) {
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);
227 // Just update the status now
228 tivoConnection.ifPresent(connection -> {
230 connection.statusRefresh();
231 } catch (InterruptedException e) {
232 // TiVo handler disposed or openHAB exiting, do nothing
239 * {@link chChannelChange} performs channel changing operations.
241 * @param commandKeyword the TiVo command object.
242 * @param command the command parameter.
243 * @return TivoStatusData status of the command.
244 * @throws InterruptedException
246 private TivoStatusData chChannelChange(String commandKeyword, String command) throws InterruptedException {
250 TivoStatusData tmpStatus = tivoConnection.get().getServiceStatus();
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());
258 if (matcher.groupCount() >= 2 && matcher.group(2) != null) {
259 subChannel = Integer.parseInt(matcher.group(2).trim());
262 // The command string was not a number, throw exception to catch & log below
263 throw new NumberFormatException();
266 String tmpCommand = commandKeyword + " " + channel + ((subChannel != -1) ? (" " + subChannel) : "");
267 logger.debug("chChannelChange '{}' sending command to tivo: '{}'", getThing().getUID(), tmpCommand);
269 // Attempt to execute the command on the tivo
270 tivoConnection.get().cmdTivoSend(tmpCommand);
271 TimeUnit.MILLISECONDS.sleep(tivoConfigData.getCmdWaitInterval() * 2);
273 tmpStatus = tivoConnection.get().getServiceStatus();
275 // Check to see if the command was successful
276 if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT && tmpStatus.isCmdOk()) {
277 if (tmpStatus.getMsg().contains("CH_STATUS")) {
280 } else if (tmpStatus.getConnectionStatus() != ConnectionStatus.INIT) {
281 logger.warn("TiVo'{}' set channel command failed '{}' with msg '{}'", getThing().getUID(), tmpCommand,
283 switch (tmpStatus.getMsg()) {
284 case "CH_FAILED NO_LIVE":
285 tmpStatus.setChannelNum(channel);
286 tmpStatus.setSubChannelNum(subChannel);
288 case "CH_FAILED RECORDING":
289 case "CH_FAILED MISSING_CHANNEL":
290 case "CH_FAILED MALFORMED_CHANNEL":
291 case "CH_FAILED INVALID_CHANNEL":
293 case "NO_STATUS_DATA_RETURNED":
294 tmpStatus.setChannelNum(-1);
295 tmpStatus.setSubChannelNum(-1);
296 tmpStatus.setRecording(false);
301 } catch (NumberFormatException e) {
302 logger.warn("TiVo'{}' unable to parse channel integer, value sent was: '{}'", getThing().getUID(),
309 * {@link updateTivoStatus} populates the items with the status / channel information.
311 * @param tivoStatusData the {@link TivoStatusData}
313 public void updateTivoStatus(TivoStatusData oldStatusData, TivoStatusData newStatusData) {
314 if (newStatusData.getConnectionStatus() != ConnectionStatus.INIT) {
315 // Update Item Status
316 if (newStatusData.getPubToUI()) {
317 if (oldStatusData.getConnectionStatus() == ConnectionStatus.INIT
318 || !(oldStatusData.getMsg().contentEquals(newStatusData.getMsg()))) {
319 updateState(CHANNEL_TIVO_STATUS, new StringType(newStatusData.getMsg()));
321 // If the cmd was successful, publish the channel numbers
322 if (newStatusData.isCmdOk() && newStatusData.getChannelNum() != -1) {
323 if (oldStatusData.getConnectionStatus() == ConnectionStatus.INIT
324 || oldStatusData.getChannelNum() != newStatusData.getChannelNum()
325 || oldStatusData.getSubChannelNum() != newStatusData.getSubChannelNum()) {
326 if (newStatusData.getSubChannelNum() == -1) {
327 updateState(CHANNEL_TIVO_CHANNEL_FORCE, new DecimalType(newStatusData.getChannelNum()));
328 updateState(CHANNEL_TIVO_CHANNEL_SET, new DecimalType(newStatusData.getChannelNum()));
330 updateState(CHANNEL_TIVO_CHANNEL_FORCE, new DecimalType(
331 newStatusData.getChannelNum() + "." + newStatusData.getSubChannelNum()));
332 updateState(CHANNEL_TIVO_CHANNEL_SET, new DecimalType(
333 newStatusData.getChannelNum() + "." + newStatusData.getSubChannelNum()));
336 updateState(CHANNEL_TIVO_IS_RECORDING, newStatusData.isRecording() ? OnOffType.ON : OnOffType.OFF);
339 // Now set the pubToUI flag to false, as we have already published this status
340 if (isLinked(CHANNEL_TIVO_STATUS) || isLinked(CHANNEL_TIVO_CHANNEL_FORCE)
341 || isLinked(CHANNEL_TIVO_CHANNEL_SET)) {
342 newStatusData.setPubToUI(false);
343 tivoConnection.get().setServiceStatus(newStatusData);
347 // Update Thing status
348 if (newStatusData.getConnectionStatus() != lastConnectionStatus) {
349 switch (newStatusData.getConnectionStatus()) {
351 this.setStatusOffline();
354 updateStatus(ThingStatus.ONLINE);
357 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
358 "STANDBY MODE: Send command TIVO to Remote Control Button (IRCODE) item to wakeup.");
361 updateStatus(ThingStatus.OFFLINE);
366 lastConnectionStatus = newStatusData.getConnectionStatus();