]> git.basschouten.com Git - openhab-addons.git/blob
cd55019f3ca0309e05e8d91de910d1d94e2bf43d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.openhab.binding.roku.internal.RokuConfiguration;
28 import org.openhab.binding.roku.internal.RokuHttpException;
29 import org.openhab.binding.roku.internal.RokuStateDescriptionOptionProvider;
30 import org.openhab.binding.roku.internal.communication.RokuCommunicator;
31 import org.openhab.binding.roku.internal.dto.Apps.App;
32 import org.openhab.binding.roku.internal.dto.DeviceInfo;
33 import org.openhab.binding.roku.internal.dto.Player;
34 import org.openhab.binding.roku.internal.dto.TvChannel;
35 import org.openhab.binding.roku.internal.dto.TvChannels.Channel;
36 import org.openhab.core.library.types.NextPreviousType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.PlayPauseType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.StateOption;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 /**
55  * The {@link RokuHandler} is responsible for handling commands, which are
56  * sent to one of the channels.
57  *
58  * @author Michael Lobstein - Initial contribution
59  */
60 @NonNullByDefault
61 public class RokuHandler extends BaseThingHandler {
62     private static final int DEFAULT_REFRESH_PERIOD_SEC = 10;
63
64     private final Logger logger = LoggerFactory.getLogger(RokuHandler.class);
65     private final HttpClient httpClient;
66     private final RokuStateDescriptionOptionProvider stateDescriptionProvider;
67
68     private @Nullable ScheduledFuture<?> refreshJob;
69     private @Nullable ScheduledFuture<?> appListJob;
70
71     private ThingTypeUID thingTypeUID = THING_TYPE_ROKU_PLAYER;
72     private RokuCommunicator communicator;
73     private DeviceInfo deviceInfo = new DeviceInfo();
74     private int refreshInterval = DEFAULT_REFRESH_PERIOD_SEC;
75     private boolean tvActive = false;
76     private Map<String, String> appMap = new HashMap<>();
77
78     private Object sequenceLock = new Object();
79
80     public RokuHandler(Thing thing, HttpClient httpClient,
81             RokuStateDescriptionOptionProvider stateDescriptionProvider) {
82         super(thing);
83         this.httpClient = httpClient;
84         this.stateDescriptionProvider = stateDescriptionProvider;
85         this.communicator = new RokuCommunicator(httpClient, EMPTY, -1);
86     }
87
88     @Override
89     public void initialize() {
90         logger.debug("Initializing Roku handler");
91         RokuConfiguration config = getConfigAs(RokuConfiguration.class);
92         this.thingTypeUID = this.getThing().getThingTypeUID();
93
94         final @Nullable String host = config.hostName;
95
96         if (host != null && !EMPTY.equals(host)) {
97             this.communicator = new RokuCommunicator(httpClient, host, config.port);
98         } else {
99             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host Name must be specified");
100             return;
101         }
102
103         if (config.refresh >= 1) {
104             refreshInterval = config.refresh;
105         }
106
107         updateStatus(ThingStatus.UNKNOWN);
108
109         try {
110             deviceInfo = communicator.getDeviceInfo();
111             thing.setProperty(PROPERTY_MODEL_NAME, deviceInfo.getModelName());
112             thing.setProperty(PROPERTY_MODEL_NUMBER, deviceInfo.getModelNumber());
113             thing.setProperty(PROPERTY_DEVICE_LOCAITON, deviceInfo.getUserDeviceLocation());
114             thing.setProperty(PROPERTY_SERIAL_NUMBER, deviceInfo.getSerialNumber());
115             thing.setProperty(PROPERTY_DEVICE_ID, deviceInfo.getDeviceId());
116             thing.setProperty(PROPERTY_SOFTWARE_VERSION, deviceInfo.getSoftwareVersion());
117             updateStatus(ThingStatus.ONLINE);
118         } catch (RokuHttpException e) {
119             logger.debug("Unable to retrieve Roku device-info. Exception: {}", e.getMessage(), e);
120         }
121         startAutomaticRefresh();
122         startAppListRefresh();
123     }
124
125     /**
126      * Start the job to periodically get status updates from the Roku
127      */
128     private void startAutomaticRefresh() {
129         ScheduledFuture<?> refreshJob = this.refreshJob;
130         if (refreshJob == null || refreshJob.isCancelled()) {
131             this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshPlayerState, 0, refreshInterval,
132                     TimeUnit.SECONDS);
133         }
134     }
135
136     /**
137      * Get a status update from the Roku and update the channels
138      */
139     private void refreshPlayerState() {
140         synchronized (sequenceLock) {
141             String activeAppId = ROKU_HOME_ID;
142             try {
143                 if (thingTypeUID.equals(THING_TYPE_ROKU_TV)) {
144                     try {
145                         deviceInfo = communicator.getDeviceInfo();
146                         String powerMode = deviceInfo.getPowerMode();
147                         updateState(POWER_STATE, new StringType(powerMode));
148                         updateState(POWER, OnOffType.from(POWER_ON.equalsIgnoreCase(powerMode)));
149                     } catch (RokuHttpException e) {
150                         logger.debug("Unable to retrieve Roku device-info.", e);
151                     }
152                 }
153
154                 activeAppId = communicator.getActiveApp().getApp().getId();
155
156                 // 562859 is now reported when on the home screen, reset to -1
157                 if (ROKU_HOME_ID_562859.equals(activeAppId)) {
158                     activeAppId = ROKU_HOME_ID;
159                 }
160
161                 updateState(ACTIVE_APP, new StringType(activeAppId));
162                 updateState(ACTIVE_APPNAME, new StringType(appMap.get(activeAppId)));
163
164                 if (TV_APP.equals(activeAppId)) {
165                     tvActive = true;
166                 } else {
167                     if (tvActive) {
168                         updateState(SIGNAL_MODE, UnDefType.UNDEF);
169                         updateState(SIGNAL_QUALITY, UnDefType.UNDEF);
170                         updateState(CHANNEL_NAME, UnDefType.UNDEF);
171                         updateState(PROGRAM_TITLE, UnDefType.UNDEF);
172                         updateState(PROGRAM_DESCRIPTION, UnDefType.UNDEF);
173                         updateState(PROGRAM_RATING, UnDefType.UNDEF);
174                     }
175                     tvActive = false;
176                 }
177                 updateStatus(ThingStatus.ONLINE);
178             } catch (RokuHttpException e) {
179                 logger.debug("Unable to retrieve Roku active-app info. Exception: {}", e.getMessage(), e);
180                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
181             }
182
183             // On the home app and when using the TV or TV inputs, do not update the play mode or time channels
184             if (!ROKU_HOME_ID.equals(activeAppId) && !activeAppId.contains(TV_INPUT)) {
185                 try {
186                     Player playerInfo = communicator.getPlayerInfo();
187                     // When nothing playing, 'close' is reported, replace with 'stop'
188                     updateState(PLAY_MODE, new StringType(playerInfo.getState().replaceAll(CLOSE, STOP)));
189                     updateState(CONTROL,
190                             PLAY.equalsIgnoreCase(playerInfo.getState()) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
191
192                     // Remove non-numeric from string, ie: ' ms'
193                     String position = playerInfo.getPosition().replaceAll(NON_DIGIT_PATTERN, EMPTY);
194                     if (!EMPTY.equals(position)) {
195                         updateState(TIME_ELAPSED,
196                                 new QuantityType<>(Integer.parseInt(position) / 1000, API_SECONDS_UNIT));
197                     } else {
198                         updateState(TIME_ELAPSED, UnDefType.UNDEF);
199                     }
200
201                     String duration = playerInfo.getDuration().replaceAll(NON_DIGIT_PATTERN, EMPTY);
202                     if (!EMPTY.equals(duration)) {
203                         updateState(TIME_TOTAL,
204                                 new QuantityType<>(Integer.parseInt(duration) / 1000, API_SECONDS_UNIT));
205                     } else {
206                         updateState(TIME_TOTAL, UnDefType.UNDEF);
207                     }
208                 } catch (NumberFormatException e) {
209                     logger.debug("Unable to parse playerInfo integer value. Exception: {}", e.getMessage());
210                 } catch (RokuHttpException e) {
211                     logger.debug("Unable to retrieve Roku media-player info. Exception: {}", e.getMessage(), e);
212                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
213                 }
214             } else {
215                 updateState(PLAY_MODE, UnDefType.UNDEF);
216                 updateState(TIME_ELAPSED, UnDefType.UNDEF);
217                 updateState(TIME_TOTAL, UnDefType.UNDEF);
218             }
219
220             if (thingTypeUID.equals(THING_TYPE_ROKU_TV) && tvActive) {
221                 try {
222                     TvChannel tvChannel = communicator.getActiveTvChannel();
223                     updateState(ACTIVE_CHANNEL, new StringType(tvChannel.getChannel().getNumber()));
224                     updateState(SIGNAL_MODE, new StringType(tvChannel.getChannel().getSignalMode()));
225                     updateState(SIGNAL_QUALITY,
226                             new QuantityType<>(tvChannel.getChannel().getSignalQuality(), API_PERCENT_UNIT));
227                     updateState(CHANNEL_NAME, new StringType(tvChannel.getChannel().getName()));
228                     updateState(PROGRAM_TITLE, new StringType(tvChannel.getChannel().getProgramTitle()));
229                     updateState(PROGRAM_DESCRIPTION, new StringType(tvChannel.getChannel().getProgramDescription()));
230                     updateState(PROGRAM_RATING, new StringType(tvChannel.getChannel().getProgramRatings()));
231                 } catch (RokuHttpException e) {
232                     logger.debug("Unable to retrieve Roku tv-active-channel info. Exception: {}", e.getMessage(), e);
233                 }
234             }
235         }
236     }
237
238     /**
239      * Start the job to periodically update list of apps installed on the the Roku
240      */
241     private void startAppListRefresh() {
242         ScheduledFuture<?> appListJob = this.appListJob;
243         if (appListJob == null || appListJob.isCancelled()) {
244             this.appListJob = scheduler.scheduleWithFixedDelay(this::refreshAppList, 10, 600, TimeUnit.SECONDS);
245         }
246     }
247
248     /**
249      * Update the dropdown that lists all apps installed on the Roku
250      */
251     private void refreshAppList() {
252         synchronized (sequenceLock) {
253             try {
254                 List<App> appList = communicator.getAppList();
255                 Map<String, String> appMap = new HashMap<>();
256
257                 List<StateOption> appListOptions = new ArrayList<>();
258                 // Roku Home will be selected in the drop-down any time an app is not running.
259                 appListOptions.add(new StateOption(ROKU_HOME_ID, ROKU_HOME));
260                 appMap.put(ROKU_HOME_ID, ROKU_HOME);
261
262                 appList.forEach(app -> {
263                     appListOptions.add(new StateOption(app.getId(), app.getValue()));
264                     appMap.put(app.getId(), app.getValue());
265                 });
266
267                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_APP),
268                         appListOptions);
269
270                 this.appMap = appMap;
271             } catch (RokuHttpException e) {
272                 logger.debug("Unable to retrieve Roku installed app-list. Exception: {}", e.getMessage(), e);
273             }
274
275             if (thingTypeUID.equals(THING_TYPE_ROKU_TV)) {
276                 try {
277                     List<Channel> channelsList = communicator.getTvChannelList();
278
279                     List<StateOption> channelListOptions = new ArrayList<>();
280                     channelsList.forEach(channel -> {
281                         if (!channel.isUserHidden()) {
282                             channelListOptions.add(new StateOption(channel.getNumber(),
283                                     channel.getNumber() + " - " + channel.getName()));
284                         }
285                     });
286
287                     stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_CHANNEL),
288                             channelListOptions);
289
290                 } catch (RokuHttpException e) {
291                     logger.debug("Unable to retrieve Roku tv-channels. Exception: {}", e.getMessage(), e);
292                 }
293             }
294         }
295     }
296
297     @Override
298     public void dispose() {
299         ScheduledFuture<?> refreshJob = this.refreshJob;
300         if (refreshJob != null) {
301             refreshJob.cancel(true);
302             this.refreshJob = null;
303         }
304
305         ScheduledFuture<?> appListJob = this.appListJob;
306         if (appListJob != null) {
307             appListJob.cancel(true);
308             this.appListJob = null;
309         }
310     }
311
312     @Override
313     public void handleCommand(ChannelUID channelUID, Command command) {
314         if (command instanceof RefreshType) {
315             logger.debug("Unsupported refresh command: {}", command);
316         } else if (channelUID.getId().equals(BUTTON)) {
317             synchronized (sequenceLock) {
318                 try {
319                     communicator.keyPress(command.toString());
320                 } catch (RokuHttpException e) {
321                     logger.debug("Unable to send keypress to Roku, key: {}, Exception: {}", command, e.getMessage());
322                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
323                 }
324             }
325         } else if (channelUID.getId().equals(ACTIVE_APP)) {
326             synchronized (sequenceLock) {
327                 try {
328                     String appId = command.toString();
329                     // Roku Home(-1) is not a real appId, just press the home button instead
330                     if (!ROKU_HOME_ID.equals(appId)) {
331                         communicator.launchApp(appId);
332                     } else {
333                         communicator.keyPress(ROKU_HOME_BUTTON);
334                     }
335                 } catch (RokuHttpException e) {
336                     logger.debug("Unable to launch app on Roku, appId: {}, Exception: {}", command, e.getMessage());
337                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
338                 }
339             }
340         } else if (channelUID.getId().equals(ACTIVE_CHANNEL)) {
341             synchronized (sequenceLock) {
342                 try {
343                     communicator.launchTvChannel(command.toString());
344                 } catch (RokuHttpException e) {
345                     logger.debug("Unable to change channel on Roku TV, channelNumber: {}, Exception: {}", command,
346                             e.getMessage());
347                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
348                 }
349             }
350         } else if (POWER.equals(channelUID.getId())) {
351             synchronized (sequenceLock) {
352                 if (command instanceof OnOffType) {
353                     try {
354                         if (command.equals(OnOffType.ON)) {
355                             communicator.keyPress(POWER_ON);
356                         } else {
357                             communicator.keyPress("PowerOff");
358                         }
359                     } catch (RokuHttpException e) {
360                         logger.debug("Unable to send keypress to Roku, key: {}, Exception: {}", command,
361                                 e.getMessage());
362                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
363                     }
364                 }
365             }
366         } else if (channelUID.getId().equals(CONTROL)) {
367             synchronized (sequenceLock) {
368                 try {
369                     if (command instanceof PlayPauseType) {
370                         communicator.keyPress(ROKU_PLAY_BUTTON);
371                     } else if (command instanceof NextPreviousType) {
372                         if (command == NextPreviousType.NEXT) {
373                             communicator.keyPress(ROKU_NEXT_BUTTON);
374                         } else if (command == NextPreviousType.PREVIOUS) {
375                             communicator.keyPress(ROKU_PREV_BUTTON);
376                         }
377                     } else {
378                         logger.warn("Unknown control command: {}", command);
379                     }
380                 } catch (RokuHttpException e) {
381                     logger.debug("Unable to send control cmd to Roku, cmd: {}, Exception: {}", command, e.getMessage());
382                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
383                 }
384             }
385         } else {
386             logger.debug("Unsupported command: {}", command);
387         }
388     }
389 }