diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoHandlerFactory.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoHandlerFactory.java index 1cc41cf0e54..1768f5c06e6 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoHandlerFactory.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoHandlerFactory.java @@ -14,15 +14,9 @@ package org.openhab.binding.wemo.internal; import static org.openhab.binding.wemo.internal.WemoBindingConstants.UDN; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Map; -import java.util.Set; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; -import org.openhab.binding.wemo.internal.discovery.WemoLinkDiscoveryService; import org.openhab.binding.wemo.internal.handler.WemoBridgeHandler; import org.openhab.binding.wemo.internal.handler.WemoCoffeeHandler; import org.openhab.binding.wemo.internal.handler.WemoCrockpotHandler; @@ -33,17 +27,14 @@ import org.openhab.binding.wemo.internal.handler.WemoLightHandler; import org.openhab.binding.wemo.internal.handler.WemoMakerHandler; import org.openhab.binding.wemo.internal.handler.WemoMotionHandler; import org.openhab.binding.wemo.internal.handler.WemoSwitchHandler; -import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.transport.upnp.UpnpIOService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; -import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; -import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -63,18 +54,14 @@ public class WemoHandlerFactory extends BaseThingHandlerFactory { private final Logger logger = LoggerFactory.getLogger(WemoHandlerFactory.class); - public static final Set SUPPORTED_THING_TYPES = WemoBindingConstants.SUPPORTED_THING_TYPES; - private final UpnpIOService upnpIOService; private final HttpClient httpClient; @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return SUPPORTED_THING_TYPES.contains(thingTypeUID); + return WemoBindingConstants.SUPPORTED_THING_TYPES.contains(thingTypeUID); } - private final Map> discoveryServiceRegs = new HashMap<>(); - @Activate public WemoHandlerFactory(final @Reference UpnpIOService upnpIOService, final @Reference HttpClientFactory httpClientFactory) { @@ -90,9 +77,7 @@ public class WemoHandlerFactory extends BaseThingHandlerFactory { if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_BRIDGE)) { logger.debug("Creating a WemoBridgeHandler for thing '{}' with UDN '{}'", thing.getUID(), thing.getConfiguration().get(UDN)); - WemoBridgeHandler handler = new WemoBridgeHandler((Bridge) thing); - registerDeviceDiscoveryService(handler); - return handler; + return new WemoBridgeHandler((Bridge) thing, upnpIOService, httpClient); } else if (WemoBindingConstants.THING_TYPE_INSIGHT.equals(thing.getThingTypeUID())) { logger.debug("Creating a WemoInsightHandler for thing '{}' with UDN '{}'", thing.getUID(), thing.getConfiguration().get(UDN)); @@ -141,21 +126,4 @@ public class WemoHandlerFactory extends BaseThingHandlerFactory { return null; } } - - @Override - protected synchronized void removeHandler(ThingHandler thingHandler) { - if (thingHandler instanceof WemoBridgeHandler) { - ServiceRegistration serviceReg = this.discoveryServiceRegs.remove(thingHandler.getThing().getUID()); - if (serviceReg != null) { - serviceReg.unregister(); - } - } - } - - private synchronized void registerDeviceDiscoveryService(WemoBridgeHandler wemoBridgeHandler) { - WemoLinkDiscoveryService discoveryService = new WemoLinkDiscoveryService(wemoBridgeHandler, upnpIOService, - httpClient); - this.discoveryServiceRegs.put(wemoBridgeHandler.getThing().getUID(), - bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); - } } diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoLinkDiscoveryService.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoLinkDiscoveryService.java index 09b8ed09e88..d4b29351c46 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoLinkDiscoveryService.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoLinkDiscoveryService.java @@ -16,7 +16,6 @@ import static org.openhab.binding.wemo.internal.WemoBindingConstants.*; import static org.openhab.binding.wemo.internal.WemoUtil.*; import java.io.StringReader; -import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -28,16 +27,16 @@ import javax.xml.parsers.DocumentBuilderFactory; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; -import org.openhab.binding.wemo.internal.ApiController; +import org.openhab.binding.wemo.internal.exception.WemoException; import org.openhab.binding.wemo.internal.handler.WemoBridgeHandler; -import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.io.transport.upnp.UpnpIOParticipant; -import org.openhab.core.io.transport.upnp.UpnpIOService; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.CharacterData; @@ -54,8 +53,10 @@ import org.xml.sax.InputSource; * @author Hans-Jörg Merk - Initial contribution * */ +@Component(scope = ServiceScope.PROTOTYPE, service = WemoLinkDiscoveryService.class) @NonNullByDefault -public class WemoLinkDiscoveryService extends AbstractDiscoveryService implements UpnpIOParticipant { +public class WemoLinkDiscoveryService extends AbstractThingHandlerDiscoveryService + implements UpnpIOParticipant { private static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_MZ100); private static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]"; @@ -63,153 +64,118 @@ public class WemoLinkDiscoveryService extends AbstractDiscoveryService implement private static final int SCAN_INTERVAL_SECONDS = 120; private final Logger logger = LoggerFactory.getLogger(WemoLinkDiscoveryService.class); - private final WemoBridgeHandler wemoBridgeHandler; private final WemoLinkScan scanningRunnable; - private final UpnpIOService service; - private final ApiController apiController; private @Nullable ScheduledFuture scanningJob; - public WemoLinkDiscoveryService(final WemoBridgeHandler wemoBridgeHandler, final UpnpIOService upnpIOService, - final HttpClient httpClient) { - super(DISCOVERY_TIMEOUT_SECONDS); - this.service = upnpIOService; - this.wemoBridgeHandler = wemoBridgeHandler; - this.apiController = new ApiController(httpClient); - + public WemoLinkDiscoveryService() { + super(WemoBridgeHandler.class, SUPPORTED_THING_TYPES, DISCOVERY_TIMEOUT_SECONDS); this.scanningRunnable = new WemoLinkScan(); - this.activate(null); - } - - public Set getSupportedThingTypeUIDs() { - return SUPPORTED_THING_TYPES; } @Override public void startScan() { - logger.trace("Starting WeMoEndDevice discovery on WeMo Link {}", wemoBridgeHandler.getThing().getUID()); + logger.trace("Starting WeMoEndDevice discovery on WeMo Link {}", thingHandler.getThing().getUID()); + + String endDevicesResponse; try { - String devUDN = "uuid:" + wemoBridgeHandler.getThing().getConfiguration().get(UDN).toString(); - logger.trace("devUDN = '{}'", devUDN); + endDevicesResponse = thingHandler.getEndDevices(this); + } catch (WemoException e) { + logger.debug("Failed to get endDevices for bridge '{}'", thingHandler.getThing().getUID(), e); + return; + } catch (InterruptedException e) { + // Stop scanning + return; + } - String soapHeader = "\"urn:Belkin:service:bridge:1#GetEndDevices\""; - String content = """ - \ - \ - \ - \ - \ - """ - + devUDN + """ - \ - PAIRED_LIST\ - \ - \ - \ - """; + try { + String stringParser = substringBetween(endDevicesResponse, "", ""); - URL descriptorURL = service.getDescriptorURL(this); + stringParser = unescapeXml(stringParser); - if (descriptorURL != null) { - String deviceURL = substringBefore(descriptorURL.toString(), "/setup.xml"); - String wemoURL = deviceURL + "/upnp/control/bridge1"; + // 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; + } - String endDeviceRequest = apiController.executeCall(wemoURL, soapHeader, content); + // Build parser for received + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + // see + // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + DocumentBuilder db = dbf.newDocumentBuilder(); + InputSource is = new InputSource(); + is.setCharacterStream(new StringReader(stringParser)); - logger.trace("endDeviceRequest answered '{}'", endDeviceRequest); + Document doc = db.parse(is); + NodeList nodes = doc.getElementsByTagName("DeviceInfo"); - try { - String stringParser = substringBetween(endDeviceRequest, "", ""); + // iterate the devices + for (int i = 0; i < nodes.getLength(); i++) { + Element element = (Element) nodes.item(i); - stringParser = unescapeXml(stringParser); + NodeList deviceIndex = element.getElementsByTagName("DeviceIndex"); + Element line = (Element) deviceIndex.item(0); + logger.trace("DeviceIndex: {}", getCharacterDataFromElement(line)); - // 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; + 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 = thingHandler.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 properties = new HashMap<>(1); + properties.put(DEVICE_ID, endDeviceID); + + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID) + .withProperties(properties).withBridge(thingHandler.getThing().getUID()) + .withLabel(endDeviceName).build(); + + thingDiscovered(discoveryResult); } - - // Build parser for received - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - // see - // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html - dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); - dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - dbf.setXIncludeAware(false); - dbf.setExpandEntityReferences(false); - 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 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.warn("Failed to parse endDevices for bridge '{}'", wemoBridgeHandler.getThing().getUID(), e); + } 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.warn("Failed to get endDevices for bridge '{}'", wemoBridgeHandler.getThing().getUID(), e); + logger.warn("Failed to parse endDevices for bridge '{}'", thingHandler.getThing().getUID(), e); } } @@ -220,8 +186,8 @@ public class WemoLinkDiscoveryService extends AbstractDiscoveryService implement ScheduledFuture job = scanningJob; if (job == null || job.isCancelled()) { - this.scanningJob = scheduler.scheduleWithFixedDelay(this.scanningRunnable, - LINK_DISCOVERY_SERVICE_INITIAL_DELAY, SCAN_INTERVAL_SECONDS, TimeUnit.SECONDS); + this.scanningJob = scheduler.scheduleWithFixedDelay(scanningRunnable, LINK_DISCOVERY_SERVICE_INITIAL_DELAY, + SCAN_INTERVAL_SECONDS, TimeUnit.SECONDS); } else { logger.trace("scanningJob active"); } @@ -232,7 +198,7 @@ public class WemoLinkDiscoveryService extends AbstractDiscoveryService implement logger.debug("Stop WeMo device background discovery"); ScheduledFuture job = scanningJob; - if (job != null && !job.isCancelled()) { + if (job != null) { job.cancel(true); } scanningJob = null; @@ -240,7 +206,7 @@ public class WemoLinkDiscoveryService extends AbstractDiscoveryService implement @Override public String getUDN() { - return (String) this.wemoBridgeHandler.getThing().getConfiguration().get(UDN); + return (String) thingHandler.getThing().getConfiguration().get(UDN); } @Override @@ -255,7 +221,7 @@ public class WemoLinkDiscoveryService extends AbstractDiscoveryService implement public void onStatusChanged(boolean status) { } - public static String getCharacterDataFromElement(Element e) { + private static String getCharacterDataFromElement(Element e) { Node child = e.getFirstChild(); if (child instanceof CharacterData cd) { return cd.getData(); @@ -263,7 +229,7 @@ public class WemoLinkDiscoveryService extends AbstractDiscoveryService implement return "?"; } - public class WemoLinkScan implements Runnable { + private class WemoLinkScan implements Runnable { @Override public void run() { startScan(); diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoBridgeHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoBridgeHandler.java index 9fb8f4ea1fa..01588baa14d 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoBridgeHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoBridgeHandler.java @@ -13,17 +13,27 @@ package org.openhab.binding.wemo.internal.handler; import static org.openhab.binding.wemo.internal.WemoBindingConstants.*; +import static org.openhab.binding.wemo.internal.WemoUtil.substringBefore; +import java.net.URL; +import java.util.Collection; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.wemo.internal.ApiController; +import org.openhab.binding.wemo.internal.discovery.WemoLinkDiscoveryService; +import org.openhab.binding.wemo.internal.exception.WemoException; import org.openhab.core.config.core.Configuration; +import org.openhab.core.io.transport.upnp.UpnpIOParticipant; +import org.openhab.core.io.transport.upnp.UpnpIOService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,10 +50,13 @@ public class WemoBridgeHandler extends BaseBridgeHandler { public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE); private final Logger logger = LoggerFactory.getLogger(WemoBridgeHandler.class); + private final UpnpIOService service; + private final ApiController apiController; - public WemoBridgeHandler(Bridge bridge) { + public WemoBridgeHandler(final Bridge bridge, final UpnpIOService upnpIOService, final HttpClient httpClient) { super(bridge); - logger.debug("Creating a WemoBridgeHandler for thing '{}'", getThing().getUID()); + this.service = upnpIOService; + this.apiController = new ApiController(httpClient); } @Override @@ -66,4 +79,44 @@ public class WemoBridgeHandler extends BaseBridgeHandler { public void handleCommand(ChannelUID channelUID, Command command) { // Not needed, all commands are handled in the {@link WemoLightHandler} } + + @Override + public Collection> getServices() { + return Set.of(WemoLinkDiscoveryService.class); + } + + public String getEndDevices(UpnpIOParticipant participant) throws InterruptedException, WemoException { + String devUDN = "uuid:" + getConfig().get(UDN).toString(); + logger.trace("getEndDevices for devUDN '{}'", devUDN); + + String soapHeader = "\"urn:Belkin:service:bridge:1#GetEndDevices\""; + String content = """ + \ + \ + \ + \ + \ + """ + + devUDN + """ + \ + PAIRED_LIST\ + \ + \ + \ + """; + + URL descriptorURL = service.getDescriptorURL(participant); + if (descriptorURL == null) { + throw new WemoException("Descriptor URL for participant " + participant.getUDN() + " not found"); + } + + String deviceURL = substringBefore(descriptorURL.toString(), "/setup.xml"); + String wemoURL = deviceURL + "/upnp/control/bridge1"; + + String endDeviceResponse = apiController.executeCall(wemoURL, soapHeader, content); + + logger.trace("endDeviceRequest answered '{}'", endDeviceResponse); + + return endDeviceResponse; + } }