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.russound.internal.discovery;
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;
25 import java.util.concurrent.ExecutorService;
26 import java.util.concurrent.Executors;
27 import java.util.concurrent.TimeUnit;
29 import org.apache.commons.net.util.SubnetUtils;
30 import org.openhab.binding.russound.internal.net.SocketChannelSession;
31 import org.openhab.binding.russound.internal.net.SocketSession;
32 import org.openhab.binding.russound.internal.net.WaitingSessionListener;
33 import org.openhab.binding.russound.internal.rio.RioConstants;
34 import org.openhab.binding.russound.internal.rio.system.RioSystemConfig;
35 import org.openhab.core.config.discovery.AbstractDiscoveryService;
36 import org.openhab.core.config.discovery.DiscoveryResult;
37 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
38 import org.openhab.core.config.discovery.DiscoveryService;
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 * This implementation of {@link DiscoveryService} will scan the network for any Russound RIO system devices. The scan
46 * will occur against all network interfaces.
48 * @author Tim Roberts - Initial contribution
50 @Component(service = DiscoveryService.class, configurationPid = "discovery.russound")
51 public class RioSystemDiscovery extends AbstractDiscoveryService {
53 private final Logger logger = LoggerFactory.getLogger(RioSystemDiscovery.class);
55 /** The timeout to connect (in milliseconds) */
56 private static final int CONN_TIMEOUT_IN_MS = 100;
58 /** The {@link ExecutorService} to use for scanning - will be null if not scanning */
59 private ExecutorService executorService = null;
61 /** The number of network interfaces being scanned */
62 private int nbrNetworkInterfacesScanning = 0;
65 * Creates the system discovery service looking for {@link RioConstants#BRIDGE_TYPE_RIO}. The scan will take at most
66 * 120 seconds (depending on how many network interfaces there are)
68 public RioSystemDiscovery() {
69 super(Collections.singleton(RioConstants.BRIDGE_TYPE_RIO), 120);
73 * Starts the scan. For each network interface (that is up and not a loopback), all addresses will be iterated
74 * and checked for something open on port 9621. If that port is open, a russound controller "type" command will be
75 * issued. If the response is a correct pattern, we assume it's a rio system device and will emit a
76 * {{@link #thingDiscovered(DiscoveryResult)}
79 protected void startScan() {
80 final List<NetworkInterface> interfaces;
82 interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
83 } catch (SocketException e1) {
84 logger.debug("Exception getting network interfaces: {}", e1.getMessage(), e1);
88 nbrNetworkInterfacesScanning = interfaces.size();
89 executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 10);
91 for (final NetworkInterface networkInterface : interfaces) {
93 if (networkInterface.isLoopback() || !networkInterface.isUp()) {
96 } catch (SocketException e) {
100 for (Iterator<InterfaceAddress> it = networkInterface.getInterfaceAddresses().iterator(); it.hasNext();) {
101 final InterfaceAddress interfaceAddress = it.next();
103 // don't bother with ipv6 addresses (russound doesn't support)
104 if (interfaceAddress.getAddress() instanceof Inet6Address) {
108 final String subnetRange = interfaceAddress.getAddress().getHostAddress() + "/"
109 + interfaceAddress.getNetworkPrefixLength();
111 logger.debug("Scanning subnet: {}", subnetRange);
112 final SubnetUtils utils = new SubnetUtils(subnetRange);
114 final String[] addresses = utils.getInfo().getAllAddresses();
116 for (final String address : addresses) {
117 executorService.execute(() -> {
118 scanAddress(address);
124 // Finishes the scan and cleans up
129 * Stops the scan by terminating the {@link #executorService} and shutting it down
132 protected synchronized void stopScan() {
134 if (executorService == null) {
139 executorService.awaitTermination(CONN_TIMEOUT_IN_MS * nbrNetworkInterfacesScanning, TimeUnit.MILLISECONDS);
140 } catch (InterruptedException e) {
141 // shutting down - doesn't matter
143 executorService.shutdown();
144 executorService = null;
148 * Helper method to scan a specific address. Will open up port 9621 on the address and if opened, query for any
149 * controller type (all 6 controllers are tested). If a valid type is found, a discovery result will be created.
151 * @param ipAddress a possibly null, possibly empty ip address (null/empty addresses will be ignored)
153 private void scanAddress(String ipAddress) {
154 if (ipAddress == null || ipAddress.isEmpty()) {
158 final SocketSession session = new SocketChannelSession(ipAddress, RioConstants.RIO_PORT);
160 final WaitingSessionListener listener = new WaitingSessionListener();
161 session.addListener(listener);
162 session.connect(CONN_TIMEOUT_IN_MS);
163 logger.debug("Connected to port {}:{} - testing to see if RIO", ipAddress, RioConstants.RIO_PORT);
165 // can't check for system properties because DMS responds to those -
166 // need to check if any controllers are defined
167 for (int c = 1; c < 7; c++) {
168 session.sendCommand("GET C[" + c + "].type");
169 final String resp = listener.getResponse();
173 if (!resp.startsWith("S C[" + c + "].type=\"")) {
176 final String type = resp.substring(13, resp.length() - 1);
177 if (!type.isBlank()) {
178 logger.debug("Found a RIO type #{}", type);
179 addResult(ipAddress, type);
183 } catch (InterruptedException e) {
184 logger.debug("Connection was interrupted to port {}:{}", ipAddress, RioConstants.RIO_PORT);
185 } catch (IOException e) {
186 logger.trace("Connection couldn't be established to port {}:{}", ipAddress, RioConstants.RIO_PORT);
189 session.disconnect();
190 } catch (IOException e) {
197 * Helper method to add our ip address and system type as a discovery result.
199 * @param ipAddress a non-null, non-empty ip address
200 * @param type a non-null, non-empty model type
201 * @throws IllegalArgumentException if ipaddress or type is null or empty
203 private void addResult(String ipAddress, String type) {
204 if (ipAddress == null || ipAddress.isEmpty()) {
205 throw new IllegalArgumentException("ipAddress cannot be null or empty");
207 if (type == null || type.isEmpty()) {
208 throw new IllegalArgumentException("type cannot be null or empty");
211 final Map<String, Object> properties = new HashMap<>(3);
212 properties.put(RioSystemConfig.IP_ADDRESS, ipAddress);
213 properties.put(RioSystemConfig.PING, 30);
214 properties.put(RioSystemConfig.RETRY_POLLING, 10);
215 properties.put(RioSystemConfig.SCAN_DEVICE, true);
217 final String id = ipAddress.replace(".", "");
218 final ThingUID uid = new ThingUID(RioConstants.BRIDGE_TYPE_RIO, id);
220 final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
221 .withLabel("Russound " + type).build();
222 thingDiscovered(result);