]> git.basschouten.com Git - openhab-addons.git/blob
5f3875249289b9ff2962348a6e09e0af8040e14e
[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.oppo.internal.discovery;
14
15 import static org.openhab.binding.oppo.internal.OppoBindingConstants.*;
16
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;
27 import java.util.Set;
28 import java.util.concurrent.ExecutorService;
29 import java.util.concurrent.Executors;
30 import java.util.concurrent.TimeUnit;
31
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;
44
45 /**
46  * Discovery class for the Oppo Blu-ray Player line.
47  * The player sends SDDP packets continuously for us to discover.
48  *
49  * @author Tim Roberts - Initial contribution
50  * @author Michael Lobstein - Adapted for the Oppo binding
51  */
52
53 @NonNullByDefault
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);
57
58     private final Logger logger = LoggerFactory.getLogger(OppoDiscoveryService.class);
59
60     /**
61      * Address SDDP broadcasts on
62      */
63     private static final String SDDP_ADDR = "239.255.255.251";
64
65     /**
66      * Port number SDDP uses
67      */
68     private static final int SDDP_PORT = 7624;
69
70     /**
71      * SDDP packet should be only 512 in size - make it 600 to give us some room
72      */
73     private static final int BUFFER_SIZE = 600;
74
75     /**
76      * Socket read timeout (in ms) - allows us to shutdown the listening every TIMEOUT
77      */
78     private static final int TIMEOUT_MS = 1000;
79
80     /**
81      * Whether we are currently scanning or not
82      */
83     private boolean scanning;
84
85     /**
86      * The {@link ExecutorService} to run the listening threads on.
87      */
88     private @Nullable ExecutorService executorService;
89
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";
93
94     /**
95      * Constructs the discovery class using the thing IDs that we can discover.
96      */
97     public OppoDiscoveryService() {
98         super(SUPPORTED_THING_TYPES_UIDS, 30, false);
99     }
100
101     @Override
102     public Set<ThingTypeUID> getSupportedThingTypes() {
103         return SUPPORTED_THING_TYPES_UIDS;
104     }
105
106     /**
107      * {@inheritDoc}
108      *
109      * Starts the scan. This discovery will:
110      * <ul>
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}
114      * and
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
117      * it</li>
118      * </ul>
119      * The process will continue until {@link #stopScan()} is called.
120      */
121     @Override
122     protected void startScan() {
123         if (executorService != null) {
124             stopScan();
125         }
126
127         logger.debug("Starting Discovery");
128
129         try {
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;
134
135             scanning = true;
136             for (final NetworkInterface netint : networkInterfaces) {
137
138                 service.execute(() -> {
139                     try {
140                         MulticastSocket multiSocket = new MulticastSocket(SDDP_PORT);
141                         multiSocket.setSoTimeout(TIMEOUT_MS);
142                         multiSocket.setNetworkInterface(netint);
143                         multiSocket.joinGroup(addr);
144
145                         while (scanning) {
146                             DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
147                             try {
148                                 multiSocket.receive(receivePacket);
149
150                                 String message = new String(receivePacket.getData(), StandardCharsets.US_ASCII).trim();
151                                 if (message.length() > 0) {
152                                     messageReceive(message);
153                                 }
154                             } catch (SocketTimeoutException e) {
155                                 // ignore
156                             }
157                         }
158
159                         multiSocket.close();
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);
163                         }
164                     }
165                 });
166             }
167         } catch (IOException e) {
168             logger.debug("OppoDiscoveryService IOException: {}", e.getMessage(), e);
169         }
170     }
171
172     /**
173      * SDDP message has the following format
174      *
175      * <pre>
176      * Notify: OPPO Player Start
177      * Server IP: 192.168.0.2
178      * Server Port: 23
179      * Server Name: OPPO UDP-203
180      * </pre>
181      *
182      *
183      * @param message possibly null, possibly empty SDDP message
184      */
185     private void messageReceive(String message) {
186         if (message.trim().length() == 0) {
187             return;
188         }
189
190         String host = null;
191         String port = null;
192         Integer model = null;
193         String displayName = null;
194
195         for (String msg : message.split("\n")) {
196             String[] line = msg.split(":");
197
198             if (line.length == 2) {
199                 if (line[0].contains("Server IP")) {
200                     host = line[1].trim();
201                 }
202
203                 if (line[0].contains("Server Port")) {
204                     port = line[1].trim();
205                 }
206
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();
211                 }
212             } else {
213                 logger.debug("messageReceive() - Unable to process line: {}", msg);
214             }
215         }
216
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)) {
220                 model = MODEL83;
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
226                 try {
227                     String result = HttpUtil.executeUrl("GET", "http://" + host + ":2870/dmr.xml", 5000);
228
229                     if (result != null && result.contains("<modelName>OPPO BDP-103</modelName>")) {
230                         model = MODEL103;
231                         displayName = DISPLAY_NAME_103;
232                     } else if (result != null && result.contains("<modelName>OPPO BDP-105</modelName>")) {
233                         model = MODEL105;
234                         displayName = DISPLAY_NAME_105;
235                     } else {
236                         model = MODEL103;
237                         displayName = DISPLAY_NAME_103;
238                     }
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
242                     model = MODEL103;
243                     displayName = DISPLAY_NAME_103;
244                 }
245             } else if (BDP20X_PORT.toString().equals(port)) {
246                 if (displayName != null && displayName.contains(Integer.toString(MODEL203))) {
247                     model = MODEL203;
248                 } else if (displayName != null && displayName.contains(Integer.toString(MODEL205))) {
249                     model = MODEL205;
250                 } else {
251                     model = MODEL203;
252                     displayName = "Unknown OPPO UDP player";
253                 }
254             }
255
256             if (model != null) {
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);
261
262                 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
263                         .withRepresentationProperty("host").withLabel(displayName + " (" + host + ")").build();
264
265                 this.thingDiscovered(result);
266             }
267         }
268     }
269
270     /**
271      * {@inheritDoc}
272      *
273      * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally
274      * within {@link #TIMEOUT_MS} * 5 time then shutdown the {@link #executorService}
275      */
276     @Override
277     protected synchronized void stopScan() {
278         super.stopScan();
279         ExecutorService service = executorService;
280         if (service == null) {
281             return;
282         }
283
284         scanning = false;
285
286         try {
287             service.awaitTermination(TIMEOUT_MS * 5, TimeUnit.MILLISECONDS);
288         } catch (InterruptedException e) {
289         }
290         service.shutdown();
291         executorService = null;
292     }
293 }