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.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.ActiveApp;
30 import org.openhab.binding.roku.internal.dto.Apps.App;
31 import org.openhab.binding.roku.internal.dto.DeviceInfo;
32 import org.openhab.binding.roku.internal.dto.Player;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.StateOption;
43 import org.openhab.core.types.UnDefType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link RokuHandler} is responsible for handling commands, which are
49 * sent to one of the channels.
51 * @author Michael Lobstein - Initial contribution
54 public class RokuHandler extends BaseThingHandler {
55 private static final int DEFAULT_REFRESH_PERIOD_SEC = 10;
57 private final Logger logger = LoggerFactory.getLogger(RokuHandler.class);
58 private final HttpClient httpClient;
59 private final RokuStateDescriptionOptionProvider stateDescriptionProvider;
61 private @Nullable ScheduledFuture<?> refreshJob;
62 private @Nullable ScheduledFuture<?> appListJob;
64 private RokuCommunicator communicator;
65 private DeviceInfo deviceInfo = new DeviceInfo();
66 private int refreshInterval = DEFAULT_REFRESH_PERIOD_SEC;
68 private Object sequenceLock = new Object();
70 public RokuHandler(Thing thing, HttpClient httpClient,
71 RokuStateDescriptionOptionProvider stateDescriptionProvider) {
73 this.httpClient = httpClient;
74 this.stateDescriptionProvider = stateDescriptionProvider;
75 this.communicator = new RokuCommunicator(httpClient, EMPTY, -1);
79 public void initialize() {
80 logger.debug("Initializing Roku handler");
81 RokuConfiguration config = getConfigAs(RokuConfiguration.class);
83 final @Nullable String host = config.hostName;
85 if (host != null && !EMPTY.equals(host)) {
86 this.communicator = new RokuCommunicator(httpClient, host, config.port);
88 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host Name must be specified");
92 if (config.refresh >= 10) {
93 refreshInterval = config.refresh;
96 updateStatus(ThingStatus.UNKNOWN);
99 deviceInfo = communicator.getDeviceInfo();
100 thing.setProperty(PROPERTY_MODEL_NAME, deviceInfo.getModelName());
101 thing.setProperty(PROPERTY_MODEL_NUMBER, deviceInfo.getModelNumber());
102 thing.setProperty(PROPERTY_DEVICE_LOCAITON, deviceInfo.getUserDeviceLocation());
103 thing.setProperty(PROPERTY_SERIAL_NUMBER, deviceInfo.getSerialNumber());
104 thing.setProperty(PROPERTY_DEVICE_ID, deviceInfo.getDeviceId());
105 thing.setProperty(PROPERTY_SOFTWARE_VERSION, deviceInfo.getSoftwareVersion());
106 updateStatus(ThingStatus.ONLINE);
107 } catch (RokuHttpException e) {
108 logger.debug("Unable to retrieve Roku device-info. Exception: {}", e.getMessage(), e);
110 startAutomaticRefresh();
111 startAppListRefresh();
115 * Start the job to periodically get status updates from the Roku
117 private void startAutomaticRefresh() {
118 ScheduledFuture<?> refreshJob = this.refreshJob;
119 if (refreshJob == null || refreshJob.isCancelled()) {
120 this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshPlayerState, 0, refreshInterval,
126 * Get a status update from the Roku and update the channels
128 private void refreshPlayerState() {
129 synchronized (sequenceLock) {
131 ActiveApp activeApp = communicator.getActiveApp();
132 updateState(ACTIVE_APP, new StringType(activeApp.getApp().getId()));
133 updateStatus(ThingStatus.ONLINE);
134 } catch (RokuHttpException e) {
135 logger.debug("Unable to retrieve Roku active-app info. Exception: {}", e.getMessage(), e);
136 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
140 Player playerInfo = communicator.getPlayerInfo();
141 // When nothing playing, 'close' is reported, replace with 'stop'
142 updateState(PLAY_MODE, new StringType(playerInfo.getState().replaceAll(CLOSE, STOP)));
144 // Remove non-numeric from string, ie: ' ms'
145 String position = playerInfo.getPosition().replaceAll(NON_DIGIT_PATTERN, EMPTY);
146 if (!EMPTY.equals(position)) {
147 updateState(TIME_ELAPSED, new QuantityType<>(Integer.parseInt(position) / 1000, API_SECONDS_UNIT));
149 updateState(TIME_ELAPSED, UnDefType.UNDEF);
152 String duration = playerInfo.getDuration().replaceAll(NON_DIGIT_PATTERN, EMPTY);
153 if (!EMPTY.equals(duration)) {
154 updateState(TIME_TOTAL, new QuantityType<>(Integer.parseInt(duration) / 1000, API_SECONDS_UNIT));
156 updateState(TIME_TOTAL, UnDefType.UNDEF);
158 } catch (RokuHttpException e) {
159 logger.debug("Unable to retrieve Roku media-player info. Exception: {}", e.getMessage(), e);
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
166 * Start the job to periodically update list of apps installed on the the Roku
168 private void startAppListRefresh() {
169 ScheduledFuture<?> appListJob = this.appListJob;
170 if (appListJob == null || appListJob.isCancelled()) {
171 this.appListJob = scheduler.scheduleWithFixedDelay(this::refreshAppList, 10, 600, TimeUnit.SECONDS);
176 * Update the dropdown that lists all apps installed on the Roku
178 private void refreshAppList() {
179 synchronized (sequenceLock) {
181 List<App> appList = communicator.getAppList();
183 List<StateOption> appListOptions = new ArrayList<>();
184 // Roku Home will be selected in the drop-down any time an app is not running.
185 appListOptions.add(new StateOption(ROKU_HOME_ID, ROKU_HOME));
187 appList.forEach(app -> {
188 appListOptions.add(new StateOption(app.getId(), app.getValue()));
191 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_APP),
194 } catch (RokuHttpException e) {
195 logger.debug("Unable to retrieve Roku installed app-list. Exception: {}", e.getMessage(), e);
201 public void dispose() {
202 ScheduledFuture<?> refreshJob = this.refreshJob;
203 if (refreshJob != null) {
204 refreshJob.cancel(true);
205 this.refreshJob = null;
208 ScheduledFuture<?> appListJob = this.appListJob;
209 if (appListJob != null) {
210 appListJob.cancel(true);
211 this.appListJob = null;
216 public void handleCommand(ChannelUID channelUID, Command command) {
217 if (command instanceof RefreshType) {
218 logger.debug("Unsupported refresh command: {}", command);
219 } else if (channelUID.getId().equals(BUTTON)) {
220 synchronized (sequenceLock) {
222 communicator.keyPress(command.toString());
223 } catch (RokuHttpException e) {
224 logger.debug("Unable to send keypress to Roku, key: {}, Exception: {}", command, e.getMessage());
225 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
228 } else if (channelUID.getId().equals(ACTIVE_APP)) {
229 synchronized (sequenceLock) {
231 String appId = command.toString();
232 // Roku Home(-1) is not a real appId, just press the home button instead
233 if (!ROKU_HOME_ID.equals(appId)) {
234 communicator.launchApp(appId);
236 communicator.keyPress(ROKU_HOME_BUTTON);
238 } catch (RokuHttpException e) {
239 logger.debug("Unable to launch app on Roku, appId: {}, Exception: {}", command, e.getMessage());
240 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
244 logger.debug("Unsupported command: {}", command);