]> git.basschouten.com Git - openhab-addons.git/blob
ddbdfe30a61b378c148ceca5e39eceb2ae93780c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.network.internal.discovery;
14
15 import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
16
17 import java.util.Collections;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Set;
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;
28
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;
47
48 /**
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.
52  *
53  * @author Marc Mettke - Initial contribution
54  * @author David Graeff - Rewritten
55  */
56 @NonNullByDefault
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);
63
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();
73
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)),
77                 false);
78     }
79
80     @Override
81     @Activate
82     public void activate(@Nullable Map<String, Object> config) {
83         super.activate(config);
84         modified(config);
85     }
86
87     @Override
88     @Modified
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));
95     }
96
97     @Override
98     @Deactivate
99     protected void deactivate() {
100         if (executorService != null) {
101             executorService.shutdown();
102         }
103         super.deactivate();
104     }
105
106     @Override
107     public void partialDetectionResult(PresenceDetectionValue value) {
108         final String ip = value.getHostAddress();
109         if (value.isPingReachable()) {
110             newPingDevice(ip);
111         } else if (value.isTCPServiceReachable()) {
112             List<Integer> tcpServices = value.getReachableTCPports();
113             for (int port : tcpServices) {
114                 newServiceDevice(ip, port);
115             }
116         }
117     }
118
119     @Override
120     public void finalDetectionResult(PresenceDetectionValue value) {
121     }
122
123     /**
124      * Starts the DiscoveryThread for each IP on each interface on the network
125      */
126     @Override
127     protected void startScan() {
128         if (executorService == null) {
129             executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
130         }
131         final ExecutorService service = executorService;
132         if (service == null) {
133             return;
134         }
135         removeOlderResults(getTimestampOfLastScan(), null);
136         logger.trace("Starting Network Device Discovery");
137
138         final Set<String> networkIPs = networkUtils.getNetworkIPs(MAXIMUM_IPS_PER_INTERFACE);
139         scannedIPcount.set(0);
140
141         for (String ip : networkIPs) {
142             final PresenceDetection s = new PresenceDetection(this, 2000);
143             s.setHostname(ip);
144             s.setIOSDevice(true);
145             s.setUseDhcpSniffing(false);
146             s.setTimeout(PING_TIMEOUT_IN_MS);
147             // Ping devices
148             s.setUseIcmpPing(true);
149             s.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod);
150             // TCP devices
151             s.setServicePorts(tcpServicePorts);
152
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);
159                     stopScan();
160                 }
161             });
162         }
163     }
164
165     @Override
166     protected synchronized void stopScan() {
167         super.stopScan();
168         final ExecutorService service = executorService;
169         if (service == null) {
170             return;
171         }
172
173         try {
174             service.awaitTermination(PING_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
175         } catch (InterruptedException e) {
176             Thread.currentThread().interrupt(); // Reset interrupt flag
177         }
178         service.shutdown();
179         executorService = null;
180     }
181
182     public static ThingUID createServiceUID(String ip, int tcpPort) {
183         // uid must not contains dots
184         return new ThingUID(SERVICE_DEVICE, ip.replace('.', '_') + "_" + tcpPort);
185     }
186
187     /**
188      * Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}.
189      *
190      * @param ip The device IP
191      * @param tcpPort The TCP port
192      */
193     public void newServiceDevice(String ip, int tcpPort) {
194         logger.trace("Found reachable service for device with IP address {} on port {}", ip, tcpPort);
195
196         String label;
197         // TCP port 548 (Apple Filing Protocol (AFP))
198         // TCP port 554 (Windows share / Linux samba)
199         // TCP port 1025 (Xbox / MS-RPC)
200         switch (tcpPort) {
201             case 80:
202                 label = "Device providing a Webserver";
203                 break;
204             case 548:
205                 label = "Device providing the Apple AFP Service";
206                 break;
207             case 554:
208                 label = "Device providing Network/Samba Shares";
209                 break;
210             case 1025:
211                 label = "Device providing Xbox/MS-RPC Capability";
212                 break;
213             default:
214                 label = "Network Device";
215         }
216         label += " (" + ip + ":" + tcpPort + ")";
217
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());
223     }
224
225     public static ThingUID createPingUID(String ip) {
226         // uid must not contains dots
227         return new ThingUID(PING_DEVICE, ip.replace('.', '_'));
228     }
229
230     /**
231      * Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}.
232      *
233      * @param ip The device IP
234      */
235     public void newPingDevice(String ip) {
236         logger.trace("Found pingable network device with IP address {}", ip);
237
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());
242     }
243 }