]> git.basschouten.com Git - openhab-addons.git/blob
340bca12ae691bb949267c69445ad9d9a5f95756
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.roku.internal.handler;
14
15 import static org.openhab.binding.roku.internal.RokuBindingConstants.*;
16
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
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;
46
47 /**
48  * The {@link RokuHandler} is responsible for handling commands, which are
49  * sent to one of the channels.
50  *
51  * @author Michael Lobstein - Initial contribution
52  */
53 @NonNullByDefault
54 public class RokuHandler extends BaseThingHandler {
55     private static final int DEFAULT_REFRESH_PERIOD_SEC = 10;
56
57     private final Logger logger = LoggerFactory.getLogger(RokuHandler.class);
58     private final HttpClient httpClient;
59     private final RokuStateDescriptionOptionProvider stateDescriptionProvider;
60
61     private @Nullable ScheduledFuture<?> refreshJob;
62     private @Nullable ScheduledFuture<?> appListJob;
63
64     private RokuCommunicator communicator;
65     private DeviceInfo deviceInfo = new DeviceInfo();
66     private int refreshInterval = DEFAULT_REFRESH_PERIOD_SEC;
67
68     private Object sequenceLock = new Object();
69
70     public RokuHandler(Thing thing, HttpClient httpClient,
71             RokuStateDescriptionOptionProvider stateDescriptionProvider) {
72         super(thing);
73         this.httpClient = httpClient;
74         this.stateDescriptionProvider = stateDescriptionProvider;
75         this.communicator = new RokuCommunicator(httpClient, EMPTY, -1);
76     }
77
78     @Override
79     public void initialize() {
80         logger.debug("Initializing Roku handler");
81         RokuConfiguration config = getConfigAs(RokuConfiguration.class);
82
83         final @Nullable String host = config.hostName;
84
85         if (host != null && !EMPTY.equals(host)) {
86             this.communicator = new RokuCommunicator(httpClient, host, config.port);
87         } else {
88             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host Name must be specified");
89             return;
90         }
91
92         if (config.refresh >= 10) {
93             refreshInterval = config.refresh;
94         }
95
96         updateStatus(ThingStatus.UNKNOWN);
97
98         try {
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);
109         }
110         startAutomaticRefresh();
111         startAppListRefresh();
112     }
113
114     /**
115      * Start the job to periodically get status updates from the Roku
116      */
117     private void startAutomaticRefresh() {
118         ScheduledFuture<?> refreshJob = this.refreshJob;
119         if (refreshJob == null || refreshJob.isCancelled()) {
120             this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshPlayerState, 0, refreshInterval,
121                     TimeUnit.SECONDS);
122         }
123     }
124
125     /**
126      * Get a status update from the Roku and update the channels
127      */
128     private void refreshPlayerState() {
129         synchronized (sequenceLock) {
130             try {
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);
137             }
138
139             try {
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)));
143
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));
148                 } else {
149                     updateState(TIME_ELAPSED, UnDefType.UNDEF);
150                 }
151
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));
155                 } else {
156                     updateState(TIME_TOTAL, UnDefType.UNDEF);
157                 }
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);
161             }
162         }
163     }
164
165     /**
166      * Start the job to periodically update list of apps installed on the the Roku
167      */
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);
172         }
173     }
174
175     /**
176      * Update the dropdown that lists all apps installed on the Roku
177      */
178     private void refreshAppList() {
179         synchronized (sequenceLock) {
180             try {
181                 List<App> appList = communicator.getAppList();
182
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));
186
187                 appList.forEach(app -> {
188                     appListOptions.add(new StateOption(app.getId(), app.getValue()));
189                 });
190
191                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_APP),
192                         appListOptions);
193
194             } catch (RokuHttpException e) {
195                 logger.debug("Unable to retrieve Roku installed app-list. Exception: {}", e.getMessage(), e);
196             }
197         }
198     }
199
200     @Override
201     public void dispose() {
202         ScheduledFuture<?> refreshJob = this.refreshJob;
203         if (refreshJob != null) {
204             refreshJob.cancel(true);
205             this.refreshJob = null;
206         }
207
208         ScheduledFuture<?> appListJob = this.appListJob;
209         if (appListJob != null) {
210             appListJob.cancel(true);
211             this.appListJob = null;
212         }
213     }
214
215     @Override
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) {
221                 try {
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);
226                 }
227             }
228         } else if (channelUID.getId().equals(ACTIVE_APP)) {
229             synchronized (sequenceLock) {
230                 try {
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);
235                     } else {
236                         communicator.keyPress(ROKU_HOME_BUTTON);
237                     }
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);
241                 }
242             }
243         } else {
244             logger.debug("Unsupported command: {}", command);
245         }
246     }
247 }