[roku] binding - initial implementation (#9571)

* Roku binding - initial implementation
* update channel names to camelCase
* review changes
* spelling
* update README.md

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein 2021-01-18 14:57:42 -06:00 committed by GitHub
parent 05c16b0395
commit 08833c7c79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 2659 additions and 0 deletions

View File

@ -217,6 +217,7 @@
/bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
/bundles/org.openhab.binding.rme/ @kgoderis
/bundles/org.openhab.binding.robonect/ @reyem
/bundles/org.openhab.binding.roku/ @mlobstein
/bundles/org.openhab.binding.rotel/ @lolodomo
/bundles/org.openhab.binding.russound/ @tmrobert8
/bundles/org.openhab.binding.sagercaster/ @clinique

View File

@ -1071,6 +1071,11 @@
<artifactId>org.openhab.binding.robonect</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.roku</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.rotel</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,111 @@
# Roku Binding
This binding connects Roku streaming media players and Roku TVs to openHAB.
The Roku device must support the Roku ECP protocol REST API.
## Supported Things
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.
Multiple Things can be added if more than one Roku is to be controlled.
## Discovery
Auto-discovery is supported if the Roku can be located on the local network using SSDP.
Otherwise the thing must be manually added.
## Binding Configuration
The binding has no configuration options, all configuration is done at Thing level.
## Thing Configuration
The thing has a few configuration parameters:
| Parameter | Description |
|-----------|------------------------------------------------------------------------------------------------------------|
| hostName | The host name or IP address of the Roku device. Mandatory. |
| port | The port on the Roku that listens for http connections. Default 8060 |
| refresh | Overrides the refresh interval for player status updates. Optional, the default and minimum is 10 seconds. |
## Channels
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. |
Some Notes:
* The values for `activeApp`, `playMode`, `timeElapsed` & `timeTotal` refresh automatically per the configured `refresh` interval (10 seconds minimum).
**List of available button commands for Roku streaming devices:**
Home
Rev
Fwd
Play
Select
Left
Right
Up
Down
Back
InstantReplay
Info
Backspace
Search
Enter
FindRemote
**List of additional button commands for Roku TVs:**
ChannelUp
ChannelDown
VolumeUp
VolumeDown
VolumeMute
InputTuner
InputHDMI1
InputHDMI2
InputHDMI3
InputHDMI4
InputAV1
PowerOff
## Full Example
roku.things:
```java
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.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" }
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.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"
}
}
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.roku</artifactId>
<name>openHAB Add-ons :: Bundles :: Roku Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.roku-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-roku" description="Roku Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.roku/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,66 @@
/**
* 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;
import java.util.Set;
import javax.measure.Unit;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link RokuBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RokuBindingConstants {
public static final String BINDING_ID = "roku";
public static final String PROPERTY_UUID = "uuid";
public static final String PROPERTY_HOST_NAME = "hostName";
public static final String PROPERTY_PORT = "port";
public static final String PROPERTY_MODEL_NAME = "Model Name";
public static final String PROPERTY_MODEL_NUMBER = "Model Number";
public static final String PROPERTY_DEVICE_LOCAITON = "Device Location";
public static final String PROPERTY_SERIAL_NUMBER = "Serial Number";
public static final String PROPERTY_DEVICE_ID = "Device Id";
public static final String PROPERTY_SOFTWARE_VERSION = "Software Version";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ROKU_PLAYER = new ThingTypeUID(BINDING_ID, "roku_player");
public static final ThingTypeUID THING_TYPE_ROKU_TV = new ThingTypeUID(BINDING_ID, "roku_tv");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ROKU_PLAYER,
THING_TYPE_ROKU_TV);
// List of all Channel id's
public static final String ACTIVE_APP = "activeApp";
public static final String BUTTON = "button";
public static final String PLAY_MODE = "playMode";
public static final String TIME_ELAPSED = "timeElapsed";
public static final String TIME_TOTAL = "timeTotal";
// Units of measurement of the data delivered by the API
public static final Unit<Time> API_SECONDS_UNIT = Units.SECOND;
public static final String STOP = "stop";
public static final String CLOSE = "close";
public static final String EMPTY = "";
public static final String ROKU_HOME = "Roku Home";
public static final String ROKU_HOME_ID = "-1";
public static final String ROKU_HOME_BUTTON = "Home";
public static final String NON_DIGIT_PATTERN = "[^\\d]";
}

View File

@ -0,0 +1,29 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link RokuConfiguration} is the class used to match the
* thing configuration.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RokuConfiguration {
public @Nullable String hostName;
public Integer port = 8060;
public Integer refresh = 10;
}

View File

@ -0,0 +1,67 @@
/**
* 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;
import static org.openhab.binding.roku.internal.RokuBindingConstants.SUPPORTED_THING_TYPES_UIDS;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.roku.internal.handler.RokuHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link RokuHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.roku")
public class RokuHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
private final RokuStateDescriptionOptionProvider stateDescriptionProvider;
@Activate
public RokuHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference RokuStateDescriptionOptionProvider provider) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.stateDescriptionProvider = provider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
RokuHandler handler = new RokuHandler(thing, httpClient, stateDescriptionProvider);
return handler;
}
return null;
}
}

View File

@ -0,0 +1,29 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link RokuHttpException} extends Exception
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RokuHttpException extends Exception {
private static final long serialVersionUID = 1L;
public RokuHttpException(String errorMessage) {
super(errorMessage);
}
}

View File

@ -0,0 +1,41 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options while leaving other state description fields as original.
*
* @author Michael Lobstein - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, RokuStateDescriptionOptionProvider.class })
@NonNullByDefault
public class RokuStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@ -0,0 +1,77 @@
/**
* 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.communication;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.roku.internal.dto.ActiveApp;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation for a static use of JAXBContext as singleton instance.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class JAXBUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(JAXBUtils.class);
public static final @Nullable JAXBContext JAXBCONTEXT_ACTIVE_APP = initJAXBContextActiveApp();
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();
private static @Nullable JAXBContext initJAXBContextActiveApp() {
try {
return JAXBContext.newInstance(ActiveApp.class);
} catch (JAXBException e) {
LOGGER.error("Exception creating JAXBContext for active app: {}", e.getLocalizedMessage(), e);
return null;
}
}
private static @Nullable JAXBContext initJAXBContextApps() {
try {
return JAXBContext.newInstance(Apps.class);
} catch (JAXBException e) {
LOGGER.error("Exception creating JAXBContext for app list: {}", e.getLocalizedMessage(), e);
return null;
}
}
private static @Nullable JAXBContext initJAXBContextDeviceInfo() {
try {
return JAXBContext.newInstance(DeviceInfo.class);
} catch (JAXBException e) {
LOGGER.error("Exception creating JAXBContext for device info: {}", e.getLocalizedMessage(), e);
return null;
}
}
private static @Nullable JAXBContext initJAXBContextPlayer() {
try {
return JAXBContext.newInstance(Player.class);
} catch (JAXBException e) {
LOGGER.error("Exception creating JAXBContext for player info: {}", e.getLocalizedMessage(), e);
return null;
}
}
}

View File

@ -0,0 +1,210 @@
/**
* 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.communication;
import java.io.StringReader;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.roku.internal.RokuHttpException;
import org.openhab.binding.roku.internal.dto.ActiveApp;
import org.openhab.binding.roku.internal.dto.Apps;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Methods for accessing the HTTP interface of the Roku
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RokuCommunicator {
private final Logger logger = LoggerFactory.getLogger(RokuCommunicator.class);
private final HttpClient httpClient;
private final String urlKeyPress;
private final String urlLaunchApp;
private final String urlQryDevice;
private final String urlQryActiveApp;
private final String urlQryApps;
private final String urlQryPlayer;
public RokuCommunicator(HttpClient httpClient, String host, int port) {
this.httpClient = httpClient;
final String baseUrl = "http://" + host + ":" + port;
urlKeyPress = baseUrl + "/keypress/";
urlLaunchApp = baseUrl + "/launch/";
urlQryDevice = baseUrl + "/query/device-info";
urlQryActiveApp = baseUrl + "/query/active-app";
urlQryApps = baseUrl + "/query/apps";
urlQryPlayer = baseUrl + "/query/media-player";
}
/**
* Send a keypress command to the Roku
*
* @param key The key code to send
*
*/
public void keyPress(String key) throws RokuHttpException {
postCommand(urlKeyPress + key);
}
/**
* Send a launch app command to the Roku
*
* @param appId The appId of the app to launch
*
*/
public void launchApp(String appId) throws RokuHttpException {
postCommand(urlLaunchApp + appId);
}
/**
* Send a command to get device-info from the Roku and return a DeviceInfo object
*
* @return A DeviceInfo object populated with information about the connected Roku
* @throws RokuHttpException
*/
public DeviceInfo getDeviceInfo() throws RokuHttpException {
try {
JAXBContext ctx = JAXBUtils.JAXBCONTEXT_DEVICE_INFO;
if (ctx != null) {
Unmarshaller unmarshaller = ctx.createUnmarshaller();
if (unmarshaller != null) {
DeviceInfo device = (DeviceInfo) unmarshaller.unmarshal(new StringReader(getCommand(urlQryDevice)));
if (device != null) {
return device;
}
}
}
throw new RokuHttpException("No DeviceInfo model in response");
} catch (JAXBException e) {
throw new RokuHttpException("Exception creating DeviceInfo Unmarshaller: " + e.getLocalizedMessage());
}
}
/**
* Send a command to get active-app from the Roku and return an ActiveApp object
*
* @return An ActiveApp object populated with information about the current running app on the Roku
* @throws RokuHttpException
*/
public ActiveApp getActiveApp() throws RokuHttpException {
try {
JAXBContext ctx = JAXBUtils.JAXBCONTEXT_ACTIVE_APP;
if (ctx != null) {
Unmarshaller unmarshaller = ctx.createUnmarshaller();
if (unmarshaller != null) {
ActiveApp activeApp = (ActiveApp) unmarshaller
.unmarshal(new StringReader(getCommand(urlQryActiveApp)));
if (activeApp != null) {
return activeApp;
}
}
}
throw new RokuHttpException("No ActiveApp model in response");
} catch (JAXBException e) {
throw new RokuHttpException("Exception creating ActiveApp Unmarshaller: " + e.getLocalizedMessage());
}
}
/**
* Send a command to get the installed app list from the Roku and return a List of App objects
*
* @return A List of App objects for all apps currently installed on the Roku
* @throws RokuHttpException
*/
public List<App> getAppList() throws RokuHttpException {
try {
JAXBContext ctx = JAXBUtils.JAXBCONTEXT_APPS;
if (ctx != null) {
Unmarshaller unmarshaller = ctx.createUnmarshaller();
if (unmarshaller != null) {
Apps appList = (Apps) unmarshaller.unmarshal(new StringReader(getCommand(urlQryApps)));
if (appList != null) {
return appList.getApp();
}
}
}
throw new RokuHttpException("No AppList model in response");
} catch (JAXBException e) {
throw new RokuHttpException("Exception creating AppList Unmarshaller: " + e.getLocalizedMessage());
}
}
/**
* Send a command to get media-player from the Roku and return a Player object
*
* @return A Player object populated with information about the current stream playing on the Roku
* @throws RokuHttpException
*/
public Player getPlayerInfo() throws RokuHttpException {
try {
JAXBContext ctx = JAXBUtils.JAXBCONTEXT_PLAYER;
if (ctx != null) {
Unmarshaller unmarshaller = ctx.createUnmarshaller();
if (unmarshaller != null) {
Player playerInfo = (Player) unmarshaller.unmarshal(new StringReader(getCommand(urlQryPlayer)));
if (playerInfo != null) {
return playerInfo;
}
}
}
throw new RokuHttpException("No Player info model in response");
} catch (JAXBException e) {
throw new RokuHttpException("Exception creating Player info Unmarshaller: " + e.getLocalizedMessage());
}
}
/**
* Sends a GET command to the Roku
*
* @param url The url to send with the command embedded in the URI
* @return The response content of the http request
*/
private String getCommand(String url) {
try {
return httpClient.GET(url).getContentAsString();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.debug("Error executing player GET command, URL: {}, {} ", url, e.getMessage());
return "";
}
}
/**
* Sends a POST command to the Roku
*
* @param url The url to send with the command embedded in the URI
* @throws RokuHttpException
*/
private void postCommand(String url) throws RokuHttpException {
try {
httpClient.POST(url).method(HttpMethod.POST).send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new RokuHttpException("Error executing player POST command, URL: " + url + e.getMessage());
}
}
}

View File

@ -0,0 +1,264 @@
/**
* 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.discovery;
import static org.openhab.binding.roku.internal.RokuBindingConstants.*;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Scanner;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.roku.internal.RokuHttpException;
import org.openhab.binding.roku.internal.communication.RokuCommunicator;
import org.openhab.binding.roku.internal.dto.DeviceInfo;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RokuDiscoveryService} is responsible for discovery of Roku devices on the local network
*
* @author William Welliver - Initial contribution
* @author Dan Cunningham - Refactoring and Improvements
* @author Michael Lobstein - Modified for Roku binding
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, configurationPid = "discovery.roku")
public class RokuDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(RokuDiscoveryService.class);
private static final String ROKU_DISCOVERY_MESSAGE = "M-SEARCH * HTTP/1.1\r\n" + "Host: 239.255.255.250:1900\r\n"
+ "Man: \"ssdp:discover\"\r\n" + "ST: roku:ecp\r\n" + "\r\n";
private static final Pattern USN_PATTERN = Pattern.compile("^(uuid:roku:)?ecp:([0-9a-zA-Z]{1,16})");
private static final Pattern IP_HOST_PATTERN = Pattern
.compile("([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):([0-9]{1,5})");
private static final String ROKU_SSDP_MATCH = "uuid:roku:ecp";
private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
private final HttpClient httpClient;
private @Nullable ScheduledFuture<?> scheduledFuture;
@Activate
public RokuDiscoveryService(final @Reference HttpClientFactory httpClientFactory) {
super(SUPPORTED_THING_TYPES_UIDS, 30, true);
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public void startBackgroundDiscovery() {
stopBackgroundDiscovery();
scheduledFuture = scheduler.scheduleWithFixedDelay(this::doNetworkScan, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
TimeUnit.SECONDS);
}
@Override
public void stopBackgroundDiscovery() {
ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
this.scheduledFuture = null;
}
@Override
public void startScan() {
doNetworkScan();
}
/**
* Enumerate all network interfaces, send the discovery broadcast and process responses.
*
*/
private synchronized void doNetworkScan() {
try {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
while (nets.hasMoreElements()) {
NetworkInterface ni = nets.nextElement();
try (DatagramSocket socket = sendDiscoveryBroacast(ni)) {
if (socket != null) {
scanResposesForKeywords(socket);
}
}
}
} catch (IOException e) {
logger.debug("Error discovering devices", e);
}
}
/**
* Broadcasts a SSDP discovery message into the network to find provided services.
*
* @return The Socket where answers to the discovery broadcast arrive
*/
private @Nullable DatagramSocket sendDiscoveryBroacast(NetworkInterface ni) {
try {
InetAddress m = InetAddress.getByName("239.255.255.250");
final int port = 1900;
if (!ni.isUp() || !ni.supportsMulticast()) {
return null;
}
Enumeration<InetAddress> addrs = ni.getInetAddresses();
InetAddress a = null;
while (addrs.hasMoreElements()) {
a = addrs.nextElement();
if (a instanceof Inet4Address) {
break;
} else {
a = null;
}
}
if (a == null) {
logger.debug("No ipv4 address on {}", ni.getName());
return null;
}
// Create the discovery message packet
byte[] requestMessage = ROKU_DISCOVERY_MESSAGE.getBytes(StandardCharsets.UTF_8);
DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
// Create socket and send the discovery message
DatagramSocket socket = new DatagramSocket();
socket.setSoTimeout(3000);
socket.send(datagramPacket);
return socket;
} catch (IOException e) {
logger.debug("sendDiscoveryBroacast() got IOException: {}", e.getMessage());
return null;
}
}
/**
* Scans all messages that arrive on the socket and process those that come from a Roku.
*
* @param socket The socket where answers to the discovery broadcast arrive
*/
private void scanResposesForKeywords(DatagramSocket socket) {
byte[] receiveData = new byte[1024];
do {
DatagramPacket packet = new DatagramPacket(receiveData, receiveData.length);
try {
socket.receive(packet);
} catch (SocketTimeoutException e) {
return;
} catch (IOException e) {
logger.debug("Got exception while trying to receive UPnP packets: {}", e.getMessage());
return;
}
String response = new String(packet.getData(), StandardCharsets.UTF_8);
if (response.contains(ROKU_SSDP_MATCH)) {
parseResponseCreateThing(response);
}
} while (true);
}
/**
* Process the response from the Roku into a DiscoveryResult.
*
*/
private void parseResponseCreateThing(String response) {
DiscoveryResult result;
String label = "Roku";
String uuid = null;
String host = null;
int port = -1;
try (Scanner scanner = new Scanner(response)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
String[] pair = line.split(":", 2);
if (pair.length != 2) {
continue;
}
String key = pair[0].toLowerCase();
String value = pair[1].trim();
logger.debug("key: {} value: {}.", key, value);
switch (key) {
case "location":
host = value;
Matcher matchIp = IP_HOST_PATTERN.matcher(value);
if (matchIp.find()) {
host = matchIp.group(1);
port = Integer.parseInt(matchIp.group(2));
}
break;
case "usn":
Matcher matchUid = USN_PATTERN.matcher(value);
if (matchUid.find()) {
uuid = matchUid.group(2);
}
break;
default:
break;
}
}
}
if (host == null || port == -1 || uuid == null) {
logger.debug("Bad Format from Roku, received data was: {}", response);
return;
} else {
logger.debug("Found Roku, uuid: {} host: {}", uuid, host);
}
uuid = uuid.replace(":", "").toLowerCase();
ThingUID thingUid = new ThingUID(THING_TYPE_ROKU_PLAYER, uuid);
// Try to query the device using discovered host and port to get extended device info
try {
RokuCommunicator communicator = new RokuCommunicator(httpClient, host, port);
DeviceInfo device = communicator.getDeviceInfo();
label = device.getModelName() + " " + device.getModelNumber();
if (device.isTv()) {
thingUid = new ThingUID(THING_TYPE_ROKU_TV, uuid);
}
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku device-info. Exception: {}", e.getMessage(), e);
}
result = DiscoveryResultBuilder.create(thingUid).withLabel(label).withRepresentationProperty(PROPERTY_UUID)
.withProperty(PROPERTY_UUID, uuid).withProperty(PROPERTY_HOST_NAME, host)
.withProperty(PROPERTY_PORT, port).build();
this.thingDiscovered(result);
}
}

View File

@ -0,0 +1,149 @@
/**
* 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.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Maps the XML response from the Roku HTTP endpoint '/query/active-app' (Active app info)
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "active-app")
public class ActiveApp {
@XmlElement
private ActiveApp.App app = new App();
@XmlElement
private ActiveApp.Screensaver screensaver = new Screensaver();
public ActiveApp.App getApp() {
return app;
}
public void setApp(ActiveApp.App value) {
this.app = value;
}
public ActiveApp.Screensaver getScreensaver() {
return screensaver;
}
public void setScreensaver(ActiveApp.Screensaver value) {
this.screensaver = value;
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class App {
@XmlValue
private String value = "";
@XmlAttribute(name = "id")
private String id = "-1";
@XmlAttribute(name = "type")
private String type = "";
@XmlAttribute(name = "version")
private String version = "";
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getId() {
return id;
}
public void setId(String value) {
this.id = value;
}
public String getType() {
return type;
}
public void setType(String value) {
this.type = value;
}
public String getVersion() {
return version;
}
public void setVersion(String value) {
this.version = value;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class Screensaver {
@XmlValue
private String value = "";
@XmlAttribute(name = "id")
private int id = -1;
@XmlAttribute(name = "type")
private String type = "";
@XmlAttribute(name = "version")
private String version = "";
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public int getId() {
return id;
}
public void setId(int value) {
this.id = value;
}
public String getType() {
return type;
}
public void setType(String value) {
this.type = value;
}
public String getVersion() {
return version;
}
public void setVersion(String value) {
this.version = value;
}
}
}

View File

@ -0,0 +1,90 @@
/**
* 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.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Maps the XML response from the Roku HTTP endpoint '/query/apps' (List of installed apps)
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "apps")
public class Apps {
@XmlElement
private List<Apps.App> app = new ArrayList<Apps.App>();
public List<Apps.App> getApp() {
return this.app;
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class App {
@XmlValue
private String value = "";
@XmlAttribute(name = "id")
private String id = "-1";
@XmlAttribute(name = "type")
private String type = "";
@XmlAttribute(name = "version")
private String version = "";
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getId() {
return id;
}
public void setId(String value) {
this.id = value;
}
public String getType() {
return type;
}
public void setType(String value) {
this.type = value;
}
public String getVersion() {
return version;
}
public void setVersion(String value) {
this.version = value;
}
}
}

View File

@ -0,0 +1,662 @@
/**
* 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/device-info' (Device information)
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "device-info")
public class DeviceInfo {
@XmlElement(name = "udn")
private String udn = "";
@XmlElement(name = "serial-number")
private String serialNumber = "";
@XmlElement(name = "device-id")
private String deviceId = "";
@XmlElement(name = "advertising-id")
private String advertisingId = "";
@XmlElement(name = "vendor-name")
private String vendorName = "";
@XmlElement(name = "model-name")
private String modelName = "";
@XmlElement(name = "model-number")
private String modelNumber = "";
@XmlElement(name = "model-region")
private String modelRegion = "";
@XmlElement(name = "is-tv")
private boolean isTv = false;
@XmlElement(name = "is-stick")
private boolean isStick = false;
@XmlElement(name = "ui-resolution")
private String uiResolution = "";
@XmlElement(name = "supports-ethernet")
private boolean supportsEthernet = false;
@XmlElement(name = "wifi-mac")
private String wifiMac = "";
@XmlElement(name = "wifi-driver")
private String wifiDriver = "";
@XmlElement(name = "has-wifi-extender")
private boolean hasWifiExtender = false;
@XmlElement(name = "has-wifi-5G-support")
private boolean hasWifi5GSupport = false;
@XmlElement(name = "can-use-wifi-extender")
private boolean canUseWifiExtender = false;
@XmlElement(name = "ethernet-mac")
private String ethernetMac = "";
@XmlElement(name = "network-type")
private String networkType = "";
@XmlElement(name = "friendly-device-name")
private String friendlyDeviceName = "";
@XmlElement(name = "friendly-model-name")
private String friendlyModelName = "";
@XmlElement(name = "default-device-name")
private String defaultDeviceName = "";
@XmlElement(name = "user-device-name")
private String userDeviceName = "";
@XmlElement(name = "user-device-location")
private String userDeviceLocation = "";
@XmlElement(name = "build-number")
private String buildNumber = "";
@XmlElement(name = "software-version")
private String softwareVersion = "";
@XmlElement(name = "software-build")
private String softwareBuild = "";
@XmlElement(name = "secure-device")
private boolean secureDevice = false;
@XmlElement(name = "language")
private String language = "";
@XmlElement(name = "country")
private String country = "";
@XmlElement(name = "locale")
private String locale = "";
@XmlElement(name = "time-zone-auto")
private boolean timeZoneAuto = false;
@XmlElement(name = "time-zone")
private String timeZone = "";
@XmlElement(name = "time-zone-name")
private String timeZoneName = "";
@XmlElement(name = "time-zone-tz")
private String timeZoneTz = "";
@XmlElement(name = "time-zone-offset")
private int timeZoneOffset = 0;
@XmlElement(name = "clock-format")
private String clockFormat = "";
@XmlElement(name = "uptime")
private int uptime = 0;
@XmlElement(name = "power-mode")
private String powerMode = "";
@XmlElement(name = "supports-suspend")
private boolean supportsSuspend = false;
@XmlElement(name = "supports-find-remote")
private boolean supportsFindRemote = false;
@XmlElement(name = "find-remote-is-possible")
private boolean findRemoteIsPossible = false;
@XmlElement(name = "supports-audio-guide")
private boolean supportsAudioGuide = false;
@XmlElement(name = "supports-rva")
private boolean supportsRva = false;
@XmlElement(name = "developer-enabled")
private boolean developerEnabled = false;
@XmlElement(name = "keyed-developer-id")
private String keyedDeveloperId = "";
@XmlElement(name = "search-enabled")
private boolean searchEnabled = false;
@XmlElement(name = "search-channels-enabled")
private boolean searchChannelsEnabled = false;
@XmlElement(name = "voice-search-enabled")
private boolean voiceSearchEnabled = false;
@XmlElement(name = "notifications-enabled")
private boolean notificationsEnabled = false;
@XmlElement(name = "notifications-first-use")
private boolean notificationsFirstUse = false;
@XmlElement(name = "supports-private-listening")
private boolean supportsPrivateListening = false;
@XmlElement(name = "headphones-connected")
private boolean headphonesConnected = false;
@XmlElement(name = "supports-ecs-textedit")
private boolean supportsEcsTextedit = false;
@XmlElement(name = "supports-ecs-microphone")
private boolean supportsEcsMicrophone = false;
@XmlElement(name = "supports-wake-on-wlan")
private boolean supportsWakeOnWlan = false;
@XmlElement(name = "has-play-on-roku")
private boolean hasPlayOnRoku = false;
@XmlElement(name = "has-mobile-screensaver")
private boolean hasMobileScreensaver = false;
@XmlElement(name = "support-url")
private String supportUrl = "";
@XmlElement(name = "grandcentral-version")
private String grandcentralVersion = "";
@XmlElement(name = "trc-version")
private String trcVersion = "";
@XmlElement(name = "trc-channel-version")
private String trcChannelVersion = "";
@XmlElement(name = "davinci-version")
private String davinciVersion = "";
public String getUdn() {
return udn;
}
public void setUdn(String value) {
this.udn = value;
}
public String getSerialNumber() {
return serialNumber;
}
public void setSerialNumber(String value) {
this.serialNumber = value;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String value) {
this.deviceId = value;
}
public String getAdvertisingId() {
return advertisingId;
}
public void setAdvertisingId(String value) {
this.advertisingId = value;
}
public String getVendorName() {
return vendorName;
}
public void setVendorName(String value) {
this.vendorName = value;
}
public String getModelName() {
return modelName;
}
public void setModelName(String value) {
this.modelName = value;
}
public String getModelNumber() {
return modelNumber;
}
public void setModelNumber(String value) {
this.modelNumber = value;
}
public String getModelRegion() {
return modelRegion;
}
public void setModelRegion(String value) {
this.modelRegion = value;
}
public boolean isTv() {
return isTv;
}
public void setIsTv(boolean value) {
this.isTv = value;
}
public boolean isStick() {
return isStick;
}
public void setIsStick(boolean value) {
this.isStick = value;
}
public String getUiResolution() {
return uiResolution;
}
public void setUiResolution(String value) {
this.uiResolution = value;
}
public boolean isSupportsEthernet() {
return supportsEthernet;
}
public void setSupportsEthernet(boolean value) {
this.supportsEthernet = value;
}
public String getWifiMac() {
return wifiMac;
}
public void setWifiMac(String value) {
this.wifiMac = value;
}
public String getWifiDriver() {
return wifiDriver;
}
public void setWifiDriver(String value) {
this.wifiDriver = value;
}
public boolean isHasWifiExtender() {
return hasWifiExtender;
}
public void setHasWifiExtender(boolean value) {
this.hasWifiExtender = value;
}
public boolean isHasWifi5GSupport() {
return hasWifi5GSupport;
}
public void setHasWifi5GSupport(boolean value) {
this.hasWifi5GSupport = value;
}
public boolean isCanUseWifiExtender() {
return canUseWifiExtender;
}
public void setCanUseWifiExtender(boolean value) {
this.canUseWifiExtender = value;
}
public String getEthernetMac() {
return ethernetMac;
}
public void setEthernetMac(String value) {
this.ethernetMac = value;
}
public String getNetworkType() {
return networkType;
}
public void setNetworkType(String value) {
this.networkType = value;
}
public String getFriendlyDeviceName() {
return friendlyDeviceName;
}
public void setFriendlyDeviceName(String value) {
this.friendlyDeviceName = value;
}
public String getFriendlyModelName() {
return friendlyModelName;
}
public void setFriendlyModelName(String value) {
this.friendlyModelName = value;
}
public String getDefaultDeviceName() {
return defaultDeviceName;
}
public void setDefaultDeviceName(String value) {
this.defaultDeviceName = value;
}
public String getUserDeviceName() {
return userDeviceName;
}
public void setUserDeviceName(String value) {
this.userDeviceName = value;
}
public String getUserDeviceLocation() {
return userDeviceLocation;
}
public void setUserDeviceLocation(String value) {
this.userDeviceLocation = value;
}
public String getBuildNumber() {
return buildNumber;
}
public void setBuildNumber(String value) {
this.buildNumber = value;
}
public String getSoftwareVersion() {
return softwareVersion;
}
public void setSoftwareVersion(String value) {
this.softwareVersion = value;
}
public String getSoftwareBuild() {
return softwareBuild;
}
public void setSoftwareBuild(String value) {
this.softwareBuild = value;
}
public boolean isSecureDevice() {
return secureDevice;
}
public void setSecureDevice(boolean value) {
this.secureDevice = value;
}
public String getLanguage() {
return language;
}
public void setLanguage(String value) {
this.language = value;
}
public String getCountry() {
return country;
}
public void setCountry(String value) {
this.country = value;
}
public String getLocale() {
return locale;
}
public void setLocale(String value) {
this.locale = value;
}
public boolean isTimeZoneAuto() {
return timeZoneAuto;
}
public void setTimeZoneAuto(boolean value) {
this.timeZoneAuto = value;
}
public String getTimeZone() {
return timeZone;
}
public void setTimeZone(String value) {
this.timeZone = value;
}
public String getTimeZoneName() {
return timeZoneName;
}
public void setTimeZoneName(String value) {
this.timeZoneName = value;
}
public String getTimeZoneTz() {
return timeZoneTz;
}
public void setTimeZoneTz(String value) {
this.timeZoneTz = value;
}
public int getTimeZoneOffset() {
return timeZoneOffset;
}
public void setTimeZoneOffset(int value) {
this.timeZoneOffset = value;
}
public String getClockFormat() {
return clockFormat;
}
public void setClockFormat(String value) {
this.clockFormat = value;
}
public int getUptime() {
return uptime;
}
public void setUptime(int value) {
this.uptime = value;
}
public String getPowerMode() {
return powerMode;
}
public void setPowerMode(String value) {
this.powerMode = value;
}
public boolean isSupportsSuspend() {
return supportsSuspend;
}
public void setSupportsSuspend(boolean value) {
this.supportsSuspend = value;
}
public boolean isSupportsFindRemote() {
return supportsFindRemote;
}
public void setSupportsFindRemote(boolean value) {
this.supportsFindRemote = value;
}
public boolean isFindRemoteIsPossible() {
return findRemoteIsPossible;
}
public void setFindRemoteIsPossible(boolean value) {
this.findRemoteIsPossible = value;
}
public boolean isSupportsAudioGuide() {
return supportsAudioGuide;
}
public void setSupportsAudioGuide(boolean value) {
this.supportsAudioGuide = value;
}
public boolean isSupportsRva() {
return supportsRva;
}
public void setSupportsRva(boolean value) {
this.supportsRva = value;
}
public boolean isDeveloperEnabled() {
return developerEnabled;
}
public void setDeveloperEnabled(boolean value) {
this.developerEnabled = value;
}
public String getKeyedDeveloperId() {
return keyedDeveloperId;
}
public void setKeyedDeveloperId(String value) {
this.keyedDeveloperId = value;
}
public boolean isSearchEnabled() {
return searchEnabled;
}
public void setSearchEnabled(boolean value) {
this.searchEnabled = value;
}
public boolean isSearchChannelsEnabled() {
return searchChannelsEnabled;
}
public void setSearchChannelsEnabled(boolean value) {
this.searchChannelsEnabled = value;
}
public boolean isVoiceSearchEnabled() {
return voiceSearchEnabled;
}
public void setVoiceSearchEnabled(boolean value) {
this.voiceSearchEnabled = value;
}
public boolean isNotificationsEnabled() {
return notificationsEnabled;
}
public void setNotificationsEnabled(boolean value) {
this.notificationsEnabled = value;
}
public boolean isNotificationsFirstUse() {
return notificationsFirstUse;
}
public void setNotificationsFirstUse(boolean value) {
this.notificationsFirstUse = value;
}
public boolean isSupportsPrivateListening() {
return supportsPrivateListening;
}
public void setSupportsPrivateListening(boolean value) {
this.supportsPrivateListening = value;
}
public boolean isHeadphonesConnected() {
return headphonesConnected;
}
public void setHeadphonesConnected(boolean value) {
this.headphonesConnected = value;
}
public boolean isSupportsEcsTextedit() {
return supportsEcsTextedit;
}
public void setSupportsEcsTextedit(boolean value) {
this.supportsEcsTextedit = value;
}
public boolean isSupportsEcsMicrophone() {
return supportsEcsMicrophone;
}
public void setSupportsEcsMicrophone(boolean value) {
this.supportsEcsMicrophone = value;
}
public boolean isSupportsWakeOnWlan() {
return supportsWakeOnWlan;
}
public void setSupportsWakeOnWlan(boolean value) {
this.supportsWakeOnWlan = value;
}
public boolean isHasPlayOnRoku() {
return hasPlayOnRoku;
}
public void setHasPlayOnRoku(boolean value) {
this.hasPlayOnRoku = value;
}
public boolean isHasMobileScreensaver() {
return hasMobileScreensaver;
}
public void setHasMobileScreensaver(boolean value) {
this.hasMobileScreensaver = value;
}
public String getSupportUrl() {
return supportUrl;
}
public void setSupportUrl(String value) {
this.supportUrl = value;
}
public String getGrandcentralVersion() {
return grandcentralVersion;
}
public void setGrandcentralVersion(String value) {
this.grandcentralVersion = value;
}
public String getTrcVersion() {
return trcVersion;
}
public void setTrcVersion(String value) {
this.trcVersion = value;
}
public String getTrcChannelVersion() {
return trcChannelVersion;
}
public void setTrcChannelVersion(String value) {
this.trcChannelVersion = value;
}
public String getDavinciVersion() {
return davinciVersion;
}
public void setDavinciVersion(String value) {
this.davinciVersion = value;
}
}

View File

@ -0,0 +1,380 @@
/**
* 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.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Maps the XML response from the Roku HTTP endpoint '/query/media-player' (Current stream playback meta-data)
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "player")
public class Player {
@XmlElement(name = "plugin")
private Player.Plugin plugin = new Plugin();
@XmlElement(name = "format")
private Player.Format format = new Format();
@XmlElement(name = "buffering")
private Player.Buffering buffering = new Buffering();
@XmlElement(name = "new_stream")
private Player.NewStream newStream = new NewStream();
@XmlElement(name = "position")
private String position = "";
@XmlElement(name = "duration")
private String duration = "";
@XmlElement(name = "is_live")
private boolean isLive = false;
@XmlElement(name = "runtime")
private String runtime = "";
@XmlElement(name = "stream_segment")
private Player.StreamSegment streamSegment = new StreamSegment();
@XmlAttribute(name = "error")
private Boolean error = false;
@XmlAttribute(name = "state")
private String state = "";
public Player.Plugin getPlugin() {
return plugin;
}
public void setPlugin(Player.Plugin value) {
this.plugin = value;
}
public Player.Format getFormat() {
return format;
}
public void setFormat(Player.Format value) {
this.format = value;
}
public Player.Buffering getBuffering() {
return buffering;
}
public void setBuffering(Player.Buffering value) {
this.buffering = value;
}
public Player.NewStream getNewStream() {
return newStream;
}
public void setNewStream(Player.NewStream value) {
this.newStream = value;
}
public String getPosition() {
return position;
}
public void setPosition(String value) {
this.position = value;
}
public String getDuration() {
return duration;
}
public void setDuration(String value) {
this.duration = value;
}
public boolean isIsLive() {
return isLive;
}
public void setIsLive(boolean value) {
this.isLive = value;
}
public String getRuntime() {
return runtime;
}
public void setRuntime(String value) {
this.runtime = value;
}
public Player.StreamSegment getStreamSegment() {
return streamSegment;
}
public void setStreamSegment(Player.StreamSegment value) {
this.streamSegment = value;
}
public Boolean isError() {
return error;
}
public void setError(Boolean value) {
this.error = value;
}
public String getState() {
return state;
}
public void setState(String value) {
this.state = value;
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class Buffering {
@XmlAttribute(name = "current")
private int current = -1;
@XmlAttribute(name = "max")
private int max = -1;
@XmlAttribute(name = "target")
private int target = -1;
public int getCurrent() {
return current;
}
public void setCurrent(int value) {
this.current = value;
}
public int getMax() {
return max;
}
public void setMax(int value) {
this.max = value;
}
public int getTarget() {
return target;
}
public void setTarget(int value) {
this.target = value;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "")
public static class Format {
@XmlAttribute(name = "audio")
private String audio = "";
@XmlAttribute(name = "captions")
private String captions = "";
@XmlAttribute(name = "container")
private String container = "";
@XmlAttribute(name = "drm")
private String drm = "";
@XmlAttribute(name = "video")
private String video = "";
@XmlAttribute(name = "video_res")
private String videoRes = "";
public String getAudio() {
return audio;
}
public void setAudio(String value) {
this.audio = value;
}
public String getCaptions() {
return captions;
}
public void setCaptions(String value) {
this.captions = value;
}
public String getContainer() {
return container;
}
public void setContainer(String value) {
this.container = value;
}
public String getDrm() {
return drm;
}
public void setDrm(String value) {
this.drm = value;
}
public String getVideo() {
return video;
}
public void setVideo(String value) {
this.video = value;
}
public String getVideoRes() {
return videoRes;
}
public void setVideoRes(String value) {
this.videoRes = value;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class NewStream {
@XmlAttribute(name = "speed")
private String speed = "";
public String getSpeed() {
return speed;
}
public void setSpeed(String value) {
this.speed = value;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class Plugin {
@XmlAttribute(name = "bandwidth")
private String bandwidth = "";
@XmlAttribute(name = "id")
private int id = -1;
@XmlAttribute(name = "name")
private String name = "";
public String getBandwidth() {
return bandwidth;
}
public void setBandwidth(String value) {
this.bandwidth = value;
}
public int getId() {
return id;
}
public void setId(int value) {
this.id = value;
}
public String getName() {
return name;
}
public void setName(String value) {
this.name = value;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "")
public static class StreamSegment {
@XmlAttribute(name = "bitrate")
private int bitrate = -1;
@XmlAttribute(name = "height")
private int height = -1;
@XmlAttribute(name = "media_sequence")
private int mediaSequence = -1;
@XmlAttribute(name = "segment_type")
private String segmentType = "";
@XmlAttribute(name = "time")
private int time = -1;
@XmlAttribute(name = "width")
private int width = -1;
public int getBitrate() {
return bitrate;
}
public void setBitrate(int value) {
this.bitrate = value;
}
public int getHeight() {
return height;
}
public void setHeight(int value) {
this.height = value;
}
public int getMediaSequence() {
return mediaSequence;
}
public void setMediaSequence(int value) {
this.mediaSequence = value;
}
public String getSegmentType() {
return segmentType;
}
public void setSegmentType(String value) {
this.segmentType = value;
}
public int getTime() {
return time;
}
public void setTime(int value) {
this.time = value;
}
public int getWidth() {
return width;
}
public void setWidth(int value) {
this.width = value;
}
}
}

View File

@ -0,0 +1,247 @@
/**
* 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.handler;
import static org.openhab.binding.roku.internal.RokuBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.roku.internal.RokuConfiguration;
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.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.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RokuHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RokuHandler extends BaseThingHandler {
private static final int DEFAULT_REFRESH_PERIOD_SEC = 10;
private final Logger logger = LoggerFactory.getLogger(RokuHandler.class);
private final HttpClient httpClient;
private final RokuStateDescriptionOptionProvider stateDescriptionProvider;
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable ScheduledFuture<?> appListJob;
private RokuCommunicator communicator;
private DeviceInfo deviceInfo = new DeviceInfo();
private int refreshInterval = DEFAULT_REFRESH_PERIOD_SEC;
private Object sequenceLock = new Object();
public RokuHandler(Thing thing, HttpClient httpClient,
RokuStateDescriptionOptionProvider stateDescriptionProvider) {
super(thing);
this.httpClient = httpClient;
this.stateDescriptionProvider = stateDescriptionProvider;
this.communicator = new RokuCommunicator(httpClient, EMPTY, -1);
}
@Override
public void initialize() {
logger.debug("Initializing Roku handler");
RokuConfiguration config = getConfigAs(RokuConfiguration.class);
final @Nullable String host = config.hostName;
if (host != null && !EMPTY.equals(host)) {
this.communicator = new RokuCommunicator(httpClient, host, config.port);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host Name must be specified");
return;
}
if (config.refresh >= 10) {
refreshInterval = config.refresh;
}
updateStatus(ThingStatus.UNKNOWN);
try {
deviceInfo = communicator.getDeviceInfo();
thing.setProperty(PROPERTY_MODEL_NAME, deviceInfo.getModelName());
thing.setProperty(PROPERTY_MODEL_NUMBER, deviceInfo.getModelNumber());
thing.setProperty(PROPERTY_DEVICE_LOCAITON, deviceInfo.getUserDeviceLocation());
thing.setProperty(PROPERTY_SERIAL_NUMBER, deviceInfo.getSerialNumber());
thing.setProperty(PROPERTY_DEVICE_ID, deviceInfo.getDeviceId());
thing.setProperty(PROPERTY_SOFTWARE_VERSION, deviceInfo.getSoftwareVersion());
updateStatus(ThingStatus.ONLINE);
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku device-info. Exception: {}", e.getMessage(), e);
}
startAutomaticRefresh();
startAppListRefresh();
}
/**
* Start the job to periodically get status updates from the Roku
*/
private void startAutomaticRefresh() {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob == null || refreshJob.isCancelled()) {
this.refreshJob = scheduler.scheduleWithFixedDelay(this::refreshPlayerState, 0, refreshInterval,
TimeUnit.SECONDS);
}
}
/**
* Get a status update from the Roku and update the channels
*/
private void refreshPlayerState() {
synchronized (sequenceLock) {
try {
ActiveApp activeApp = communicator.getActiveApp();
updateState(ACTIVE_APP, new StringType(activeApp.getApp().getId()));
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);
}
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);
}
}
}
/**
* Start the job to periodically update list of apps installed on the the Roku
*/
private void startAppListRefresh() {
ScheduledFuture<?> appListJob = this.appListJob;
if (appListJob == null || appListJob.isCancelled()) {
this.appListJob = scheduler.scheduleWithFixedDelay(this::refreshAppList, 10, 600, TimeUnit.SECONDS);
}
}
/**
* Update the dropdown that lists all apps installed on the Roku
*/
private void refreshAppList() {
synchronized (sequenceLock) {
try {
List<App> appList = communicator.getAppList();
List<StateOption> appListOptions = new ArrayList<>();
// Roku Home will be selected in the drop-down any time an app is not running.
appListOptions.add(new StateOption(ROKU_HOME_ID, ROKU_HOME));
appList.forEach(app -> {
appListOptions.add(new StateOption(app.getId(), app.getValue()));
});
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), ACTIVE_APP),
appListOptions);
} catch (RokuHttpException e) {
logger.debug("Unable to retrieve Roku installed app-list. Exception: {}", e.getMessage(), e);
}
}
}
@Override
public void dispose() {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob != null) {
refreshJob.cancel(true);
this.refreshJob = null;
}
ScheduledFuture<?> appListJob = this.appListJob;
if (appListJob != null) {
appListJob.cancel(true);
this.appListJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
logger.debug("Unsupported refresh command: {}", command);
} else if (channelUID.getId().equals(BUTTON)) {
synchronized (sequenceLock) {
try {
communicator.keyPress(command.toString());
} catch (RokuHttpException e) {
logger.debug("Unable to send keypress to Roku, key: {}, Exception: {}", command, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
} else if (channelUID.getId().equals(ACTIVE_APP)) {
synchronized (sequenceLock) {
try {
String appId = command.toString();
// Roku Home(-1) is not a real appId, just press the home button instead
if (!ROKU_HOME_ID.equals(appId)) {
communicator.launchApp(appId);
} else {
communicator.keyPress(ROKU_HOME_BUTTON);
}
} catch (RokuHttpException e) {
logger.debug("Unable to launch app on Roku, appId: {}, Exception: {}", command, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
} else {
logger.debug("Unsupported command: {}", command);
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="roku" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Roku Binding</name>
<description>Controls Roku Streaming Media Players and TVs</description>
</binding:binding>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:roku:rokuconfig">
<parameter name="hostName" type="text" required="true">
<context>network-address</context>
<label>Host Name/IP Address</label>
<description>Host Name or IP Address of the Roku device</description>
</parameter>
<parameter name="port" type="integer" min="1" max="65535" required="true">
<label>Port</label>
<description>Port for the ECP Connector of the Roku device</description>
<default>8060</default>
<advanced>true</advanced>
</parameter>
<parameter name="refresh" type="integer" min="10" required="false" unit="s">
<label>Refresh Interval</label>
<description>Specifies the Refresh Interval in Seconds</description>
<default>10</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="roku"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Roku Player Thing -->
<thing-type id="roku_player">
<label>Roku</label>
<description>
A Roku Streaming Media Player
</description>
<channels>
<channel id="activeApp" typeId="activeApp"/>
<channel id="button" typeId="button"/>
<channel id="playMode" typeId="playMode"/>
<channel id="timeElapsed" typeId="timeElapsed"/>
<channel id="timeTotal" typeId="timeTotal"/>
</channels>
<properties>
<property name="Model Name">unknown</property>
<property name="Model Number">unknown</property>
<property name="Device Location">unknown</property>
<property name="Serial Number">unknown</property>
<property name="Device Id">unknown</property>
<property name="Software Version">unknown</property>
</properties>
<representation-property>uuid</representation-property>
<config-description-ref uri="thing-type:roku:rokuconfig"/>
</thing-type>
<!-- Roku TV Thing -->
<thing-type id="roku_tv">
<label>Roku TV</label>
<description>
A Roku Streaming Media TV
</description>
<channels>
<channel id="activeApp" typeId="activeApp"/>
<channel id="button" typeId="buttonTv"/>
<channel id="playMode" typeId="playMode"/>
<channel id="timeElapsed" typeId="timeElapsed"/>
<channel id="timeTotal" typeId="timeTotal"/>
</channels>
<properties>
<property name="Model Name">unknown</property>
<property name="Model Number">unknown</property>
<property name="Device Location">unknown</property>
<property name="Serial Number">unknown</property>
<property name="Device Id">unknown</property>
<property name="Software Version">unknown</property>
</properties>
<representation-property>uuid</representation-property>
<config-description-ref uri="thing-type:roku:rokuconfig"/>
</thing-type>
<channel-type id="button">
<item-type>String</item-type>
<label>Remote Button</label>
<description>A Remote Button Press to Send to the Roku</description>
<state>
<options>
<option value="Home">Home</option>
<option value="Rev">Reverse</option>
<option value="Fwd">Forward</option>
<option value="Play">Play</option>
<option value="Select">Select</option>
<option value="Left">Left</option>
<option value="Right">Right</option>
<option value="Down">Down</option>
<option value="Up">Up</option>
<option value="Back">Back</option>
<option value="InstantReplay">Instant Replay</option>
<option value="Info">Info</option>
<option value="Backspace">Backspace</option>
<option value="Search">Search</option>
<option value="Enter">Enter</option>
<option value="FindRemote">Find Remote</option>
</options>
</state>
</channel-type>
<channel-type id="buttonTv">
<item-type>String</item-type>
<label>Remote Button</label>
<description>A Remote Button Press to Send to the Roku TV</description>
<state>
<options>
<option value="Home">Home</option>
<option value="Rev">Reverse</option>
<option value="Fwd">Forward</option>
<option value="Play">Play</option>
<option value="Select">Select</option>
<option value="Left">Left</option>
<option value="Right">Right</option>
<option value="Down">Down</option>
<option value="Up">Up</option>
<option value="Back">Back</option>
<option value="InstantReplay">Instant Replay</option>
<option value="Info">Info</option>
<option value="Backspace">Backspace</option>
<option value="Search">Search</option>
<option value="Enter">Enter</option>
<option value="FindRemote">Find Remote</option>
<option value="VolumeUp">Volume Up</option>
<option value="VolumeDown">Volume Down</option>
<option value="VolumeMute">Volume Mute</option>
<option value="ChannelUp">Channel Up</option>
<option value="Channel Down">Channel Down</option>
<option value="InputTuner">Input Tuner</option>
<option value="InputHDMI1">Input HDMI1</option>
<option value="InputHDMI2">Input HDMI2</option>
<option value="InputHDMI3">Input HDMI3</option>
<option value="InputHDMI4">Input HDMI4</option>
<option value="InputAV1">Input AV1</option>
<option value="PowerOff">Power Off</option>
</options>
</state>
</channel-type>
<channel-type id="activeApp">
<item-type>String</item-type>
<label>Active App</label>
<description>The Currently Running App on the Roku</description>
</channel-type>
<channel-type id="playMode">
<item-type>String</item-type>
<label>Play Mode</label>
<description>The Current Playback Mode</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="timeElapsed">
<item-type>Number:Time</item-type>
<label>Playback Time</label>
<description>The Current Playback Time Elapsed</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="timeTotal">
<item-type>Number:Time</item-type>
<label>Total Time</label>
<description>The Total Length of the Current Title</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -248,6 +248,7 @@
<module>org.openhab.binding.rfxcom</module>
<module>org.openhab.binding.rme</module>
<module>org.openhab.binding.robonect</module>
<module>org.openhab.binding.roku</module>
<module>org.openhab.binding.rotel</module>
<module>org.openhab.binding.russound</module>
<module>org.openhab.binding.sagercaster</module>