Simplify discovery registration (#18609)

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
pull/18725/head
Jacob Laursen 2025-05-29 20:42:51 +02:00 committed by GitHub
parent 84e1508054
commit fe31deff03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 160 additions and 173 deletions

View File

@ -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<ThingTypeUID> 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<ThingUID, ServiceRegistration<?>> 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<>()));
}
}

View File

@ -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<WemoBridgeHandler>
implements UpnpIOParticipant {
private static final Set<ThingTypeUID> 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<ThingTypeUID> 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 = """
<?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>\
""";
try {
String stringParser = substringBetween(endDevicesResponse, "<DeviceLists>", "</DeviceLists>");
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 <DeviceList>
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, "<DeviceLists>", "</DeviceLists>");
// 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<String, Object> 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 <DeviceList>
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<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.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();

View File

@ -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<ThingTypeUID> 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<Class<? extends ThingHandlerService>> 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 = """
<?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(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;
}
}