Skip to content

Commit

Permalink
Add Roku TV channels (openhab#11087)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Lobstein <[email protected]>
Signed-off-by: Dave J Schoepel <[email protected]>
mlobstein authored and dschoepel committed Nov 9, 2021
1 parent 17841aa commit dd9dd6a
Showing 8 changed files with 737 additions and 37 deletions.
67 changes: 52 additions & 15 deletions bundles/org.openhab.binding.roku/README.md
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ The Roku device must support the Roku ECP protocol REST API.

There are two supported thing types, which represent either a standalone Roku device or a Roku TV.
A supported Roku streaming media player or streaming stick uses the `roku_player` id and a supported Roku TV uses the `roku_tv` id.
The binding functionality is the same for both types, but the Roku TV type adds additional button commands to the button channel dropdown.
The Roku TV type adds additional channels and button commands to the button channel dropdown for TV specific functionality.
Multiple Things can be added if more than one Roku is to be controlled.

## Discovery
@@ -33,17 +33,24 @@ The thing has a few configuration parameters:

The following channels are available:

| Channel ID | Item Type | Description |
|-----------------|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| activeApp | String | A dropdown containing a list of all apps installed on the Roku. The app currently running is automatically selected. The list updates every 10 minutes. |
| button | String | Sends a remote control command the Roku. See list of available commands below. |
| playMode | String | The current playback mode ie: stop, play, pause (ReadOnly). |
| timeElapsed | Number:Time | The total number of seconds of playback time elapsed for the current playing title (ReadOnly). |
| timeTotal | Number:Time | The total length of the current playing title in seconds (ReadOnly). This data is not provided by all streaming apps. |
| Channel ID | Item Type | Description |
|--------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| activeApp | String | A dropdown containing a list of all apps installed on the Roku. The app currently running is automatically selected. The list updates every 10 minutes. |
| button | String | Sends a remote control command the Roku. See list of available commands below. |
| playMode | String | The current playback mode ie: stop, play, pause (ReadOnly). |
| timeElapsed | Number:Time | The total number of seconds of playback time elapsed for the current playing title (ReadOnly). |
| timeTotal | Number:Time | The total length of the current playing title in seconds (ReadOnly). This data is not provided by all streaming apps. |
| activeChannel | String | A dropdown containing a list of available TV channels on the Roku TV. The channel currently tuned is automatically selected. The list updates every 10 minutes. |
| signalMode | String | The signal type of the current TV channel, ie: 1080i (ReadOnly). |
| signalQuality | Number:Dimensionless | The signal quality of the current TV channel, 0-100% (ReadOnly). |
| channelName | String | The name of the channel currently selected (ReadOnly). |
| programTitle | String | The name of the current TV program (ReadOnly). |
| programDescription | String | The description of the current TV program (ReadOnly). |
| programRating | String | The TV parental guideline rating of the current TV program (ReadOnly). |

Some Notes:

* The values for `activeApp`, `playMode`, `timeElapsed` & `timeTotal` refresh automatically per the configured `refresh` interval (10 seconds minimum).
* The values for `activeApp`, `playMode`, `timeElapsed`, `timeTotal`, `activeChannel`, `signalMode`, `signalQuality`, `channelName`, `programTitle`, `programDescription` & `programRating` refresh automatically per the configured `refresh` interval (10 seconds minimum).

**List of available button commands for Roku streaming devices:**
Home
@@ -81,31 +88,61 @@ PowerOff

roku.things:

```java
```
// Roku streaming media player
roku:roku_player:myplayer1 "My Roku" [ hostName="192.168.10.1", refresh=10 ]
roku:roku_tv:myplayer1 "My Roku TV" [ hostName="192.168.10.1", refresh=10 ]
// Roku TV
roku:roku_tv:mytv1 "My Roku TV" [ hostName="192.168.10.1", refresh=10 ]
```

roku.items:

```java
String Player_ActiveApp "Current App: [%s]" { channel="roku:roku_player:myplayer1:activeApp" }
String Player_Button "Send Command to Roku" { channel="roku:roku_player:myplayer1:button" }
```
// Roku streaming media player items:
String Player_ActiveApp "Current App: [%s]" { channel="roku:roku_player:myplayer1:activeApp" }
String Player_Button "Send Command to Roku" { channel="roku:roku_player:myplayer1:button" }
String Player_PlayMode "Status: [%s]" { channel="roku:roku_player:myplayer1:playMode" }
Number:Time Player_TimeElapsed "Elapsed Time: [%d %unit%]" { channel="roku:roku_player:myplayer1:timeElapsed" }
Number:Time Player_TimeTotal "Total Time: [%d %unit%]" { channel="roku:roku_player:myplayer1:timeTotal" }
// Roku TV items:
String Player_ActiveApp "Current App: [%s]" { channel="roku:roku_tv:mytv1:activeApp" }
String Player_Button "Send Command to Roku" { channel="roku:roku_tv:mytv1:button" }
String Player_PlayMode "Status: [%s]" { channel="roku:roku_tv:mytv1:playMode" }
Number:Time Player_TimeElapsed "Elapsed Time: [%d %unit%]" { channel="roku:roku_tv:mytv1:timeElapsed" }
Number:Time Player_TimeTotal "Total Time: [%d %unit%]" { channel="roku:roku_tv:mytv1:timeTotal" }
String Player_ActiveChannel "Current Channel: [%s]" { channel="roku:roku_tv:mytv1:activeChannel" }
String Player_SignalMode "Signal Mode: [%s]" { channel="roku:roku_tv:mytv1:signalMode" }
Number Player_SignalQuality "Signal Quality: [%d %%]" { channel="roku:roku_tv:mytv1:signalQuality" }
String Player_ChannelName "Channel Name: [%s]" { channel="roku:roku_tv:mytv1:channelName" }
String Player_ProgramTitle "Program Title: [%s]" { channel="roku:roku_tv:mytv1:programTitle" }
String Player_ProgramDescription "Program Description: [%s]" { channel="roku:roku_tv:mytv1:programDescription" }
String Player_ProgramRating "Program Rating: [%s]" { channel="roku:roku_tv:mytv1:programRating" }
```

roku.sitemap:

```perl
```
sitemap roku label="Roku" {
Frame label="My Roku" {
Selection item=Player_ActiveApp icon="screen"
Selection item=Player_Button icon="screen"
Text item=Player_PlayMode
Text item=Player_TimeElapsed icon="time"
Text item=Player_TimeTotal icon="time"
// The following items apply to Roku TVs only
Selection item=Player_ActiveChannel icon="screen"
Text item=Player_SignalMode
Text item=Player_SignalQuality
Text item=Player_ChannelName
Text item=Player_ProgramTitle
Text item=Player_ProgramDescription
Text item=Player_ProgramRating
}
}
```
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
import java.util.Set;

import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Time;

import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -52,9 +53,17 @@ public class RokuBindingConstants {
public static final String PLAY_MODE = "playMode";
public static final String TIME_ELAPSED = "timeElapsed";
public static final String TIME_TOTAL = "timeTotal";
public static final String ACTIVE_CHANNEL = "activeChannel";
public static final String SIGNAL_MODE = "signalMode";
public static final String SIGNAL_QUALITY = "signalQuality";
public static final String CHANNEL_NAME = "channelName";
public static final String PROGRAM_TITLE = "programTitle";
public static final String PROGRAM_DESCRIPTION = "programDescription";
public static final String PROGRAM_RATING = "programRating";

// Units of measurement of the data delivered by the API
public static final Unit<Time> API_SECONDS_UNIT = Units.SECOND;
public static final Unit<Dimensionless> API_PERCENT_UNIT = Units.PERCENT;

public static final String STOP = "stop";
public static final String CLOSE = "close";
@@ -63,4 +72,6 @@ public class RokuBindingConstants {
public static final String ROKU_HOME_ID = "-1";
public static final String ROKU_HOME_BUTTON = "Home";
public static final String NON_DIGIT_PATTERN = "[^\\d]";
public static final String TV_APP = "tvinput.dtv";
public static final String TV_INPUT = "tvinput";
}
Original file line number Diff line number Diff line change
@@ -22,6 +22,8 @@
import org.openhab.binding.roku.internal.dto.Apps;
import org.openhab.binding.roku.internal.dto.DeviceInfo;
import org.openhab.binding.roku.internal.dto.Player;
import org.openhab.binding.roku.internal.dto.TvChannel;
import org.openhab.binding.roku.internal.dto.TvChannels;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@@ -39,6 +41,8 @@ public class JAXBUtils {
public static final @Nullable JAXBContext JAXBCONTEXT_APPS = initJAXBContextApps();
public static final @Nullable JAXBContext JAXBCONTEXT_DEVICE_INFO = initJAXBContextDeviceInfo();
public static final @Nullable JAXBContext JAXBCONTEXT_PLAYER = initJAXBContextPlayer();
public static final @Nullable JAXBContext JAXBCONTEXT_TVCHANNEL = initJAXBContextTvChannel();
public static final @Nullable JAXBContext JAXBCONTEXT_TVCHANNELS = initJAXBContextTvChannels();
public static final XMLInputFactory XMLINPUTFACTORY = initXMLInputFactory();

private static @Nullable JAXBContext initJAXBContextActiveApp() {
@@ -77,6 +81,24 @@ public class JAXBUtils {
}
}

private static @Nullable JAXBContext initJAXBContextTvChannel() {
try {
return JAXBContext.newInstance(TvChannel.class);
} catch (JAXBException e) {
LOGGER.error("Exception creating JAXBContext for TvChannel info: {}", e.getLocalizedMessage(), e);
return null;
}
}

private static @Nullable JAXBContext initJAXBContextTvChannels() {
try {
return JAXBContext.newInstance(TvChannels.class);
} catch (JAXBException e) {
LOGGER.error("Exception creating JAXBContext for TvChannels info: {}", e.getLocalizedMessage(), e);
return null;
}
}

private static XMLInputFactory initXMLInputFactory() {
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
Original file line number Diff line number Diff line change
@@ -32,6 +32,9 @@
import org.openhab.binding.roku.internal.dto.Apps.App;
import org.openhab.binding.roku.internal.dto.DeviceInfo;
import org.openhab.binding.roku.internal.dto.Player;
import org.openhab.binding.roku.internal.dto.TvChannel;
import org.openhab.binding.roku.internal.dto.TvChannels;
import org.openhab.binding.roku.internal.dto.TvChannels.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@@ -47,21 +50,27 @@ public class RokuCommunicator {

private final String urlKeyPress;
private final String urlLaunchApp;
private final String urlLaunchTvChannel;
private final String urlQryDevice;
private final String urlQryActiveApp;
private final String urlQryApps;
private final String urlQryPlayer;
private final String urlQryActiveTvChannel;
private final String urlQryTvChannels;

public RokuCommunicator(HttpClient httpClient, String host, int port) {
this.httpClient = httpClient;

final String baseUrl = "http://" + host + ":" + port;
urlKeyPress = baseUrl + "/keypress/";
urlLaunchApp = baseUrl + "/launch/";
urlLaunchTvChannel = baseUrl + "/launch/tvinput.dtv?ch=";
urlQryDevice = baseUrl + "/query/device-info";
urlQryActiveApp = baseUrl + "/query/active-app";
urlQryApps = baseUrl + "/query/apps";
urlQryPlayer = baseUrl + "/query/media-player";
urlQryActiveTvChannel = baseUrl + "/query/tv-active-channel";
urlQryTvChannels = baseUrl + "/query/tv-channels";
}

/**
@@ -84,6 +93,16 @@ public void launchApp(String appId) throws RokuHttpException {
postCommand(urlLaunchApp + appId);
}

/**
* Send a TV channel change command to the Roku TV
*
* @param channelNumber The channel number of the channel to tune into, ie: 2.1
*
*/
public void launchTvChannel(String channelNumber) throws RokuHttpException {
postCommand(urlLaunchTvChannel + channelNumber);
}

/**
* Send a command to get device-info from the Roku and return a DeviceInfo object
*
@@ -188,6 +207,58 @@ public Player getPlayerInfo() throws RokuHttpException {
}
}

/**
* Send a command to get tv-active-channel from the Roku TV and return a TvChannel object
*
* @return A TvChannel object populated with information about the current active TV Channel
* @throws RokuHttpException
*/
public TvChannel getActiveTvChannel() throws RokuHttpException {
try {
JAXBContext ctx = JAXBUtils.JAXBCONTEXT_TVCHANNEL;
if (ctx != null) {
Unmarshaller unmarshaller = ctx.createUnmarshaller();
if (unmarshaller != null) {
XMLStreamReader xsr = JAXBUtils.XMLINPUTFACTORY
.createXMLStreamReader(new StringReader(getCommand(urlQryActiveTvChannel)));
TvChannel tvChannelInfo = (TvChannel) unmarshaller.unmarshal(xsr);
if (tvChannelInfo != null) {
return tvChannelInfo;
}
}
}
throw new RokuHttpException("No TvChannel info model in response");
} catch (JAXBException | XMLStreamException e) {
throw new RokuHttpException("Exception creating TvChannel info Unmarshaller: " + e.getLocalizedMessage());
}
}

/**
* Send a command to get tv-channels from the Roku TV and return a list of Channel objects
*
* @return A List of Channel objects for all TV channels currently available on the Roku TV
* @throws RokuHttpException
*/
public List<Channel> getTvChannelList() throws RokuHttpException {
try {
JAXBContext ctx = JAXBUtils.JAXBCONTEXT_TVCHANNELS;
if (ctx != null) {
Unmarshaller unmarshaller = ctx.createUnmarshaller();
if (unmarshaller != null) {
XMLStreamReader xsr = JAXBUtils.XMLINPUTFACTORY
.createXMLStreamReader(new StringReader(getCommand(urlQryTvChannels)));
TvChannels tvChannels = (TvChannels) unmarshaller.unmarshal(xsr);
if (tvChannels != null) {
return tvChannels.getChannel();
}
}
}
throw new RokuHttpException("No TvChannels info model in response");
} catch (JAXBException | XMLStreamException e) {
throw new RokuHttpException("Exception creating TvChannel info Unmarshaller: " + e.getLocalizedMessage());
}
}

/**
* Sends a GET command to the Roku
*
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.roku.internal.dto;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* Maps the XML response from the Roku HTTP endpoint '/query/tv-active-channel' (Active TV channel information)
*
* @author Michael Lobstein - Initial contribution
*/

@NonNullByDefault
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "tv-channel")
public class TvChannel {
@XmlElement
private TvChannel.Channel channel = new Channel();

public TvChannel.Channel getChannel() {
return this.channel;
}

public void setChannel(TvChannel.Channel value) {
this.channel = value;
}

@XmlAccessorType(XmlAccessType.FIELD)
public static class Channel {
@XmlElement(name = "number")
private String number = "";

@XmlElement(name = "name")
private String name = "";

@XmlElement(name = "type")
private String type = "";

@XmlElement(name = "user-hidden")
private boolean userHidden = false;

@XmlElement(name = "user-favorite")
private boolean userFavorite = false;

@XmlElement(name = "physical-channel")
private int physicalChannel = 0;

@XmlElement(name = "physical-frequency")
private int physicalFrequency = 0;

@XmlElement(name = "active-input")
private boolean activeInput = false;

@XmlElement(name = "signal-state")
private String signalState = "";

@XmlElement(name = "signal-mode")
private String signalMode = "";

@XmlElement(name = "signal-quality")
private int signalQuality = 0;

@XmlElement(name = "signal-strength")
private int signalStrength = 0;

@XmlElement(name = "signal-stalled-pts-cnt")
private int signalStalledPtsCnt = 0;

@XmlElement(name = "program-title")
private String programTitle = "";

@XmlElement(name = "program-description")
private String programDescription = "";

@XmlElement(name = "program-ratings")
private String programRatings = "";

@XmlElement(name = "program-is-blocked")
private boolean programIsBlocked = false;

@XmlElement(name = "program-analog-audio")
private String programAnalogAudio = "";

@XmlElement(name = "program-digital-audio")
private String programDigitalAudio = "";

@XmlElement(name = "program-audio-languages")
private String programAudioLanguages = "";

@XmlElement(name = "program-audio-formats")
private String programAudioFormats = "";

@XmlElement(name = "program-audio-language")
private String programAudioLanguage = "";

@XmlElement(name = "program-audio-format")
private String programAudioFormat = "";

@XmlElement(name = "program-has-cc")
private boolean programHasCc = false;

public String getNumber() {
return number;
}

public void setNumber(String value) {
this.number = value;
}

public String getName() {
return name;
}

public void setName(String value) {
this.name = value;
}

public String getType() {
return type;
}

public void setType(String value) {
this.type = value;
}

public boolean isUserHidden() {
return userHidden;
}

public void setUserHidden(boolean value) {
this.userHidden = value;
}

public boolean isUserFavorite() {
return userFavorite;
}

public void setUserFavorite(boolean value) {
this.userFavorite = value;
}

public int getPhysicalChannel() {
return physicalChannel;
}

public void setPhysicalChannel(int value) {
this.physicalChannel = value;
}

public int getPhysicalFrequency() {
return physicalFrequency;
}

public void setPhysicalFrequency(int value) {
this.physicalFrequency = value;
}

public boolean isActiveInput() {
return activeInput;
}

public void setActiveInput(boolean value) {
this.activeInput = value;
}

public String getSignalState() {
return signalState;
}

public void setSignalState(String value) {
this.signalState = value;
}

public String getSignalMode() {
return signalMode;
}

public void setSignalMode(String value) {
this.signalMode = value;
}

public int getSignalQuality() {
return signalQuality;
}

public void setSignalQuality(int value) {
this.signalQuality = value;
}

public int getSignalStrength() {
return signalStrength;
}

public void setSignalStrength(int value) {
this.signalStrength = value;
}

public int getSignalStalledPtsCnt() {
return signalStalledPtsCnt;
}

public void setSignalStalledPtsCnt(int value) {
this.signalStalledPtsCnt = value;
}

public String getProgramTitle() {
return programTitle;
}

public void setProgramTitle(String value) {
this.programTitle = value;
}

public String getProgramDescription() {
return programDescription;
}

public void setProgramDescription(String value) {
this.programDescription = value;
}

public String getProgramRatings() {
return programRatings;
}

public void setProgramRatings(String value) {
this.programRatings = value;
}

public boolean isProgramIsBlocked() {
return programIsBlocked;
}

public void setProgramIsBlocked(boolean value) {
this.programIsBlocked = value;
}

public String getProgramAnalogAudio() {
return programAnalogAudio;
}

public void setProgramAnalogAudio(String value) {
this.programAnalogAudio = value;
}

public String getProgramDigitalAudio() {
return programDigitalAudio;
}

public void setProgramDigitalAudio(String value) {
this.programDigitalAudio = value;
}

public String getProgramAudioLanguages() {
return programAudioLanguages;
}

public void setProgramAudioLanguages(String value) {
this.programAudioLanguages = value;
}

public String getProgramAudioFormats() {
return programAudioFormats;
}

public void setProgramAudioFormats(String value) {
this.programAudioFormats = value;
}

public String getProgramAudioLanguage() {
return programAudioLanguage;
}

public void setProgramAudioLanguage(String value) {
this.programAudioLanguage = value;
}

public String getProgramAudioFormat() {
return programAudioFormat;
}

public void setProgramAudioFormat(String value) {
this.programAudioFormat = value;
}

public boolean isProgramHasCc() {
return programHasCc;
}

public void setProgramHasCc(boolean value) {
this.programHasCc = value;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.roku.internal.dto;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* Maps the XML response from the Roku HTTP endpoint '/query/tv-channels' (List of available TV channels)
*
* @author Michael Lobstein - Initial contribution
*/

@NonNullByDefault
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "tv-channels")
public class TvChannels {
@XmlElement
private List<TvChannels.Channel> channel = new ArrayList<TvChannels.Channel>();

public List<TvChannels.Channel> getChannel() {
return this.channel;
}

@XmlAccessorType(XmlAccessType.FIELD)
public static class Channel {
@XmlElement(name = "number")
private String number = "";

@XmlElement(name = "name")
private String name = "";

@XmlElement(name = "type")
private String type = "";

@XmlElement(name = "user-hidden")
private boolean userHidden = false;

@XmlElement(name = "user-favorite")
private boolean userFavorite = false;

@XmlElement(name = "physical-channel")
private int physicalChannel = 0;

@XmlElement(name = "physical-frequency")
private int physicalFrequency = 0;

public String getNumber() {
return number;
}

public void setNumber(String value) {
this.number = value;
}

public String getName() {
return name;
}

public void setName(String value) {
this.name = value;
}

public String getType() {
return type;
}

public void setType(String value) {
this.type = value;
}

public boolean isUserHidden() {
return userHidden;
}

public void setUserHidden(boolean value) {
this.userHidden = value;
}

public boolean isUserFavorite() {
return userFavorite;
}

public void setUserFavorite(boolean value) {
this.userFavorite = value;
}

public int getPhysicalChannel() {
return physicalChannel;
}

public void setPhysicalChannel(int value) {
this.physicalChannel = value;
}

public int getPhysicalFrequency() {
return physicalFrequency;
}

public void setPhysicalFrequency(int value) {
this.physicalFrequency = value;
}
}
}
Original file line number Diff line number Diff line change
@@ -26,16 +26,18 @@
import org.openhab.binding.roku.internal.RokuHttpException;
import org.openhab.binding.roku.internal.RokuStateDescriptionOptionProvider;
import org.openhab.binding.roku.internal.communication.RokuCommunicator;
import org.openhab.binding.roku.internal.dto.ActiveApp;
import org.openhab.binding.roku.internal.dto.Apps.App;
import org.openhab.binding.roku.internal.dto.DeviceInfo;
import org.openhab.binding.roku.internal.dto.Player;
import org.openhab.binding.roku.internal.dto.TvChannel;
import org.openhab.binding.roku.internal.dto.TvChannels.Channel;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
@@ -61,9 +63,11 @@ public class RokuHandler extends BaseThingHandler {
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable ScheduledFuture<?> appListJob;

private ThingTypeUID thingTypeUID = THING_TYPE_ROKU_PLAYER;
private RokuCommunicator communicator;
private DeviceInfo deviceInfo = new DeviceInfo();
private int refreshInterval = DEFAULT_REFRESH_PERIOD_SEC;
private boolean tvActive = false;

private Object sequenceLock = new Object();

@@ -79,6 +83,7 @@ public RokuHandler(Thing thing, HttpClient httpClient,
public void initialize() {
logger.debug("Initializing Roku handler");
RokuConfiguration config = getConfigAs(RokuConfiguration.class);
this.thingTypeUID = this.getThing().getThingTypeUID();

final @Nullable String host = config.hostName;

@@ -127,37 +132,76 @@ private void startAutomaticRefresh() {
*/
private void refreshPlayerState() {
synchronized (sequenceLock) {
String activeAppId = ROKU_HOME_ID;
try {
ActiveApp activeApp = communicator.getActiveApp();
updateState(ACTIVE_APP, new StringType(activeApp.getApp().getId()));
activeAppId = communicator.getActiveApp().getApp().getId();
updateState(ACTIVE_APP, new StringType(activeAppId));
if (TV_APP.equals(activeAppId)) {
tvActive = true;
} else {
if (tvActive) {
updateState(SIGNAL_MODE, UnDefType.UNDEF);
updateState(SIGNAL_QUALITY, UnDefType.UNDEF);
updateState(CHANNEL_NAME, UnDefType.UNDEF);
updateState(PROGRAM_TITLE, UnDefType.UNDEF);
updateState(PROGRAM_DESCRIPTION, UnDefType.UNDEF);
updateState(PROGRAM_RATING, UnDefType.UNDEF);
}
tvActive = false;
}
updateStatus(ThingStatus.ONLINE);
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku active-app info. Exception: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}

try {
Player playerInfo = communicator.getPlayerInfo();
// When nothing playing, 'close' is reported, replace with 'stop'
updateState(PLAY_MODE, new StringType(playerInfo.getState().replaceAll(CLOSE, STOP)));

// Remove non-numeric from string, ie: ' ms'
String position = playerInfo.getPosition().replaceAll(NON_DIGIT_PATTERN, EMPTY);
if (!EMPTY.equals(position)) {
updateState(TIME_ELAPSED, new QuantityType<>(Integer.parseInt(position) / 1000, API_SECONDS_UNIT));
} else {
updateState(TIME_ELAPSED, UnDefType.UNDEF);
// On the home app and when using the TV or TV inputs, do not update the play mode or time channels
if (!ROKU_HOME_ID.equals(activeAppId) && !activeAppId.contains(TV_INPUT)) {
try {
Player playerInfo = communicator.getPlayerInfo();
// When nothing playing, 'close' is reported, replace with 'stop'
updateState(PLAY_MODE, new StringType(playerInfo.getState().replaceAll(CLOSE, STOP)));

// Remove non-numeric from string, ie: ' ms'
String position = playerInfo.getPosition().replaceAll(NON_DIGIT_PATTERN, EMPTY);
if (!EMPTY.equals(position)) {
updateState(TIME_ELAPSED,
new QuantityType<>(Integer.parseInt(position) / 1000, API_SECONDS_UNIT));
} else {
updateState(TIME_ELAPSED, UnDefType.UNDEF);
}

String duration = playerInfo.getDuration().replaceAll(NON_DIGIT_PATTERN, EMPTY);
if (!EMPTY.equals(duration)) {
updateState(TIME_TOTAL,
new QuantityType<>(Integer.parseInt(duration) / 1000, API_SECONDS_UNIT));
} else {
updateState(TIME_TOTAL, UnDefType.UNDEF);
}
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku media-player info. Exception: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
} else {
updateState(PLAY_MODE, UnDefType.UNDEF);
updateState(TIME_ELAPSED, UnDefType.UNDEF);
updateState(TIME_TOTAL, UnDefType.UNDEF);
}

String duration = playerInfo.getDuration().replaceAll(NON_DIGIT_PATTERN, EMPTY);
if (!EMPTY.equals(duration)) {
updateState(TIME_TOTAL, new QuantityType<>(Integer.parseInt(duration) / 1000, API_SECONDS_UNIT));
} else {
updateState(TIME_TOTAL, UnDefType.UNDEF);
if (thingTypeUID.equals(THING_TYPE_ROKU_TV) && tvActive) {
try {
TvChannel tvChannel = communicator.getActiveTvChannel();
updateState(ACTIVE_CHANNEL, new StringType(tvChannel.getChannel().getNumber()));
updateState(SIGNAL_MODE, new StringType(tvChannel.getChannel().getSignalMode()));
updateState(SIGNAL_QUALITY,
new QuantityType<>(tvChannel.getChannel().getSignalQuality(), API_PERCENT_UNIT));
updateState(CHANNEL_NAME, new StringType(tvChannel.getChannel().getName()));
updateState(PROGRAM_TITLE, new StringType(tvChannel.getChannel().getProgramTitle()));
updateState(PROGRAM_DESCRIPTION, new StringType(tvChannel.getChannel().getProgramDescription()));
updateState(PROGRAM_RATING, new StringType(tvChannel.getChannel().getProgramRatings()));
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku tv-active-channel info. Exception: {}", e.getMessage(), e);
}
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku media-player info. Exception: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
}
@@ -194,6 +238,26 @@ private void refreshAppList() {
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku installed app-list. Exception: {}", e.getMessage(), e);
}

if (thingTypeUID.equals(THING_TYPE_ROKU_TV)) {
try {
List<Channel> channelsList = communicator.getTvChannelList();

List<StateOption> channelListOptions = new ArrayList<>();
channelsList.forEach(channel -> {
if (!channel.isUserHidden()) {
channelListOptions.add(new StateOption(channel.getNumber(),
channel.getNumber() + " - " + channel.getName()));
}
});

stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_CHANNEL),
channelListOptions);

} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku tv-channels. Exception: {}", e.getMessage(), e);
}
}
}
}

@@ -240,6 +304,16 @@ public void handleCommand(ChannelUID channelUID, Command command) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
} else if (channelUID.getId().equals(ACTIVE_CHANNEL)) {
synchronized (sequenceLock) {
try {
communicator.launchTvChannel(command.toString());
} catch (RokuHttpException e) {
logger.debug("Unable to change channel on Roku TV, channelNumber: {}, Exception: {}", command,
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
} else {
logger.debug("Unsupported command: {}", command);
}
Original file line number Diff line number Diff line change
@@ -46,6 +46,13 @@
<channel id="playMode" typeId="playMode"/>
<channel id="timeElapsed" typeId="timeElapsed"/>
<channel id="timeTotal" typeId="timeTotal"/>
<channel id="activeChannel" typeId="activeChannel"/>
<channel id="signalMode" typeId="signalMode"/>
<channel id="signalQuality" typeId="signalQuality"/>
<channel id="channelName" typeId="channelName"/>
<channel id="programTitle" typeId="programTitle"/>
<channel id="programDescription" typeId="programDescription"/>
<channel id="programRating" typeId="programRating"/>
</channels>

<properties>
@@ -153,4 +160,52 @@
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>

<channel-type id="activeChannel">
<item-type>String</item-type>
<label>Active Channel</label>
<description>The TV Channel Currently Selected on the Roku TV</description>
</channel-type>

<channel-type id="signalMode">
<item-type>String</item-type>
<label>Signal Mode</label>
<description>The Signal Type of the Current TV Channel, ie: 1080i</description>
<state readOnly="true"/>
</channel-type>

<channel-type id="signalQuality">
<item-type>Number:Dimensionless</item-type>
<label>Signal Quality</label>
<description>The Signal Quality of the Current TV Channel</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>

<channel-type id="channelName">
<item-type>String</item-type>
<label>Channel Name</label>
<description>The Name of the Channel Currently Selected</description>
<state readOnly="true"/>
</channel-type>

<channel-type id="programTitle">
<item-type>String</item-type>
<label>Program Title</label>
<description>The Name of the Current TV Program</description>
<state readOnly="true"/>
</channel-type>

<channel-type id="programDescription">
<item-type>String</item-type>
<label>Program Description</label>
<description>The Description of the Current TV Program</description>
<state readOnly="true"/>
</channel-type>

<channel-type id="programRating">
<item-type>String</item-type>
<label>Program Rating</label>
<description>The TV Parental Guideline Rating of the Current TV Program</description>
<state readOnly="true"/>
</channel-type>

</thing:thing-descriptions>

0 comments on commit dd9dd6a

Please sign in to comment.