[roku] Fix jobs not starting if initial connection fails (#20214)

* Fix jobs not starting if initial connection fails

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein
2026-03-17 23:33:44 +01:00
committed by GitHub
parent 504402f74f
commit 1f848d8273
3 changed files with 84 additions and 39 deletions
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2026 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 RokuUnknownHostException} extends RokuHttpException
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RokuUnknownHostException extends RokuHttpException {
private static final long serialVersionUID = 1L;
public RokuUnknownHostException(String errorMessage, Throwable t) {
super(errorMessage, t);
}
public RokuUnknownHostException(String errorMessage) {
super(errorMessage);
}
}
@@ -13,6 +13,8 @@
package org.openhab.binding.roku.internal.communication;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -29,6 +31,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.RokuUnknownHostException;
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;
@@ -37,6 +40,7 @@ import org.openhab.binding.roku.internal.dto.Player;
import org.openhab.binding.roku.internal.dto.TvChannel;
import org.openhab.binding.roku.internal.dto.TvChannels;
import org.openhab.binding.roku.internal.dto.TvChannels.Channel;
import org.openhab.core.net.NetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -53,6 +57,7 @@ public class RokuCommunicator {
private final Logger logger = LoggerFactory.getLogger(RokuCommunicator.class);
private final HttpClient httpClient;
private final String host;
private final String urlKeyPress;
private final String urlLaunchApp;
private final String urlLaunchTvChannel;
@@ -65,6 +70,7 @@ public class RokuCommunicator {
public RokuCommunicator(HttpClient httpClient, String host, int port) {
this.httpClient = httpClient;
this.host = host;
final String baseUrl = "http://" + host + ":" + port;
urlKeyPress = baseUrl + "/keypress/";
@@ -285,7 +291,7 @@ public class RokuCommunicator {
*/
private String getCommand(String url) throws RokuHttpException {
try {
final String response = httpClient.newRequest(url).method(HttpMethod.GET)
final String response = httpClient.newRequest(resolveHostName(url)).method(HttpMethod.GET)
.timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send().getContentAsString();
if (response != null && response.contains(LIMITED_MODE_RESPONSE)) {
throw new RokuLimitedModeException(url + ": " + response);
@@ -308,7 +314,8 @@ public class RokuCommunicator {
private void postCommand(String url) throws RokuHttpException {
try {
logger.trace("Sending POST command: {}", url);
httpClient.POST(url).method(HttpMethod.POST).timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
httpClient.POST(resolveHostName(url)).method(HttpMethod.POST)
.timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
} catch (TimeoutException | ExecutionException e) {
throw new RokuHttpException("Error executing POST command, URL: " + url, e);
} catch (InterruptedException e) {
@@ -316,4 +323,21 @@ public class RokuCommunicator {
throw new RokuHttpException("InterruptedException executing POST command for URL: " + url, e);
}
}
/**
* Replace the host name with the resolved IP address (if required) to use for the HTTP request.
* Because if the host name was to be used, a Host header with the IP would be required in the request
*/
private String resolveHostName(String url) throws RokuUnknownHostException {
if (NetUtil.isValidIPConfig(host)) {
return url;
} else {
try {
return url.replaceAll("(http:\\/\\/)([^:\\/]+)(:(\\d+)(\\/.*))",
"$1" + InetAddress.getByName(host).getHostAddress() + "$3");
} catch (UnknownHostException e) {
throw new RokuUnknownHostException("UnknownHostException: " + e.getMessage(), e);
}
}
}
}
@@ -15,8 +15,6 @@ package org.openhab.binding.roku.internal.handler;
import static org.openhab.binding.roku.internal.RokuBindingConstants.*;
import java.math.BigDecimal;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
@@ -32,6 +30,7 @@ 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.RokuUnknownHostException;
import org.openhab.binding.roku.internal.communication.RokuCommunicator;
import org.openhab.binding.roku.internal.dto.Apps.App;
import org.openhab.binding.roku.internal.dto.DeviceInfo;
@@ -45,7 +44,6 @@ import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.net.NetUtil;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@@ -80,6 +78,7 @@ public class RokuHandler extends BaseThingHandler {
private RokuCommunicator communicator;
private DeviceInfo deviceInfo = new DeviceInfo();
private int refreshInterval = DEFAULT_REFRESH_PERIOD_SEC;
private boolean deviceInfoLoaded = false;
private boolean tvActive = false;
private int limitedMode = -1;
private Map<String, String> appMap = new HashMap<>();
@@ -102,8 +101,10 @@ public class RokuHandler extends BaseThingHandler {
final @Nullable String host = config.hostName;
if (host == null || EMPTY.equals(host)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host Name must be specified");
if (host != null && !host.isBlank()) {
this.communicator = new RokuCommunicator(httpClient, host, config.port);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host name must be specified");
return;
}
@@ -113,37 +114,8 @@ public class RokuHandler extends BaseThingHandler {
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(() -> {
try {
String resolvedHost = host;
if (!NetUtil.isValidIPConfig(host)) {
resolvedHost = InetAddress.getByName(host).getHostAddress();
}
this.communicator = new RokuCommunicator(httpClient, resolvedHost, config.port);
deviceInfo = communicator.getDeviceInfo();
thing.setProperty(PROPERTY_MODEL_NAME, deviceInfo.getModelName());
thing.setProperty(PROPERTY_MODEL_NUMBER, deviceInfo.getModelNumber());
thing.setProperty(PROPERTY_DEVICE_LOCAITON, deviceInfo.getUserDeviceLocation());
thing.setProperty(PROPERTY_SERIAL_NUMBER, deviceInfo.getSerialNumber());
thing.setProperty(PROPERTY_DEVICE_ID, deviceInfo.getDeviceId());
thing.setProperty(PROPERTY_SOFTWARE_VERSION, deviceInfo.getSoftwareVersion());
thing.setProperty(PROPERTY_UUID, deviceInfo.getSerialNumber().toLowerCase());
updateStatus(ThingStatus.ONLINE);
startAutomaticRefresh();
startAppListRefresh();
} catch (UnknownHostException e) {
logger.debug("Unable to resolve host", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot resolve hostname");
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku device-info. Exception: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot connect to device");
}
});
startAutomaticRefresh();
startAppListRefresh();
}
/**
@@ -164,12 +136,23 @@ public class RokuHandler extends BaseThingHandler {
synchronized (sequenceLock) {
String activeAppId = ROKU_HOME_ID;
try {
if (thingTypeUID.equals(THING_TYPE_ROKU_TV)) {
if (thingTypeUID.equals(THING_TYPE_ROKU_TV) || !deviceInfoLoaded) {
try {
deviceInfo = communicator.getDeviceInfo();
String powerMode = deviceInfo.getPowerMode();
updateState(POWER_STATE, new StringType(powerMode));
updateState(POWER, OnOffType.from(POWER_ON.equalsIgnoreCase(powerMode)));
if (!deviceInfoLoaded) {
thing.setProperty(PROPERTY_MODEL_NAME, deviceInfo.getModelName());
thing.setProperty(PROPERTY_MODEL_NUMBER, deviceInfo.getModelNumber());
thing.setProperty(PROPERTY_DEVICE_LOCAITON, deviceInfo.getUserDeviceLocation());
thing.setProperty(PROPERTY_SERIAL_NUMBER, deviceInfo.getSerialNumber());
thing.setProperty(PROPERTY_DEVICE_ID, deviceInfo.getDeviceId());
thing.setProperty(PROPERTY_SOFTWARE_VERSION, deviceInfo.getSoftwareVersion());
thing.setProperty(PROPERTY_UUID, deviceInfo.getSerialNumber().toLowerCase());
deviceInfoLoaded = true;
}
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku device-info.", e);
}
@@ -198,8 +181,13 @@ public class RokuHandler extends BaseThingHandler {
}
tvActive = false;
}
} catch (RokuUnknownHostException e) {
logger.debug("Unable to resolve hostname: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unable to resolve host name");
return;
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku active-app info. Exception: {}", e.getMessage(), e);
deviceInfoLoaded = false;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
return;
}