]> git.basschouten.com Git - openhab-addons.git/blob
0e056221e8b29c50b167f08e7c9714b35c6a82a3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.webthing.internal.discovery;
14
15 import static org.openhab.binding.webthing.internal.WebThingBindingConstants.MDNS_SERVICE_TYPE;
16 import static org.openhab.binding.webthing.internal.WebThingBindingConstants.THING_TYPE_UID;
17
18 import java.io.IOException;
19 import java.net.URI;
20 import java.time.Duration;
21 import java.time.Instant;
22 import java.util.*;
23 import java.util.concurrent.*;
24
25 import javax.jmdns.ServiceEvent;
26 import javax.jmdns.ServiceInfo;
27 import javax.jmdns.ServiceListener;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.webthing.internal.client.DescriptionLoader;
32 import org.openhab.core.config.discovery.AbstractDiscoveryService;
33 import org.openhab.core.config.discovery.DiscoveryResult;
34 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
35 import org.openhab.core.config.discovery.DiscoveryService;
36 import org.openhab.core.io.net.http.HttpClientFactory;
37 import org.openhab.core.io.transport.mdns.MDNSClient;
38 import org.openhab.core.scheduler.Scheduler;
39 import org.openhab.core.thing.ThingTypeUID;
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.Reference;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 /**
49  * WebThing discovery service based on mDNS. Refer https://iot.mozilla.org/wot/#web-thing-discovery
50  *
51  * @author Gregor Roth - Initial contribution
52  */
53 @NonNullByDefault
54 @Component(service = DiscoveryService.class, configurationPid = "webthingdiscovery.mdns")
55 public class WebthingDiscoveryService extends AbstractDiscoveryService implements ServiceListener {
56     private static final Duration FOREGROUND_SCAN_TIMEOUT = Duration.ofMillis(200);
57     public static final String ID = "id";
58     public static final String SCHEMAS = "schemas";
59     public static final String WEB_THING_URI = "webThingURI";
60     private final Logger logger = LoggerFactory.getLogger(WebthingDiscoveryService.class);
61     private final DescriptionLoader descriptionLoader;
62     private final MDNSClient mdnsClient;
63     private final List<Future<Set<DiscoveryResult>>> runningDiscoveryTasks = new CopyOnWriteArrayList<>();
64
65     /**
66      * constructor
67      *
68      * @param configProperties the config props
69      * @param mdnsClient the underlying mDNS client
70      */
71     @Activate
72     public WebthingDiscoveryService(@Nullable Map<String, Object> configProperties, @Reference MDNSClient mdnsClient,
73             @Reference Scheduler executor, @Reference HttpClientFactory httpClientFactory) {
74         super(30);
75         this.mdnsClient = mdnsClient;
76         this.descriptionLoader = new DescriptionLoader(httpClientFactory.getCommonHttpClient());
77         super.activate(configProperties);
78         if (isBackgroundDiscoveryEnabled()) {
79             mdnsClient.addServiceListener(MDNS_SERVICE_TYPE, this);
80         }
81     }
82
83     @Override
84     public Set<ThingTypeUID> getSupportedThingTypes() {
85         return Set.of(THING_TYPE_UID);
86     }
87
88     @Deactivate
89     @Override
90     protected void deactivate() {
91         super.deactivate();
92         mdnsClient.removeServiceListener(MDNS_SERVICE_TYPE, this);
93     }
94
95     @Override
96     public void serviceAdded(@NonNullByDefault({}) ServiceEvent serviceEvent) {
97         considerService(serviceEvent);
98     }
99
100     @Override
101     public void serviceResolved(@NonNullByDefault({}) ServiceEvent serviceEvent) {
102         considerService(serviceEvent);
103     }
104
105     @Override
106     public void serviceRemoved(@NonNullByDefault({}) ServiceEvent serviceEvent) {
107         for (var discoveryResult : discoverWebThing(serviceEvent.getInfo())) {
108             thingRemoved(discoveryResult.getThingUID());
109         }
110     }
111
112     @Override
113     protected void startBackgroundDiscovery() {
114         mdnsClient.addServiceListener(MDNS_SERVICE_TYPE, this);
115         startScan(true);
116     }
117
118     @Override
119     protected void stopBackgroundDiscovery() {
120         mdnsClient.removeServiceListener(MDNS_SERVICE_TYPE, this);
121     }
122
123     private void startScan(boolean isBackground) {
124         scheduler.submit(() -> scan(isBackground));
125     }
126
127     @Override
128     protected void startScan() {
129         startScan(false);
130     }
131
132     @Override
133     protected synchronized void stopScan() {
134         removeOlderResults(Instant.now().minus(Duration.ofMinutes(10)).toEpochMilli());
135
136         // stop running discovery tasks
137         for (var future : runningDiscoveryTasks) {
138             future.cancel(true);
139             runningDiscoveryTasks.remove(future);
140         }
141         super.stopScan();
142     }
143
144     /**
145      * scans the network via mDNS
146      *
147      * @param isBackground true, if is background task
148      */
149     private void scan(boolean isBackground) {
150         var serviceInfos = isBackground ? mdnsClient.list(MDNS_SERVICE_TYPE)
151                 : mdnsClient.list(MDNS_SERVICE_TYPE, FOREGROUND_SCAN_TIMEOUT);
152         logger.debug("got {} mDNS entries", serviceInfos.length);
153
154         // create discovery task for each detected service and process these in parallel to increase total
155         // discovery speed
156         for (var serviceInfo : serviceInfos) {
157             var future = scheduler.submit(new DiscoveryTask(serviceInfo));
158             runningDiscoveryTasks.add(future);
159         }
160
161         // wait until all tasks are completed
162         for (var future : runningDiscoveryTasks) {
163             try {
164                 future.get(5, TimeUnit.MINUTES);
165             } catch (InterruptedException | ExecutionException | TimeoutException e) {
166                 logger.warn("discovering task {} terminated", future);
167             }
168             runningDiscoveryTasks.remove(future);
169         }
170     }
171
172     private class DiscoveryTask implements Callable<Set<DiscoveryResult>> {
173         private final ServiceInfo serviceInfo;
174
175         DiscoveryTask(ServiceInfo serviceInfo) {
176             this.serviceInfo = serviceInfo;
177         }
178
179         @Override
180         public Set<DiscoveryResult> call() {
181             var results = new HashSet<DiscoveryResult>();
182             for (var discoveryResult : discoverWebThing(serviceInfo)) {
183                 results.add(discoveryResult);
184                 thingDiscovered(discoveryResult);
185                 logger.debug("WebThing '{}' (uri: {}, id: {}, schemas: {}) discovered", discoveryResult.getLabel(),
186                         discoveryResult.getProperties().get(WEB_THING_URI), discoveryResult.getProperties().get(ID),
187                         discoveryResult.getProperties().get(SCHEMAS));
188             }
189             return results;
190         }
191
192         @Override
193         public String toString() {
194             return "DiscoveryTask{" + "serviceInfo=" + serviceInfo + '}';
195         }
196     }
197
198     /**
199      * convert the serviceInfo result of the mDNS scan to discovery results
200      *
201      * @param serviceInfo the service info
202      * @return the associated discovery result
203      */
204     private Set<DiscoveryResult> discoverWebThing(ServiceInfo serviceInfo) {
205         var discoveryResults = new HashSet<DiscoveryResult>();
206
207         if (serviceInfo.getHostAddresses().length > 0) {
208             var host = serviceInfo.getHostAddresses()[0];
209             var port = serviceInfo.getPort();
210             var path = "/";
211             if (Collections.list(serviceInfo.getPropertyNames()).contains("path")) {
212                 path = serviceInfo.getPropertyString("path");
213                 if (!path.endsWith("/")) {
214                     path = path + "/";
215                 }
216             }
217
218             // There are two kinds of WebThing endpoints: Endpoints supporting a single WebThing as well as
219             // endpoints supporting multiple WebThings.
220             //
221             // In the routine below the enpoint will be checked for single WebThings first, than for multiple
222             // WebThings if a ingle WebTHing has not been found.
223             // Furthermore, first it will be tried to connect the endpoint using https. If this fails, as fallback
224             // plain http is used.
225
226             // check single WebThing path via https (e.g. https://192.168.0.23:8433/)
227             var optionalDiscoveryResult = discoverWebThing(toURI(host, port, path, true));
228             if (optionalDiscoveryResult.isPresent()) {
229                 discoveryResults.add(optionalDiscoveryResult.get());
230             } else {
231                 // check single WebThing path via plain http (e.g. http://192.168.0.23:8433/)
232                 optionalDiscoveryResult = discoverWebThing(toURI(host, port, path, false));
233                 if (optionalDiscoveryResult.isPresent()) {
234                     discoveryResults.add(optionalDiscoveryResult.get());
235                 } else {
236                     // check multiple WebThing path via https (e.g. https://192.168.0.23:8433/0,
237                     // https://192.168.0.23:8433/1,...)
238                     outer: for (int i = 0; i < 50; i++) { // search 50 entries at maximum
239                         optionalDiscoveryResult = discoverWebThing(toURI(host, port, path + i + "/", true));
240                         if (optionalDiscoveryResult.isPresent()) {
241                             discoveryResults.add(optionalDiscoveryResult.get());
242                         } else if (i == 0) {
243                             // check multiple WebThing path via plain http (e.g. http://192.168.0.23:8433/0,
244                             // http://192.168.0.23:8433/1,...)
245                             for (int j = 0; j < 50; j++) { // search 50 entries at maximum
246                                 optionalDiscoveryResult = discoverWebThing(toURI(host, port, path + j + "/", false));
247                                 if (optionalDiscoveryResult.isPresent()) {
248                                     discoveryResults.add(optionalDiscoveryResult.get());
249                                 } else {
250                                     break outer;
251                                 }
252                             }
253                         } else {
254                             break;
255                         }
256                     }
257                 }
258             }
259         }
260
261         return discoveryResults;
262     }
263
264     private Optional<DiscoveryResult> discoverWebThing(URI uri) {
265         try {
266             var description = descriptionLoader.loadWebthingDescription(uri, Duration.ofSeconds(5));
267
268             var id = uri.getHost().replaceAll("\\W", "_") + "_" + uri.getPort();
269             if (uri.getPath().length() > 1) {
270                 id = id + "_" + uri.getPath().replaceAll("\\W", "");
271             }
272
273             var thingUID = new ThingUID(THING_TYPE_UID, id);
274             Map<String, Object> properties = new HashMap<>(2);
275             properties.put(ID, id);
276             properties.put(SCHEMAS, description.contextKeyword);
277             return Optional.of(DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_UID)
278                     .withProperty(WEB_THING_URI, uri).withLabel(description.title).withProperties(properties)
279                     .withRepresentationProperty(ID).build());
280         } catch (IOException ioe) {
281             return Optional.empty();
282         }
283     }
284
285     private URI toURI(String host, int port, String path, boolean isHttps) {
286         return isHttps ? URI.create("https://" + host + ":" + port + path)
287                 : URI.create("http://" + host + ":" + port + path);
288     }
289
290     private void considerService(ServiceEvent serviceEvent) {
291         if (isBackgroundDiscoveryEnabled()) {
292             for (var discoveryResult : discoverWebThing(serviceEvent.getInfo())) {
293                 thingDiscovered(discoveryResult);
294             }
295         }
296     }
297 }