2 * Copyright (c) 2010-2020 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.network.internal.discovery;
15 import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
17 import java.util.Collections;
18 import java.util.HashMap;
19 import java.util.List;
22 import java.util.concurrent.ExecutorService;
23 import java.util.concurrent.Executors;
24 import java.util.concurrent.TimeUnit;
25 import java.util.stream.Collectors;
26 import java.util.stream.Stream;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.network.internal.NetworkBindingConfiguration;
31 import org.openhab.binding.network.internal.PresenceDetection;
32 import org.openhab.binding.network.internal.PresenceDetectionListener;
33 import org.openhab.binding.network.internal.PresenceDetectionValue;
34 import org.openhab.binding.network.internal.utils.NetworkUtils;
35 import org.openhab.core.config.core.Configuration;
36 import org.openhab.core.config.discovery.AbstractDiscoveryService;
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.Activate;
41 import org.osgi.service.component.annotations.Component;
42 import org.osgi.service.component.annotations.Deactivate;
43 import org.osgi.service.component.annotations.Modified;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link NetworkDiscoveryService} is responsible for discovering devices on
49 * the current Network. It uses every Network Interface which is connected to a network.
50 * It tries common TCP ports to connect to, ICMP pings and ARP pings.
52 * @author Marc Mettke - Initial contribution
53 * @author David Graeff - Rewritten
56 @Component(service = DiscoveryService.class, configurationPid = "discovery.network")
57 public class NetworkDiscoveryService extends AbstractDiscoveryService implements PresenceDetectionListener {
58 static final int PING_TIMEOUT_IN_MS = 500;
59 static final int MAXIMUM_IPS_PER_INTERFACE = 255;
60 private static final long DISCOVERY_RESULT_TTL = TimeUnit.MINUTES.toSeconds(10);
61 private final Logger logger = LoggerFactory.getLogger(NetworkDiscoveryService.class);
63 // TCP port 548 (Apple Filing Protocol (AFP))
64 // TCP port 554 (Windows share / Linux samba)
65 // TCP port 1025 (Xbox / MS-RPC)
66 private Set<Integer> tcpServicePorts = Collections
67 .unmodifiableSet(Stream.of(80, 548, 554, 1025).collect(Collectors.toSet()));
68 private Integer scannedIPcount = 0;
69 private @Nullable ExecutorService executorService = null;
70 private final NetworkBindingConfiguration configuration = new NetworkBindingConfiguration();
71 private final NetworkUtils networkUtils = new NetworkUtils();
73 public NetworkDiscoveryService() {
74 super(SUPPORTED_THING_TYPES_UIDS, (int) Math.round(
75 new NetworkUtils().getNetworkIPs(MAXIMUM_IPS_PER_INTERFACE).size() * (PING_TIMEOUT_IN_MS / 1000.0)),
81 public void activate(@Nullable Map<String, @Nullable Object> config) {
82 super.activate(config);
88 protected void modified(@Nullable Map<String, @Nullable Object> config) {
89 super.modified(config);
90 // We update instead of replace the configuration object, so that if the user updates the
91 // configuration, the values are automatically available in all handlers. Because they all
92 // share the same instance.
93 configuration.update(new Configuration(config).as(NetworkBindingConfiguration.class));
98 protected void deactivate() {
99 if (executorService != null) {
100 executorService.shutdown();
106 public void partialDetectionResult(PresenceDetectionValue value) {
107 final String ip = value.getHostAddress();
108 if (value.isPingReachable()) {
110 } else if (value.isTCPServiceReachable()) {
111 List<Integer> tcpServices = value.getReachableTCPports();
112 for (int port : tcpServices) {
113 newServiceDevice(ip, port);
119 public void finalDetectionResult(PresenceDetectionValue value) {
123 * Starts the DiscoveryThread for each IP on each interface on the network
126 protected void startScan() {
127 if (executorService == null) {
128 executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
130 final ExecutorService service = executorService;
131 if (service == null) {
134 removeOlderResults(getTimestampOfLastScan(), null);
135 logger.trace("Starting Network Device Discovery");
137 final Set<String> networkIPs = networkUtils.getNetworkIPs(MAXIMUM_IPS_PER_INTERFACE);
140 for (String ip : networkIPs) {
141 final PresenceDetection s = new PresenceDetection(this, 2000);
143 s.setIOSDevice(true);
144 s.setUseDhcpSniffing(false);
145 s.setTimeout(PING_TIMEOUT_IN_MS);
147 s.setUseIcmpPing(true);
148 s.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod);
150 s.setServicePorts(tcpServicePorts);
152 service.execute(() -> {
153 Thread.currentThread().setName("Discovery thread " + ip);
154 s.performPresenceDetection(true);
155 synchronized (scannedIPcount) {
157 if (scannedIPcount == networkIPs.size()) {
158 logger.trace("Scan of {} IPs successful", scannedIPcount);
167 protected synchronized void stopScan() {
169 final ExecutorService service = executorService;
170 if (service == null) {
175 service.awaitTermination(PING_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
176 } catch (InterruptedException e) {
177 Thread.currentThread().interrupt(); // Reset interrupt flag
180 executorService = null;
183 public static ThingUID createServiceUID(String ip, int tcpPort) {
184 // uid must not contains dots
185 return new ThingUID(SERVICE_DEVICE, ip.replace('.', '_') + "_" + String.valueOf(tcpPort));
189 * Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}.
191 * @param ip The device IP
192 * @param tcpPort The TCP port
194 public void newServiceDevice(String ip, int tcpPort) {
195 logger.trace("Found reachable service for device with IP address {} on port {}", ip, tcpPort);
198 // TCP port 548 (Apple Filing Protocol (AFP))
199 // TCP port 554 (Windows share / Linux samba)
200 // TCP port 1025 (Xbox / MS-RPC)
203 label = "Device providing a Webserver";
206 label = "Device providing the Apple AFP Service";
209 label = "Device providing Network/Samba Shares";
212 label = "Device providing Xbox/MS-RPC Capability";
215 label = "Network Device";
217 label += " (" + ip + ":" + tcpPort + ")";
219 Map<String, Object> properties = new HashMap<>();
220 properties.put(PARAMETER_HOSTNAME, ip);
221 properties.put(PARAMETER_PORT, tcpPort);
222 thingDiscovered(DiscoveryResultBuilder.create(createServiceUID(ip, tcpPort)).withTTL(DISCOVERY_RESULT_TTL)
223 .withProperties(properties).withLabel(label).build());
226 public static ThingUID createPingUID(String ip) {
227 // uid must not contains dots
228 return new ThingUID(PING_DEVICE, ip.replace('.', '_'));
232 * Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}.
234 * @param ip The device IP
236 public void newPingDevice(String ip) {
237 logger.trace("Found pingable network device with IP address {}", ip);
239 Map<String, Object> properties = new HashMap<>();
240 properties.put(PARAMETER_HOSTNAME, ip);
241 thingDiscovered(DiscoveryResultBuilder.create(createPingUID(ip)).withTTL(DISCOVERY_RESULT_TTL)
242 .withProperties(properties).withLabel("Network Device (" + ip + ")").build());