]> git.basschouten.com Git - openhab-addons.git/blob
6473cc9804c8889d686ee59be89b0e64d2b926a9
[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.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.concurrent.ExecutorService;
26 import java.util.concurrent.Executors;
27 import java.util.concurrent.TimeUnit;
28
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;
43
44 /**
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.
47  *
48  * @author Tim Roberts - Initial contribution
49  */
50 @Component(service = DiscoveryService.class, configurationPid = "discovery.russound")
51 public class RioSystemDiscovery extends AbstractDiscoveryService {
52     /** The logger */
53     private final Logger logger = LoggerFactory.getLogger(RioSystemDiscovery.class);
54
55     /** The timeout to connect (in milliseconds) */
56     private static final int CONN_TIMEOUT_IN_MS = 100;
57
58     /** The {@link ExecutorService} to use for scanning - will be null if not scanning */
59     private ExecutorService executorService = null;
60
61     /** The number of network interfaces being scanned */
62     private int nbrNetworkInterfacesScanning = 0;
63
64     /**
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)
67      */
68     public RioSystemDiscovery() {
69         super(Collections.singleton(RioConstants.BRIDGE_TYPE_RIO), 120);
70     }
71
72     /**
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)}
77      */
78     @Override
79     protected void startScan() {
80         final List<NetworkInterface> interfaces;
81         try {
82             interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
83         } catch (SocketException e1) {
84             logger.debug("Exception getting network interfaces: {}", e1.getMessage(), e1);
85             return;
86         }
87
88         nbrNetworkInterfacesScanning = interfaces.size();
89         executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 10);
90
91         for (final NetworkInterface networkInterface : interfaces) {
92             try {
93                 if (networkInterface.isLoopback() || !networkInterface.isUp()) {
94                     continue;
95                 }
96             } catch (SocketException e) {
97                 continue;
98             }
99
100             for (Iterator<InterfaceAddress> it = networkInterface.getInterfaceAddresses().iterator(); it.hasNext();) {
101                 final InterfaceAddress interfaceAddress = it.next();
102
103                 // don't bother with ipv6 addresses (russound doesn't support)
104                 if (interfaceAddress.getAddress() instanceof Inet6Address) {
105                     continue;
106                 }
107
108                 final String subnetRange = interfaceAddress.getAddress().getHostAddress() + "/"
109                         + interfaceAddress.getNetworkPrefixLength();
110
111                 logger.debug("Scanning subnet: {}", subnetRange);
112                 final SubnetUtils utils = new SubnetUtils(subnetRange);
113
114                 final String[] addresses = utils.getInfo().getAllAddresses();
115
116                 for (final String address : addresses) {
117                     executorService.execute(() -> {
118                         scanAddress(address);
119                     });
120                 }
121             }
122         }
123
124         // Finishes the scan and cleans up
125         stopScan();
126     }
127
128     /**
129      * Stops the scan by terminating the {@link #executorService} and shutting it down
130      */
131     @Override
132     protected synchronized void stopScan() {
133         super.stopScan();
134         if (executorService == null) {
135             return;
136         }
137
138         try {
139             executorService.awaitTermination(CONN_TIMEOUT_IN_MS * nbrNetworkInterfacesScanning, TimeUnit.MILLISECONDS);
140         } catch (InterruptedException e) {
141             // shutting down - doesn't matter
142         }
143         executorService.shutdown();
144         executorService = null;
145     }
146
147     /**
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.
150      *
151      * @param ipAddress a possibly null, possibly empty ip address (null/empty addresses will be ignored)
152      */
153     private void scanAddress(String ipAddress) {
154         if (ipAddress == null || ipAddress.isEmpty()) {
155             return;
156         }
157
158         final SocketSession session = new SocketChannelSession(ipAddress, RioConstants.RIO_PORT);
159         try {
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);
164
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();
170                 if (resp == null) {
171                     continue;
172                 }
173                 if (!resp.startsWith("S C[" + c + "].type=\"")) {
174                     continue;
175                 }
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);
180                     break;
181                 }
182             }
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);
187         } finally {
188             try {
189                 session.disconnect();
190             } catch (IOException e) {
191                 // do nothing
192             }
193         }
194     }
195
196     /**
197      * Helper method to add our ip address and system type as a discovery result.
198      *
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
202      */
203     private void addResult(String ipAddress, String type) {
204         if (ipAddress == null || ipAddress.isEmpty()) {
205             throw new IllegalArgumentException("ipAddress cannot be null or empty");
206         }
207         if (type == null || type.isEmpty()) {
208             throw new IllegalArgumentException("type cannot be null or empty");
209         }
210
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);
216
217         final String id = ipAddress.replace(".", "");
218         final ThingUID uid = new ThingUID(RioConstants.BRIDGE_TYPE_RIO, id);
219         if (uid != null) {
220             final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
221                     .withLabel("Russound " + type).build();
222             thingDiscovered(result);
223         }
224     }
225 }