2 * Copyright (c) 2010-2021 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.webthing.internal.discovery;
15 import static org.openhab.binding.webthing.internal.WebThingBindingConstants.MDNS_SERVICE_TYPE;
16 import static org.openhab.binding.webthing.internal.WebThingBindingConstants.THING_TYPE_UID;
18 import java.io.IOException;
20 import java.time.Duration;
21 import java.time.Instant;
23 import java.util.concurrent.*;
25 import javax.jmdns.ServiceEvent;
26 import javax.jmdns.ServiceInfo;
27 import javax.jmdns.ServiceListener;
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;
49 * WebThing discovery service based on mDNS. Refer https://iot.mozilla.org/wot/#web-thing-discovery
51 * @author Gregor Roth - Initial contribution
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<>();
68 * @param configProperties the config props
69 * @param mdnsClient the underlying mDNS client
72 public WebthingDiscoveryService(@Nullable Map<String, Object> configProperties, @Reference MDNSClient mdnsClient,
73 @Reference Scheduler executor, @Reference HttpClientFactory httpClientFactory) {
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);
84 public Set<ThingTypeUID> getSupportedThingTypes() {
85 return Set.of(THING_TYPE_UID);
90 protected void deactivate() {
92 mdnsClient.removeServiceListener(MDNS_SERVICE_TYPE, this);
96 public void serviceAdded(@NonNullByDefault({}) ServiceEvent serviceEvent) {
97 considerService(serviceEvent);
101 public void serviceResolved(@NonNullByDefault({}) ServiceEvent serviceEvent) {
102 considerService(serviceEvent);
106 public void serviceRemoved(@NonNullByDefault({}) ServiceEvent serviceEvent) {
107 for (var discoveryResult : discoverWebThing(serviceEvent.getInfo())) {
108 thingRemoved(discoveryResult.getThingUID());
113 protected void startBackgroundDiscovery() {
114 mdnsClient.addServiceListener(MDNS_SERVICE_TYPE, this);
119 protected void stopBackgroundDiscovery() {
120 mdnsClient.removeServiceListener(MDNS_SERVICE_TYPE, this);
123 private void startScan(boolean isBackground) {
124 scheduler.submit(() -> scan(isBackground));
128 protected void startScan() {
133 protected synchronized void stopScan() {
134 removeOlderResults(Instant.now().minus(Duration.ofMinutes(10)).toEpochMilli());
136 // stop running discovery tasks
137 for (var future : runningDiscoveryTasks) {
139 runningDiscoveryTasks.remove(future);
145 * scans the network via mDNS
147 * @param isBackground true, if is background task
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);
154 // create discovery task for each detected service and process these in parallel to increase total
156 for (var serviceInfo : serviceInfos) {
157 var future = scheduler.submit(new DiscoveryTask(serviceInfo));
158 runningDiscoveryTasks.add(future);
161 // wait until all tasks are completed
162 for (var future : runningDiscoveryTasks) {
164 future.get(5, TimeUnit.MINUTES);
165 } catch (InterruptedException | ExecutionException | TimeoutException e) {
166 logger.warn("discovering task {} terminated", future);
168 runningDiscoveryTasks.remove(future);
172 private class DiscoveryTask implements Callable<Set<DiscoveryResult>> {
173 private final ServiceInfo serviceInfo;
175 DiscoveryTask(ServiceInfo serviceInfo) {
176 this.serviceInfo = serviceInfo;
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));
193 public String toString() {
194 return "DiscoveryTask{" + "serviceInfo=" + serviceInfo + '}';
199 * convert the serviceInfo result of the mDNS scan to discovery results
201 * @param serviceInfo the service info
202 * @return the associated discovery result
204 private Set<DiscoveryResult> discoverWebThing(ServiceInfo serviceInfo) {
205 var discoveryResults = new HashSet<DiscoveryResult>();
207 if (serviceInfo.getHostAddresses().length > 0) {
208 var host = serviceInfo.getHostAddresses()[0];
209 var port = serviceInfo.getPort();
211 if (Collections.list(serviceInfo.getPropertyNames()).contains("path")) {
212 path = serviceInfo.getPropertyString("path");
213 if (!path.endsWith("/")) {
218 // There are two kinds of WebThing endpoints: Endpoints supporting a single WebThing as well as
219 // endpoints supporting multiple WebThings.
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.
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());
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());
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());
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());
261 return discoveryResults;
264 private Optional<DiscoveryResult> discoverWebThing(URI uri) {
266 var description = descriptionLoader.loadWebthingDescription(uri, Duration.ofSeconds(5));
268 var id = uri.getHost().replaceAll("\\W", "_") + "_" + uri.getPort();
269 if (uri.getPath().length() > 1) {
270 id = id + "_" + uri.getPath().replaceAll("\\W", "");
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();
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);
290 private void considerService(ServiceEvent serviceEvent) {
291 if (isBackgroundDiscoveryEnabled()) {
292 for (var discoveryResult : discoverWebThing(serviceEvent.getInfo())) {
293 thingDiscovered(discoveryResult);