]> git.basschouten.com Git - openhab-addons.git/blob
041e87e87e79095f25c57f3c72187ab41b82d30c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.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;
48
49 /**
50  * The {@link RokuHandler} is responsible for handling commands, which are
51  * sent to one of the channels.
52  *
53  * @author Michael Lobstein - Initial contribution
54  */
55 @NonNullByDefault
56 public class RokuHandler extends BaseThingHandler {
57     private static final int DEFAULT_REFRESH_PERIOD_SEC = 10;
58
59     private final Logger logger = LoggerFactory.getLogger(RokuHandler.class);
60     private final HttpClient httpClient;
61     private final RokuStateDescriptionOptionProvider stateDescriptionProvider;
62
63     private @Nullable ScheduledFuture<?> refreshJob;
64     private @Nullable ScheduledFuture<?> appListJob;
65
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;
71
72     private Object sequenceLock = new Object();
73
74     public RokuHandler(Thing thing, HttpClient httpClient,
75             RokuStateDescriptionOptionProvider stateDescriptionProvider) {
76         super(thing);
77         this.httpClient = httpClient;
78         this.stateDescriptionProvider = stateDescriptionProvider;
79         this.communicator = new RokuCommunicator(httpClient, EMPTY, -1);
80     }
81
82     @Override
83     public void initialize() {
84         logger.debug("Initializing Roku handler");
85         RokuConfiguration config = getConfigAs(RokuConfiguration.class);
86         this.thingTypeUID = this.getThing().getThingTypeUID();
87
88         final @Nullable String host = config.hostName;
89
90         if (host != null && !EMPTY.equals(host)) {
91             this.communicator = new RokuCommunicator(httpClient, host, config.port);
92         } else {
93             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host Name must be specified");
94             return;
95         }
96
97         if (config.refresh >= 10) {
98             refreshInterval = config.refresh;
99         }
100
101         updateStatus(ThingStatus.UNKNOWN);
102
103         try {
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);
114         }
115         startAutomaticRefresh();
116         startAppListRefresh();
117     }
118
119     /**
120      * Start the job to periodically get status updates from the Roku
121      */
122     private void startAutomaticRefresh() {
123         ScheduledFuture<?> refreshJob = this.refreshJob;
124         if (refreshJob == null || refreshJob.isCancelled()) {
125             this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshPlayerState, 0, refreshInterval,
126                     TimeUnit.SECONDS);
127         }
128     }
129
130     /**
131      * Get a status update from the Roku and update the channels
132      */
133     private void refreshPlayerState() {
134         synchronized (sequenceLock) {
135             String activeAppId = ROKU_HOME_ID;
136             try {
137                 activeAppId = communicator.getActiveApp().getApp().getId();
138
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;
142                 }
143
144                 updateState(ACTIVE_APP, new StringType(activeAppId));
145                 if (TV_APP.equals(activeAppId)) {
146                     tvActive = true;
147                 } else {
148                     if (tvActive) {
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);
155                     }
156                     tvActive = false;
157                 }
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);
162             }
163
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)) {
166                 try {
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)));
170
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));
176                     } else {
177                         updateState(TIME_ELAPSED, UnDefType.UNDEF);
178                     }
179
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));
184                     } else {
185                         updateState(TIME_TOTAL, UnDefType.UNDEF);
186                     }
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);
190                 }
191             } else {
192                 updateState(PLAY_MODE, UnDefType.UNDEF);
193                 updateState(TIME_ELAPSED, UnDefType.UNDEF);
194                 updateState(TIME_TOTAL, UnDefType.UNDEF);
195             }
196
197             if (thingTypeUID.equals(THING_TYPE_ROKU_TV) && tvActive) {
198                 try {
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);
210                 }
211             }
212         }
213     }
214
215     /**
216      * Start the job to periodically update list of apps installed on the the Roku
217      */
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);
222         }
223     }
224
225     /**
226      * Update the dropdown that lists all apps installed on the Roku
227      */
228     private void refreshAppList() {
229         synchronized (sequenceLock) {
230             try {
231                 List<App> appList = communicator.getAppList();
232
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));
236
237                 appList.forEach(app -> {
238                     appListOptions.add(new StateOption(app.getId(), app.getValue()));
239                 });
240
241                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_APP),
242                         appListOptions);
243
244             } catch (RokuHttpException e) {
245                 logger.debug("Unable to retrieve Roku installed app-list. Exception: {}", e.getMessage(), e);
246             }
247
248             if (thingTypeUID.equals(THING_TYPE_ROKU_TV)) {
249                 try {
250                     List<Channel> channelsList = communicator.getTvChannelList();
251
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()));
257                         }
258                     });
259
260                     stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_CHANNEL),
261                             channelListOptions);
262
263                 } catch (RokuHttpException e) {
264                     logger.debug("Unable to retrieve Roku tv-channels. Exception: {}", e.getMessage(), e);
265                 }
266             }
267         }
268     }
269
270     @Override
271     public void dispose() {
272         ScheduledFuture<?> refreshJob = this.refreshJob;
273         if (refreshJob != null) {
274             refreshJob.cancel(true);
275             this.refreshJob = null;
276         }
277
278         ScheduledFuture<?> appListJob = this.appListJob;
279         if (appListJob != null) {
280             appListJob.cancel(true);
281             this.appListJob = null;
282         }
283     }
284
285     @Override
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) {
291                 try {
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);
296                 }
297             }
298         } else if (channelUID.getId().equals(ACTIVE_APP)) {
299             synchronized (sequenceLock) {
300                 try {
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);
305                     } else {
306                         communicator.keyPress(ROKU_HOME_BUTTON);
307                     }
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);
311                 }
312             }
313         } else if (channelUID.getId().equals(ACTIVE_CHANNEL)) {
314             synchronized (sequenceLock) {
315                 try {
316                     communicator.launchTvChannel(command.toString());
317                 } catch (RokuHttpException e) {
318                     logger.debug("Unable to change channel on Roku TV, channelNumber: {}, Exception: {}", command,
319                             e.getMessage());
320                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
321                 }
322             }
323         } else {
324             logger.debug("Unsupported command: {}", command);
325         }
326     }
327 }