2 * Copyright (c) 2010-2024 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;
26 import java.util.concurrent.ExecutorService;
27 import java.util.concurrent.Executors;
28 import java.util.concurrent.TimeUnit;
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;
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.
49 * @author Tim Roberts - Initial contribution
51 @Component(service = DiscoveryService.class, configurationPid = "discovery.russound")
52 public class RioSystemDiscovery extends AbstractDiscoveryService {
54 private final Logger logger = LoggerFactory.getLogger(RioSystemDiscovery.class);
56 /** The timeout to connect (in milliseconds) */
57 private static final int CONN_TIMEOUT_IN_MS = 100;
59 /** The {@link ExecutorService} to use for scanning - will be null if not scanning */
60 private ExecutorService executorService = null;
62 /** The number of network interfaces being scanned */
63 private int nbrNetworkInterfacesScanning = 0;
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)
69 public RioSystemDiscovery() {
70 super(Set.of(RioConstants.BRIDGE_TYPE_RIO), 120);
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)}
80 protected void startScan() {
81 final List<NetworkInterface> interfaces;
83 interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
84 } catch (SocketException e1) {
85 logger.debug("Exception getting network interfaces: {}", e1.getMessage(), e1);
89 nbrNetworkInterfacesScanning = interfaces.size();
90 executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 10);
92 for (final NetworkInterface networkInterface : interfaces) {
94 if (networkInterface.isLoopback() || !networkInterface.isUp()) {
97 } catch (SocketException e) {
101 for (Iterator<InterfaceAddress> it = networkInterface.getInterfaceAddresses().iterator(); it.hasNext();) {
102 final InterfaceAddress interfaceAddress = it.next();
104 // don't bother with ipv6 addresses (russound doesn't support)
105 if (interfaceAddress.getAddress() instanceof Inet6Address) {
109 final String subnetRange = interfaceAddress.getAddress().getHostAddress() + "/"
110 + interfaceAddress.getNetworkPrefixLength();
112 logger.debug("Scanning subnet: {}", subnetRange);
113 final SubnetUtils utils = new SubnetUtils(subnetRange);
115 final String[] addresses = utils.getInfo().getAllAddresses();
117 for (final String address : addresses) {
118 executorService.execute(() -> {
119 scanAddress(address);
125 // Finishes the scan and cleans up
130 * Stops the scan by terminating the {@link #executorService} and shutting it down
133 protected synchronized void stopScan() {
135 if (executorService == null) {
140 executorService.awaitTermination(CONN_TIMEOUT_IN_MS * nbrNetworkInterfacesScanning, TimeUnit.MILLISECONDS);
141 } catch (InterruptedException e) {
142 // shutting down - doesn't matter
144 executorService.shutdown();
145 executorService = null;
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.
152 * @param ipAddress a possibly null, possibly empty ip address (null/empty addresses will be ignored)
154 private void scanAddress(String ipAddress) {
155 if (ipAddress == null || ipAddress.isEmpty()) {
159 final SocketSession session = new SocketChannelSession(ipAddress, RioConstants.RIO_PORT);
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);
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();
174 if (!resp.startsWith("S C[" + c + "].type=\"")) {
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);
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);
190 session.disconnect();
191 } catch (IOException e) {
198 * Helper method to add our ip address and system type as a discovery result.
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
204 private void addResult(String ipAddress, String type) {
205 if (ipAddress == null || ipAddress.isEmpty()) {
206 throw new IllegalArgumentException("ipAddress cannot be null or empty");
208 if (type == null || type.isEmpty()) {
209 throw new IllegalArgumentException("type cannot be null or empty");
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);
218 final String id = ipAddress.replace(".", "");
219 final ThingUID uid = new ThingUID(RioConstants.BRIDGE_TYPE_RIO, id);
221 final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
222 .withLabel("Russound " + type).build();
223 thingDiscovered(result);