2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.roku.internal.discovery;
15 import static org.openhab.binding.roku.internal.RokuBindingConstants.*;
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.DatagramSocket;
20 import java.net.Inet4Address;
21 import java.net.InetAddress;
22 import java.net.NetworkInterface;
23 import java.net.SocketTimeoutException;
24 import java.nio.charset.StandardCharsets;
25 import java.util.Enumeration;
26 import java.util.Scanner;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.openhab.binding.roku.internal.RokuHttpException;
36 import org.openhab.binding.roku.internal.communication.RokuCommunicator;
37 import org.openhab.binding.roku.internal.dto.DeviceInfo;
38 import org.openhab.core.config.discovery.AbstractDiscoveryService;
39 import org.openhab.core.config.discovery.DiscoveryResult;
40 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
41 import org.openhab.core.config.discovery.DiscoveryService;
42 import org.openhab.core.io.net.http.HttpClientFactory;
43 import org.openhab.core.thing.ThingUID;
44 import org.osgi.service.component.annotations.Activate;
45 import org.osgi.service.component.annotations.Component;
46 import org.osgi.service.component.annotations.Reference;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * The {@link RokuDiscoveryService} is responsible for discovery of Roku devices on the local network
53 * @author William Welliver - Initial contribution
54 * @author Dan Cunningham - Refactoring and Improvements
55 * @author Michael Lobstein - Modified for Roku binding
59 @Component(service = DiscoveryService.class, configurationPid = "discovery.roku")
60 public class RokuDiscoveryService extends AbstractDiscoveryService {
61 private final Logger logger = LoggerFactory.getLogger(RokuDiscoveryService.class);
62 private static final String ROKU_DISCOVERY_MESSAGE = """
64 Host: 239.255.255.250:1900\r
65 Man: "ssdp:discover"\r
70 private static final Pattern USN_PATTERN = Pattern.compile("^(uuid:roku:)?ecp:([0-9a-zA-Z]{1,16})");
72 private static final Pattern IP_HOST_PATTERN = Pattern
73 .compile("([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):([0-9]{1,5})");
75 private static final String ROKU_SSDP_MATCH = "uuid:roku:ecp";
76 private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
78 private final HttpClient httpClient;
80 private @Nullable ScheduledFuture<?> scheduledFuture;
83 public RokuDiscoveryService(final @Reference HttpClientFactory httpClientFactory) {
84 super(SUPPORTED_THING_TYPES_UIDS, 30, true);
85 this.httpClient = httpClientFactory.getCommonHttpClient();
89 public void startBackgroundDiscovery() {
90 stopBackgroundDiscovery();
91 scheduledFuture = scheduler.scheduleWithFixedDelay(this::doNetworkScan, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
96 public void stopBackgroundDiscovery() {
97 ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
98 if (scheduledFuture != null) {
99 scheduledFuture.cancel(true);
101 this.scheduledFuture = null;
105 public void startScan() {
110 * Enumerate all network interfaces, send the discovery broadcast and process responses.
113 private synchronized void doNetworkScan() {
115 Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
116 while (nets.hasMoreElements()) {
117 NetworkInterface ni = nets.nextElement();
118 try (DatagramSocket socket = sendDiscoveryBroacast(ni)) {
119 if (socket != null) {
120 scanResposesForKeywords(socket);
124 } catch (IOException e) {
125 logger.debug("Error discovering devices", e);
130 * Broadcasts a SSDP discovery message into the network to find provided services.
132 * @return The Socket where answers to the discovery broadcast arrive
134 private @Nullable DatagramSocket sendDiscoveryBroacast(NetworkInterface ni) {
136 InetAddress m = InetAddress.getByName("239.255.255.250");
137 final int port = 1900;
139 if (!ni.isUp() || !ni.supportsMulticast()) {
143 Enumeration<InetAddress> addrs = ni.getInetAddresses();
144 InetAddress a = null;
145 while (addrs.hasMoreElements()) {
146 a = addrs.nextElement();
147 if (a instanceof Inet4Address) {
154 logger.debug("No ipv4 address on {}", ni.getName());
158 // Create the discovery message packet
159 byte[] requestMessage = ROKU_DISCOVERY_MESSAGE.getBytes(StandardCharsets.UTF_8);
160 DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
162 // Create socket and send the discovery message
163 DatagramSocket socket = new DatagramSocket();
164 socket.setSoTimeout(3000);
165 socket.send(datagramPacket);
167 } catch (IOException e) {
168 logger.debug("sendDiscoveryBroacast() got IOException: {}", e.getMessage());
174 * Scans all messages that arrive on the socket and process those that come from a Roku.
176 * @param socket The socket where answers to the discovery broadcast arrive
178 private void scanResposesForKeywords(DatagramSocket socket) {
179 byte[] receiveData = new byte[1024];
181 DatagramPacket packet = new DatagramPacket(receiveData, receiveData.length);
183 socket.receive(packet);
184 } catch (SocketTimeoutException e) {
186 } catch (IOException e) {
187 logger.debug("Got exception while trying to receive UPnP packets: {}", e.getMessage());
190 String response = new String(packet.getData(), StandardCharsets.UTF_8);
191 if (response.contains(ROKU_SSDP_MATCH)) {
192 parseResponseCreateThing(response);
198 * Process the response from the Roku into a DiscoveryResult.
201 private void parseResponseCreateThing(String response) {
202 DiscoveryResult result;
204 String label = "Roku";
209 try (Scanner scanner = new Scanner(response)) {
210 while (scanner.hasNextLine()) {
211 String line = scanner.nextLine();
212 String[] pair = line.split(":", 2);
213 if (pair.length != 2) {
217 String key = pair[0].toLowerCase();
218 String value = pair[1].trim();
219 logger.debug("key: {} value: {}.", key, value);
223 Matcher matchIp = IP_HOST_PATTERN.matcher(value);
224 if (matchIp.find()) {
225 host = matchIp.group(1);
226 port = Integer.parseInt(matchIp.group(2));
230 Matcher matchUid = USN_PATTERN.matcher(value);
231 if (matchUid.find()) {
232 uuid = matchUid.group(2);
241 if (host == null || port == -1 || uuid == null) {
242 logger.debug("Bad Format from Roku, received data was: {}", response);
245 logger.debug("Found Roku, uuid: {} host: {}", uuid, host);
248 uuid = uuid.replace(":", "").toLowerCase();
250 ThingUID thingUid = new ThingUID(THING_TYPE_ROKU_PLAYER, uuid);
252 // Try to query the device using discovered host and port to get extended device info
254 RokuCommunicator communicator = new RokuCommunicator(httpClient, host, port);
255 DeviceInfo device = communicator.getDeviceInfo();
256 label = device.getModelName() + " " + device.getModelNumber();
258 thingUid = new ThingUID(THING_TYPE_ROKU_TV, uuid);
260 } catch (RokuHttpException e) {
261 logger.debug("Unable to retrieve Roku device-info. Exception: {}", e.getMessage(), e);
264 result = DiscoveryResultBuilder.create(thingUid).withLabel(label).withRepresentationProperty(PROPERTY_UUID)
265 .withProperty(PROPERTY_UUID, uuid).withProperty(PROPERTY_HOST_NAME, host)
266 .withProperty(PROPERTY_PORT, port).build();
267 this.thingDiscovered(result);