]> git.basschouten.com Git - openhab-addons.git/blob
0e13395617f59d47bf204b3b323a7869345ed8ee
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.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.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
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 @NonNullByDefault
57 public class WemoLinkDiscoveryService extends AbstractDiscoveryService implements UpnpIOParticipant {
58
59     private final Logger logger = LoggerFactory.getLogger(WemoLinkDiscoveryService.class);
60
61     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_MZ100);
62
63     public static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]";
64
65     /**
66      * Maximum time to search for devices in seconds.
67      */
68     private static final int SEARCH_TIME = 20;
69
70     /**
71      * Scan interval for scanning job in seconds.
72      */
73     private static final int SCAN_INTERVAL = 120;
74
75     /**
76      * The handler for WeMo Link bridge
77      */
78     private final WemoBridgeHandler wemoBridgeHandler;
79
80     /**
81      * Job which will do the background scanning
82      */
83     private final WemoLinkScan scanningRunnable;
84
85     /**
86      * Schedule for scanning
87      */
88     private @Nullable ScheduledFuture<?> scanningJob;
89
90     /**
91      * The Upnp service
92      */
93     private UpnpIOService service;
94
95     private final WemoHttpCall wemoHttpCaller;
96
97     public WemoLinkDiscoveryService(WemoBridgeHandler wemoBridgeHandler, UpnpIOService upnpIOService,
98             WemoHttpCall wemoHttpCaller) {
99         super(SEARCH_TIME);
100         this.service = upnpIOService;
101         this.wemoBridgeHandler = wemoBridgeHandler;
102
103         this.wemoHttpCaller = wemoHttpCaller;
104
105         this.scanningRunnable = new WemoLinkScan();
106         this.activate(null);
107     }
108
109     public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
110         return SUPPORTED_THING_TYPES;
111     }
112
113     @Override
114     public void startScan() {
115         logger.trace("Starting WeMoEndDevice discovery on WeMo Link {}", wemoBridgeHandler.getThing().getUID());
116         try {
117             String devUDN = "uuid:" + wemoBridgeHandler.getThing().getConfiguration().get(UDN).toString();
118             logger.trace("devUDN = '{}'", devUDN);
119
120             String soapHeader = "\"urn:Belkin:service:bridge:1#GetEndDevices\"";
121             String content = """
122                     <?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>\
125                     <u:GetEndDevices xmlns:u="urn:Belkin:service:bridge:1">\
126                     <DevUDN>\
127                     """
128                     + devUDN + """
129                             </DevUDN>\
130                             <ReqListType>PAIRED_LIST</ReqListType>\
131                             </u:GetEndDevices>\
132                             </s:Body>\
133                             </s:Envelope>\
134                             """;
135
136             URL descriptorURL = service.getDescriptorURL(this);
137
138             if (descriptorURL != null) {
139                 String deviceURL = substringBefore(descriptorURL.toString(), "/setup.xml");
140                 String wemoURL = deviceURL + "/upnp/control/bridge1";
141
142                 String endDeviceRequest = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
143
144                 logger.trace("endDeviceRequest answered '{}'", endDeviceRequest);
145
146                 try {
147                     String stringParser = substringBetween(endDeviceRequest, "<DeviceLists>", "</DeviceLists>");
148
149                     stringParser = 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).withBridge(wemoBridgeHandler.getThing().getUID())
218                                         .withLabel(endDeviceName).build();
219
220                                 thingDiscovered(discoveryResult);
221                             }
222                         } else {
223                             logger.debug("Discovered an unsupported device :");
224                             logger.debug("DeviceIndex : {}", getCharacterDataFromElement(line));
225                             logger.debug("DeviceID    : {}", endDeviceID);
226                             logger.debug("FriendlyName: {}", endDeviceName);
227                             logger.debug("Manufacturer: {}", endDeviceVendor);
228                             logger.debug("ModelCode   : {}", endDeviceModelID);
229                         }
230
231                     }
232                 } catch (Exception e) {
233                     logger.warn("Failed to parse endDevices for bridge '{}'", wemoBridgeHandler.getThing().getUID(), e);
234                 }
235             }
236         } catch (Exception e) {
237             logger.warn("Failed to get endDevices for bridge '{}'", wemoBridgeHandler.getThing().getUID(), e);
238         }
239     }
240
241     @Override
242     protected void startBackgroundDiscovery() {
243         logger.trace("Start WeMo device background discovery");
244
245         ScheduledFuture<?> job = scanningJob;
246
247         if (job == null || job.isCancelled()) {
248             this.scanningJob = scheduler.scheduleWithFixedDelay(this.scanningRunnable,
249                     LINK_DISCOVERY_SERVICE_INITIAL_DELAY, SCAN_INTERVAL, TimeUnit.SECONDS);
250         } else {
251             logger.trace("scanningJob active");
252         }
253     }
254
255     @Override
256     protected void stopBackgroundDiscovery() {
257         logger.debug("Stop WeMo device background discovery");
258
259         ScheduledFuture<?> job = scanningJob;
260         if (job != null && !job.isCancelled()) {
261             job.cancel(true);
262         }
263         scanningJob = null;
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(@Nullable String service, boolean succeeded) {
273     }
274
275     @Override
276     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable 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 cd) {
286             return cd.getData();
287         }
288         return "?";
289     }
290
291     public class WemoLinkScan implements Runnable {
292         @Override
293         public void run() {
294             startScan();
295         }
296     }
297 }