2 * Copyright (c) 2010-2022 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.wemo.internal.discovery;
15 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
16 import static org.openhab.binding.wemo.internal.WemoUtil.*;
18 import java.io.StringReader;
20 import java.util.Collections;
21 import java.util.HashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import javax.xml.parsers.DocumentBuilder;
28 import javax.xml.parsers.DocumentBuilderFactory;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.wemo.internal.handler.WemoBridgeHandler;
33 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
34 import org.openhab.core.config.discovery.AbstractDiscoveryService;
35 import org.openhab.core.config.discovery.DiscoveryResult;
36 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
37 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
38 import org.openhab.core.io.transport.upnp.UpnpIOService;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.thing.ThingUID;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.w3c.dom.CharacterData;
44 import org.w3c.dom.Document;
45 import org.w3c.dom.Element;
46 import org.w3c.dom.Node;
47 import org.w3c.dom.NodeList;
48 import org.xml.sax.InputSource;
51 * The {@link WemoLinkDiscoveryService} is responsible for discovering new and
52 * removed WeMo devices connected to the WeMo Link Bridge.
54 * @author Hans-Jörg Merk - Initial contribution
58 public class WemoLinkDiscoveryService extends AbstractDiscoveryService implements UpnpIOParticipant {
60 private final Logger logger = LoggerFactory.getLogger(WemoLinkDiscoveryService.class);
62 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MZ100);
64 public static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]";
67 * Maximum time to search for devices in seconds.
69 private static final int SEARCH_TIME = 20;
72 * Scan interval for scanning job in seconds.
74 private static final int SCAN_INTERVAL = 120;
77 * The handler for WeMo Link bridge
79 private final WemoBridgeHandler wemoBridgeHandler;
82 * Job which will do the background scanning
84 private final WemoLinkScan scanningRunnable;
87 * Schedule for scanning
89 private @Nullable ScheduledFuture<?> scanningJob;
94 private UpnpIOService service;
96 private final WemoHttpCall wemoHttpCaller;
98 public WemoLinkDiscoveryService(WemoBridgeHandler wemoBridgeHandler, UpnpIOService upnpIOService,
99 WemoHttpCall wemoHttpCaller) {
101 this.service = upnpIOService;
102 this.wemoBridgeHandler = wemoBridgeHandler;
104 this.wemoHttpCaller = wemoHttpCaller;
106 this.scanningRunnable = new WemoLinkScan();
110 public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
111 return SUPPORTED_THING_TYPES;
115 public void startScan() {
116 logger.trace("Starting WeMoEndDevice discovery on WeMo Link {}", wemoBridgeHandler.getThing().getUID());
118 String devUDN = "uuid:" + wemoBridgeHandler.getThing().getConfiguration().get(UDN).toString();
119 logger.trace("devUDN = '{}'", devUDN);
121 String soapHeader = "\"urn:Belkin:service:bridge:1#GetEndDevices\"";
122 String content = "<?xml version=\"1.0\"?>"
123 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
124 + "<s:Body>" + "<u:GetEndDevices xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DevUDN>" + devUDN
125 + "</DevUDN><ReqListType>PAIRED_LIST</ReqListType>" + "</u:GetEndDevices>" + "</s:Body>"
128 URL descriptorURL = service.getDescriptorURL(this);
130 if (descriptorURL != null) {
131 String deviceURL = substringBefore(descriptorURL.toString(), "/setup.xml");
132 String wemoURL = deviceURL + "/upnp/control/bridge1";
134 String endDeviceRequest = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
136 if (endDeviceRequest != null) {
137 logger.trace("endDeviceRequest answered '{}'", endDeviceRequest);
140 String stringParser = substringBetween(endDeviceRequest, "<DeviceLists>", "</DeviceLists>");
142 stringParser = unescapeXml(stringParser);
144 // check if there are already paired devices with WeMo Link
145 if ("0".equals(stringParser)) {
146 logger.debug("There are no devices connected with WeMo Link. Exit discovery");
150 // Build parser for received <DeviceList>
151 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
153 // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
154 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
155 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
156 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
157 dbf.setXIncludeAware(false);
158 dbf.setExpandEntityReferences(false);
159 DocumentBuilder db = dbf.newDocumentBuilder();
160 InputSource is = new InputSource();
161 is.setCharacterStream(new StringReader(stringParser));
163 Document doc = db.parse(is);
164 NodeList nodes = doc.getElementsByTagName("DeviceInfo");
166 // iterate the devices
167 for (int i = 0; i < nodes.getLength(); i++) {
168 Element element = (Element) nodes.item(i);
170 NodeList deviceIndex = element.getElementsByTagName("DeviceIndex");
171 Element line = (Element) deviceIndex.item(0);
172 logger.trace("DeviceIndex: {}", getCharacterDataFromElement(line));
174 NodeList deviceID = element.getElementsByTagName("DeviceID");
175 line = (Element) deviceID.item(0);
176 String endDeviceID = getCharacterDataFromElement(line);
177 logger.trace("DeviceID: {}", endDeviceID);
179 NodeList friendlyName = element.getElementsByTagName("FriendlyName");
180 line = (Element) friendlyName.item(0);
181 String endDeviceName = getCharacterDataFromElement(line);
182 logger.trace("FriendlyName: {}", endDeviceName);
184 NodeList vendor = element.getElementsByTagName("Manufacturer");
185 line = (Element) vendor.item(0);
186 String endDeviceVendor = getCharacterDataFromElement(line);
187 logger.trace("Manufacturer: {}", endDeviceVendor);
189 NodeList model = element.getElementsByTagName("ModelCode");
190 line = (Element) model.item(0);
191 String endDeviceModelID = getCharacterDataFromElement(line);
192 endDeviceModelID = endDeviceModelID.replaceAll(NORMALIZE_ID_REGEX, "_");
194 logger.trace("ModelCode: {}", endDeviceModelID);
196 if (SUPPORTED_THING_TYPES.contains(new ThingTypeUID(BINDING_ID, endDeviceModelID))) {
197 logger.debug("Discovered a WeMo LED Light thing with ID '{}'", endDeviceID);
199 ThingUID bridgeUID = wemoBridgeHandler.getThing().getUID();
200 ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, endDeviceModelID);
202 if (thingTypeUID.equals(THING_TYPE_MZ100)) {
203 String thingLightId = endDeviceID;
204 ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, thingLightId);
206 Map<String, Object> properties = new HashMap<>(1);
207 properties.put(DEVICE_ID, endDeviceID);
209 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
210 .withProperties(properties)
211 .withBridge(wemoBridgeHandler.getThing().getUID()).withLabel(endDeviceName)
214 thingDiscovered(discoveryResult);
217 logger.debug("Discovered an unsupported device :");
218 logger.debug("DeviceIndex : {}", getCharacterDataFromElement(line));
219 logger.debug("DeviceID : {}", endDeviceID);
220 logger.debug("FriendlyName: {}", endDeviceName);
221 logger.debug("Manufacturer: {}", endDeviceVendor);
222 logger.debug("ModelCode : {}", endDeviceModelID);
226 } catch (Exception e) {
227 logger.error("Failed to parse endDevices for bridge '{}'",
228 wemoBridgeHandler.getThing().getUID(), e);
232 } catch (Exception e) {
233 logger.error("Failed to get endDevices for bridge '{}'", wemoBridgeHandler.getThing().getUID(), e);
238 protected void startBackgroundDiscovery() {
239 logger.trace("Start WeMo device background discovery");
241 ScheduledFuture<?> job = scanningJob;
243 if (job == null || job.isCancelled()) {
244 this.scanningJob = scheduler.scheduleWithFixedDelay(this.scanningRunnable,
245 LINK_DISCOVERY_SERVICE_INITIAL_DELAY, SCAN_INTERVAL, TimeUnit.SECONDS);
247 logger.trace("scanningJob active");
252 protected void stopBackgroundDiscovery() {
253 logger.debug("Stop WeMo device background discovery");
255 ScheduledFuture<?> job = scanningJob;
256 if (job != null && !job.isCancelled()) {
263 public String getUDN() {
264 return (String) this.wemoBridgeHandler.getThing().getConfiguration().get(UDN);
268 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
272 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
276 public void onStatusChanged(boolean status) {
279 public static String getCharacterDataFromElement(Element e) {
280 Node child = e.getFirstChild();
281 if (child instanceof CharacterData) {
282 CharacterData cd = (CharacterData) child;
288 public class WemoLinkScan implements Runnable {