]> git.basschouten.com Git - openhab-addons.git/blob
4de31f229d873afaccaeda2133d582e6440ca638
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.roku.internal.discovery;
14
15 import static org.openhab.binding.roku.internal.RokuBindingConstants.*;
16
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;
31
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;
49
50 /**
51  * The {@link RokuDiscoveryService} is responsible for discovery of Roku devices on the local network
52  *
53  * @author William Welliver - Initial contribution
54  * @author Dan Cunningham - Refactoring and Improvements
55  * @author Michael Lobstein - Modified for Roku binding
56  */
57
58 @NonNullByDefault
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 = """
63             M-SEARCH * HTTP/1.1\r
64             Host: 239.255.255.250:1900\r
65             Man: "ssdp:discover"\r
66             ST: roku:ecp\r
67             \r
68             """;
69
70     private static final Pattern USN_PATTERN = Pattern.compile("^(uuid:roku:)?ecp:([0-9a-zA-Z]{1,16})");
71
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})");
74
75     private static final String ROKU_SSDP_MATCH = "uuid:roku:ecp";
76     private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
77
78     private final HttpClient httpClient;
79
80     private @Nullable ScheduledFuture<?> scheduledFuture;
81
82     @Activate
83     public RokuDiscoveryService(final @Reference HttpClientFactory httpClientFactory) {
84         super(SUPPORTED_THING_TYPES_UIDS, 30, true);
85         this.httpClient = httpClientFactory.getCommonHttpClient();
86     }
87
88     @Override
89     public void startBackgroundDiscovery() {
90         stopBackgroundDiscovery();
91         scheduledFuture = scheduler.scheduleWithFixedDelay(this::doNetworkScan, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
92                 TimeUnit.SECONDS);
93     }
94
95     @Override
96     public void stopBackgroundDiscovery() {
97         ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
98         if (scheduledFuture != null) {
99             scheduledFuture.cancel(true);
100         }
101         this.scheduledFuture = null;
102     }
103
104     @Override
105     public void startScan() {
106         doNetworkScan();
107     }
108
109     /**
110      * Enumerate all network interfaces, send the discovery broadcast and process responses.
111      *
112      */
113     private synchronized void doNetworkScan() {
114         try {
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);
121                     }
122                 }
123             }
124         } catch (IOException e) {
125             logger.debug("Error discovering devices", e);
126         }
127     }
128
129     /**
130      * Broadcasts a SSDP discovery message into the network to find provided services.
131      *
132      * @return The Socket where answers to the discovery broadcast arrive
133      */
134     private @Nullable DatagramSocket sendDiscoveryBroacast(NetworkInterface ni) {
135         try {
136             InetAddress m = InetAddress.getByName("239.255.255.250");
137             final int port = 1900;
138
139             if (!ni.isUp() || !ni.supportsMulticast()) {
140                 return null;
141             }
142
143             Enumeration<InetAddress> addrs = ni.getInetAddresses();
144             InetAddress a = null;
145             while (addrs.hasMoreElements()) {
146                 a = addrs.nextElement();
147                 if (a instanceof Inet4Address) {
148                     break;
149                 } else {
150                     a = null;
151                 }
152             }
153             if (a == null) {
154                 logger.debug("No ipv4 address on {}", ni.getName());
155                 return null;
156             }
157
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);
161
162             // Create socket and send the discovery message
163             DatagramSocket socket = new DatagramSocket();
164             socket.setSoTimeout(3000);
165             socket.send(datagramPacket);
166             return socket;
167         } catch (IOException e) {
168             logger.debug("sendDiscoveryBroacast() got IOException: {}", e.getMessage());
169             return null;
170         }
171     }
172
173     /**
174      * Scans all messages that arrive on the socket and process those that come from a Roku.
175      *
176      * @param socket The socket where answers to the discovery broadcast arrive
177      */
178     private void scanResposesForKeywords(DatagramSocket socket) {
179         byte[] receiveData = new byte[1024];
180         do {
181             DatagramPacket packet = new DatagramPacket(receiveData, receiveData.length);
182             try {
183                 socket.receive(packet);
184             } catch (SocketTimeoutException e) {
185                 return;
186             } catch (IOException e) {
187                 logger.debug("Got exception while trying to receive UPnP packets: {}", e.getMessage());
188                 return;
189             }
190             String response = new String(packet.getData(), StandardCharsets.UTF_8);
191             if (response.contains(ROKU_SSDP_MATCH)) {
192                 parseResponseCreateThing(response);
193             }
194         } while (true);
195     }
196
197     /**
198      * Process the response from the Roku into a DiscoveryResult.
199      *
200      */
201     private void parseResponseCreateThing(String response) {
202         DiscoveryResult result;
203
204         String label = "Roku";
205         String uuid = null;
206         String host = null;
207         int port = -1;
208
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) {
214                     continue;
215                 }
216
217                 String key = pair[0].toLowerCase();
218                 String value = pair[1].trim();
219                 logger.debug("key: {} value: {}.", key, value);
220                 switch (key) {
221                     case "location":
222                         host = 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));
227                         }
228                         break;
229                     case "usn":
230                         Matcher matchUid = USN_PATTERN.matcher(value);
231                         if (matchUid.find()) {
232                             uuid = matchUid.group(2);
233                         }
234                         break;
235                     default:
236                         break;
237                 }
238             }
239         }
240
241         if (host == null || port == -1 || uuid == null) {
242             logger.debug("Bad Format from Roku, received data was: {}", response);
243             return;
244         } else {
245             logger.debug("Found Roku, uuid: {} host: {}", uuid, host);
246         }
247
248         uuid = uuid.replace(":", "").toLowerCase();
249
250         ThingUID thingUid = new ThingUID(THING_TYPE_ROKU_PLAYER, uuid);
251
252         // Try to query the device using discovered host and port to get extended device info
253         try {
254             RokuCommunicator communicator = new RokuCommunicator(httpClient, host, port);
255             DeviceInfo device = communicator.getDeviceInfo();
256             label = device.getModelName() + " " + device.getModelNumber();
257             if (device.isTv()) {
258                 thingUid = new ThingUID(THING_TYPE_ROKU_TV, uuid);
259             }
260         } catch (RokuHttpException e) {
261             logger.debug("Unable to retrieve Roku device-info. Exception: {}", e.getMessage(), e);
262         }
263
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);
268     }
269 }