2 * Copyright (c) 2010-2023 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.oppo.internal.discovery;
15 import static org.openhab.binding.oppo.internal.OppoBindingConstants.*;
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.InetAddress;
20 import java.net.MulticastSocket;
21 import java.net.NetworkInterface;
22 import java.net.SocketTimeoutException;
23 import java.nio.charset.StandardCharsets;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
28 import java.util.concurrent.ExecutorService;
29 import java.util.concurrent.Executors;
30 import java.util.concurrent.TimeUnit;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.core.config.discovery.AbstractDiscoveryService;
35 import org.openhab.core.config.discovery.DiscoveryResult;
36 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
37 import org.openhab.core.config.discovery.DiscoveryService;
38 import org.openhab.core.io.net.http.HttpUtil;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.thing.ThingUID;
41 import org.osgi.service.component.annotations.Component;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * Discovery class for the Oppo Blu-ray Player line.
47 * The player sends SDDP packets continuously for us to discover.
49 * @author Tim Roberts - Initial contribution
50 * @author Michael Lobstein - Adapted for the Oppo binding
54 @Component(service = DiscoveryService.class, configurationPid = "discovery.oppo")
55 public class OppoDiscoveryService extends AbstractDiscoveryService {
56 private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PLAYER);
58 private final Logger logger = LoggerFactory.getLogger(OppoDiscoveryService.class);
61 * Address SDDP broadcasts on
63 private static final String SDDP_ADDR = "239.255.255.251";
66 * Port number SDDP uses
68 private static final int SDDP_PORT = 7624;
71 * SDDP packet should be only 512 in size - make it 600 to give us some room
73 private static final int BUFFER_SIZE = 600;
76 * Socket read timeout (in ms) - allows us to shutdown the listening every TIMEOUT
78 private static final int TIMEOUT_MS = 1000;
81 * Whether we are currently scanning or not
83 private boolean scanning;
86 * The {@link ExecutorService} to run the listening threads on.
88 private @Nullable ExecutorService executorService;
90 private static final String DISPLAY_NAME_83 = "OPPO BDP-83/93/95";
91 private static final String DISPLAY_NAME_103 = "OPPO BDP-103";
92 private static final String DISPLAY_NAME_105 = "OPPO BDP-105";
95 * Constructs the discovery class using the thing IDs that we can discover.
97 public OppoDiscoveryService() {
98 super(SUPPORTED_THING_TYPES_UIDS, 30, false);
102 public Set<ThingTypeUID> getSupportedThingTypes() {
103 return SUPPORTED_THING_TYPES_UIDS;
109 * Starts the scan. This discovery will:
111 * <li>Request all the network interfaces</li>
112 * <li>For each network interface, create a listening thread using {@link #executorService}</li>
113 * <li>Each listening thread will open up a {@link MulticastSocket} using {@link #SDDP_ADDR} and {@link #SDDP_PORT}
115 * will receive any {@link DatagramPacket} that comes in</li>
116 * <li>The {@link DatagramPacket} is then investigated to see if is a SDDP packet and will create a new thing from
119 * The process will continue until {@link #stopScan()} is called.
122 protected void startScan() {
123 if (executorService != null) {
127 logger.debug("Starting Discovery");
130 final InetAddress addr = InetAddress.getByName(SDDP_ADDR);
131 final List<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
132 final ExecutorService service = Executors.newFixedThreadPool(networkInterfaces.size());
133 executorService = service;
136 for (final NetworkInterface netint : networkInterfaces) {
138 service.execute(() -> {
140 MulticastSocket multiSocket = new MulticastSocket(SDDP_PORT);
141 multiSocket.setSoTimeout(TIMEOUT_MS);
142 multiSocket.setNetworkInterface(netint);
143 multiSocket.joinGroup(addr);
146 DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
148 multiSocket.receive(receivePacket);
150 String message = new String(receivePacket.getData(), StandardCharsets.US_ASCII).trim();
151 if (message.length() > 0) {
152 messageReceive(message);
154 } catch (SocketTimeoutException e) {
160 } catch (IOException e) {
161 if (e.getMessage() != null && !e.getMessage().contains("No IP addresses bound to interface")) {
162 logger.debug("OppoDiscoveryService IOException: {}", e.getMessage(), e);
167 } catch (IOException e) {
168 logger.debug("OppoDiscoveryService IOException: {}", e.getMessage(), e);
173 * SDDP message has the following format
176 * Notify: OPPO Player Start
177 * Server IP: 192.168.0.2
179 * Server Name: OPPO UDP-203
183 * @param message possibly null, possibly empty SDDP message
185 private void messageReceive(String message) {
186 if (message.trim().length() == 0) {
192 Integer model = null;
193 String displayName = null;
195 for (String msg : message.split("\n")) {
196 String[] line = msg.split(":");
198 if (line.length == 2) {
199 if (line[0].contains("Server IP")) {
200 host = line[1].trim();
203 if (line[0].contains("Server Port")) {
204 port = line[1].trim();
207 if (line[0].contains("Server Name")) {
208 // example: "OPPO UDP-203"
209 // note: Server Name only provided on UDP models, not present on BDP models
210 displayName = line[1].trim();
213 logger.debug("messageReceive() - Unable to process line: {}", msg);
217 // by looking at the port number we can mostly determine what the model number is
218 if (host != null && port != null) {
219 if (BDP83_PORT.toString().equals(port)) {
221 displayName = DISPLAY_NAME_83;
222 } else if (BDP10X_PORT.toString().equals(port)) {
223 // The older models do not have the "Server Name" in the discovery packet
224 // for the 10x we need to get the DLNA service list page and find modelNumber there
225 // in order to determine if this is a BDP-103 or BDP-105
227 String result = HttpUtil.executeUrl("GET", "http://" + host + ":2870/dmr.xml", 5000);
229 if (result != null && result.contains("<modelName>OPPO BDP-103</modelName>")) {
231 displayName = DISPLAY_NAME_103;
232 } else if (result != null && result.contains("<modelName>OPPO BDP-105</modelName>")) {
234 displayName = DISPLAY_NAME_105;
237 displayName = DISPLAY_NAME_103;
239 } catch (IOException e) {
240 logger.debug("Error getting player DLNA info page: {}", e.getMessage());
241 // the call failed for some reason, just assume we are a 103
243 displayName = DISPLAY_NAME_103;
245 } else if (BDP20X_PORT.toString().equals(port)) {
246 if (displayName != null && displayName.contains(Integer.toString(MODEL203))) {
248 } else if (displayName != null && displayName.contains(Integer.toString(MODEL205))) {
252 displayName = "Unknown OPPO UDP player";
257 ThingUID uid = new ThingUID(THING_TYPE_PLAYER, host.replace(".", "_"));
258 HashMap<String, Object> properties = new HashMap<>();
259 properties.put("model", model);
260 properties.put("host", host);
262 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
263 .withRepresentationProperty("host").withLabel(displayName + " (" + host + ")").build();
265 this.thingDiscovered(result);
273 * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally
274 * within {@link #TIMEOUT) * 5 time then shutdown the {@link #executorService}
277 protected synchronized void stopScan() {
279 ExecutorService service = executorService;
280 if (service == null) {
287 service.awaitTermination(TIMEOUT_MS * 5, TimeUnit.MILLISECONDS);
288 } catch (InterruptedException e) {
291 executorService = null;