]> git.basschouten.com Git - openhab-addons.git/blob
eb9a659f850387de605fcdb2aeaca7795fb9fe8d
[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.atlona.internal.discovery;
14
15 import static org.openhab.binding.atlona.internal.AtlonaBindingConstants.*;
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.util.Collections;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
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;
32
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;
43
44 /**
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).
48  *
49  * @author Tim Roberts - Initial contribution
50  */
51 @Component(service = DiscoveryService.class, configurationPid = "discovery.atlona")
52 public class AtlonaDiscovery extends AbstractDiscoveryService {
53
54     private final Logger logger = LoggerFactory.getLogger(AtlonaDiscovery.class);
55
56     /**
57      * Address SDDP broadcasts on
58      */
59     private static final String SDDP_ADDR = "239.255.255.250";
60
61     /**
62      * Port number SDDP uses
63      */
64     private static final int SDDP_PORT = 1902;
65
66     /**
67      * SDDP packet should be only 512 in size - make it 600 to give us some room
68      */
69     private static final int BUFFER_SIZE = 600;
70
71     /**
72      * Socket read timeout (in ms) - allows us to shutdown the listening every TIMEOUT
73      */
74     private static final int TIMEOUT = 1000;
75
76     /**
77      * Whether we are currently scanning or not
78      */
79     private boolean scanning;
80
81     /**
82      * The {@link ExecutorService} to run the listening threads on.
83      */
84     private ExecutorService executorService;
85
86     /**
87      * Constructs the discovery class using the thing IDs that we can discover.
88      */
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())),
93                 30, false);
94     }
95
96     /**
97      * {@inheritDoc}
98      *
99      * Starts the scan. This discovery will:
100      * <ul>
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}
104      * and
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
107      * it</li>
108      * </ul>
109      * The process will continue until {@link #stopScan()} is called.
110      */
111     @Override
112     protected void startScan() {
113         if (executorService != null) {
114             stopScan();
115         }
116
117         logger.debug("Starting Discovery");
118
119         try {
120             final InetAddress addr = InetAddress.getByName(SDDP_ADDR);
121             final List<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
122
123             executorService = Executors.newFixedThreadPool(networkInterfaces.size());
124             scanning = true;
125             for (final NetworkInterface netint : networkInterfaces) {
126
127                 executorService.execute(() -> {
128                     try {
129                         MulticastSocket multiSocket = new MulticastSocket(SDDP_PORT);
130                         multiSocket.setSoTimeout(TIMEOUT);
131                         multiSocket.setNetworkInterface(netint);
132                         multiSocket.joinGroup(addr);
133
134                         while (scanning) {
135                             DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
136                             try {
137                                 multiSocket.receive(receivePacket);
138
139                                 String message = new String(receivePacket.getData()).trim();
140                                 if (message.length() > 0) {
141                                     messageReceive(message);
142                                 }
143                             } catch (SocketTimeoutException e) {
144                                 // ignore
145                             }
146                         }
147
148                         multiSocket.close();
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);
152                         }
153                     }
154                 });
155             }
156         } catch (IOException e) {
157             logger.debug("Error getting ip addresses: {}", e.getMessage(), e);
158         }
159     }
160
161     /**
162      * SDDP message has the following format
163      *
164      * <pre>
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"
169      * Max-Age: 1800
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/"
176      * </pre>
177      *
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.
181      *
182      * @param message possibly null, possibly empty SDDP message
183      */
184     private void messageReceive(String message) {
185         if (message == null || message.trim().length() == 0) {
186             return;
187         }
188
189         String host = null;
190         String model = null;
191         String from = null;
192         String manufacturer = null;
193
194         for (String msg : message.split("\r\n")) {
195             int idx = msg.indexOf(':');
196             if (idx > 0) {
197                 String name = msg.substring(0, idx);
198
199                 if ("Host".equalsIgnoreCase(name)) {
200                     host = msg.substring(idx + 1).trim().replace("\"", "");
201                     int sep = host.indexOf('_');
202                     if (sep >= 0) {
203                         host = host.substring(sep + 1);
204                     }
205                 } else if ("Model".equalsIgnoreCase(name)) {
206                     model = msg.substring(idx + 1).trim().replace("\"", "");
207                 } else if ("Manufacturer".equalsIgnoreCase(name)) {
208                     manufacturer = msg.substring(idx + 1).trim().replace("\"", "");
209                 } else if ("From".equalsIgnoreCase(name)) {
210                     from = msg.substring(idx + 1).trim().replace("\"", "");
211                     int sep = from.indexOf(':');
212                     if (sep >= 0) {
213                         from = from.substring(0, sep);
214                     }
215                 }
216             }
217
218         }
219
220         if (!"Atlona".equalsIgnoreCase(manufacturer)) {
221             return;
222         }
223
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;
234             } else {
235                 logger.warn("Unknown model #: {}", model);
236             }
237
238             if (typeId != null) {
239                 logger.debug("Creating binding for {} ({})", model, from);
240                 ThingUID j = new ThingUID(typeId, host);
241
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);
247             }
248         }
249     }
250
251     /**
252      * {@inheritDoc}
253      *
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}
256      */
257     @Override
258     protected synchronized void stopScan() {
259         super.stopScan();
260         if (executorService == null) {
261             return;
262         }
263
264         scanning = false;
265
266         try {
267             executorService.awaitTermination(TIMEOUT * 5, TimeUnit.MILLISECONDS);
268         } catch (InterruptedException e) {
269         }
270         executorService.shutdown();
271         executorService = null;
272     }
273 }