From 1bb95afa9f571d3c2e07574d9124109ba9942377 Mon Sep 17 00:00:00 2001 From: Michael Lobstein Date: Tue, 17 Dec 2024 23:18:11 -0600 Subject: [PATCH 1/2] Check for ECP Limited Mode Signed-off-by: Michael Lobstein --- bundles/org.openhab.binding.roku/README.md | 4 ++ .../roku/internal/RokuBindingConstants.java | 2 + .../communication/RokuCommunicator.java | 10 ++++- .../roku/internal/handler/RokuHandler.java | 44 ++++++++++++++++--- .../resources/OH-INF/i18n/roku.properties | 4 ++ 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/bundles/org.openhab.binding.roku/README.md b/bundles/org.openhab.binding.roku/README.md index 65bf9720cba89..512d94ace171c 100644 --- a/bundles/org.openhab.binding.roku/README.md +++ b/bundles/org.openhab.binding.roku/README.md @@ -3,6 +3,10 @@ This binding connects Roku streaming media players and Roku TVs to openHAB. The Roku device must support the Roku ECP protocol REST API. +In order for the binding to control the Roku, the following setting: +**Settings-> System-> Advanced system settings-> Control by mobile apps** +must be configured as `Enabled` or `Permissive`. + ## Supported Things There are two supported thing types, which represent either a standalone Roku device or a Roku TV. diff --git a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuBindingConstants.java b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuBindingConstants.java index 328eb3086562f..4861c07256043 100644 --- a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuBindingConstants.java +++ b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuBindingConstants.java @@ -84,4 +84,6 @@ public class RokuBindingConstants { public static final String TV_APP = "tvinput.dtv"; public static final String TV_INPUT = "tvinput"; public static final String POWER_ON = "POWERON"; + + public static final String LIMITED_MODE_RESPONSE = "ECP command not allowed"; } diff --git a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/communication/RokuCommunicator.java b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/communication/RokuCommunicator.java index c9fa33ecda44b..485de4c863bb3 100644 --- a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/communication/RokuCommunicator.java +++ b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/communication/RokuCommunicator.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.roku.internal.communication; +import static org.openhab.binding.roku.internal.RokuBindingConstants.*; + import java.io.StringReader; import java.util.List; import java.util.concurrent.ExecutionException; @@ -283,8 +285,12 @@ public List getTvChannelList() throws RokuHttpException { */ private String getCommand(String url) throws RokuHttpException { try { - return httpClient.newRequest(url).method(HttpMethod.GET).timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS) - .send().getContentAsString(); + final String response = httpClient.newRequest(url).method(HttpMethod.GET) + .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send().getContentAsString(); + if (response != null && response.contains(LIMITED_MODE_RESPONSE)) { + throw new RokuHttpException(LIMITED_MODE_RESPONSE); + } + return response != null ? response : EMPTY; } catch (TimeoutException | ExecutionException e) { throw new RokuHttpException("Error executing GET command for URL: " + url, e); } catch (InterruptedException e) { diff --git a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/handler/RokuHandler.java b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/handler/RokuHandler.java index 9df1b33ea06c3..2f4d3920eb168 100644 --- a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/handler/RokuHandler.java +++ b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/handler/RokuHandler.java @@ -73,6 +73,7 @@ public class RokuHandler extends BaseThingHandler { private DeviceInfo deviceInfo = new DeviceInfo(); private int refreshInterval = DEFAULT_REFRESH_PERIOD_SEC; private boolean tvActive = false; + private int limitedMode = -1; private Map appMap = new HashMap<>(); private Object sequenceLock = new Object(); @@ -175,18 +176,20 @@ private void refreshPlayerState() { } tvActive = false; } - updateStatus(ThingStatus.ONLINE); } catch (RokuHttpException e) { logger.debug("Unable to retrieve Roku active-app info. Exception: {}", e.getMessage(), e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return; } // On the home app and when using the TV or TV inputs, do not update the play mode or time channels - if (!ROKU_HOME_ID.equals(activeAppId) && !activeAppId.contains(TV_INPUT)) { + // if in limitedMode, keep checking getPlayerInfo to see if the error goes away + if ((!ROKU_HOME_ID.equals(activeAppId) && !activeAppId.contains(TV_INPUT)) || limitedMode != 0) { try { Player playerInfo = communicator.getPlayerInfo(); + limitedMode = 0; // When nothing playing, 'close' is reported, replace with 'stop' - updateState(PLAY_MODE, new StringType(playerInfo.getState().replaceAll(CLOSE, STOP))); + updateState(PLAY_MODE, new StringType(playerInfo.getState().replace(CLOSE, STOP))); updateState(CONTROL, PLAY.equalsIgnoreCase(playerInfo.getState()) ? PlayPauseType.PLAY : PlayPauseType.PAUSE); @@ -209,8 +212,13 @@ private void refreshPlayerState() { } catch (NumberFormatException e) { logger.debug("Unable to parse playerInfo integer value. Exception: {}", e.getMessage()); } catch (RokuHttpException e) { - logger.debug("Unable to retrieve Roku media-player info. Exception: {}", e.getMessage(), e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + if (isLimitedModeResponse(e)) { + limitedMode = 1; + } else { + logger.debug("Unable to retrieve Roku media-player info. Exception: {}", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return; + } } } else { updateState(PLAY_MODE, UnDefType.UNDEF); @@ -221,6 +229,7 @@ private void refreshPlayerState() { if (thingTypeUID.equals(THING_TYPE_ROKU_TV) && tvActive) { try { TvChannel tvChannel = communicator.getActiveTvChannel(); + limitedMode = 0; updateState(ACTIVE_CHANNEL, new StringType(tvChannel.getChannel().getNumber())); updateState(SIGNAL_MODE, new StringType(tvChannel.getChannel().getSignalMode())); updateState(SIGNAL_QUALITY, @@ -230,12 +239,35 @@ private void refreshPlayerState() { updateState(PROGRAM_DESCRIPTION, new StringType(tvChannel.getChannel().getProgramDescription())); updateState(PROGRAM_RATING, new StringType(tvChannel.getChannel().getProgramRatings())); } catch (RokuHttpException e) { - logger.debug("Unable to retrieve Roku tv-active-channel info. Exception: {}", e.getMessage(), e); + if (isLimitedModeResponse(e)) { + limitedMode = 1; + } else { + logger.debug("Unable to retrieve Roku tv-active-channel. Exception: {}", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return; + } } } + + if (limitedMode < 1) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.limited"); + } } } + /** + * Determines if the Roku is configured for Limited mode by examining the exception message + * + * @param ex the RokuHttpException + * @return boolean indicating if the Roku is configured for Limited Mode + */ + private boolean isLimitedModeResponse(RokuHttpException ex) { + final String message = ex.getMessage(); + return message != null && message.contains(LIMITED_MODE_RESPONSE); + } + /** * Start the job to periodically update list of apps installed on the the Roku */ diff --git a/bundles/org.openhab.binding.roku/src/main/resources/OH-INF/i18n/roku.properties b/bundles/org.openhab.binding.roku/src/main/resources/OH-INF/i18n/roku.properties index dd6c31dbe3782..cc4c681250a97 100644 --- a/bundles/org.openhab.binding.roku/src/main/resources/OH-INF/i18n/roku.properties +++ b/bundles/org.openhab.binding.roku/src/main/resources/OH-INF/i18n/roku.properties @@ -101,3 +101,7 @@ channel-type.roku.timeElapsed.label = Playback Time channel-type.roku.timeElapsed.description = The Current Playback Time Elapsed channel-type.roku.timeTotal.label = Total Time channel-type.roku.timeTotal.description = The Total Length of the Current Title + +# error status descriptions + +error.limited = Roku device is configured incorrectly - see README From 017f12bc789b5709c0e548b784f79ef859e6482f Mon Sep 17 00:00:00 2001 From: Michael Lobstein Date: Wed, 18 Dec 2024 17:05:12 -0600 Subject: [PATCH 2/2] Review changes Signed-off-by: Michael Lobstein --- .../roku/internal/RokuBindingConstants.java | 2 - .../internal/RokuLimitedModeException.java | 33 ++++++++++++++++ .../communication/RokuCommunicator.java | 8 ++-- .../roku/internal/handler/RokuHandler.java | 38 +++++++------------ 4 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuLimitedModeException.java diff --git a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuBindingConstants.java b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuBindingConstants.java index 4861c07256043..328eb3086562f 100644 --- a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuBindingConstants.java +++ b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuBindingConstants.java @@ -84,6 +84,4 @@ public class RokuBindingConstants { public static final String TV_APP = "tvinput.dtv"; public static final String TV_INPUT = "tvinput"; public static final String POWER_ON = "POWERON"; - - public static final String LIMITED_MODE_RESPONSE = "ECP command not allowed"; } diff --git a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuLimitedModeException.java b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuLimitedModeException.java new file mode 100644 index 0000000000000..1d73d94796b8e --- /dev/null +++ b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/RokuLimitedModeException.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.roku.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link RokuLimitedModeException} extends RokuHttpException + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public class RokuLimitedModeException extends RokuHttpException { + private static final long serialVersionUID = 1L; + + public RokuLimitedModeException(String errorMessage, Throwable t) { + super(errorMessage, t); + } + + public RokuLimitedModeException(String errorMessage) { + super(errorMessage); + } +} diff --git a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/communication/RokuCommunicator.java b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/communication/RokuCommunicator.java index 485de4c863bb3..1c2ab0edeba65 100644 --- a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/communication/RokuCommunicator.java +++ b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/communication/RokuCommunicator.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.roku.internal.communication; -import static org.openhab.binding.roku.internal.RokuBindingConstants.*; - import java.io.StringReader; import java.util.List; import java.util.concurrent.ExecutionException; @@ -30,6 +28,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.http.HttpMethod; import org.openhab.binding.roku.internal.RokuHttpException; +import org.openhab.binding.roku.internal.RokuLimitedModeException; import org.openhab.binding.roku.internal.dto.ActiveApp; import org.openhab.binding.roku.internal.dto.Apps; import org.openhab.binding.roku.internal.dto.Apps.App; @@ -49,6 +48,7 @@ @NonNullByDefault public class RokuCommunicator { private static final int REQUEST_TIMEOUT = 5000; + private static final String LIMITED_MODE_RESPONSE = "ECP command not allowed"; private final Logger logger = LoggerFactory.getLogger(RokuCommunicator.class); private final HttpClient httpClient; @@ -288,9 +288,9 @@ private String getCommand(String url) throws RokuHttpException { final String response = httpClient.newRequest(url).method(HttpMethod.GET) .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send().getContentAsString(); if (response != null && response.contains(LIMITED_MODE_RESPONSE)) { - throw new RokuHttpException(LIMITED_MODE_RESPONSE); + throw new RokuLimitedModeException(url + ": " + response); } - return response != null ? response : EMPTY; + return response != null ? response : ""; } catch (TimeoutException | ExecutionException e) { throw new RokuHttpException("Error executing GET command for URL: " + url, e); } catch (InterruptedException e) { diff --git a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/handler/RokuHandler.java b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/handler/RokuHandler.java index 2f4d3920eb168..dc5099dd8faa9 100644 --- a/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/handler/RokuHandler.java +++ b/bundles/org.openhab.binding.roku/src/main/java/org/openhab/binding/roku/internal/handler/RokuHandler.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.roku.internal.RokuConfiguration; import org.openhab.binding.roku.internal.RokuHttpException; +import org.openhab.binding.roku.internal.RokuLimitedModeException; import org.openhab.binding.roku.internal.RokuStateDescriptionOptionProvider; import org.openhab.binding.roku.internal.communication.RokuCommunicator; import org.openhab.binding.roku.internal.dto.Apps.App; @@ -211,14 +212,13 @@ private void refreshPlayerState() { } } catch (NumberFormatException e) { logger.debug("Unable to parse playerInfo integer value. Exception: {}", e.getMessage()); + } catch (RokuLimitedModeException e) { + logger.debug("RokuLimitedModeException: {}", e.getMessage()); + limitedMode = 1; } catch (RokuHttpException e) { - if (isLimitedModeResponse(e)) { - limitedMode = 1; - } else { - logger.debug("Unable to retrieve Roku media-player info. Exception: {}", e.getMessage(), e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - return; - } + logger.debug("Unable to retrieve Roku media-player info. Exception: {}", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return; } } else { updateState(PLAY_MODE, UnDefType.UNDEF); @@ -238,14 +238,13 @@ private void refreshPlayerState() { updateState(PROGRAM_TITLE, new StringType(tvChannel.getChannel().getProgramTitle())); updateState(PROGRAM_DESCRIPTION, new StringType(tvChannel.getChannel().getProgramDescription())); updateState(PROGRAM_RATING, new StringType(tvChannel.getChannel().getProgramRatings())); + } catch (RokuLimitedModeException e) { + logger.debug("RokuLimitedModeException: {}", e.getMessage()); + limitedMode = 1; } catch (RokuHttpException e) { - if (isLimitedModeResponse(e)) { - limitedMode = 1; - } else { - logger.debug("Unable to retrieve Roku tv-active-channel. Exception: {}", e.getMessage(), e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - return; - } + logger.debug("Unable to retrieve Roku tv-active-channel info. Exception: {}", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return; } } @@ -257,17 +256,6 @@ private void refreshPlayerState() { } } - /** - * Determines if the Roku is configured for Limited mode by examining the exception message - * - * @param ex the RokuHttpException - * @return boolean indicating if the Roku is configured for Limited Mode - */ - private boolean isLimitedModeResponse(RokuHttpException ex) { - final String message = ex.getMessage(); - return message != null && message.contains(LIMITED_MODE_RESPONSE); - } - /** * Start the job to periodically update list of apps installed on the the Roku */