/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.binding.wemo.discovery; import static org.eclipse.smarthome.binding.wemo.WemoBindingConstants.*; import java.io.StringReader; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.eclipse.smarthome.binding.wemo.handler.WemoBridgeHandler; import org.eclipse.smarthome.binding.wemo.internal.http.WemoHttpCall; import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.io.transport.upnp.UpnpIOParticipant; import org.eclipse.smarthome.io.transport.upnp.UpnpIOService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.CharacterData; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; /** * The {@link WemoLinkDiscoveryService} is responsible for discovering new and * removed WeMo devices connected to the WeMo Link Bridge. * * @author Hans-Jörg Merk - Initial contribution * */ public class WemoLinkDiscoveryService extends AbstractDiscoveryService implements UpnpIOParticipant { private Logger logger = LoggerFactory.getLogger(WemoLinkDiscoveryService.class); public final static Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MZ100); public static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]"; /** * Maximum time to search for devices in seconds. */ private final static int SEARCH_TIME = 20; /** * Initial delay for scanning job in seconds. */ private final static int INITIAL_DELAY = 5; /** * Scan interval for scanning job in seconds. */ private final static int SCAN_INTERVAL = 120; /** * The handler for WeMo Link bridge */ private WemoBridgeHandler wemoBridgeHandler; /** * Job which will do the background scanning */ private WemoLinkScan scanningRunnable; /** * Schedule for scanning */ private ScheduledFuture<?> scanningJob; /** * The Upnp service */ private UpnpIOService service; public WemoLinkDiscoveryService(WemoBridgeHandler wemoBridgeHandler, UpnpIOService upnpIOService) { super(SEARCH_TIME); this.wemoBridgeHandler = wemoBridgeHandler; if (upnpIOService != null) { this.service = upnpIOService; } else { logger.debug("upnpIOService not set."); } this.scanningRunnable = new WemoLinkScan(); if (wemoBridgeHandler == null) { logger.warn("no bridge handler for scan given"); } this.activate(null); } public Set<ThingTypeUID> getSupportedThingTypeUIDs() { return SUPPORTED_THING_TYPES; } @Override protected void startScan() { logger.trace("Starting WeMoEndDevice discovery on WeMo Link {}", wemoBridgeHandler.getThing().getUID()); try { String devUDN = "uuid:" + wemoBridgeHandler.getThing().getConfiguration().get(UDN).toString(); logger.trace("devUDN = '{}'", devUDN); String soapHeader = "\"urn:Belkin:service:bridge:1#GetEndDevices\""; String content = "<?xml version=\"1.0\"?>" + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<s:Body>" + "<u:GetEndDevices xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DevUDN>" + devUDN + "</DevUDN><ReqListType>PAIRED_LIST</ReqListType>" + "</u:GetEndDevices>" + "</s:Body>" + "</s:Envelope>"; URL descriptorURL = service.getDescriptorURL(this); if (descriptorURL != null) { String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml"); String wemoURL = deviceURL + "/upnp/control/bridge1"; String endDeviceRequest = WemoHttpCall.executeCall(wemoURL, soapHeader, content); if (endDeviceRequest != null) { logger.trace("endDeviceRequest answered '{}'", endDeviceRequest); try { String stringParser = StringUtils.substringBetween(endDeviceRequest, "<DeviceLists>", "</DeviceLists>"); stringParser = StringEscapeUtils.unescapeXml(stringParser); // check if there are already paired devices with WeMo Link if ("0".equals(stringParser)) { logger.debug("There are no devices connected with WeMo Link. Exit discovery"); return; } // Build parser for received <DeviceList> DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); InputSource is = new InputSource(); is.setCharacterStream(new StringReader(stringParser)); Document doc = db.parse(is); NodeList nodes = doc.getElementsByTagName("DeviceInfo"); // iterate the devices for (int i = 0; i < nodes.getLength(); i++) { Element element = (Element) nodes.item(i); NodeList deviceIndex = element.getElementsByTagName("DeviceIndex"); Element line = (Element) deviceIndex.item(0); logger.trace("DeviceIndex: " + getCharacterDataFromElement(line)); NodeList deviceID = element.getElementsByTagName("DeviceID"); line = (Element) deviceID.item(0); String endDeviceID = getCharacterDataFromElement(line); logger.trace("DeviceID: " + endDeviceID); NodeList friendlyName = element.getElementsByTagName("FriendlyName"); line = (Element) friendlyName.item(0); String endDeviceName = getCharacterDataFromElement(line); logger.trace("FriendlyName: " + endDeviceName); NodeList vendor = element.getElementsByTagName("Manufacturer"); line = (Element) vendor.item(0); String endDeviceVendor = getCharacterDataFromElement(line); logger.trace("Manufacturer: " + endDeviceVendor); NodeList model = element.getElementsByTagName("ModelCode"); line = (Element) model.item(0); String endDeviceModelID = getCharacterDataFromElement(line); endDeviceModelID = endDeviceModelID.replaceAll(NORMALIZE_ID_REGEX, "_"); logger.trace("ModelCode: " + endDeviceModelID); if (SUPPORTED_THING_TYPES.contains(new ThingTypeUID(BINDING_ID, endDeviceModelID))) { logger.debug("Discovered a WeMo LED Light thing with ID '{}'", endDeviceID); ThingUID bridgeUID = wemoBridgeHandler.getThing().getUID(); ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, endDeviceModelID); if (thingTypeUID.equals(THING_TYPE_MZ100)) { String thingLightId = endDeviceID; ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, thingLightId); Map<String, Object> properties = new HashMap<>(1); properties.put(DEVICE_ID, endDeviceID); DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID) .withProperties(properties) .withBridge(wemoBridgeHandler.getThing().getUID()).withLabel(endDeviceName) .build(); thingDiscovered(discoveryResult); } } else { logger.debug("Discovered an unsupported device :"); logger.debug("DeviceIndex : " + getCharacterDataFromElement(line)); logger.debug("DeviceID : " + endDeviceID); logger.debug("FriendlyName: " + endDeviceName); logger.debug("Manufacturer: " + endDeviceVendor); logger.debug("ModelCode : " + endDeviceModelID); } } } catch (Exception e) { logger.error("Failed to parse endDevices for bridge '{}'", wemoBridgeHandler.getThing().getUID(), e); } } } } catch (Exception e) { logger.error("Failed to get endDevices for bridge '{}'", wemoBridgeHandler.getThing().getUID(), e); } } @Override protected void startBackgroundDiscovery() { logger.trace("Start WeMo device background discovery"); if (scanningJob == null || scanningJob.isCancelled()) { this.scanningJob = AbstractDiscoveryService.scheduler.scheduleWithFixedDelay(this.scanningRunnable, INITIAL_DELAY, SCAN_INTERVAL, TimeUnit.SECONDS); } else { logger.trace("scanningJob active"); } } @Override protected void stopBackgroundDiscovery() { logger.debug("Stop WeMo device background discovery"); if (scanningJob != null && !scanningJob.isCancelled()) { scanningJob.cancel(true); scanningJob = null; } } @Override public String getUDN() { return (String) this.wemoBridgeHandler.getThing().getConfiguration().get(UDN); } @Override public void onServiceSubscribed(String service, boolean succeeded) { } @Override public void onValueReceived(String variable, String value, String service) { } @Override public void onStatusChanged(boolean status) { } public static String getCharacterDataFromElement(Element e) { Node child = e.getFirstChild(); if (child instanceof CharacterData) { CharacterData cd = (CharacterData) child; return cd.getData(); } return "?"; } public class WemoLinkScan implements Runnable { @Override public void run() { startScan(); } } }