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.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.concurrent.atomic.AtomicInteger;
26 import java.util.stream.Collectors;
27 import java.util.stream.Stream;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.network.internal.NetworkBindingConfiguration;
32 import org.openhab.binding.network.internal.PresenceDetection;
33 import org.openhab.binding.network.internal.PresenceDetectionListener;
34 import org.openhab.binding.network.internal.PresenceDetectionValue;
35 import org.openhab.binding.network.internal.utils.NetworkUtils;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.config.discovery.AbstractDiscoveryService;
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.Activate;
42 import org.osgi.service.component.annotations.Component;
43 import org.osgi.service.component.annotations.Deactivate;
44 import org.osgi.service.component.annotations.Modified;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The {@link NetworkDiscoveryService} is responsible for discovering devices on
50 * the current Network. It uses every Network Interface which is connected to a network.
51 * It tries common TCP ports to connect to, ICMP pings and ARP pings.
53 * @author Marc Mettke - Initial contribution
54 * @author David Graeff - Rewritten
57 @Component(service = DiscoveryService.class, configurationPid = "discovery.network")
58 public class NetworkDiscoveryService extends AbstractDiscoveryService implements PresenceDetectionListener {
59 static final int PING_TIMEOUT_IN_MS = 500;
60 static final int MAXIMUM_IPS_PER_INTERFACE = 255;
61 private static final long DISCOVERY_RESULT_TTL = TimeUnit.MINUTES.toSeconds(10);
62 private final Logger logger = LoggerFactory.getLogger(NetworkDiscoveryService.class);
64 // TCP port 548 (Apple Filing Protocol (AFP))
65 // TCP port 554 (Windows share / Linux samba)
66 // TCP port 1025 (Xbox / MS-RPC)
67 private Set<Integer> tcpServicePorts = Collections
68 .unmodifiableSet(Stream.of(80, 548, 554, 1025).collect(Collectors.toSet()));
69 private AtomicInteger scannedIPcount = new AtomicInteger(0);
70 private @Nullable ExecutorService executorService = null;
71 private final NetworkBindingConfiguration configuration = new NetworkBindingConfiguration();
72 private final NetworkUtils networkUtils = new NetworkUtils();
74 public NetworkDiscoveryService() {
75 super(SUPPORTED_THING_TYPES_UIDS, (int) Math.round(
76 new NetworkUtils().getNetworkIPs(MAXIMUM_IPS_PER_INTERFACE).size() * (PING_TIMEOUT_IN_MS / 1000.0)),
82 public void activate(@Nullable Map<String, Object> config) {
83 super.activate(config);
89 protected void modified(@Nullable Map<String, Object> config) {
90 super.modified(config);
91 // We update instead of replace the configuration object, so that if the user updates the
92 // configuration, the values are automatically available in all handlers. Because they all
93 // share the same instance.
94 configuration.update(new Configuration(config).as(NetworkBindingConfiguration.class));
99 protected void deactivate() {
100 if (executorService != null) {
101 executorService.shutdown();
107 public void partialDetectionResult(PresenceDetectionValue value) {
108 final String ip = value.getHostAddress();
109 if (value.isPingReachable()) {
111 } else if (value.isTCPServiceReachable()) {
112 List<Integer> tcpServices = value.getReachableTCPports();
113 for (int port : tcpServices) {
114 newServiceDevice(ip, port);
120 public void finalDetectionResult(PresenceDetectionValue value) {
124 * Starts the DiscoveryThread for each IP on each interface on the network
127 protected void startScan() {
128 if (executorService == null) {
129 executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
131 final ExecutorService service = executorService;
132 if (service == null) {
135 removeOlderResults(getTimestampOfLastScan(), null);
136 logger.trace("Starting Network Device Discovery");
138 final Set<String> networkIPs = networkUtils.getNetworkIPs(MAXIMUM_IPS_PER_INTERFACE);
139 scannedIPcount.set(0);
141 for (String ip : networkIPs) {
142 final PresenceDetection s = new PresenceDetection(this, 2000);
144 s.setIOSDevice(true);
145 s.setUseDhcpSniffing(false);
146 s.setTimeout(PING_TIMEOUT_IN_MS);
148 s.setUseIcmpPing(true);
149 s.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod);
151 s.setServicePorts(tcpServicePorts);
153 service.execute(() -> {
154 Thread.currentThread().setName("Discovery thread " + ip);
155 s.performPresenceDetection(true);
156 int count = scannedIPcount.incrementAndGet();
157 if (count == networkIPs.size()) {
158 logger.trace("Scan of {} IPs successful", scannedIPcount);
166 protected synchronized void stopScan() {
168 final ExecutorService service = executorService;
169 if (service == null) {
174 service.awaitTermination(PING_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
175 } catch (InterruptedException e) {
176 Thread.currentThread().interrupt(); // Reset interrupt flag
179 executorService = null;
182 public static ThingUID createServiceUID(String ip, int tcpPort) {
183 // uid must not contains dots
184 return new ThingUID(SERVICE_DEVICE, ip.replace('.', '_') + "_" + tcpPort);
188 * Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}.
190 * @param ip The device IP
191 * @param tcpPort The TCP port
193 public void newServiceDevice(String ip, int tcpPort) {
194 logger.trace("Found reachable service for device with IP address {} on port {}", ip, tcpPort);
197 // TCP port 548 (Apple Filing Protocol (AFP))
198 // TCP port 554 (Windows share / Linux samba)
199 // TCP port 1025 (Xbox / MS-RPC)
202 label = "Device providing a Webserver";
205 label = "Device providing the Apple AFP Service";
208 label = "Device providing Network/Samba Shares";
211 label = "Device providing Xbox/MS-RPC Capability";
214 label = "Network Device";
216 label += " (" + ip + ":" + tcpPort + ")";
218 Map<String, Object> properties = new HashMap<>();
219 properties.put(PARAMETER_HOSTNAME, ip);
220 properties.put(PARAMETER_PORT, tcpPort);
221 thingDiscovered(DiscoveryResultBuilder.create(createServiceUID(ip, tcpPort)).withTTL(DISCOVERY_RESULT_TTL)
222 .withProperties(properties).withLabel(label).build());
225 public static ThingUID createPingUID(String ip) {
226 // uid must not contains dots
227 return new ThingUID(PING_DEVICE, ip.replace('.', '_'));
231 * Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}.
233 * @param ip The device IP
235 public void newPingDevice(String ip) {
236 logger.trace("Found pingable network device with IP address {}", ip);
238 Map<String, Object> properties = new HashMap<>();
239 properties.put(PARAMETER_HOSTNAME, ip);
240 thingDiscovered(DiscoveryResultBuilder.create(createPingUID(ip)).withTTL(DISCOVERY_RESULT_TTL)
241 .withProperties(properties).withLabel("Network Device (" + ip + ")").build());