]> git.basschouten.com Git - openhab-addons.git/blob
16818119d7dd7a5c5b0a6fb13cdb830a52eb4a9d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.russound.internal.discovery;
14
15 import java.io.IOException;
16 import java.net.Inet6Address;
17 import java.net.InterfaceAddress;
18 import java.net.NetworkInterface;
19 import java.net.SocketException;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.concurrent.ExecutorService;
27 import java.util.concurrent.Executors;
28 import java.util.concurrent.TimeUnit;
29
30 import org.apache.commons.net.util.SubnetUtils;
31 import org.openhab.binding.russound.internal.net.SocketChannelSession;
32 import org.openhab.binding.russound.internal.net.SocketSession;
33 import org.openhab.binding.russound.internal.net.WaitingSessionListener;
34 import org.openhab.binding.russound.internal.rio.RioConstants;
35 import org.openhab.binding.russound.internal.rio.system.RioSystemConfig;
36 import org.openhab.core.config.discovery.AbstractDiscoveryService;
37 import org.openhab.core.config.discovery.DiscoveryResult;
38 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
39 import org.openhab.core.config.discovery.DiscoveryService;
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  * This implementation of {@link DiscoveryService} will scan the network for any Russound RIO system devices. The scan
47  * will occur against all network interfaces.
48  *
49  * @author Tim Roberts - Initial contribution
50  */
51 @Component(service = DiscoveryService.class, configurationPid = "discovery.russound")
52 public class RioSystemDiscovery extends AbstractDiscoveryService {
53     /** The logger */
54     private final Logger logger = LoggerFactory.getLogger(RioSystemDiscovery.class);
55
56     /** The timeout to connect (in milliseconds) */
57     private static final int CONN_TIMEOUT_IN_MS = 100;
58
59     /** The {@link ExecutorService} to use for scanning - will be null if not scanning */
60     private ExecutorService executorService = null;
61
62     /** The number of network interfaces being scanned */
63     private int nbrNetworkInterfacesScanning = 0;
64
65     /**
66      * Creates the system discovery service looking for {@link RioConstants#BRIDGE_TYPE_RIO}. The scan will take at most
67      * 120 seconds (depending on how many network interfaces there are)
68      */
69     public RioSystemDiscovery() {
70         super(Set.of(RioConstants.BRIDGE_TYPE_RIO), 120);
71     }
72
73     /**
74      * Starts the scan. For each network interface (that is up and not a loopback), all addresses will be iterated
75      * and checked for something open on port 9621. If that port is open, a russound controller "type" command will be
76      * issued. If the response is a correct pattern, we assume it's a rio system device and will emit a
77      * {{@link #thingDiscovered(DiscoveryResult)}
78      */
79     @Override
80     protected void startScan() {
81         final List<NetworkInterface> interfaces;
82         try {
83             interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
84         } catch (SocketException e1) {
85             logger.debug("Exception getting network interfaces: {}", e1.getMessage(), e1);
86             return;
87         }
88
89         nbrNetworkInterfacesScanning = interfaces.size();
90         executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 10);
91
92         for (final NetworkInterface networkInterface : interfaces) {
93             try {
94                 if (networkInterface.isLoopback() || !networkInterface.isUp()) {
95                     continue;
96                 }
97             } catch (SocketException e) {
98                 continue;
99             }
100
101             for (Iterator<InterfaceAddress> it = networkInterface.getInterfaceAddresses().iterator(); it.hasNext();) {
102                 final InterfaceAddress interfaceAddress = it.next();
103
104                 // don't bother with ipv6 addresses (russound doesn't support)
105                 if (interfaceAddress.getAddress() instanceof Inet6Address) {
106                     continue;
107                 }
108
109                 final String subnetRange = interfaceAddress.getAddress().getHostAddress() + "/"
110                         + interfaceAddress.getNetworkPrefixLength();
111
112                 logger.debug("Scanning subnet: {}", subnetRange);
113                 final SubnetUtils utils = new SubnetUtils(subnetRange);
114
115                 final String[] addresses = utils.getInfo().getAllAddresses();
116
117                 for (final String address : addresses) {
118                     executorService.execute(() -> {
119                         scanAddress(address);
120                     });
121                 }
122             }
123         }
124
125         // Finishes the scan and cleans up
126         stopScan();
127     }
128
129     /**
130      * Stops the scan by terminating the {@link #executorService} and shutting it down
131      */
132     @Override
133     protected synchronized void stopScan() {
134         super.stopScan();
135         if (executorService == null) {
136             return;
137         }
138
139         try {
140             executorService.awaitTermination(CONN_TIMEOUT_IN_MS * nbrNetworkInterfacesScanning, TimeUnit.MILLISECONDS);
141         } catch (InterruptedException e) {
142             // shutting down - doesn't matter
143         }
144         executorService.shutdown();
145         executorService = null;
146     }
147
148     /**
149      * Helper method to scan a specific address. Will open up port 9621 on the address and if opened, query for any
150      * controller type (all 6 controllers are tested). If a valid type is found, a discovery result will be created.
151      *
152      * @param ipAddress a possibly null, possibly empty ip address (null/empty addresses will be ignored)
153      */
154     private void scanAddress(String ipAddress) {
155         if (ipAddress == null || ipAddress.isEmpty()) {
156             return;
157         }
158
159         final SocketSession session = new SocketChannelSession(ipAddress, RioConstants.RIO_PORT);
160         try {
161             final WaitingSessionListener listener = new WaitingSessionListener();
162             session.addListener(listener);
163             session.connect(CONN_TIMEOUT_IN_MS);
164             logger.debug("Connected to port {}:{} - testing to see if RIO", ipAddress, RioConstants.RIO_PORT);
165
166             // can't check for system properties because DMS responds to those -
167             // need to check if any controllers are defined
168             for (int c = 1; c < 7; c++) {
169                 session.sendCommand("GET C[" + c + "].type");
170                 final String resp = listener.getResponse();
171                 if (resp == null) {
172                     continue;
173                 }
174                 if (!resp.startsWith("S C[" + c + "].type=\"")) {
175                     continue;
176                 }
177                 final String type = resp.substring(13, resp.length() - 1);
178                 if (!type.isBlank()) {
179                     logger.debug("Found a RIO type #{}", type);
180                     addResult(ipAddress, type);
181                     break;
182                 }
183             }
184         } catch (InterruptedException e) {
185             logger.debug("Connection was interrupted to port {}:{}", ipAddress, RioConstants.RIO_PORT);
186         } catch (IOException e) {
187             logger.trace("Connection couldn't be established to port {}:{}", ipAddress, RioConstants.RIO_PORT);
188         } finally {
189             try {
190                 session.disconnect();
191             } catch (IOException e) {
192                 // do nothing
193             }
194         }
195     }
196
197     /**
198      * Helper method to add our ip address and system type as a discovery result.
199      *
200      * @param ipAddress a non-null, non-empty ip address
201      * @param type a non-null, non-empty model type
202      * @throws IllegalArgumentException if ipaddress or type is null or empty
203      */
204     private void addResult(String ipAddress, String type) {
205         if (ipAddress == null || ipAddress.isEmpty()) {
206             throw new IllegalArgumentException("ipAddress cannot be null or empty");
207         }
208         if (type == null || type.isEmpty()) {
209             throw new IllegalArgumentException("type cannot be null or empty");
210         }
211
212         final Map<String, Object> properties = new HashMap<>(3);
213         properties.put(RioSystemConfig.IP_ADDRESS, ipAddress);
214         properties.put(RioSystemConfig.PING, 30);
215         properties.put(RioSystemConfig.RETRY_POLLING, 10);
216         properties.put(RioSystemConfig.SCAN_DEVICE, true);
217
218         final String id = ipAddress.replace(".", "");
219         final ThingUID uid = new ThingUID(RioConstants.BRIDGE_TYPE_RIO, id);
220         if (uid != null) {
221             final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
222                     .withLabel("Russound " + type).build();
223             thingDiscovered(result);
224         }
225     }
226 }