]> git.basschouten.com Git - openhab-addons.git/blob
23033036e0e1427d08c0329120acc50e6ea3a0c0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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 = "M-SEARCH * HTTP/1.1\r\n" + "Host: 239.255.255.250:1900\r\n"
63             + "Man: \"ssdp:discover\"\r\n" + "ST: roku:ecp\r\n" + "\r\n";
64
65     private static final Pattern USN_PATTERN = Pattern.compile("^(uuid:roku:)?ecp:([0-9a-zA-Z]{1,16})");
66
67     private static final Pattern IP_HOST_PATTERN = Pattern
68             .compile("([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):([0-9]{1,5})");
69
70     private static final String ROKU_SSDP_MATCH = "uuid:roku:ecp";
71     private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
72
73     private final HttpClient httpClient;
74
75     private @Nullable ScheduledFuture<?> scheduledFuture;
76
77     @Activate
78     public RokuDiscoveryService(final @Reference HttpClientFactory httpClientFactory) {
79         super(SUPPORTED_THING_TYPES_UIDS, 30, true);
80         this.httpClient = httpClientFactory.getCommonHttpClient();
81     }
82
83     @Override
84     public void startBackgroundDiscovery() {
85         stopBackgroundDiscovery();
86         scheduledFuture = scheduler.scheduleWithFixedDelay(this::doNetworkScan, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
87                 TimeUnit.SECONDS);
88     }
89
90     @Override
91     public void stopBackgroundDiscovery() {
92         ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
93         if (scheduledFuture != null) {
94             scheduledFuture.cancel(true);
95         }
96         this.scheduledFuture = null;
97     }
98
99     @Override
100     public void startScan() {
101         doNetworkScan();
102     }
103
104     /**
105      * Enumerate all network interfaces, send the discovery broadcast and process responses.
106      *
107      */
108     private synchronized void doNetworkScan() {
109         try {
110             Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
111             while (nets.hasMoreElements()) {
112                 NetworkInterface ni = nets.nextElement();
113                 try (DatagramSocket socket = sendDiscoveryBroacast(ni)) {
114                     if (socket != null) {
115                         scanResposesForKeywords(socket);
116                     }
117                 }
118             }
119         } catch (IOException e) {
120             logger.debug("Error discovering devices", e);
121         }
122     }
123
124     /**
125      * Broadcasts a SSDP discovery message into the network to find provided services.
126      *
127      * @return The Socket where answers to the discovery broadcast arrive
128      */
129     private @Nullable DatagramSocket sendDiscoveryBroacast(NetworkInterface ni) {
130         try {
131             InetAddress m = InetAddress.getByName("239.255.255.250");
132             final int port = 1900;
133
134             if (!ni.isUp() || !ni.supportsMulticast()) {
135                 return null;
136             }
137
138             Enumeration<InetAddress> addrs = ni.getInetAddresses();
139             InetAddress a = null;
140             while (addrs.hasMoreElements()) {
141                 a = addrs.nextElement();
142                 if (a instanceof Inet4Address) {
143                     break;
144                 } else {
145                     a = null;
146                 }
147             }
148             if (a == null) {
149                 logger.debug("No ipv4 address on {}", ni.getName());
150                 return null;
151             }
152
153             // Create the discovery message packet
154             byte[] requestMessage = ROKU_DISCOVERY_MESSAGE.getBytes(StandardCharsets.UTF_8);
155             DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
156
157             // Create socket and send the discovery message
158             DatagramSocket socket = new DatagramSocket();
159             socket.setSoTimeout(3000);
160             socket.send(datagramPacket);
161             return socket;
162         } catch (IOException e) {
163             logger.debug("sendDiscoveryBroacast() got IOException: {}", e.getMessage());
164             return null;
165         }
166     }
167
168     /**
169      * Scans all messages that arrive on the socket and process those that come from a Roku.
170      *
171      * @param socket The socket where answers to the discovery broadcast arrive
172      */
173     private void scanResposesForKeywords(DatagramSocket socket) {
174         byte[] receiveData = new byte[1024];
175         do {
176             DatagramPacket packet = new DatagramPacket(receiveData, receiveData.length);
177             try {
178                 socket.receive(packet);
179             } catch (SocketTimeoutException e) {
180                 return;
181             } catch (IOException e) {
182                 logger.debug("Got exception while trying to receive UPnP packets: {}", e.getMessage());
183                 return;
184             }
185             String response = new String(packet.getData(), StandardCharsets.UTF_8);
186             if (response.contains(ROKU_SSDP_MATCH)) {
187                 parseResponseCreateThing(response);
188             }
189         } while (true);
190     }
191
192     /**
193      * Process the response from the Roku into a DiscoveryResult.
194      *
195      */
196     private void parseResponseCreateThing(String response) {
197         DiscoveryResult result;
198
199         String label = "Roku";
200         String uuid = null;
201         String host = null;
202         int port = -1;
203
204         try (Scanner scanner = new Scanner(response)) {
205             while (scanner.hasNextLine()) {
206                 String line = scanner.nextLine();
207                 String[] pair = line.split(":", 2);
208                 if (pair.length != 2) {
209                     continue;
210                 }
211
212                 String key = pair[0].toLowerCase();
213                 String value = pair[1].trim();
214                 logger.debug("key: {} value: {}.", key, value);
215                 switch (key) {
216                     case "location":
217                         host = value;
218                         Matcher matchIp = IP_HOST_PATTERN.matcher(value);
219                         if (matchIp.find()) {
220                             host = matchIp.group(1);
221                             port = Integer.parseInt(matchIp.group(2));
222                         }
223                         break;
224                     case "usn":
225                         Matcher matchUid = USN_PATTERN.matcher(value);
226                         if (matchUid.find()) {
227                             uuid = matchUid.group(2);
228                         }
229                         break;
230                     default:
231                         break;
232                 }
233             }
234         }
235
236         if (host == null || port == -1 || uuid == null) {
237             logger.debug("Bad Format from Roku, received data was: {}", response);
238             return;
239         } else {
240             logger.debug("Found Roku, uuid: {} host: {}", uuid, host);
241         }
242
243         uuid = uuid.replace(":", "").toLowerCase();
244
245         ThingUID thingUid = new ThingUID(THING_TYPE_ROKU_PLAYER, uuid);
246
247         // Try to query the device using discovered host and port to get extended device info
248         try {
249             RokuCommunicator communicator = new RokuCommunicator(httpClient, host, port);
250             DeviceInfo device = communicator.getDeviceInfo();
251             label = device.getModelName() + " " + device.getModelNumber();
252             if (device.isTv()) {
253                 thingUid = new ThingUID(THING_TYPE_ROKU_TV, uuid);
254             }
255         } catch (RokuHttpException e) {
256             logger.debug("Unable to retrieve Roku device-info. Exception: {}", e.getMessage(), e);
257         }
258
259         result = DiscoveryResultBuilder.create(thingUid).withLabel(label).withRepresentationProperty(PROPERTY_UUID)
260                 .withProperty(PROPERTY_UUID, uuid).withProperty(PROPERTY_HOST_NAME, host)
261                 .withProperty(PROPERTY_PORT, port).build();
262         this.thingDiscovered(result);
263     }
264 }