[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
23 changed files with 2659 additions and 0 deletions

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>