]> git.basschouten.com Git - openhab-addons.git/blob
535661bb1a2b967958f122b15e3b39ff7c5108a1
[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.wemo.internal.discovery;
14
15 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
16
17 import java.io.StringReader;
18 import java.net.URL;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import javax.xml.parsers.DocumentBuilder;
27 import javax.xml.parsers.DocumentBuilderFactory;
28
29 import org.apache.commons.lang.StringEscapeUtils;
30 import org.apache.commons.lang.StringUtils;
31 import org.openhab.binding.wemo.internal.handler.WemoBridgeHandler;
32 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
33 import org.openhab.core.config.discovery.AbstractDiscoveryService;
34 import org.openhab.core.config.discovery.DiscoveryResult;
35 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
36 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
37 import org.openhab.core.io.transport.upnp.UpnpIOService;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.thing.ThingUID;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42 import org.w3c.dom.CharacterData;
43 import org.w3c.dom.Document;
44 import org.w3c.dom.Element;
45 import org.w3c.dom.Node;
46 import org.w3c.dom.NodeList;
47 import org.xml.sax.InputSource;
48
49 /**
50  * The {@link WemoLinkDiscoveryService} is responsible for discovering new and
51  * removed WeMo devices connected to the WeMo Link Bridge.
52  *
53  * @author Hans-Jörg Merk - Initial contribution
54  *
55  */
56 public class WemoLinkDiscoveryService extends AbstractDiscoveryService implements UpnpIOParticipant {
57
58     private final Logger logger = LoggerFactory.getLogger(WemoLinkDiscoveryService.class);
59
60     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MZ100);
61
62     public static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]";
63
64     /**
65      * Maximum time to search for devices in seconds.
66      */
67     private static final int SEARCH_TIME = 20;
68
69     /**
70      * Scan interval for scanning job in seconds.
71      */
72     private static final int SCAN_INTERVAL = 120;
73
74     /**
75      * The handler for WeMo Link bridge
76      */
77     private final WemoBridgeHandler wemoBridgeHandler;
78
79     /**
80      * Job which will do the background scanning
81      */
82     private final WemoLinkScan scanningRunnable;
83
84     /**
85      * Schedule for scanning
86      */
87     private ScheduledFuture<?> scanningJob;
88
89     /**
90      * The Upnp service
91      */
92     private UpnpIOService service;
93
94     private final WemoHttpCall wemoHttpCaller;
95
96     public WemoLinkDiscoveryService(WemoBridgeHandler wemoBridgeHandler, UpnpIOService upnpIOService,
97             WemoHttpCall wemoHttpCaller) {
98         super(SEARCH_TIME);
99         this.wemoBridgeHandler = wemoBridgeHandler;
100
101         this.wemoHttpCaller = wemoHttpCaller;
102
103         if (upnpIOService != null) {
104             this.service = upnpIOService;
105         } else {
106             logger.debug("upnpIOService not set.");
107         }
108
109         this.scanningRunnable = new WemoLinkScan();
110         if (wemoBridgeHandler == null) {
111             logger.warn("no bridge handler for scan given");
112         }
113         this.activate(null);
114     }
115
116     public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
117         return SUPPORTED_THING_TYPES;
118     }
119
120     @Override
121     public void startScan() {
122         logger.trace("Starting WeMoEndDevice discovery on WeMo Link {}", wemoBridgeHandler.getThing().getUID());
123         try {
124             String devUDN = "uuid:" + wemoBridgeHandler.getThing().getConfiguration().get(UDN).toString();
125             logger.trace("devUDN = '{}'", devUDN);
126
127             String soapHeader = "\"urn:Belkin:service:bridge:1#GetEndDevices\"";
128             String content = "<?xml version=\"1.0\"?>"
129                     + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
130                     + "<s:Body>" + "<u:GetEndDevices xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DevUDN>" + devUDN
131                     + "</DevUDN><ReqListType>PAIRED_LIST</ReqListType>" + "</u:GetEndDevices>" + "</s:Body>"
132                     + "</s:Envelope>";
133
134             URL descriptorURL = service.getDescriptorURL(this);
135
136             if (descriptorURL != null) {
137                 String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
138                 String wemoURL = deviceURL + "/upnp/control/bridge1";
139
140                 String endDeviceRequest = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
141
142                 if (endDeviceRequest != null) {
143                     logger.trace("endDeviceRequest answered '{}'", endDeviceRequest);
144
145                     try {
146                         String stringParser = StringUtils.substringBetween(endDeviceRequest, "<DeviceLists>",
147                                 "</DeviceLists>");
148
149                         stringParser = StringEscapeUtils.unescapeXml(stringParser);
150
151                         // check if there are already paired devices with WeMo Link
152                         if ("0".equals(stringParser)) {
153                             logger.debug("There are no devices connected with WeMo Link. Exit discovery");
154                             return;
155                         }
156
157                         // Build parser for received <DeviceList>
158                         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
159                         // see
160                         // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
161                         dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
162                         dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
163                         dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
164                         dbf.setXIncludeAware(false);
165                         dbf.setExpandEntityReferences(false);
166                         DocumentBuilder db = dbf.newDocumentBuilder();
167                         InputSource is = new InputSource();
168                         is.setCharacterStream(new StringReader(stringParser));
169
170                         Document doc = db.parse(is);
171                         NodeList nodes = doc.getElementsByTagName("DeviceInfo");
172
173                         // iterate the devices
174                         for (int i = 0; i < nodes.getLength(); i++) {
175                             Element element = (Element) nodes.item(i);
176
177                             NodeList deviceIndex = element.getElementsByTagName("DeviceIndex");
178                             Element line = (Element) deviceIndex.item(0);
179                             logger.trace("DeviceIndex: {}", getCharacterDataFromElement(line));
180
181                             NodeList deviceID = element.getElementsByTagName("DeviceID");
182                             line = (Element) deviceID.item(0);
183                             String endDeviceID = getCharacterDataFromElement(line);
184                             logger.trace("DeviceID: {}", endDeviceID);
185
186                             NodeList friendlyName = element.getElementsByTagName("FriendlyName");
187                             line = (Element) friendlyName.item(0);
188                             String endDeviceName = getCharacterDataFromElement(line);
189                             logger.trace("FriendlyName: {}", endDeviceName);
190
191                             NodeList vendor = element.getElementsByTagName("Manufacturer");
192                             line = (Element) vendor.item(0);
193                             String endDeviceVendor = getCharacterDataFromElement(line);
194                             logger.trace("Manufacturer: {}", endDeviceVendor);
195
196                             NodeList model = element.getElementsByTagName("ModelCode");
197                             line = (Element) model.item(0);
198                             String endDeviceModelID = getCharacterDataFromElement(line);
199                             endDeviceModelID = endDeviceModelID.replaceAll(NORMALIZE_ID_REGEX, "_");
200
201                             logger.trace("ModelCode: {}", endDeviceModelID);
202
203                             if (SUPPORTED_THING_TYPES.contains(new ThingTypeUID(BINDING_ID, endDeviceModelID))) {
204                                 logger.debug("Discovered a WeMo LED Light thing with ID '{}'", endDeviceID);
205
206                                 ThingUID bridgeUID = wemoBridgeHandler.getThing().getUID();
207                                 ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, endDeviceModelID);
208
209                                 if (thingTypeUID.equals(THING_TYPE_MZ100)) {
210                                     String thingLightId = endDeviceID;
211                                     ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, thingLightId);
212
213                                     Map<String, Object> properties = new HashMap<>(1);
214                                     properties.put(DEVICE_ID, endDeviceID);
215
216                                     DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
217                                             .withProperties(properties)
218                                             .withBridge(wemoBridgeHandler.getThing().getUID()).withLabel(endDeviceName)
219                                             .build();
220
221                                     thingDiscovered(discoveryResult);
222                                 }
223                             } else {
224                                 logger.debug("Discovered an unsupported device :");
225                                 logger.debug("DeviceIndex : {}", getCharacterDataFromElement(line));
226                                 logger.debug("DeviceID    : {}", endDeviceID);
227                                 logger.debug("FriendlyName: {}", endDeviceName);
228                                 logger.debug("Manufacturer: {}", endDeviceVendor);
229                                 logger.debug("ModelCode   : {}", endDeviceModelID);
230                             }
231
232                         }
233                     } catch (Exception e) {
234                         logger.error("Failed to parse endDevices for bridge '{}'",
235                                 wemoBridgeHandler.getThing().getUID(), e);
236                     }
237                 }
238             }
239         } catch (Exception e) {
240             logger.error("Failed to get endDevices for bridge '{}'", wemoBridgeHandler.getThing().getUID(), e);
241         }
242     }
243
244     @Override
245     protected void startBackgroundDiscovery() {
246         logger.trace("Start WeMo device background discovery");
247
248         if (scanningJob == null || scanningJob.isCancelled()) {
249             this.scanningJob = scheduler.scheduleWithFixedDelay(this.scanningRunnable,
250                     LINK_DISCOVERY_SERVICE_INITIAL_DELAY, SCAN_INTERVAL, TimeUnit.SECONDS);
251         } else {
252             logger.trace("scanningJob active");
253         }
254     }
255
256     @Override
257     protected void stopBackgroundDiscovery() {
258         logger.debug("Stop WeMo device background discovery");
259
260         if (scanningJob != null && !scanningJob.isCancelled()) {
261             scanningJob.cancel(true);
262             scanningJob = null;
263         }
264     }
265
266     @Override
267     public String getUDN() {
268         return (String) this.wemoBridgeHandler.getThing().getConfiguration().get(UDN);
269     }
270
271     @Override
272     public void onServiceSubscribed(String service, boolean succeeded) {
273     }
274
275     @Override
276     public void onValueReceived(String variable, String value, String service) {
277     }
278
279     @Override
280     public void onStatusChanged(boolean status) {
281     }
282
283     public static String getCharacterDataFromElement(Element e) {
284         Node child = e.getFirstChild();
285         if (child instanceof CharacterData) {
286             CharacterData cd = (CharacterData) child;
287             return cd.getData();
288         }
289         return "?";
290     }
291
292     public class WemoLinkScan implements Runnable {
293         @Override
294         public void run() {
295             startScan();
296         }
297     }
298 }