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.atlona.internal.discovery;
15 import static org.openhab.binding.atlona.internal.AtlonaBindingConstants.*;
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.util.Collections;
24 import java.util.HashMap;
25 import java.util.List;
27 import java.util.concurrent.ExecutorService;
28 import java.util.concurrent.Executors;
29 import java.util.concurrent.TimeUnit;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
33 import org.openhab.binding.atlona.internal.pro3.AtlonaPro3Config;
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.thing.ThingTypeUID;
39 import org.openhab.core.thing.ThingUID;
40 import org.osgi.service.component.annotations.Component;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * Discovery class for the Atlona PRO3 line. The PRO3 line uses SDDP (simple device discovery protocol) for discovery
46 * (similar to UPNP but defined by Control4). The user should start the discovery process in openhab and then log into
47 * the switch, go to the Network options and press the SDDP button (which initiates the SDDP conversation).
49 * @author Tim Roberts - Initial contribution
51 @Component(service = DiscoveryService.class, configurationPid = "discovery.atlona")
52 public class AtlonaDiscovery extends AbstractDiscoveryService {
54 private final Logger logger = LoggerFactory.getLogger(AtlonaDiscovery.class);
57 * Address SDDP broadcasts on
59 private static final String SDDP_ADDR = "239.255.255.250";
62 * Port number SDDP uses
64 private static final int SDDP_PORT = 1902;
67 * SDDP packet should be only 512 in size - make it 600 to give us some room
69 private static final int BUFFER_SIZE = 600;
72 * Socket read timeout (in ms) - allows us to shutdown the listening every TIMEOUT
74 private static final int TIMEOUT = 1000;
77 * Whether we are currently scanning or not
79 private boolean scanning;
82 * The {@link ExecutorService} to run the listening threads on.
84 private ExecutorService executorService;
87 * Constructs the discovery class using the thing IDs that we can discover.
89 public AtlonaDiscovery() {
90 super(Collections.unmodifiableSet(
91 Stream.of(THING_TYPE_PRO3_44M, THING_TYPE_PRO3_66M, THING_TYPE_PRO3_88M, THING_TYPE_PRO3_1616M)
92 .collect(Collectors.toSet())),
99 * Starts the scan. This discovery will:
101 * <li>Request all the network interfaces</li>
102 * <li>For each network interface, create a listening thread using {@link #executorService}</li>
103 * <li>Each listening thread will open up a {@link MulticastSocket} using {@link #SDDP_ADDR} and {@link #SDDP_PORT}
105 * will receive any {@link DatagramPacket} that comes in</li>
106 * <li>The {@link DatagramPacket} is then investigated to see if is a SDDP packet and will create a new thing from
109 * The process will continue until {@link #stopScan()} is called.
112 protected void startScan() {
113 if (executorService != null) {
117 logger.debug("Starting Discovery");
120 final InetAddress addr = InetAddress.getByName(SDDP_ADDR);
121 final List<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
123 executorService = Executors.newFixedThreadPool(networkInterfaces.size());
125 for (final NetworkInterface netint : networkInterfaces) {
127 executorService.execute(() -> {
129 MulticastSocket multiSocket = new MulticastSocket(SDDP_PORT);
130 multiSocket.setSoTimeout(TIMEOUT);
131 multiSocket.setNetworkInterface(netint);
132 multiSocket.joinGroup(addr);
135 DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
137 multiSocket.receive(receivePacket);
139 String message = new String(receivePacket.getData()).trim();
140 if (message.length() > 0) {
141 messageReceive(message);
143 } catch (SocketTimeoutException e) {
149 } catch (Exception e) {
150 if (!e.getMessage().contains("No IP addresses bound to interface")) {
151 logger.debug("Error getting ip addresses: {}", e.getMessage(), e);
156 } catch (IOException e) {
157 logger.debug("Error getting ip addresses: {}", e.getMessage(), e);
162 * SDDP message has the following format
165 * NOTIFY ALIVE SDDP/1.0
166 * From: "192.168.1.30:1902"
167 * Host: "AT-UHD-PRO3-88M_B898B0030F4D"
168 * Type: "AT-UHD-PRO3-88M"
170 * Primary-Proxy: "avswitch"
171 * Proxies: "avswitch"
172 * Manufacturer: "Atlona"
173 * Model: "AT-UHD-PRO3-88M"
174 * Driver: "avswitch_Atlona_AT-UHD-PRO3-88M_IP.c4i"
175 * Config-URL: "http://192.168.1.30/"
178 * First parse the manufacturer, host, model and IP address from the message. For the "Host" field, we parse out the
179 * serial #. For the From field, we parse out the IP address (minus the port #). If we successfully found all four
180 * and the manufacturer is "Atlona" and it's a model we recognize, we then create our thing from it.
182 * @param message possibly null, possibly empty SDDP message
184 private void messageReceive(String message) {
185 if (message == null || message.trim().length() == 0) {
192 String manufacturer = null;
194 for (String msg : message.split("\r\n")) {
195 int idx = msg.indexOf(':');
197 String name = msg.substring(0, idx);
199 if ("Host".equalsIgnoreCase(name)) {
200 host = msg.substring(idx + 1).trim().replaceAll("\"", "");
201 int sep = host.indexOf('_');
203 host = host.substring(sep + 1);
205 } else if ("Model".equalsIgnoreCase(name)) {
206 model = msg.substring(idx + 1).trim().replaceAll("\"", "");
207 } else if ("Manufacturer".equalsIgnoreCase(name)) {
208 manufacturer = msg.substring(idx + 1).trim().replaceAll("\"", "");
209 } else if ("From".equalsIgnoreCase(name)) {
210 from = msg.substring(idx + 1).trim().replaceAll("\"", "");
211 int sep = from.indexOf(':');
213 from = from.substring(0, sep);
220 if (!"Atlona".equalsIgnoreCase(manufacturer)) {
224 if (host != null && model != null && from != null) {
225 ThingTypeUID typeId = null;
226 if ("AT-UHD-PRO3-44M".equalsIgnoreCase(model)) {
227 typeId = THING_TYPE_PRO3_44M;
228 } else if ("AT-UHD-PRO3-66M".equalsIgnoreCase(model)) {
229 typeId = THING_TYPE_PRO3_66M;
230 } else if ("AT-UHD-PRO3-88M".equalsIgnoreCase(model)) {
231 typeId = THING_TYPE_PRO3_88M;
232 } else if ("AT-UHD-PRO3-1616M".equalsIgnoreCase(model)) {
233 typeId = THING_TYPE_PRO3_1616M;
235 logger.warn("Unknown model #: {}", model);
238 if (typeId != null) {
239 logger.debug("Creating binding for {} ({})", model, from);
240 ThingUID j = new ThingUID(typeId, host);
242 Map<String, Object> properties = new HashMap<>(1);
243 properties.put(AtlonaPro3Config.IP_ADDRESS, from);
244 DiscoveryResult result = DiscoveryResultBuilder.create(j).withProperties(properties)
245 .withLabel(model + " (" + from + ")").build();
246 thingDiscovered(result);
254 * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally
255 * within {@link #TIMEOUT) * 5 time then shutdown the {@link #executorService}
258 protected synchronized void stopScan() {
260 if (executorService == null) {
267 executorService.awaitTermination(TIMEOUT * 5, TimeUnit.MILLISECONDS);
268 } catch (InterruptedException e) {
270 executorService.shutdown();
271 executorService = null;