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