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.roku.internal.handler;
15 import static org.openhab.binding.roku.internal.RokuBindingConstants.*;
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpClient;
25 import org.openhab.binding.roku.internal.RokuConfiguration;
26 import org.openhab.binding.roku.internal.RokuHttpException;
27 import org.openhab.binding.roku.internal.RokuStateDescriptionOptionProvider;
28 import org.openhab.binding.roku.internal.communication.RokuCommunicator;
29 import org.openhab.binding.roku.internal.dto.Apps.App;
30 import org.openhab.binding.roku.internal.dto.DeviceInfo;
31 import org.openhab.binding.roku.internal.dto.Player;
32 import org.openhab.binding.roku.internal.dto.TvChannel;
33 import org.openhab.binding.roku.internal.dto.TvChannels.Channel;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.ThingTypeUID;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.StateOption;
45 import org.openhab.core.types.UnDefType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * The {@link RokuHandler} is responsible for handling commands, which are
51 * sent to one of the channels.
53 * @author Michael Lobstein - Initial contribution
56 public class RokuHandler extends BaseThingHandler {
57 private static final int DEFAULT_REFRESH_PERIOD_SEC = 10;
59 private final Logger logger = LoggerFactory.getLogger(RokuHandler.class);
60 private final HttpClient httpClient;
61 private final RokuStateDescriptionOptionProvider stateDescriptionProvider;
63 private @Nullable ScheduledFuture<?> refreshJob;
64 private @Nullable ScheduledFuture<?> appListJob;
66 private ThingTypeUID thingTypeUID = THING_TYPE_ROKU_PLAYER;
67 private RokuCommunicator communicator;
68 private DeviceInfo deviceInfo = new DeviceInfo();
69 private int refreshInterval = DEFAULT_REFRESH_PERIOD_SEC;
70 private boolean tvActive = false;
72 private Object sequenceLock = new Object();
74 public RokuHandler(Thing thing, HttpClient httpClient,
75 RokuStateDescriptionOptionProvider stateDescriptionProvider) {
77 this.httpClient = httpClient;
78 this.stateDescriptionProvider = stateDescriptionProvider;
79 this.communicator = new RokuCommunicator(httpClient, EMPTY, -1);
83 public void initialize() {
84 logger.debug("Initializing Roku handler");
85 RokuConfiguration config = getConfigAs(RokuConfiguration.class);
86 this.thingTypeUID = this.getThing().getThingTypeUID();
88 final @Nullable String host = config.hostName;
90 if (host != null && !EMPTY.equals(host)) {
91 this.communicator = new RokuCommunicator(httpClient, host, config.port);
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host Name must be specified");
97 if (config.refresh >= 10) {
98 refreshInterval = config.refresh;
101 updateStatus(ThingStatus.UNKNOWN);
104 deviceInfo = communicator.getDeviceInfo();
105 thing.setProperty(PROPERTY_MODEL_NAME, deviceInfo.getModelName());
106 thing.setProperty(PROPERTY_MODEL_NUMBER, deviceInfo.getModelNumber());
107 thing.setProperty(PROPERTY_DEVICE_LOCAITON, deviceInfo.getUserDeviceLocation());
108 thing.setProperty(PROPERTY_SERIAL_NUMBER, deviceInfo.getSerialNumber());
109 thing.setProperty(PROPERTY_DEVICE_ID, deviceInfo.getDeviceId());
110 thing.setProperty(PROPERTY_SOFTWARE_VERSION, deviceInfo.getSoftwareVersion());
111 updateStatus(ThingStatus.ONLINE);
112 } catch (RokuHttpException e) {
113 logger.debug("Unable to retrieve Roku device-info. Exception: {}", e.getMessage(), e);
115 startAutomaticRefresh();
116 startAppListRefresh();
120 * Start the job to periodically get status updates from the Roku
122 private void startAutomaticRefresh() {
123 ScheduledFuture<?> refreshJob = this.refreshJob;
124 if (refreshJob == null || refreshJob.isCancelled()) {
125 this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshPlayerState, 0, refreshInterval,
131 * Get a status update from the Roku and update the channels
133 private void refreshPlayerState() {
134 synchronized (sequenceLock) {
135 String activeAppId = ROKU_HOME_ID;
137 activeAppId = communicator.getActiveApp().getApp().getId();
139 // 562859 is now reported when on the home screen, reset to -1
140 if (ROKU_HOME_ID_562859.equals(activeAppId)) {
141 activeAppId = ROKU_HOME_ID;
144 updateState(ACTIVE_APP, new StringType(activeAppId));
145 if (TV_APP.equals(activeAppId)) {
149 updateState(SIGNAL_MODE, UnDefType.UNDEF);
150 updateState(SIGNAL_QUALITY, UnDefType.UNDEF);
151 updateState(CHANNEL_NAME, UnDefType.UNDEF);
152 updateState(PROGRAM_TITLE, UnDefType.UNDEF);
153 updateState(PROGRAM_DESCRIPTION, UnDefType.UNDEF);
154 updateState(PROGRAM_RATING, UnDefType.UNDEF);
158 updateStatus(ThingStatus.ONLINE);
159 } catch (RokuHttpException e) {
160 logger.debug("Unable to retrieve Roku active-app info. Exception: {}", e.getMessage(), e);
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
164 // On the home app and when using the TV or TV inputs, do not update the play mode or time channels
165 if (!ROKU_HOME_ID.equals(activeAppId) && !activeAppId.contains(TV_INPUT)) {
167 Player playerInfo = communicator.getPlayerInfo();
168 // When nothing playing, 'close' is reported, replace with 'stop'
169 updateState(PLAY_MODE, new StringType(playerInfo.getState().replaceAll(CLOSE, STOP)));
171 // Remove non-numeric from string, ie: ' ms'
172 String position = playerInfo.getPosition().replaceAll(NON_DIGIT_PATTERN, EMPTY);
173 if (!EMPTY.equals(position)) {
174 updateState(TIME_ELAPSED,
175 new QuantityType<>(Integer.parseInt(position) / 1000, API_SECONDS_UNIT));
177 updateState(TIME_ELAPSED, UnDefType.UNDEF);
180 String duration = playerInfo.getDuration().replaceAll(NON_DIGIT_PATTERN, EMPTY);
181 if (!EMPTY.equals(duration)) {
182 updateState(TIME_TOTAL,
183 new QuantityType<>(Integer.parseInt(duration) / 1000, API_SECONDS_UNIT));
185 updateState(TIME_TOTAL, UnDefType.UNDEF);
187 } catch (RokuHttpException e) {
188 logger.debug("Unable to retrieve Roku media-player info. Exception: {}", e.getMessage(), e);
189 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
192 updateState(PLAY_MODE, UnDefType.UNDEF);
193 updateState(TIME_ELAPSED, UnDefType.UNDEF);
194 updateState(TIME_TOTAL, UnDefType.UNDEF);
197 if (thingTypeUID.equals(THING_TYPE_ROKU_TV) && tvActive) {
199 TvChannel tvChannel = communicator.getActiveTvChannel();
200 updateState(ACTIVE_CHANNEL, new StringType(tvChannel.getChannel().getNumber()));
201 updateState(SIGNAL_MODE, new StringType(tvChannel.getChannel().getSignalMode()));
202 updateState(SIGNAL_QUALITY,
203 new QuantityType<>(tvChannel.getChannel().getSignalQuality(), API_PERCENT_UNIT));
204 updateState(CHANNEL_NAME, new StringType(tvChannel.getChannel().getName()));
205 updateState(PROGRAM_TITLE, new StringType(tvChannel.getChannel().getProgramTitle()));
206 updateState(PROGRAM_DESCRIPTION, new StringType(tvChannel.getChannel().getProgramDescription()));
207 updateState(PROGRAM_RATING, new StringType(tvChannel.getChannel().getProgramRatings()));
208 } catch (RokuHttpException e) {
209 logger.debug("Unable to retrieve Roku tv-active-channel info. Exception: {}", e.getMessage(), e);
216 * Start the job to periodically update list of apps installed on the the Roku
218 private void startAppListRefresh() {
219 ScheduledFuture<?> appListJob = this.appListJob;
220 if (appListJob == null || appListJob.isCancelled()) {
221 this.appListJob = scheduler.scheduleWithFixedDelay(this::refreshAppList, 10, 600, TimeUnit.SECONDS);
226 * Update the dropdown that lists all apps installed on the Roku
228 private void refreshAppList() {
229 synchronized (sequenceLock) {
231 List<App> appList = communicator.getAppList();
233 List<StateOption> appListOptions = new ArrayList<>();
234 // Roku Home will be selected in the drop-down any time an app is not running.
235 appListOptions.add(new StateOption(ROKU_HOME_ID, ROKU_HOME));
237 appList.forEach(app -> {
238 appListOptions.add(new StateOption(app.getId(), app.getValue()));
241 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_APP),
244 } catch (RokuHttpException e) {
245 logger.debug("Unable to retrieve Roku installed app-list. Exception: {}", e.getMessage(), e);
248 if (thingTypeUID.equals(THING_TYPE_ROKU_TV)) {
250 List<Channel> channelsList = communicator.getTvChannelList();
252 List<StateOption> channelListOptions = new ArrayList<>();
253 channelsList.forEach(channel -> {
254 if (!channel.isUserHidden()) {
255 channelListOptions.add(new StateOption(channel.getNumber(),
256 channel.getNumber() + " - " + channel.getName()));
260 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_CHANNEL),
263 } catch (RokuHttpException e) {
264 logger.debug("Unable to retrieve Roku tv-channels. Exception: {}", e.getMessage(), e);
271 public void dispose() {
272 ScheduledFuture<?> refreshJob = this.refreshJob;
273 if (refreshJob != null) {
274 refreshJob.cancel(true);
275 this.refreshJob = null;
278 ScheduledFuture<?> appListJob = this.appListJob;
279 if (appListJob != null) {
280 appListJob.cancel(true);
281 this.appListJob = null;
286 public void handleCommand(ChannelUID channelUID, Command command) {
287 if (command instanceof RefreshType) {
288 logger.debug("Unsupported refresh command: {}", command);
289 } else if (channelUID.getId().equals(BUTTON)) {
290 synchronized (sequenceLock) {
292 communicator.keyPress(command.toString());
293 } catch (RokuHttpException e) {
294 logger.debug("Unable to send keypress to Roku, key: {}, Exception: {}", command, e.getMessage());
295 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
298 } else if (channelUID.getId().equals(ACTIVE_APP)) {
299 synchronized (sequenceLock) {
301 String appId = command.toString();
302 // Roku Home(-1) is not a real appId, just press the home button instead
303 if (!ROKU_HOME_ID.equals(appId)) {
304 communicator.launchApp(appId);
306 communicator.keyPress(ROKU_HOME_BUTTON);
308 } catch (RokuHttpException e) {
309 logger.debug("Unable to launch app on Roku, appId: {}, Exception: {}", command, e.getMessage());
310 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
313 } else if (channelUID.getId().equals(ACTIVE_CHANNEL)) {
314 synchronized (sequenceLock) {
316 communicator.launchTvChannel(command.toString());
317 } catch (RokuHttpException e) {
318 logger.debug("Unable to change channel on Roku TV, channelNumber: {}, Exception: {}", command,
320 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
324 logger.debug("Unsupported command: {}", command);