]> git.basschouten.com Git - openhab-addons.git/commitdiff
[wemo] make UPnP more robust and fix polling/portscan (#12001)
authorHans-Jörg Merk <github@hmerk.de>
Sun, 23 Jan 2022 21:49:52 +0000 (22:49 +0100)
committerGitHub <noreply@github.com>
Sun, 23 Jan 2022 21:49:52 +0000 (22:49 +0100)
* [wemo] make UPnP more robust
* [wemo] change thing status updates

Signed-off-by: Hans-Jörg Merk <github@hmerk.de>
17 files changed:
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoBindingConstants.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoHandlerFactory.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoUtil.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryParticipant.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryService.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoBridgeHandler.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCoffeeHandler.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCrockpotHandler.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoDimmerHandler.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHandler.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHolmesHandler.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoLightHandler.java
bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoMakerHandler.java
bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/i18n/wemo.properties
bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/thing/thing-types.xml
itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoLightHandlerOSGiTest.java
itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoMakerHandlerOSGiTest.java

index 16298b4dc76d2febba408fd68a13eb5693a5a726..82a8a9a5eddeae609af6e2053787f15dc960f9d7 100644 (file)
@@ -116,6 +116,15 @@ public class WemoBindingConstants {
     public static final int LINK_DISCOVERY_SERVICE_INITIAL_DELAY = 5;
     public static final String HTTP_CALL_CONTENT_HEADER = "text/xml; charset=utf-8";
 
+    public static final String BASICACTION = "basicevent";
+    public static final String BASICEVENT = "basicevent1";
+    public static final String BRIDGEACTION = "bridge";
+    public static final String BRIDGEEVENT = "bridge1";
+    public static final String DEVICEACTION = "deviceevent";
+    public static final String DEVICEEVENT = "deviceevent1";
+    public static final String INSIGHTACTION = "insight";
+    public static final String INSIGHTEVENT = "insight1";
+
     public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
 
     public static final Set<ThingTypeUID> SUPPORTED_LIGHT_THING_TYPES = Collections.singleton(THING_TYPE_MZ100);
index 9e09b1214b9457f11a2dc075916a38574201d6a8..56299260c52aab6d0d03f409a83abe80ad6ec1c1 100644 (file)
@@ -64,7 +64,7 @@ public class WemoHandlerFactory extends BaseThingHandlerFactory {
 
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = WemoBindingConstants.SUPPORTED_THING_TYPES;
 
-    private UpnpIOService upnpIOService;
+    private final UpnpIOService upnpIOService;
     private @Nullable WemoHttpCallFactory wemoHttpCallFactory;
 
     @Override
@@ -103,14 +103,14 @@ public class WemoHandlerFactory extends BaseThingHandlerFactory {
             WemoBridgeHandler handler = new WemoBridgeHandler((Bridge) thing);
             registerDeviceDiscoveryService(handler, wemoHttpcaller);
             return handler;
-        } else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_MAKER)) {
-            logger.debug("Creating a WemoMakerHandler for thing '{}' with UDN '{}'", thing.getUID(),
-                    thing.getConfiguration().get(UDN));
-            return new WemoMakerHandler(thing, upnpIOService, wemoHttpcaller);
         } else if (WemoBindingConstants.SUPPORTED_DEVICE_THING_TYPES.contains(thing.getThingTypeUID())) {
             logger.debug("Creating a WemoHandler for thing '{}' with UDN '{}'", thing.getUID(),
                     thing.getConfiguration().get(UDN));
             return new WemoHandler(thing, upnpIOService, wemoHttpcaller);
+        } else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_MAKER)) {
+            logger.debug("Creating a WemoMakerHandler for thing '{}' with UDN '{}'", thing.getUID(),
+                    thing.getConfiguration().get(UDN));
+            return new WemoMakerHandler(thing, upnpIOService, wemoHttpcaller);
         } else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_COFFEE)) {
             logger.debug("Creating a WemoCoffeeHandler for thing '{}' with UDN '{}'", thing.getUID(),
                     thing.getConfiguration().get(UDN));
index 2c0d7eb686c53c349d2c3f2112be9e5b5e6097f5..872048ac9a551bdf9179eecfb9a33a4098597761 100644 (file)
@@ -13,7 +13,6 @@
 package org.openhab.binding.wemo.internal;
 
 import java.io.IOException;
-import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
@@ -23,6 +22,9 @@ import java.util.regex.Pattern;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.core.io.net.http.HttpUtil;
+import org.w3c.dom.CharacterData;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
 
 /**
  * {@link WemoUtil} implements some helper functions.
@@ -123,11 +125,10 @@ public class WemoUtil {
         return unescapedOutput.toString();
     }
 
-    public static @Nullable String getWemoURL(URL descriptorURL, String actionService) {
+    public static @Nullable String getWemoURL(String host, String actionService) {
         int portCheckStart = 49151;
         int portCheckStop = 49157;
         String port = null;
-        String host = substringBetween(descriptorURL.toString(), "://", ":");
         for (int i = portCheckStart; i < portCheckStop; i++) {
             if (serviceAvailableFunction.apply(host, i)) {
                 port = String.valueOf(i);
@@ -155,4 +156,30 @@ public class WemoUtil {
         entities.put("quot", "\"");
         return entities;
     }
+
+    public static String createBinaryStateContent(boolean binaryState) {
+        String binary = binaryState == true ? "1" : "0";
+        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:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<BinaryState>"
+                + binary + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>" + "</s:Envelope>";
+        return content;
+    }
+
+    public static String createStateRequestContent(String action, String actionService) {
+        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:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
+                + action + ">" + "</s:Body>" + "</s:Envelope>";
+        return content;
+    }
+
+    public static String getCharacterDataFromElement(Element e) {
+        Node child = e.getFirstChild();
+        if (child instanceof CharacterData) {
+            CharacterData cd = (CharacterData) child;
+            return cd.getData();
+        }
+        return "?";
+    }
 }
index 0db9798c2ff8f009c0f513181e0ccea389f0a755..f92e6cf5fd29e4a3241f6afc8c739b9d16d4ff67 100644 (file)
@@ -25,7 +25,6 @@ import org.openhab.binding.wemo.internal.WemoBindingConstants;
 import org.openhab.core.config.discovery.DiscoveryResult;
 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
 import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
-import org.openhab.core.config.discovery.upnp.internal.UpnpDiscoveryService;
 import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.ThingUID;
 import org.osgi.service.component.annotations.Component;
@@ -34,7 +33,7 @@ import org.slf4j.LoggerFactory;
 
 /**
  * The {@link WemoDiscoveryParticipant} is responsible for discovering new and
- * removed Wemo devices. It uses the central {@link UpnpDiscoveryService}.
+ * removed Wemo devices.
  *
  * @author Hans-Jörg Merk - Initial contribution
  * @author Kai Kreuzer - some refactoring for performance and simplification
index f7eada0a224dda67351b9439633efefe67406b63..6091b2ba57d399de1fe955a4f394962d706473b6 100644 (file)
@@ -65,8 +65,9 @@ public class WemoDiscoveryService extends AbstractDiscoveryService {
     @Override
     protected void startScan() {
         logger.debug("Starting UPnP RootDevice search...");
-        if (upnpService != null) {
-            upnpService.getControlPoint().search(new RootDeviceHeader());
+        UpnpService localService = upnpService;
+        if (localService != null) {
+            localService.getControlPoint().search(new RootDeviceHeader());
         } else {
             logger.debug("upnpService not set");
         }
index 89795ad8ab75a536de37b442b4c9dada358f9031..701b4f1cb6740cc2aa048a3eed23ad28236da5d3 100644 (file)
@@ -58,7 +58,8 @@ public class WemoBridgeHandler extends BaseBridgeHandler {
             updateStatus(ThingStatus.ONLINE);
         } else {
             logger.debug("Cannot initalize WemoBridgeHandler. UDN not set.");
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/config-status.error.missing-udn");
         }
     }
 
index 399f2e96067c8a789331417cfc2fc820ae37ce41..cd3264867caf0a1177ab5995bf2612cc1f7df473 100644 (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.math.BigDecimal;
 import java.net.URL;
 import java.time.Instant;
 import java.time.ZonedDateTime;
@@ -51,10 +50,8 @@ import org.openhab.core.types.RefreshType;
 import org.openhab.core.types.State;
 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;
 
@@ -72,31 +69,18 @@ public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOPart
 
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_COFFEE);
 
-    private Map<String, Boolean> subscriptionState = new HashMap<>();
+    private final Object upnpLock = new Object();
+    private final Object jobLock = new Object();
 
-    private UpnpIOService service;
+    private @Nullable UpnpIOService service;
 
     private WemoHttpCall wemoCall;
 
-    private @Nullable ScheduledFuture<?> refreshJob;
+    private String host = "";
 
-    private final Runnable refreshRunnable = new Runnable() {
+    private Map<String, Boolean> subscriptionState = new HashMap<>();
 
-        @Override
-        public void run() {
-            try {
-                if (!isUpnpDeviceRegistered()) {
-                    logger.debug("WeMo UPnP device {} not yet registered", getUDN());
-                }
-
-                updateWemoState();
-                onSubscription();
-            } catch (Exception e) {
-                logger.debug("Exception during poll", e);
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-            }
-        }
-    };
+    private @Nullable ScheduledFuture<?> pollingJob;
 
     public WemoCoffeeHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
         super(thing, wemoHttpCaller);
@@ -104,19 +88,26 @@ public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOPart
         this.wemoCall = wemoHttpCaller;
         this.service = upnpIOService;
 
-        logger.debug("Creating a WemoCoffeeHandler V0.4 for thing '{}'", getThing().getUID());
+        logger.debug("Creating a WemoCoffeeHandler for thing '{}'", getThing().getUID());
     }
 
     @Override
     public void initialize() {
         Configuration configuration = getConfig();
 
-        if (configuration.get("udn") != null) {
-            logger.debug("Initializing WemoCoffeeHandler for UDN '{}'", configuration.get("udn"));
-            onSubscription();
-            onUpdate();
+        if (configuration.get(UDN) != null) {
+            logger.debug("Initializing WemoCoffeeHandler for UDN '{}'", configuration.get(UDN));
+            UpnpIOService localService = service;
+            if (localService != null) {
+                localService.registerParticipant(this);
+            }
+            host = getHost();
+            pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
+                    TimeUnit.SECONDS);
             updateStatus(ThingStatus.ONLINE);
         } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/config-status.error.missing-udn");
             logger.debug("Cannot initalize WemoCoffeeHandler. UDN not set.");
         }
     }
@@ -124,19 +115,61 @@ public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOPart
     @Override
     public void dispose() {
         logger.debug("WeMoCoffeeHandler disposed.");
-
-        ScheduledFuture<?> job = refreshJob;
+        ScheduledFuture<?> job = this.pollingJob;
         if (job != null && !job.isCancelled()) {
             job.cancel(true);
         }
-        refreshJob = null;
+        this.pollingJob = null;
         removeSubscription();
     }
 
+    private void poll() {
+        synchronized (jobLock) {
+            if (pollingJob == null) {
+                return;
+            }
+            try {
+                logger.debug("Polling job");
+
+                host = getHost();
+                // Check if the Wemo device is set in the UPnP service registry
+                // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
+                if (!isUpnpDeviceRegistered()) {
+                    logger.debug("UPnP device {} not yet registered", getUDN());
+                    updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                            "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
+                    synchronized (upnpLock) {
+                        subscriptionState = new HashMap<>();
+                    }
+                    return;
+                }
+                updateStatus(ThingStatus.ONLINE);
+                updateWemoState();
+                addSubscription();
+            } catch (Exception e) {
+                logger.debug("Exception during poll: {}", e.getMessage(), e);
+            }
+        }
+    }
+
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.trace("Command '{}' received for channel '{}'", command, channelUID);
-
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String wemoURL = getWemoURL(localHost, BASICACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         if (command instanceof RefreshType) {
             try {
                 updateWemoState();
@@ -162,15 +195,19 @@ public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOPart
                                 + "&lt;attribute&gt;&lt;name&gt;Cleaning&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;</attributeList>"
                                 + "</u:SetAttributes>" + "</s:Body>" + "</s:Envelope>";
 
-                        URL descriptorURL = service.getDescriptorURL(this);
-                        String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
-                        if (wemoURL != null) {
-                            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
-                            if (wemoCallResponse != null) {
-                                updateState(CHANNEL_STATE, OnOffType.ON);
-                                State newMode = new StringType("Brewing");
-                                updateState(CHANNEL_COFFEEMODE, newMode);
+                        String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+                        if (wemoCallResponse != null) {
+                            updateState(CHANNEL_STATE, OnOffType.ON);
+                            State newMode = new StringType("Brewing");
+                            updateState(CHANNEL_COFFEEMODE, newMode);
+                            if (logger.isTraceEnabled()) {
+                                logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                                logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader,
+                                        getThing().getUID());
+                                logger.trace("wemoCall with content '{}' for device '{}'", content,
+                                        getThing().getUID());
+                                logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
+                                        getThing().getUID());
                             }
                         }
                     } catch (Exception e) {
@@ -179,7 +216,8 @@ public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOPart
                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
                     }
                 }
-                // if command.equals(OnOffType.OFF) we do nothing because WeMo Coffee Maker cannot be switched off
+                // if command.equals(OnOffType.OFF) we do nothing because WeMo Coffee Maker cannot be switched
+                // off
                 // remotely
                 updateStatus(ThingStatus.ONLINE);
             }
@@ -200,53 +238,53 @@ public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOPart
         // We can subscribe to GENA events, but there is no usefull response right now.
     }
 
-    private synchronized void onSubscription() {
-        if (service.isRegistered(this)) {
-            logger.debug("Checking WeMo GENA subscription for '{}'", this);
-
-            String subscription = "deviceevent1";
-            if (subscriptionState.get(subscription) == null) {
-                logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
-                service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
-                subscriptionState.put(subscription, true);
+    private synchronized void addSubscription() {
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
+
+                    String subscription = DEVICEEVENT;
+                    if (subscriptionState.get(subscription) == null) {
+                        logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+                                subscription);
+                        localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+                        subscriptionState.put(subscription, true);
+                    }
+                } else {
+                    logger.debug(
+                            "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+                            getThing().getUID());
+                }
             }
-        } else {
-            logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
-                    this);
         }
     }
 
     private synchronized void removeSubscription() {
-        logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
-        if (service.isRegistered(this)) {
-            String subscription = "deviceevent1";
-            if (subscriptionState.get(subscription) != null) {
-                logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
-                service.removeSubscription(this, subscription);
-            }
-
-            subscriptionState = new HashMap<>();
-            service.unregisterParticipant(this);
-        }
-    }
-
-    private synchronized void onUpdate() {
-        ScheduledFuture<?> job = refreshJob;
-        if (job == null || job.isCancelled()) {
-            Configuration config = getThing().getConfiguration();
-            int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
-            Object refreshConfig = config.get("pollingInterval");
-            if (refreshConfig != null) {
-                refreshInterval = ((BigDecimal) refreshConfig).intValue();
-                logger.debug("Setting WemoCoffeeHandler refreshInterval to '{}' seconds", refreshInterval);
+        logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    String subscription = DEVICEEVENT;
+                    if (subscriptionState.get(subscription) != null) {
+                        logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+                        localService.removeSubscription(this, subscription);
+                    }
+                    subscriptionState = new HashMap<>();
+                    localService.unregisterParticipant(this);
+                }
             }
-            refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
         }
     }
 
     private boolean isUpnpDeviceRegistered() {
-        return service.isRegistered(this);
+        UpnpIOService localService = service;
+        if (localService != null) {
+            return localService.isRegistered(this);
+        }
+        return false;
     }
 
     @Override
@@ -258,154 +296,163 @@ public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOPart
      * The {@link updateWemoState} polls the actual state of a WeMo CoffeeMaker.
      */
     protected void updateWemoState() {
-        String action = "GetAttributes";
-        String actionService = "deviceevent";
-
-        String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
-        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:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
-                + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String actionService = DEVICEACTION;
+        String wemoURL = getWemoURL(host, actionService);
+        if (wemoURL == null) {
+            logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         try {
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, actionService);
-
-            if (wemoURL != null) {
-                String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
-                if (wemoCallResponse != null) {
-                    try {
-                        String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
-
-                        // Due to Belkins bad response formatting, we need to run this twice.
-                        stringParser = unescapeXml(stringParser);
-                        stringParser = unescapeXml(stringParser);
-
-                        logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser,
-                                getThing().getUID());
-
-                        stringParser = "<data>" + stringParser + "</data>";
-
-                        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("attribute");
-
-                        // iterate the attributes
-                        for (int i = 0; i < nodes.getLength(); i++) {
-                            Element element = (Element) nodes.item(i);
-
-                            NodeList deviceIndex = element.getElementsByTagName("name");
-                            Element line = (Element) deviceIndex.item(0);
-                            String attributeName = getCharacterDataFromElement(line);
-                            logger.trace("attributeName: {}", attributeName);
-
-                            NodeList deviceID = element.getElementsByTagName("value");
-                            line = (Element) deviceID.item(0);
-                            String attributeValue = getCharacterDataFromElement(line);
-                            logger.trace("attributeValue: {}", attributeValue);
-
-                            switch (attributeName) {
-                                case "Mode":
-                                    State newMode = new StringType("Brewing");
-                                    State newAttributeValue;
-
-                                    switch (attributeValue) {
-                                        case "0":
-                                            updateState(CHANNEL_STATE, OnOffType.ON);
-                                            newMode = new StringType("Refill");
-                                            updateState(CHANNEL_COFFEEMODE, newMode);
-                                            break;
-                                        case "1":
-                                            updateState(CHANNEL_STATE, OnOffType.OFF);
-                                            newMode = new StringType("PlaceCarafe");
-                                            updateState(CHANNEL_COFFEEMODE, newMode);
-                                            break;
-                                        case "2":
-                                            updateState(CHANNEL_STATE, OnOffType.OFF);
-                                            newMode = new StringType("RefillWater");
-                                            updateState(CHANNEL_COFFEEMODE, newMode);
-                                            break;
-                                        case "3":
-                                            updateState(CHANNEL_STATE, OnOffType.OFF);
-                                            newMode = new StringType("Ready");
-                                            updateState(CHANNEL_COFFEEMODE, newMode);
-                                            break;
-                                        case "4":
-                                            updateState(CHANNEL_STATE, OnOffType.ON);
-                                            newMode = new StringType("Brewing");
-                                            updateState(CHANNEL_COFFEEMODE, newMode);
-                                            break;
-                                        case "5":
-                                            updateState(CHANNEL_STATE, OnOffType.OFF);
-                                            newMode = new StringType("Brewed");
-                                            updateState(CHANNEL_COFFEEMODE, newMode);
-                                            break;
-                                        case "6":
-                                            updateState(CHANNEL_STATE, OnOffType.OFF);
-                                            newMode = new StringType("CleaningBrewing");
-                                            updateState(CHANNEL_COFFEEMODE, newMode);
-                                            break;
-                                        case "7":
-                                            updateState(CHANNEL_STATE, OnOffType.OFF);
-                                            newMode = new StringType("CleaningSoaking");
-                                            updateState(CHANNEL_COFFEEMODE, newMode);
-                                            break;
-                                        case "8":
-                                            updateState(CHANNEL_STATE, OnOffType.OFF);
-                                            newMode = new StringType("BrewFailCarafeRemoved");
-                                            updateState(CHANNEL_COFFEEMODE, newMode);
-                                            break;
-                                    }
-                                    break;
-                                case "ModeTime":
-                                    newAttributeValue = new DecimalType(attributeValue);
-                                    updateState(CHANNEL_MODETIME, newAttributeValue);
-                                    break;
-                                case "TimeRemaining":
-                                    newAttributeValue = new DecimalType(attributeValue);
-                                    updateState(CHANNEL_TIMEREMAINING, newAttributeValue);
-                                    break;
-                                case "WaterLevelReached":
-                                    newAttributeValue = new DecimalType(attributeValue);
-                                    updateState(CHANNEL_WATERLEVELREACHED, newAttributeValue);
-                                    break;
-                                case "CleanAdvise":
-                                    newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
-                                    updateState(CHANNEL_CLEANADVISE, newAttributeValue);
-                                    break;
-                                case "FilterAdvise":
-                                    newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
-                                    updateState(CHANNEL_FILTERADVISE, newAttributeValue);
-                                    break;
-                                case "Brewed":
-                                    newAttributeValue = getDateTimeState(attributeValue);
-                                    if (newAttributeValue != null) {
-                                        updateState(CHANNEL_BREWED, newAttributeValue);
-                                    }
-                                    break;
-                                case "LastCleaned":
-                                    newAttributeValue = getDateTimeState(attributeValue);
-                                    if (newAttributeValue != null) {
-                                        updateState(CHANNEL_LASTCLEANED, newAttributeValue);
-                                    }
-                                    break;
-                            }
+            String action = "GetAttributes";
+            String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
+            String content = createStateRequestContent(action, actionService);
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null) {
+                if (logger.isTraceEnabled()) {
+                    logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                    logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                    logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                    logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+                }
+                try {
+                    String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
+
+                    // Due to Belkins bad response formatting, we need to run this twice.
+                    stringParser = unescapeXml(stringParser);
+                    stringParser = unescapeXml(stringParser);
+
+                    logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser,
+                            getThing().getUID());
+
+                    stringParser = "<data>" + stringParser + "</data>";
+
+                    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("attribute");
+
+                    // iterate the attributes
+                    for (int i = 0; i < nodes.getLength(); i++) {
+                        Element element = (Element) nodes.item(i);
+
+                        NodeList deviceIndex = element.getElementsByTagName("name");
+                        Element line = (Element) deviceIndex.item(0);
+                        String attributeName = getCharacterDataFromElement(line);
+                        logger.trace("attributeName: {}", attributeName);
+
+                        NodeList deviceID = element.getElementsByTagName("value");
+                        line = (Element) deviceID.item(0);
+                        String attributeValue = getCharacterDataFromElement(line);
+                        logger.trace("attributeValue: {}", attributeValue);
+
+                        switch (attributeName) {
+                            case "Mode":
+                                State newMode = new StringType("Brewing");
+                                State newAttributeValue;
+
+                                switch (attributeValue) {
+                                    case "0":
+                                        updateState(CHANNEL_STATE, OnOffType.ON);
+                                        newMode = new StringType("Refill");
+                                        updateState(CHANNEL_COFFEEMODE, newMode);
+                                        break;
+                                    case "1":
+                                        updateState(CHANNEL_STATE, OnOffType.OFF);
+                                        newMode = new StringType("PlaceCarafe");
+                                        updateState(CHANNEL_COFFEEMODE, newMode);
+                                        break;
+                                    case "2":
+                                        updateState(CHANNEL_STATE, OnOffType.OFF);
+                                        newMode = new StringType("RefillWater");
+                                        updateState(CHANNEL_COFFEEMODE, newMode);
+                                        break;
+                                    case "3":
+                                        updateState(CHANNEL_STATE, OnOffType.OFF);
+                                        newMode = new StringType("Ready");
+                                        updateState(CHANNEL_COFFEEMODE, newMode);
+                                        break;
+                                    case "4":
+                                        updateState(CHANNEL_STATE, OnOffType.ON);
+                                        newMode = new StringType("Brewing");
+                                        updateState(CHANNEL_COFFEEMODE, newMode);
+                                        break;
+                                    case "5":
+                                        updateState(CHANNEL_STATE, OnOffType.OFF);
+                                        newMode = new StringType("Brewed");
+                                        updateState(CHANNEL_COFFEEMODE, newMode);
+                                        break;
+                                    case "6":
+                                        updateState(CHANNEL_STATE, OnOffType.OFF);
+                                        newMode = new StringType("CleaningBrewing");
+                                        updateState(CHANNEL_COFFEEMODE, newMode);
+                                        break;
+                                    case "7":
+                                        updateState(CHANNEL_STATE, OnOffType.OFF);
+                                        newMode = new StringType("CleaningSoaking");
+                                        updateState(CHANNEL_COFFEEMODE, newMode);
+                                        break;
+                                    case "8":
+                                        updateState(CHANNEL_STATE, OnOffType.OFF);
+                                        newMode = new StringType("BrewFailCarafeRemoved");
+                                        updateState(CHANNEL_COFFEEMODE, newMode);
+                                        break;
+                                }
+                                break;
+                            case "ModeTime":
+                                newAttributeValue = new DecimalType(attributeValue);
+                                updateState(CHANNEL_MODETIME, newAttributeValue);
+                                break;
+                            case "TimeRemaining":
+                                newAttributeValue = new DecimalType(attributeValue);
+                                updateState(CHANNEL_TIMEREMAINING, newAttributeValue);
+                                break;
+                            case "WaterLevelReached":
+                                newAttributeValue = new DecimalType(attributeValue);
+                                updateState(CHANNEL_WATERLEVELREACHED, newAttributeValue);
+                                break;
+                            case "CleanAdvise":
+                                newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
+                                updateState(CHANNEL_CLEANADVISE, newAttributeValue);
+                                break;
+                            case "FilterAdvise":
+                                newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
+                                updateState(CHANNEL_FILTERADVISE, newAttributeValue);
+                                break;
+                            case "Brewed":
+                                newAttributeValue = getDateTimeState(attributeValue);
+                                if (newAttributeValue != null) {
+                                    updateState(CHANNEL_BREWED, newAttributeValue);
+                                }
+                                break;
+                            case "LastCleaned":
+                                newAttributeValue = getDateTimeState(attributeValue);
+                                if (newAttributeValue != null) {
+                                    updateState(CHANNEL_LASTCLEANED, newAttributeValue);
+                                }
+                                break;
                         }
-                    } catch (Exception e) {
-                        logger.error("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(),
-                                e);
                     }
+                } catch (Exception e) {
+                    logger.error("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(), e);
                 }
             }
         } catch (Exception e) {
@@ -428,13 +475,19 @@ public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOPart
         return dateTimeState;
     }
 
-    public static String getCharacterDataFromElement(Element e) {
-        Node child = e.getFirstChild();
-        if (child instanceof CharacterData) {
-            CharacterData cd = (CharacterData) child;
-            return cd.getData();
+    public String getHost() {
+        String localHost = host;
+        if (!localHost.isEmpty()) {
+            return localHost;
+        }
+        UpnpIOService localService = service;
+        if (localService != null) {
+            URL descriptorURL = localService.getDescriptorURL(this);
+            if (descriptorURL != null) {
+                return descriptorURL.getHost();
+            }
         }
-        return "?";
+        return "";
     }
 
     @Override
index dcf6b7c075fd50469baf4d189962bbaa9894d829..047f82b8bcba4f4a7b916a49f99df17eaf9071d6 100644 (file)
@@ -15,7 +15,6 @@ package org.openhab.binding.wemo.internal.handler;
 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
 import static org.openhab.binding.wemo.internal.WemoUtil.*;
 
-import java.math.BigDecimal;
 import java.net.URL;
 import java.util.Collections;
 import java.util.HashMap;
@@ -56,23 +55,20 @@ public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOPa
 
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_CROCKPOT);
 
-    private final Map<String, Boolean> subscriptionState = new HashMap<>();
+    private final Object upnpLock = new Object();
+    private final Object jobLock = new Object();
+
     private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
 
-    private UpnpIOService service;
+    private @Nullable UpnpIOService service;
 
     private WemoHttpCall wemoCall;
 
-    private @Nullable ScheduledFuture<?> refreshJob;
+    private String host = "";
 
-    private final Runnable refreshRunnable = () -> {
-        updateWemoState();
-        if (!isUpnpDeviceRegistered()) {
-            logger.debug("WeMo UPnP device {} not yet registered", getUDN());
-        } else {
-            onSubscription();
-        }
-    };
+    private Map<String, Boolean> subscriptionState = new HashMap<>();
+
+    private @Nullable ScheduledFuture<?> pollingJob;
 
     public WemoCrockpotHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
         super(thing, wemoHttpCaller);
@@ -87,13 +83,19 @@ public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOPa
     public void initialize() {
         Configuration configuration = getConfig();
 
-        if (configuration.get("udn") != null) {
-            logger.debug("Initializing WemoCrockpotHandler for UDN '{}'", configuration.get("udn"));
-            service.registerParticipant(this);
-            onSubscription();
-            onUpdate();
+        if (configuration.get(UDN) != null) {
+            logger.debug("Initializing WemoCrockpotHandler for UDN '{}'", configuration.get(UDN));
+            UpnpIOService localService = service;
+            if (localService != null) {
+                localService.registerParticipant(this);
+            }
+            host = getHost();
+            pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
+                    TimeUnit.SECONDS);
             updateStatus(ThingStatus.ONLINE);
         } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/config-status.error.missing-udn");
             logger.debug("Cannot initalize WemoCrockpotHandler. UDN not set.");
         }
     }
@@ -101,18 +103,60 @@ public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOPa
     @Override
     public void dispose() {
         logger.debug("WeMoCrockpotHandler disposed.");
-
-        ScheduledFuture<?> job = refreshJob;
+        ScheduledFuture<?> job = this.pollingJob;
         if (job != null && !job.isCancelled()) {
             job.cancel(true);
         }
-        refreshJob = null;
+        this.pollingJob = null;
         removeSubscription();
     }
 
+    private void poll() {
+        synchronized (jobLock) {
+            if (pollingJob == null) {
+                return;
+            }
+            try {
+                logger.debug("Polling job");
+                host = getHost();
+                // Check if the Wemo device is set in the UPnP service registry
+                // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
+                if (!isUpnpDeviceRegistered()) {
+                    logger.debug("UPnP device {} not yet registered", getUDN());
+                    updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                            "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
+                    synchronized (upnpLock) {
+                        subscriptionState = new HashMap<>();
+                    }
+                    return;
+                }
+                updateStatus(ThingStatus.ONLINE);
+                updateWemoState();
+                addSubscription();
+            } catch (Exception e) {
+                logger.debug("Exception during poll: {}", e.getMessage(), e);
+            }
+        }
+    }
+
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.trace("Command '{}' received for channel '{}'", command, channelUID);
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String wemoURL = getWemoURL(localHost, BASICACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         String mode = "0";
         String time = null;
 
@@ -142,12 +186,12 @@ public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOPa
                         + "<s:Body>" + "<u:SetCrockpotState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<mode>"
                         + mode + "</mode>" + "<time>" + time + "</time>" + "</u:SetCrockpotState>" + "</s:Body>"
                         + "</s:Envelope>";
-
-                URL descriptorURL = service.getDescriptorURL(this);
-                String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
-                if (wemoURL != null) {
-                    wemoCall.executeCall(wemoURL, soapHeader, content);
+                String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+                if (wemoCallResponse != null && logger.isTraceEnabled()) {
+                    logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                    logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                    logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                    logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
                 }
             } catch (RuntimeException e) {
                 logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
@@ -177,54 +221,55 @@ public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOPa
         }
     }
 
-    private synchronized void onSubscription() {
-        if (service.isRegistered(this)) {
-            logger.debug("Checking WeMo GENA subscription for '{}'", this);
+    private synchronized void addSubscription() {
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
 
-            String subscription = "basicevent1";
+                    String subscription = BASICEVENT;
 
-            if (subscriptionState.get(subscription) == null) {
-                logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
-                service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
-                subscriptionState.put(subscription, true);
+                    if (subscriptionState.get(subscription) == null) {
+                        logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+                                subscription);
+                        localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+                        subscriptionState.put(subscription, true);
+                    }
+                } else {
+                    logger.debug(
+                            "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+                            getThing().getUID());
+                }
             }
-
-        } else {
-            logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
-                    this);
         }
     }
 
     private synchronized void removeSubscription() {
-        logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
-        if (service.isRegistered(this)) {
-            String subscription = "basicevent1";
-
-            if (subscriptionState.get(subscription) != null) {
-                logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
-                service.removeSubscription(this, subscription);
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+                    String subscription = BASICEVENT;
+
+                    if (subscriptionState.get(subscription) != null) {
+                        logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+                        localService.removeSubscription(this, subscription);
+                    }
+                    subscriptionState.remove(subscription);
+                    localService.unregisterParticipant(this);
+                }
             }
-
-            subscriptionState.remove(subscription);
-            service.unregisterParticipant(this);
-        }
-    }
-
-    private synchronized void onUpdate() {
-        ScheduledFuture<?> job = refreshJob;
-        if (job == null || job.isCancelled()) {
-            Configuration config = getThing().getConfiguration();
-            int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
-            Object refreshConfig = config.get("refresh");
-            refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVALL_SECONDS
-                    : ((BigDecimal) refreshConfig).intValue();
-            refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
         }
     }
 
     private boolean isUpnpDeviceRegistered() {
-        return service.isRegistered(this);
+        UpnpIOService localService = service;
+        if (localService != null) {
+            return localService.isRegistered(this);
+        }
+        return false;
     }
 
     @Override
@@ -238,52 +283,61 @@ public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOPa
      *
      */
     protected void updateWemoState() {
-        String action = "GetCrockpotState";
-        String actionService = "basicevent";
-
-        String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
-        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:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
-                + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String actionService = BASICEVENT;
+        String wemoURL = getWemoURL(localHost, actionService);
+        if (wemoURL == null) {
+            logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         try {
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, actionService);
-
-            if (wemoURL != null) {
-                String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
-                if (wemoCallResponse != null) {
-                    logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
-                    String mode = substringBetween(wemoCallResponse, "<mode>", "</mode>");
-                    String time = substringBetween(wemoCallResponse, "<time>", "</time>");
-                    String coockedTime = substringBetween(wemoCallResponse, "<coockedTime>", "</coockedTime>");
-
-                    State newMode = new StringType(mode);
-                    State newCoockedTime = DecimalType.valueOf(coockedTime);
-                    switch (mode) {
-                        case "0":
-                            newMode = new StringType("OFF");
-                            break;
-                        case "50":
-                            newMode = new StringType("WARM");
-                            State warmTime = DecimalType.valueOf(time);
-                            updateState(CHANNEL_WARMCOOKTIME, warmTime);
-                            break;
-                        case "51":
-                            newMode = new StringType("LOW");
-                            State lowTime = DecimalType.valueOf(time);
-                            updateState(CHANNEL_LOWCOOKTIME, lowTime);
-                            break;
-                        case "52":
-                            newMode = new StringType("HIGH");
-                            State highTime = DecimalType.valueOf(time);
-                            updateState(CHANNEL_HIGHCOOKTIME, highTime);
-                            break;
-                    }
-                    updateState(CHANNEL_COOKMODE, newMode);
-                    updateState(CHANNEL_COOKEDTIME, newCoockedTime);
+            String action = "GetCrockpotState";
+            String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
+            String content = createStateRequestContent(action, actionService);
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null) {
+                if (logger.isTraceEnabled()) {
+                    logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                    logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                    logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                    logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+                }
+                String mode = substringBetween(wemoCallResponse, "<mode>", "</mode>");
+                String time = substringBetween(wemoCallResponse, "<time>", "</time>");
+                String coockedTime = substringBetween(wemoCallResponse, "<coockedTime>", "</coockedTime>");
+
+                State newMode = new StringType(mode);
+                State newCoockedTime = DecimalType.valueOf(coockedTime);
+                switch (mode) {
+                    case "0":
+                        newMode = new StringType("OFF");
+                        break;
+                    case "50":
+                        newMode = new StringType("WARM");
+                        State warmTime = DecimalType.valueOf(time);
+                        updateState(CHANNEL_WARMCOOKTIME, warmTime);
+                        break;
+                    case "51":
+                        newMode = new StringType("LOW");
+                        State lowTime = DecimalType.valueOf(time);
+                        updateState(CHANNEL_LOWCOOKTIME, lowTime);
+                        break;
+                    case "52":
+                        newMode = new StringType("HIGH");
+                        State highTime = DecimalType.valueOf(time);
+                        updateState(CHANNEL_HIGHCOOKTIME, highTime);
+                        break;
                 }
+                updateState(CHANNEL_COOKMODE, newMode);
+                updateState(CHANNEL_COOKEDTIME, newCoockedTime);
             }
         } catch (RuntimeException e) {
             logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage(), e);
@@ -295,4 +349,19 @@ public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOPa
     @Override
     public void onStatusChanged(boolean status) {
     }
+
+    public String getHost() {
+        String localHost = host;
+        if (!localHost.isEmpty()) {
+            return localHost;
+        }
+        UpnpIOService localService = service;
+        if (localService != null) {
+            URL descriptorURL = localService.getDescriptorURL(this);
+            if (descriptorURL != null) {
+                return descriptorURL.getHost();
+            }
+        }
+        return "";
+    }
 }
index a47171ef8aff8a650e02cd165f4800ff75c9795c..f864d402500aec005de68bd4731d3debf0f3a68a 100644 (file)
@@ -15,7 +15,6 @@ package org.openhab.binding.wemo.internal.handler;
 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
 import static org.openhab.binding.wemo.internal.WemoUtil.*;
 
-import java.math.BigDecimal;
 import java.net.URL;
 import java.time.Instant;
 import java.time.ZonedDateTime;
@@ -62,12 +61,21 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
 
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DIMMER);
 
-    private Map<String, Boolean> subscriptionState = new HashMap<>();
-    private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
+    private final Object upnpLock = new Object();
+    private final Object jobLock = new Object();
+
+    private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
+
+    private @Nullable UpnpIOService service;
 
-    private UpnpIOService service;
     private WemoHttpCall wemoCall;
 
+    private String host = "";
+
+    private Map<String, Boolean> subscriptionState = new HashMap<>();
+
+    private @Nullable ScheduledFuture<?> pollingJob;
+
     private int currentBrightness;
     private int currentNightModeBrightness;
     private @Nullable String currentNightModeState;
@@ -76,23 +84,6 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
      */
     private static final int DIM_STEPSIZE = 5;
 
-    private @Nullable ScheduledFuture<?> refreshJob;
-    private Runnable refreshRunnable = new Runnable() {
-
-        @Override
-        public void run() {
-            try {
-                if (!isUpnpDeviceRegistered()) {
-                    logger.debug("WeMo UPnP device {} not yet registered", getUDN());
-                }
-                updateWemoState();
-                onSubscription();
-            } catch (Exception e) {
-                logger.debug("Exception during poll : {}", e.getMessage(), e);
-            }
-        }
-    };
-
     public WemoDimmerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
         super(thing, wemoHttpCaller);
 
@@ -105,12 +96,20 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
     @Override
     public void initialize() {
         Configuration configuration = getConfig();
-        if (configuration.get("udn") != null) {
-            logger.debug("Initializing WemoDimmerHandler for UDN '{}'", configuration.get("udn"));
-            service.registerParticipant(this);
-            onSubscription();
-            onUpdate();
+
+        if (configuration.get(UDN) != null) {
+            logger.debug("Initializing WemoDimmerHandler for UDN '{}'", configuration.get(UDN));
+            UpnpIOService localService = service;
+            if (localService != null) {
+                localService.registerParticipant(this);
+            }
+            host = getHost();
+            pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
+                    TimeUnit.SECONDS);
+            updateStatus(ThingStatus.ONLINE);
         } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/config-status.error.missing-udn");
             logger.debug("Cannot initalize WemoDimmerHandler. UDN not set.");
         }
     }
@@ -119,15 +118,42 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
     public void dispose() {
         logger.debug("WeMoDimmerHandler disposed.");
 
-        ScheduledFuture<?> job = refreshJob;
+        ScheduledFuture<?> job = this.pollingJob;
         if (job != null && !job.isCancelled()) {
             job.cancel(true);
         }
-        refreshJob = null;
-
+        this.pollingJob = null;
         removeSubscription();
     }
 
+    private void poll() {
+        synchronized (jobLock) {
+            if (pollingJob == null) {
+                return;
+            }
+            try {
+                logger.debug("Polling job");
+                host = getHost();
+                // Check if the Wemo device is set in the UPnP service registry
+                // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
+                if (!isUpnpDeviceRegistered()) {
+                    logger.debug("UPnP device {} not yet registered", getUDN());
+                    updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                            "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
+                    synchronized (upnpLock) {
+                        subscriptionState = new HashMap<>();
+                    }
+                    return;
+                }
+                updateStatus(ThingStatus.ONLINE);
+                updateWemoState();
+                addSubscription();
+            } catch (Exception e) {
+                logger.debug("Exception during poll: {}", e.getMessage(), e);
+            }
+        }
+    }
+
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
         logger.trace("Command '{}' received for channel '{}'", command, channelUID);
@@ -161,7 +187,7 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
                         value = String.valueOf(newBrightness);
                         currentBrightness = newBrightness;
                         argument = "brightness";
-                        if (value.equals("0")) {
+                        if ("0".equals(value)) {
                             value = "1";
                             argument = "brightness";
                             setBinaryState(action, argument, "1");
@@ -195,7 +221,7 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
                                 break;
                         }
                         argument = "brightness";
-                        if (value.equals("0")) {
+                        if ("0".equals(value)) {
                             value = "1";
                             argument = "brightness";
                             setBinaryState(action, argument, "1");
@@ -251,12 +277,12 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
                         }
                     }
                     if (faderSeconds != null && faderEnabled != null) {
-                        if (command.equals(OnOffType.ON)) {
+                        if (OnOffType.ON.equals(command)) {
                             value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
                                     + "<brightness></brightness>" + "<fader>" + faderSeconds + ":" + timeStamp + ":"
                                     + faderEnabled + ":0:0</fader>" + "<UDN></UDN>";
                             updateState(CHANNEL_STATE, OnOffType.ON);
-                        } else if (command.equals(OnOffType.OFF)) {
+                        } else if (OnOffType.OFF.equals(command)) {
                             value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
                                     + "<brightness></brightness>" + "<fader>" + faderSeconds + ":-1:" + faderEnabled
                                     + ":0:0</fader>" + "<UDN></UDN>";
@@ -268,10 +294,10 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
                     action = "ConfigureNightMode";
                     argument = "NightModeConfiguration";
                     String nightModeBrightness = String.valueOf(currentNightModeBrightness);
-                    if (command.equals(OnOffType.ON)) {
+                    if (OnOffType.ON.equals(command)) {
                         value = "&lt;startTime&gt;0&lt;/startTime&gt; \\n&lt;nightMode&gt;1&lt;/nightMode&gt; \\n&lt;endTime&gt;23400&lt;/endTime&gt; \\n&lt;nightModeBrightness&gt;"
                                 + nightModeBrightness + "&lt;/nightModeBrightness&gt; \\n";
-                    } else if (command.equals(OnOffType.OFF)) {
+                    } else if (OnOffType.OFF.equals(command)) {
                         value = "&lt;startTime&gt;0&lt;/startTime&gt; \\n&lt;nightMode&gt;0&lt;/nightMode&gt; \\n&lt;endTime&gt;23400&lt;/endTime&gt; \\n&lt;nightModeBrightness&gt;"
                                 + nightModeBrightness + "&lt;/nightModeBrightness&gt; \\n";
                     }
@@ -338,7 +364,7 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
             switch (variable) {
                 case "BinaryState":
                     if (oldBinaryState == null || !oldBinaryState.equals(value)) {
-                        State state = value.equals("0") ? OnOffType.OFF : OnOffType.ON;
+                        State state = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
                         logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
                         updateState(CHANNEL_BRIGHTNESS, state);
                         if (state.equals(OnOffType.OFF)) {
@@ -352,7 +378,7 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
                     State newBrightnessState = new PercentType(newBrightnessValue);
                     String binaryState = this.stateMap.get("BinaryState");
                     if (binaryState != null) {
-                        if (binaryState.equals("1")) {
+                        if ("1".equals(binaryState)) {
                             updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
                         }
                     }
@@ -385,7 +411,7 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
                     }
                     break;
                 case "nightMode":
-                    State nightModeState = value.equals("0") ? OnOffType.OFF : OnOffType.ON;
+                    State nightModeState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
                     currentNightModeState = value;
                     logger.debug("nightModeState '{}' for device '{}' received", nightModeState, getThing().getUID());
                     updateState(CHANNEL_NIGHTMODE, nightModeState);
@@ -413,53 +439,50 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
                     updateState(CHANNEL_NIGHTMODEBRIGHTNESS, nightModeBrightnessState);
                     break;
             }
-
         }
     }
 
-    private synchronized void onSubscription() {
-        if (service.isRegistered(this)) {
-            logger.debug("Checking WeMo GENA subscription for '{}'", this);
-            String subscription = "basicevent1";
-            if (subscriptionState.get(subscription) == null) {
-                logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
-                service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
-                subscriptionState.put(subscription, true);
+    private synchronized void addSubscription() {
+        UpnpIOService localService = service;
+        if (localService != null) {
+            if (localService.isRegistered(this)) {
+                logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
+                String subscription = BASICEVENT;
+                if (subscriptionState.get(subscription) == null) {
+                    logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+                            subscription);
+                    localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+                    subscriptionState.put(subscription, true);
+                }
+            } else {
+                logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+                        getThing().getUID());
             }
-        } else {
-            logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
-                    this);
         }
     }
 
     private synchronized void removeSubscription() {
-        logger.debug("Removing WeMo GENA subscription for '{}'", this);
-        if (service.isRegistered(this)) {
-            String subscription = "basicevent1";
-            if (subscriptionState.get(subscription) != null) {
-                logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
-                service.removeSubscription(this, subscription);
-            }
-            subscriptionState = new HashMap<>();
-            service.unregisterParticipant(this);
-        }
-    }
-
-    private synchronized void onUpdate() {
-        ScheduledFuture<?> job = refreshJob;
-        if (job == null || job.isCancelled()) {
-            Configuration config = getThing().getConfiguration();
-            int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
-            Object refreshConfig = config.get("refresh");
-            if (refreshConfig != null) {
-                refreshInterval = ((BigDecimal) refreshConfig).intValue();
+        logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+        UpnpIOService localService = service;
+        if (localService != null) {
+            if (localService.isRegistered(this)) {
+                String subscription = BASICEVENT;
+                if (subscriptionState.get(subscription) != null) {
+                    logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+                    localService.removeSubscription(this, subscription);
+                }
+                subscriptionState = new HashMap<>();
+                localService.unregisterParticipant(this);
             }
-            refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 10, refreshInterval, TimeUnit.SECONDS);
         }
     }
 
     private boolean isUpnpDeviceRegistered() {
-        return service.isRegistered(this);
+        UpnpIOService localService = service;
+        if (localService != null) {
+            return localService.isRegistered(this);
+        }
+        return false;
     }
 
     @Override
@@ -473,83 +496,84 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
      *
      */
     protected void updateWemoState() {
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String wemoURL = getWemoURL(localHost, BASICACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         String action = "GetBinaryState";
         String variable = null;
-        String actionService = "basicevent";
+        String actionService = BASICACTION;
         String value = null;
         String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
-        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:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
-                + action + ">" + "</s:Body>" + "</s:Envelope>";
+        String content = createStateRequestContent(action, actionService);
         try {
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
-            if (wemoURL != null) {
-                String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
-                if (wemoCallResponse != null) {
-                    logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
-                    value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
-                    variable = "BinaryState";
-                    logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
-                    this.onValueReceived(variable, value, actionService + "1");
-                    value = substringBetween(wemoCallResponse, "<brightness>", "</brightness>");
-                    variable = "brightness";
-                    logger.trace("New brightness '{}' for device '{}' received", value, getThing().getUID());
-                    this.onValueReceived(variable, value, actionService + "1");
-                    value = substringBetween(wemoCallResponse, "<fader>", "</fader>");
-                    variable = "fader";
-                    logger.trace("New fader value '{}' for device '{}' received", value, getThing().getUID());
-                    this.onValueReceived(variable, value, actionService + "1");
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null) {
+                if (logger.isTraceEnabled()) {
+                    logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                    logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                    logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                    logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
                 }
+                value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
+                variable = "BinaryState";
+                this.onValueReceived(variable, value, actionService + "1");
+                value = substringBetween(wemoCallResponse, "<brightness>", "</brightness>");
+                variable = "brightness";
+                this.onValueReceived(variable, value, actionService + "1");
+                value = substringBetween(wemoCallResponse, "<fader>", "</fader>");
+                variable = "fader";
+                this.onValueReceived(variable, value, actionService + "1");
+                updateStatus(ThingStatus.ONLINE);
             }
         } catch (Exception e) {
             logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
         }
-        updateStatus(ThingStatus.ONLINE);
         action = "GetNightModeConfiguration";
         variable = null;
         value = null;
         soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
-        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:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
-                + action + ">" + "</s:Body>" + "</s:Envelope>";
+        content = createStateRequestContent(action, actionService);
         try {
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
-            if (wemoURL != null) {
-                String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
-                if (wemoCallResponse != null) {
-                    logger.trace("GetNightModeConfiguration response '{}' for device '{}' received", wemoCallResponse,
-                            getThing().getUID());
-                    value = substringBetween(wemoCallResponse, "<startTime>", "</startTime>");
-                    variable = "startTime";
-                    logger.trace("New startTime '{}' for device '{}' received", value, getThing().getUID());
-                    this.onValueReceived(variable, value, actionService + "1");
-                    value = substringBetween(wemoCallResponse, "<endTime>", "</endTime>");
-                    variable = "endTime";
-                    logger.trace("New endTime '{}' for device '{}' received", value, getThing().getUID());
-                    this.onValueReceived(variable, value, actionService + "1");
-                    value = substringBetween(wemoCallResponse, "<nightMode>", "</nightMode>");
-                    variable = "nightMode";
-                    logger.trace("New nightMode state '{}' for device '{}' received", value, getThing().getUID());
-                    this.onValueReceived(variable, value, actionService + "1");
-                    value = substringBetween(wemoCallResponse, "<nightModeBrightness>", "</nightModeBrightness>");
-                    variable = "nightModeBrightness";
-                    logger.trace("New nightModeBrightness  '{}' for device '{}' received", value, getThing().getUID());
-                    this.onValueReceived(variable, value, actionService + "1");
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null) {
+                if (logger.isTraceEnabled()) {
+                    logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                    logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                    logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                    logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
                 }
+                value = substringBetween(wemoCallResponse, "<startTime>", "</startTime>");
+                variable = "startTime";
+                this.onValueReceived(variable, value, actionService + "1");
+                value = substringBetween(wemoCallResponse, "<endTime>", "</endTime>");
+                variable = "endTime";
+                this.onValueReceived(variable, value, actionService + "1");
+                value = substringBetween(wemoCallResponse, "<nightMode>", "</nightMode>");
+                variable = "nightMode";
+                this.onValueReceived(variable, value, actionService + "1");
+                value = substringBetween(wemoCallResponse, "<nightModeBrightness>", "</nightModeBrightness>");
+                variable = "nightModeBrightness";
+                this.onValueReceived(variable, value, actionService + "1");
+                updateStatus(ThingStatus.ONLINE);
+
             }
         } catch (Exception e) {
             logger.debug("Failed to get actual NightMode state for device '{}': {}", getThing().getUID(),
                     e.getMessage());
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
         }
-        updateStatus(ThingStatus.ONLINE);
     }
 
     public @Nullable State getDateTimeState(String attributeValue) {
@@ -568,6 +592,20 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
     }
 
     public void setBinaryState(String action, String argument, String value) {
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to set binary state for device '{}': IP address missing", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String wemoURL = getWemoURL(localHost, BASICACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to set binary state for device '{}': URL cannot be created", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         try {
             String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
             String content = "<?xml version=\"1.0\"?>"
@@ -575,11 +613,12 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
                     + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<" + argument
                     + ">" + value + "</" + argument + ">" + "</u:" + action + ">" + "</s:Body>" + "</s:Envelope>";
 
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
-            if (wemoURL != null) {
-                wemoCall.executeCall(wemoURL, soapHeader, content);
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null && logger.isTraceEnabled()) {
+                logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
             }
         } catch (Exception e) {
             logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
@@ -589,26 +628,55 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart
     }
 
     public void setTimerStart(String action, String argument, String value) {
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to set timerStart for device '{}': IP address missing", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String wemoURL = getWemoURL(localHost, BASICACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to set timerStart for device '{}': URL cannot be created", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         try {
             String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
             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:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + value
                     + "</u:SetBinaryState>" + "</s:Body>" + "</s:Envelope>";
-
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
-            if (wemoURL != null) {
-                wemoCall.executeCall(wemoURL, soapHeader, content);
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null && logger.isTraceEnabled()) {
+                logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
             }
         } catch (Exception e) {
-            logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
+            logger.debug("Failed to set timerStart '{}' for device '{}': {}", value, getThing().getUID(),
                     e.getMessage());
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
         }
     }
 
+    public String getHost() {
+        String localHost = host;
+        if (!localHost.isEmpty()) {
+            return localHost;
+        }
+        UpnpIOService localService = service;
+        if (localService != null) {
+            URL descriptorURL = localService.getDescriptorURL(this);
+            if (descriptorURL != null) {
+                return descriptorURL.getHost();
+            }
+        }
+        return "";
+    }
+
     @Override
     public void onStatusChanged(boolean status) {
     }
index 3e7bacbf0d19b105c64f067c7d0fdade544d136d..23c2a99a42964ad01ed154002df31fb1d6606491 100644 (file)
@@ -71,32 +71,20 @@ public class WemoHandler extends AbstractWemoHandler implements UpnpIOParticipan
             .of(THING_TYPE_SOCKET, THING_TYPE_INSIGHT, THING_TYPE_LIGHTSWITCH, THING_TYPE_MOTION)
             .collect(Collectors.toSet());
 
-    private Map<String, Boolean> subscriptionState = new HashMap<>();
+    private final Object upnpLock = new Object();
+    private final Object jobLock = new Object();
 
     private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
 
-    protected UpnpIOService service;
-    private WemoHttpCall wemoCall;
+    private @Nullable UpnpIOService service;
 
-    private @Nullable ScheduledFuture<?> refreshJob;
+    private WemoHttpCall wemoCall;
 
-    private final Runnable refreshRunnable = new Runnable() {
+    private Map<String, Boolean> subscriptionState = new HashMap<>();
 
-        @Override
-        public void run() {
-            try {
-                if (!isUpnpDeviceRegistered()) {
-                    logger.debug("WeMo UPnP device {} not yet registered", getUDN());
-                }
+    private @Nullable ScheduledFuture<?> pollingJob;
 
-                updateWemoState();
-                onSubscription();
-            } catch (Exception e) {
-                logger.debug("Exception during poll", e);
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-            }
-        }
-    };
+    private String host = "";
 
     public WemoHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
         super(thing, wemoHttpCaller);
@@ -111,63 +99,100 @@ public class WemoHandler extends AbstractWemoHandler implements UpnpIOParticipan
     public void initialize() {
         Configuration configuration = getConfig();
 
-        if (configuration.get("udn") != null) {
-            logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get("udn"));
-            service.registerParticipant(this);
-            onSubscription();
-            onUpdate();
+        if (configuration.get(UDN) != null) {
+            logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get(UDN));
+            UpnpIOService localService = service;
+            if (localService != null) {
+                localService.registerParticipant(this);
+            }
+            host = getHost();
+            pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
+                    TimeUnit.SECONDS);
             updateStatus(ThingStatus.ONLINE);
         } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/config-status.error.missing-udn");
             logger.debug("Cannot initalize WemoHandler. UDN not set.");
         }
     }
 
     @Override
     public void dispose() {
-        logger.debug("WeMoHandler disposed.");
+        logger.debug("WemoHandler disposed for thing {}", getThing().getUID());
 
-        ScheduledFuture<?> job = refreshJob;
-        if (job != null && !job.isCancelled()) {
+        ScheduledFuture<?> job = this.pollingJob;
+        if (job != null) {
             job.cancel(true);
         }
-        refreshJob = null;
+        this.pollingJob = null;
         removeSubscription();
     }
 
+    private void poll() {
+        synchronized (jobLock) {
+            if (pollingJob == null) {
+                return;
+            }
+            try {
+                logger.debug("Polling job");
+                host = getHost();
+                // Check if the Wemo device is set in the UPnP service registry
+                // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
+                if (!isUpnpDeviceRegistered()) {
+                    logger.debug("UPnP device {} not yet registered", getUDN());
+                    updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                            "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
+                    synchronized (upnpLock) {
+                        subscriptionState = new HashMap<>();
+                    }
+                    return;
+                }
+                updateStatus(ThingStatus.ONLINE);
+                updateWemoState();
+                addSubscription();
+            } catch (Exception e) {
+                logger.debug("Exception during poll: {}", e.getMessage(), e);
+            }
+        }
+    }
+
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.trace("Command '{}' received for channel '{}'", command, channelUID);
-
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String wemoURL = getWemoURL(localHost, BASICACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         if (command instanceof RefreshType) {
             try {
                 updateWemoState();
             } catch (Exception e) {
                 logger.debug("Exception during poll", e);
             }
-        } else if (channelUID.getId().equals(CHANNEL_STATE)) {
+        } else if (CHANNEL_STATE.equals(channelUID.getId())) {
             if (command instanceof OnOffType) {
                 try {
-                    String binaryState = null;
-
-                    if (command.equals(OnOffType.ON)) {
-                        binaryState = "1";
-                    } else if (command.equals(OnOffType.OFF)) {
-                        binaryState = "0";
-                    }
-
+                    boolean binaryState = OnOffType.ON.equals(command) ? true : false;
                     String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
-
-                    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:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">"
-                            + "<BinaryState>" + binaryState + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>"
-                            + "</s:Envelope>";
-
-                    URL descriptorURL = service.getDescriptorURL(this);
-                    String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
-                    if (wemoURL != null) {
-                        wemoCall.executeCall(wemoURL, soapHeader, content);
+                    String content = createBinaryStateContent(binaryState);
+                    String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+                    if (wemoCallResponse != null && logger.isTraceEnabled()) {
+                        logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                        logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                        logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                        logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
+                                getThing().getUID());
                     }
                 } catch (Exception e) {
                     logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
@@ -195,19 +220,23 @@ public class WemoHandler extends AbstractWemoHandler implements UpnpIOParticipan
 
         updateStatus(ThingStatus.ONLINE);
 
+        if (!"BinaryState".equals(variable) && !"InsightParams".equals(variable)) {
+            return;
+        }
+
+        String oldValue = this.stateMap.get(variable);
         if (variable != null && value != null) {
             this.stateMap.put(variable, value);
         }
 
-        if (getThing().getThingTypeUID().getId().equals("insight")) {
-            String insightParams = stateMap.get("InsightParams");
+        if (value != null && value.length() > 1) {
+            String insightParams = stateMap.get(variable);
 
             if (insightParams != null) {
                 String[] splitInsightParams = insightParams.split("\\|");
 
                 if (splitInsightParams[0] != null) {
-                    OnOffType binaryState = null;
-                    binaryState = splitInsightParams[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
+                    OnOffType binaryState = "0".equals(splitInsightParams[0]) ? OnOffType.OFF : OnOffType.ON;
                     logger.trace("New InsightParam binaryState '{}' for device '{}' received", binaryState,
                             getThing().getUID());
                     updateState(CHANNEL_STATE, binaryState);
@@ -278,106 +307,112 @@ public class WemoHandler extends AbstractWemoHandler implements UpnpIOParticipan
                         getThing().getUID());
                 updateState(CHANNEL_ENERGYTOTAL, energyTotal);
 
-                BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
-                State standByLimit = new QuantityType<>(
-                        standByLimitMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP), Units.WATT); // recalculate
-                // mW to W
-                logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
-                        getThing().getUID());
-                updateState(CHANNEL_STANDBYLIMIT, standByLimit);
+                if (splitInsightParams.length > 10 && splitInsightParams[10] != null) {
+                    BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
+                    State standByLimit = new QuantityType<>(
+                            standByLimitMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP), Units.WATT); // recalculate
+                    // mW to W
+                    logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
+                            getThing().getUID());
+                    updateState(CHANNEL_STANDBYLIMIT, standByLimit);
 
-                if (currentMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue() > standByLimitMW
-                        .divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue()) {
-                    updateState(CHANNEL_ONSTANDBY, OnOffType.OFF);
-                } else {
-                    updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
+                    if (currentMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue() > standByLimitMW
+                            .divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue()) {
+                        updateState(CHANNEL_ONSTANDBY, OnOffType.OFF);
+                    } else {
+                        updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
+                    }
                 }
             }
-        } else {
+        } else if (value != null && value.length() == 1) {
             String binaryState = stateMap.get("BinaryState");
             if (binaryState != null) {
-                State state = binaryState.equals("0") ? OnOffType.OFF : OnOffType.ON;
-                logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
-                if (getThing().getThingTypeUID().getId().equals("motion")) {
-                    updateState(CHANNEL_MOTIONDETECTION, state);
-                    if (state.equals(OnOffType.ON)) {
-                        State lastMotionDetected = new DateTimeType();
-                        updateState(CHANNEL_LASTMOTIONDETECTED, lastMotionDetected);
+                if (oldValue == null || !oldValue.equals(binaryState)) {
+                    State state = "0".equals(binaryState) ? OnOffType.OFF : OnOffType.ON;
+                    logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
+                    if ("motion".equals(getThing().getThingTypeUID().getId())) {
+                        updateState(CHANNEL_MOTIONDETECTION, state);
+                        if (OnOffType.ON.equals(state)) {
+                            State lastMotionDetected = new DateTimeType();
+                            updateState(CHANNEL_LASTMOTIONDETECTED, lastMotionDetected);
+                        }
+                    } else {
+                        updateState(CHANNEL_STATE, state);
                     }
-                } else {
-                    updateState(CHANNEL_STATE, state);
                 }
             }
         }
     }
 
-    private synchronized void onSubscription() {
-        if (service.isRegistered(this)) {
-            logger.debug("Checking WeMo GENA subscription for '{}'", this);
-
-            ThingTypeUID thingTypeUID = thing.getThingTypeUID();
-            String subscription = "basicevent1";
-
-            if (subscriptionState.get(subscription) == null) {
-                logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
-                service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
-                subscriptionState.put(subscription, true);
-            }
+    private synchronized void addSubscription() {
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
+
+                    ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+                    String subscription = BASICEVENT;
+
+                    if (subscriptionState.get(subscription) == null) {
+                        logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+                                subscription);
+                        localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+                        subscriptionState.put(subscription, true);
+                    }
 
-            if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
-                subscription = "insight1";
-                if (subscriptionState.get(subscription) == null) {
-                    logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
-                            subscription);
-                    service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
-                    subscriptionState.put(subscription, true);
+                    if (THING_TYPE_INSIGHT.equals(thingTypeUID)) {
+                        subscription = INSIGHTEVENT;
+                        if (subscriptionState.get(subscription) == null) {
+                            logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+                                    subscription);
+                            localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+                            subscriptionState.put(subscription, true);
+                        }
+                    }
+                } else {
+                    logger.debug(
+                            "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+                            getThing().getUID());
                 }
             }
-        } else {
-            logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
-                    this);
         }
     }
 
     private synchronized void removeSubscription() {
-        logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
-        if (service.isRegistered(this)) {
-            ThingTypeUID thingTypeUID = thing.getThingTypeUID();
-            String subscription = "basicevent1";
-
-            if (subscriptionState.get(subscription) != null) {
-                logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
-                service.removeSubscription(this, subscription);
-            }
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+                    ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+                    String subscription = BASICEVENT;
+
+                    if (subscriptionState.get(subscription) != null) {
+                        logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+                        localService.removeSubscription(this, subscription);
+                    }
 
-            if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
-                subscription = "insight1";
-                if (subscriptionState.get(subscription) != null) {
-                    logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
-                    service.removeSubscription(this, subscription);
+                    if (THING_TYPE_INSIGHT.equals(thingTypeUID)) {
+                        subscription = INSIGHTEVENT;
+                        if (subscriptionState.get(subscription) != null) {
+                            logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+                            localService.removeSubscription(this, subscription);
+                        }
+                    }
+                    subscriptionState = new HashMap<>();
+                    localService.unregisterParticipant(this);
                 }
             }
-            subscriptionState = new HashMap<>();
-            service.unregisterParticipant(this);
-        }
-    }
-
-    private synchronized void onUpdate() {
-        ScheduledFuture<?> job = refreshJob;
-        if (job == null || job.isCancelled()) {
-            Configuration config = getThing().getConfiguration();
-            int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
-            Object refreshConfig = config.get("refresh");
-            if (refreshConfig != null) {
-                refreshInterval = ((BigDecimal) refreshConfig).intValue();
-            }
-            refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
         }
     }
 
     private boolean isUpnpDeviceRegistered() {
-        return service.isRegistered(this);
+        UpnpIOService localService = service;
+        if (localService != null) {
+            return localService.isRegistered(this);
+        }
+        return false;
     }
 
     @Override
@@ -385,46 +420,69 @@ public class WemoHandler extends AbstractWemoHandler implements UpnpIOParticipan
         return (String) this.getThing().getConfiguration().get(UDN);
     }
 
+    public String getHost() {
+        String localHost = host;
+        if (!localHost.isEmpty()) {
+            return localHost;
+        }
+        UpnpIOService localService = service;
+        if (localService != null) {
+            URL descriptorURL = localService.getDescriptorURL(this);
+            if (descriptorURL != null) {
+                return descriptorURL.getHost();
+            }
+        }
+        return "";
+    }
+
     /**
      * The {@link updateWemoState} polls the actual state of a WeMo device and
      * calls {@link onValueReceived} to update the statemap and channels..
      *
      */
     protected void updateWemoState() {
+        String actionService = BASICACTION;
+        String localhost = getHost();
+        if (localhost.isEmpty()) {
+            logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
         String action = "GetBinaryState";
         String variable = "BinaryState";
-        String actionService = "basicevent";
         String value = null;
-
-        if (getThing().getThingTypeUID().getId().equals("insight")) {
+        if ("insight".equals(getThing().getThingTypeUID().getId())) {
             action = "GetInsightParams";
             variable = "InsightParams";
-            actionService = "insight";
+            actionService = INSIGHTACTION;
+        }
+        String wemoURL = getWemoURL(localhost, actionService);
+        if (wemoURL == null) {
+            logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
         }
-
         String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
-        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:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
-                + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+        String content = createStateRequestContent(action, actionService);
         try {
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, actionService);
-
-            if (wemoURL != null) {
-                String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
-                if (wemoCallResponse != null) {
-                    logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
-                    if (variable.equals("InsightParams")) {
-                        value = substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
-                    } else {
-                        value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
-                    }
-                    if (value.length() != 0) {
-                        logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
-                        this.onValueReceived(variable, value, actionService + "1");
-                    }
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null) {
+                if (logger.isTraceEnabled()) {
+                    logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                    logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                    logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                    logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+                }
+                if ("InsightParams".equals(variable)) {
+                    value = substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
+                } else {
+                    value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
+                }
+                if (value.length() != 0) {
+                    logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
+                    this.onValueReceived(variable, value, actionService + "1");
                 }
             }
         } catch (Exception e) {
index 6840fc8e7cd321dc9ff3e8372e7084bddbf10eb0..a97a99f41bf56ac88530069e06b5fcf4372541a1 100644 (file)
@@ -17,7 +17,6 @@ import static org.openhab.binding.wemo.internal.WemoUtil.*;
 
 import java.io.IOException;
 import java.io.StringReader;
-import java.math.BigDecimal;
 import java.net.URL;
 import java.util.Collections;
 import java.util.HashMap;
@@ -49,10 +48,8 @@ import org.openhab.core.types.RefreshType;
 import org.openhab.core.types.State;
 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;
 import org.xml.sax.SAXException;
@@ -72,22 +69,21 @@ public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOPart
 
     private static final int FILTER_LIFE_DAYS = 330;
     private static final int FILTER_LIFE_MINS = FILTER_LIFE_DAYS * 24 * 60;
-    private final Map<String, Boolean> subscriptionState = new HashMap<>();
+
+    private final Object upnpLock = new Object();
+    private final Object jobLock = new Object();
+
     private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
 
-    private UpnpIOService service;
+    private @Nullable UpnpIOService service;
+
     private WemoHttpCall wemoCall;
 
-    private @Nullable ScheduledFuture<?> refreshJob;
+    private String host = "";
 
-    private final Runnable refreshRunnable = () -> {
-        if (!isUpnpDeviceRegistered()) {
-            logger.debug("WeMo UPnP device {} not yet registered", getUDN());
-        } else {
-            updateWemoState();
-            onSubscription();
-        }
-    };
+    private Map<String, Boolean> subscriptionState = new HashMap<>();
+
+    private @Nullable ScheduledFuture<?> pollingJob;
 
     public WemoHolmesHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
         super(thing, wemoHttpCaller);
@@ -102,13 +98,19 @@ public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOPart
     public void initialize() {
         Configuration configuration = getConfig();
 
-        if (configuration.get("udn") != null) {
-            logger.debug("Initializing WemoHolmesHandler for UDN '{}'", configuration.get("udn"));
-            service.registerParticipant(this);
-            onSubscription();
-            onUpdate();
+        if (configuration.get(UDN) != null) {
+            logger.debug("Initializing WemoHolmesHandler for UDN '{}'", configuration.get(UDN));
+            UpnpIOService localService = service;
+            if (localService != null) {
+                localService.registerParticipant(this);
+            }
+            host = getHost();
+            pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
+                    TimeUnit.SECONDS);
             updateStatus(ThingStatus.ONLINE);
         } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/config-status.error.missing-udn");
             logger.debug("Cannot initalize WemoHolmesHandler. UDN not set.");
         }
     }
@@ -117,18 +119,60 @@ public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOPart
     public void dispose() {
         logger.debug("WemoHolmesHandler disposed.");
 
-        ScheduledFuture<?> job = refreshJob;
+        ScheduledFuture<?> job = this.pollingJob;
         if (job != null && !job.isCancelled()) {
             job.cancel(true);
         }
-        refreshJob = null;
+        this.pollingJob = null;
         removeSubscription();
     }
 
+    private void poll() {
+        synchronized (jobLock) {
+            if (pollingJob == null) {
+                return;
+            }
+            try {
+                logger.debug("Polling job");
+                host = getHost();
+                // Check if the Wemo device is set in the UPnP service registry
+                // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
+                if (!isUpnpDeviceRegistered()) {
+                    logger.debug("UPnP device {} not yet registered", getUDN());
+                    updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                            "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
+                    synchronized (upnpLock) {
+                        subscriptionState = new HashMap<>();
+                    }
+                    return;
+                }
+                updateStatus(ThingStatus.ONLINE);
+                updateWemoState();
+                addSubscription();
+            } catch (Exception e) {
+                logger.debug("Exception during poll: {}", e.getMessage(), e);
+            }
+        }
+    }
+
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.trace("Command '{}' received for channel '{}'", command, channelUID);
-
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String wemoURL = getWemoURL(localHost, DEVICEACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         String attribute = null;
         String value = null;
 
@@ -236,12 +280,12 @@ public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOPart
                     + "<attributeList>&lt;attribute&gt;&lt;name&gt;" + attribute + "&lt;/name&gt;&lt;value&gt;" + value
                     + "&lt;/value&gt;&lt;/attribute&gt;</attributeList>" + "</u:SetAttributes>" + "</s:Body>"
                     + "</s:Envelope>";
-
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, "deviceevent");
-
-            if (wemoURL != null) {
-                wemoCall.executeCall(wemoURL, soapHeader, content);
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null && logger.isTraceEnabled()) {
+                logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
             }
         } catch (RuntimeException e) {
             logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
@@ -270,54 +314,55 @@ public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOPart
         }
     }
 
-    private synchronized void onSubscription() {
-        if (service.isRegistered(this)) {
-            logger.debug("Checking WeMo GENA subscription for '{}'", this);
+    private synchronized void addSubscription() {
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
 
-            String subscription = "basicevent1";
+                    String subscription = BASICEVENT;
 
-            if (subscriptionState.get(subscription) == null) {
-                logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
-                service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
-                subscriptionState.put(subscription, true);
+                    if (subscriptionState.get(subscription) == null) {
+                        logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+                                subscription);
+                        localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+                        subscriptionState.put(subscription, true);
+                    }
+                } else {
+                    logger.debug(
+                            "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+                            getThing().getUID());
+                }
             }
-
-        } else {
-            logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
-                    this);
         }
     }
 
     private synchronized void removeSubscription() {
-        logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
-        if (service.isRegistered(this)) {
-            String subscription = "basicevent1";
-
-            if (subscriptionState.get(subscription) != null) {
-                logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
-                service.removeSubscription(this, subscription);
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+                    String subscription = BASICEVENT;
+
+                    if (subscriptionState.get(subscription) != null) {
+                        logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+                        localService.removeSubscription(this, subscription);
+                    }
+                    subscriptionState.remove(subscription);
+                    localService.unregisterParticipant(this);
+                }
             }
-
-            subscriptionState.remove(subscription);
-            service.unregisterParticipant(this);
-        }
-    }
-
-    private synchronized void onUpdate() {
-        ScheduledFuture<?> job = refreshJob;
-        if (job == null || job.isCancelled()) {
-            Configuration config = getThing().getConfiguration();
-            int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
-            Object refreshConfig = config.get("refresh");
-            refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVALL_SECONDS
-                    : ((BigDecimal) refreshConfig).intValue();
-            refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
         }
     }
 
     private boolean isUpnpDeviceRegistered() {
-        return service.isRegistered(this);
+        UpnpIOService localService = service;
+        if (localService != null) {
+            return localService.isRegistered(this);
+        }
+        return false;
     }
 
     @Override
@@ -331,164 +376,77 @@ public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOPart
      *
      */
     protected void updateWemoState() {
-        String action = "GetAttributes";
-        String actionService = "deviceevent";
-
-        String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
-        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:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
-                + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String actionService = DEVICEACTION;
+        String wemoURL = getWemoURL(localHost, actionService);
+        if (wemoURL == null) {
+            logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         try {
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, actionService);
-
-            if (wemoURL != null) {
-                String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
-                if (wemoCallResponse != null) {
-                    logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
-
-                    String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
-
-                    // Due to Belkins bad response formatting, we need to run this twice.
-                    stringParser = unescapeXml(stringParser);
-                    stringParser = unescapeXml(stringParser);
-
-                    logger.trace("AirPurifier response '{}' for device '{}' received", stringParser,
-                            getThing().getUID());
+            String action = "GetAttributes";
+            String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
+            String content = createStateRequestContent(action, actionService);
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null) {
+                if (logger.isTraceEnabled()) {
+                    logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                    logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                    logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                    logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+                }
 
-                    stringParser = "<data>" + stringParser + "</data>";
-
-                    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("attribute");
-
-                    // iterate the attributes
-                    for (int i = 0; i < nodes.getLength(); i++) {
-                        Element element = (Element) nodes.item(i);
-
-                        NodeList deviceIndex = element.getElementsByTagName("name");
-                        Element line = (Element) deviceIndex.item(0);
-                        String attributeName = getCharacterDataFromElement(line);
-                        logger.trace("attributeName: {}", attributeName);
-
-                        NodeList deviceID = element.getElementsByTagName("value");
-                        line = (Element) deviceID.item(0);
-                        String attributeValue = getCharacterDataFromElement(line);
-                        logger.trace("attributeValue: {}", attributeValue);
-
-                        State newMode = new StringType();
-                        switch (attributeName) {
-                            case "Mode":
-                                if ("purifier".equals(getThing().getThingTypeUID().getId())) {
-                                    switch (attributeValue) {
-                                        case "0":
-                                            newMode = new StringType("OFF");
-                                            break;
-                                        case "1":
-                                            newMode = new StringType("LOW");
-                                            break;
-                                        case "2":
-                                            newMode = new StringType("MED");
-                                            break;
-                                        case "3":
-                                            newMode = new StringType("HIGH");
-                                            break;
-                                        case "4":
-                                            newMode = new StringType("AUTO");
-                                            break;
-                                    }
-                                    updateState(CHANNEL_PURIFIERMODE, newMode);
-                                } else {
-                                    switch (attributeValue) {
-                                        case "0":
-                                            newMode = new StringType("OFF");
-                                            break;
-                                        case "1":
-                                            newMode = new StringType("FROSTPROTECT");
-                                            break;
-                                        case "2":
-                                            newMode = new StringType("HIGH");
-                                            break;
-                                        case "3":
-                                            newMode = new StringType("LOW");
-                                            break;
-                                        case "4":
-                                            newMode = new StringType("ECO");
-                                            break;
-                                    }
-                                    updateState(CHANNEL_HEATERMODE, newMode);
-                                }
-                                break;
-                            case "Ionizer":
-                                switch (attributeValue) {
-                                    case "0":
-                                        newMode = OnOffType.OFF;
-                                        break;
-                                    case "1":
-                                        newMode = OnOffType.ON;
-                                        break;
-                                }
-                                updateState(CHANNEL_IONIZER, newMode);
-                                break;
-                            case "AirQuality":
-                                switch (attributeValue) {
-                                    case "0":
-                                        newMode = new StringType("POOR");
-                                        break;
-                                    case "1":
-                                        newMode = new StringType("MODERATE");
-                                        break;
-                                    case "2":
-                                        newMode = new StringType("GOOD");
-                                        break;
-                                }
-                                updateState(CHANNEL_AIRQUALITY, newMode);
-                                break;
-                            case "FilterLife":
-                                int filterLife = Integer.valueOf(attributeValue);
-                                if ("purifier".equals(getThing().getThingTypeUID().getId())) {
-                                    filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
-                                } else {
-                                    filterLife = Math.round((filterLife / 60480) * 100);
-                                }
-                                updateState(CHANNEL_FILTERLIFE, new PercentType(String.valueOf(filterLife)));
-                                break;
-                            case "ExpiredFilterTime":
-                                switch (attributeValue) {
-                                    case "0":
-                                        newMode = OnOffType.OFF;
-                                        break;
-                                    case "1":
-                                        newMode = OnOffType.ON;
-                                        break;
-                                }
-                                updateState(CHANNEL_EXPIREDFILTERTIME, newMode);
-                                break;
-                            case "FilterPresent":
-                                switch (attributeValue) {
-                                    case "0":
-                                        newMode = OnOffType.OFF;
-                                        break;
-                                    case "1":
-                                        newMode = OnOffType.ON;
-                                        break;
-                                }
-                                updateState(CHANNEL_FILTERPRESENT, newMode);
-                                break;
-                            case "FANMode":
+                String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
+
+                // Due to Belkins bad response formatting, we need to run this twice.
+                stringParser = unescapeXml(stringParser);
+                stringParser = unescapeXml(stringParser);
+
+                logger.trace("AirPurifier response '{}' for device '{}' received", stringParser, getThing().getUID());
+
+                stringParser = "<data>" + stringParser + "</data>";
+
+                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("attribute");
+
+                // iterate the attributes
+                for (int i = 0; i < nodes.getLength(); i++) {
+                    Element element = (Element) nodes.item(i);
+
+                    NodeList deviceIndex = element.getElementsByTagName("name");
+                    Element line = (Element) deviceIndex.item(0);
+                    String attributeName = getCharacterDataFromElement(line);
+                    logger.trace("attributeName: {}", attributeName);
+
+                    NodeList deviceID = element.getElementsByTagName("value");
+                    line = (Element) deviceID.item(0);
+                    String attributeValue = getCharacterDataFromElement(line);
+                    logger.trace("attributeValue: {}", attributeValue);
+
+                    State newMode = new StringType();
+                    switch (attributeName) {
+                        case "Mode":
+                            if ("purifier".equals(getThing().getThingTypeUID().getId())) {
                                 switch (attributeValue) {
                                     case "0":
                                         newMode = new StringType("OFF");
@@ -507,48 +465,143 @@ public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOPart
                                         break;
                                 }
                                 updateState(CHANNEL_PURIFIERMODE, newMode);
-                                break;
-                            case "DesiredHumidity":
+                            } else {
                                 switch (attributeValue) {
                                     case "0":
-                                        newMode = new PercentType("45");
+                                        newMode = new StringType("OFF");
                                         break;
                                     case "1":
-                                        newMode = new PercentType("50");
+                                        newMode = new StringType("FROSTPROTECT");
                                         break;
                                     case "2":
-                                        newMode = new PercentType("55");
+                                        newMode = new StringType("HIGH");
                                         break;
                                     case "3":
-                                        newMode = new PercentType("60");
+                                        newMode = new StringType("LOW");
                                         break;
                                     case "4":
-                                        newMode = new PercentType("100");
+                                        newMode = new StringType("ECO");
                                         break;
                                 }
-                                updateState(CHANNEL_DESIREDHUMIDITY, newMode);
-                                break;
-                            case "CurrentHumidity":
-                                newMode = new StringType(attributeValue);
-                                updateState(CHANNEL_CURRENTHUMIDITY, newMode);
-                                break;
-                            case "Temperature":
-                                newMode = new StringType(attributeValue);
-                                updateState(CHANNEL_CURRENTTEMP, newMode);
-                                break;
-                            case "SetTemperature":
-                                newMode = new StringType(attributeValue);
-                                updateState(CHANNEL_TARGETTEMP, newMode);
-                                break;
-                            case "AutoOffTime":
-                                newMode = new StringType(attributeValue);
-                                updateState(CHANNEL_AUTOOFFTIME, newMode);
-                                break;
-                            case "TimeRemaining":
-                                newMode = new StringType(attributeValue);
-                                updateState(CHANNEL_HEATINGREMAINING, newMode);
-                                break;
-                        }
+                                updateState(CHANNEL_HEATERMODE, newMode);
+                            }
+                            break;
+                        case "Ionizer":
+                            switch (attributeValue) {
+                                case "0":
+                                    newMode = OnOffType.OFF;
+                                    break;
+                                case "1":
+                                    newMode = OnOffType.ON;
+                                    break;
+                            }
+                            updateState(CHANNEL_IONIZER, newMode);
+                            break;
+                        case "AirQuality":
+                            switch (attributeValue) {
+                                case "0":
+                                    newMode = new StringType("POOR");
+                                    break;
+                                case "1":
+                                    newMode = new StringType("MODERATE");
+                                    break;
+                                case "2":
+                                    newMode = new StringType("GOOD");
+                                    break;
+                            }
+                            updateState(CHANNEL_AIRQUALITY, newMode);
+                            break;
+                        case "FilterLife":
+                            int filterLife = Integer.valueOf(attributeValue);
+                            if ("purifier".equals(getThing().getThingTypeUID().getId())) {
+                                filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
+                            } else {
+                                filterLife = Math.round((filterLife / 60480) * 100);
+                            }
+                            updateState(CHANNEL_FILTERLIFE, new PercentType(String.valueOf(filterLife)));
+                            break;
+                        case "ExpiredFilterTime":
+                            switch (attributeValue) {
+                                case "0":
+                                    newMode = OnOffType.OFF;
+                                    break;
+                                case "1":
+                                    newMode = OnOffType.ON;
+                                    break;
+                            }
+                            updateState(CHANNEL_EXPIREDFILTERTIME, newMode);
+                            break;
+                        case "FilterPresent":
+                            switch (attributeValue) {
+                                case "0":
+                                    newMode = OnOffType.OFF;
+                                    break;
+                                case "1":
+                                    newMode = OnOffType.ON;
+                                    break;
+                            }
+                            updateState(CHANNEL_FILTERPRESENT, newMode);
+                            break;
+                        case "FANMode":
+                            switch (attributeValue) {
+                                case "0":
+                                    newMode = new StringType("OFF");
+                                    break;
+                                case "1":
+                                    newMode = new StringType("LOW");
+                                    break;
+                                case "2":
+                                    newMode = new StringType("MED");
+                                    break;
+                                case "3":
+                                    newMode = new StringType("HIGH");
+                                    break;
+                                case "4":
+                                    newMode = new StringType("AUTO");
+                                    break;
+                            }
+                            updateState(CHANNEL_PURIFIERMODE, newMode);
+                            break;
+                        case "DesiredHumidity":
+                            switch (attributeValue) {
+                                case "0":
+                                    newMode = new PercentType("45");
+                                    break;
+                                case "1":
+                                    newMode = new PercentType("50");
+                                    break;
+                                case "2":
+                                    newMode = new PercentType("55");
+                                    break;
+                                case "3":
+                                    newMode = new PercentType("60");
+                                    break;
+                                case "4":
+                                    newMode = new PercentType("100");
+                                    break;
+                            }
+                            updateState(CHANNEL_DESIREDHUMIDITY, newMode);
+                            break;
+                        case "CurrentHumidity":
+                            newMode = new StringType(attributeValue);
+                            updateState(CHANNEL_CURRENTHUMIDITY, newMode);
+                            break;
+                        case "Temperature":
+                            newMode = new StringType(attributeValue);
+                            updateState(CHANNEL_CURRENTTEMP, newMode);
+                            break;
+                        case "SetTemperature":
+                            newMode = new StringType(attributeValue);
+                            updateState(CHANNEL_TARGETTEMP, newMode);
+                            break;
+                        case "AutoOffTime":
+                            newMode = new StringType(attributeValue);
+                            updateState(CHANNEL_AUTOOFFTIME, newMode);
+                            break;
+                        case "TimeRemaining":
+                            newMode = new StringType(attributeValue);
+                            updateState(CHANNEL_HEATINGREMAINING, newMode);
+                            break;
                     }
                 }
             }
@@ -559,13 +612,19 @@ public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOPart
         updateStatus(ThingStatus.ONLINE);
     }
 
-    public static String getCharacterDataFromElement(Element e) {
-        Node child = e.getFirstChild();
-        if (child instanceof CharacterData) {
-            CharacterData cd = (CharacterData) child;
-            return cd.getData();
+    public String getHost() {
+        String localHost = host;
+        if (!localHost.isEmpty()) {
+            return localHost;
+        }
+        UpnpIOService localService = service;
+        if (localService != null) {
+            URL descriptorURL = localService.getDescriptorURL(this);
+            if (descriptorURL != null) {
+                return descriptorURL.getHost();
+            }
         }
-        return "?";
+        return "";
     }
 
     @Override
index af8d43bc916d6afbb50fa82c934217c143f865e0..1aad41ee31cf7dfd4583640bab109d045e9e9a13 100644 (file)
@@ -15,7 +15,6 @@ package org.openhab.binding.wemo.internal.handler;
 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
 import static org.openhab.binding.wemo.internal.WemoUtil.*;
 
-import java.math.BigDecimal;
 import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
@@ -57,15 +56,21 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
 
     private Map<String, Boolean> subscriptionState = new HashMap<>();
 
-    private UpnpIOService service;
-    private WemoHttpCall wemoCall;
+    private final Object upnpLock = new Object();
+    private final Object jobLock = new Object();
 
     private @Nullable WemoBridgeHandler wemoBridgeHandler;
 
+    private @Nullable UpnpIOService service;
+
+    private String host = "";
+
     private @Nullable String wemoLightID;
 
     private int currentBrightness;
 
+    private WemoHttpCall wemoCall;
+
     /**
      * Set dimming stepsize to 5%
      */
@@ -78,31 +83,15 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
      */
     private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
 
-    private @Nullable ScheduledFuture<?> refreshJob;
-
-    private final Runnable refreshRunnable = new Runnable() {
-
-        @Override
-        public void run() {
-            try {
-                if (!isUpnpDeviceRegistered()) {
-                    logger.debug("WeMo UPnP device {} not yet registered", getUDN());
-                }
-
-                getDeviceState();
-                onSubscription();
-            } catch (Exception e) {
-                logger.debug("Exception during poll", e);
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-            }
-        }
-    };
+    private @Nullable ScheduledFuture<?> pollingJob;
 
     public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
         super(thing, wemoHttpcaller);
 
         this.service = upnpIOService;
         this.wemoCall = wemoHttpcaller;
+
+        logger.debug("Creating a WemoLightHandler for thing '{}'", getThing().getUID());
     }
 
     @Override
@@ -112,9 +101,14 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
 
         final Bridge bridge = getBridge();
         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                localService.registerParticipant(this);
+            }
+            host = getHost();
+            pollingJob = scheduler.scheduleWithFixedDelay(this::poll, DEFAULT_REFRESH_INITIAL_DELAY,
+                    DEFAULT_REFRESH_INTERVALL_SECONDS, TimeUnit.SECONDS);
             updateStatus(ThingStatus.ONLINE);
-            onSubscription();
-            onUpdate();
         } else {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
         }
@@ -124,15 +118,13 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
         if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
             updateStatus(ThingStatus.ONLINE);
-            onSubscription();
-            onUpdate();
         } else {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
-            ScheduledFuture<?> job = refreshJob;
+            ScheduledFuture<?> job = this.pollingJob;
             if (job != null && !job.isCancelled()) {
                 job.cancel(true);
             }
-            refreshJob = null;
+            this.pollingJob = null;
         }
     }
 
@@ -140,11 +132,11 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
     public void dispose() {
         logger.debug("WeMoLightHandler disposed.");
 
-        ScheduledFuture<?> job = refreshJob;
+        ScheduledFuture<?> job = this.pollingJob;
         if (job != null && !job.isCancelled()) {
             job.cancel(true);
         }
-        refreshJob = null;
+        this.pollingJob = null;
         removeSubscription();
     }
 
@@ -164,8 +156,52 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
         return this.wemoBridgeHandler;
     }
 
+    private void poll() {
+        synchronized (jobLock) {
+            if (pollingJob == null) {
+                return;
+            }
+            try {
+                logger.debug("Polling job");
+                host = getHost();
+                // Check if the Wemo device is set in the UPnP service registry
+                // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
+                if (!isUpnpDeviceRegistered()) {
+                    logger.debug("UPnP device {} not yet registered", getUDN());
+                    updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                            "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
+                    synchronized (upnpLock) {
+                        subscriptionState = new HashMap<>();
+                    }
+                    return;
+                }
+                updateStatus(ThingStatus.ONLINE);
+                getDeviceState();
+                addSubscription();
+            } catch (Exception e) {
+                logger.debug("Exception during poll: {}", e.getMessage(), e);
+            }
+        }
+    }
+
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String wemoURL = getWemoURL(localHost, BASICACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         if (command instanceof RefreshType) {
             try {
                 getDeviceState();
@@ -239,27 +275,32 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
                     break;
             }
             try {
-                String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
-                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:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
-                        + "<DeviceStatusList>"
-                        + "&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;DeviceStatus&gt;&lt;DeviceID&gt;"
-                        + wemoLightID
-                        + "&lt;/DeviceID&gt;&lt;IsGroupAction&gt;NO&lt;/IsGroupAction&gt;&lt;CapabilityID&gt;"
-                        + capability + "&lt;/CapabilityID&gt;&lt;CapabilityValue&gt;" + value
-                        + "&lt;/CapabilityValue&gt;&lt;/DeviceStatus&gt;" + "</DeviceStatusList>"
-                        + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
-
-                URL descriptorURL = service.getDescriptorURL(this);
-                String wemoURL = getWemoURL(descriptorURL, "bridge");
-
-                if (wemoURL != null && capability != null && value != null) {
+                if (capability != null && value != null) {
+                    String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
+                    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:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
+                            + "<DeviceStatusList>"
+                            + "&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;DeviceStatus&gt;&lt;DeviceID&gt;"
+                            + wemoLightID
+                            + "&lt;/DeviceID&gt;&lt;IsGroupAction&gt;NO&lt;/IsGroupAction&gt;&lt;CapabilityID&gt;"
+                            + capability + "&lt;/CapabilityID&gt;&lt;CapabilityValue&gt;" + value
+                            + "&lt;/CapabilityValue&gt;&lt;/DeviceStatus&gt;" + "</DeviceStatusList>"
+                            + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
+
                     String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
                     if (wemoCallResponse != null) {
-                        if (capability.equals("10008")) {
+                        if (logger.isTraceEnabled()) {
+                            logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                            logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader,
+                                    getThing().getUID());
+                            logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                            logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
+                                    getThing().getUID());
+                        }
+                        if ("10008".equals(capability)) {
                             OnOffType binaryState = null;
-                            binaryState = value.equals("0") ? OnOffType.OFF : OnOffType.ON;
+                            binaryState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
                             updateState(CHANNEL_STATE, binaryState);
                         }
                     }
@@ -285,7 +326,21 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
      * channel states.
      */
     public void getDeviceState() {
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
         logger.debug("Request actual state for LightID '{}'", wemoLightID);
+        String wemoURL = getWemoURL(localHost, BRIDGEACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         try {
             String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
             String content = "<?xml version=\"1.0\"?>"
@@ -293,31 +348,32 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
                     + "<s:Body>" + "<u:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>"
                     + wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
 
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, "bridge");
-
-            if (wemoURL != null) {
-                String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
-                if (wemoCallResponse != null) {
-                    wemoCallResponse = unescapeXml(wemoCallResponse);
-                    String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
-                    logger.trace("wemoNewLightState = {}", response);
-                    String[] splitResponse = response.split(",");
-                    if (splitResponse[0] != null) {
-                        OnOffType binaryState = null;
-                        binaryState = splitResponse[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
-                        updateState(CHANNEL_STATE, binaryState);
-                    }
-                    if (splitResponse[1] != null) {
-                        String splitBrightness[] = splitResponse[1].split(":");
-                        if (splitBrightness[0] != null) {
-                            int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
-                            int newBrightness = Math.round(newBrightnessValue * 100 / 255);
-                            logger.trace("newBrightness = {}", newBrightness);
-                            State newBrightnessState = new PercentType(newBrightness);
-                            updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
-                            currentBrightness = newBrightness;
-                        }
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null) {
+                if (logger.isTraceEnabled()) {
+                    logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                    logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                    logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                    logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+                }
+                wemoCallResponse = unescapeXml(wemoCallResponse);
+                String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
+                logger.trace("wemoNewLightState = {}", response);
+                String[] splitResponse = response.split(",");
+                if (splitResponse[0] != null) {
+                    OnOffType binaryState = null;
+                    binaryState = "0".equals(splitResponse[0]) ? OnOffType.OFF : OnOffType.ON;
+                    updateState(CHANNEL_STATE, binaryState);
+                }
+                if (splitResponse[1] != null) {
+                    String splitBrightness[] = splitResponse[1].split(":");
+                    if (splitBrightness[0] != null) {
+                        int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
+                        int newBrightness = Math.round(newBrightnessValue * 100 / 255);
+                        logger.trace("newBrightness = {}", newBrightness);
+                        State newBrightnessState = new PercentType(newBrightness);
+                        updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
+                        currentBrightness = newBrightness;
                     }
                 }
             }
@@ -339,7 +395,7 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
         switch (capabilityId) {
             case "10006":
                 OnOffType binaryState = null;
-                binaryState = newValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
+                binaryState = "0".equals(newValue) ? OnOffType.OFF : OnOffType.ON;
                 updateState(CHANNEL_STATE, binaryState);
                 break;
             case "10008":
@@ -359,51 +415,66 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti
     public void onStatusChanged(boolean status) {
     }
 
-    private synchronized void onSubscription() {
-        if (service.isRegistered(this)) {
-            logger.debug("Checking WeMo GENA subscription for '{}'", this);
-
-            if (subscriptionState.get(SUBSCRIPTION) == null) {
-                logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), SUBSCRIPTION);
-                service.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION_SECONDS);
-                subscriptionState.put(SUBSCRIPTION, true);
+    private synchronized void addSubscription() {
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
+
+                    if (subscriptionState.get(SUBSCRIPTION) == null) {
+                        logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+                                SUBSCRIPTION);
+                        localService.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION_SECONDS);
+                        subscriptionState.put(SUBSCRIPTION, true);
+                    }
+                } else {
+                    logger.debug(
+                            "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+                            getThing().getUID());
+                }
             }
-        } else {
-            logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
-                    this);
         }
     }
 
     private synchronized void removeSubscription() {
-        if (service.isRegistered(this)) {
-            logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
-            if (subscriptionState.get(SUBSCRIPTION) != null) {
-                logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
-                service.removeSubscription(this, SUBSCRIPTION);
+        synchronized (upnpLock) {
+            UpnpIOService localService = service;
+            if (localService != null) {
+                if (localService.isRegistered(this)) {
+                    logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+
+                    if (subscriptionState.get(SUBSCRIPTION) != null) {
+                        logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
+                        localService.removeSubscription(this, SUBSCRIPTION);
+                    }
+                    subscriptionState = new HashMap<>();
+                    localService.unregisterParticipant(this);
+                }
             }
-
-            subscriptionState = new HashMap<>();
-            service.unregisterParticipant(this);
         }
     }
 
-    private synchronized void onUpdate() {
-        ScheduledFuture<?> job = refreshJob;
-        if (job == null || job.isCancelled()) {
-            Configuration config = getThing().getConfiguration();
-            int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
-            Object refreshConfig = config.get("refresh");
-            if (refreshConfig != null) {
-                refreshInterval = ((BigDecimal) refreshConfig).intValue();
-            }
-            logger.trace("Start polling job for LightID '{}'", wemoLightID);
-            refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, DEFAULT_REFRESH_INITIAL_DELAY,
-                    refreshInterval, TimeUnit.SECONDS);
+    private boolean isUpnpDeviceRegistered() {
+        UpnpIOService localService = service;
+        if (localService != null) {
+            return localService.isRegistered(this);
         }
+        return false;
     }
 
-    private boolean isUpnpDeviceRegistered() {
-        return service.isRegistered(this);
+    public String getHost() {
+        String localHost = host;
+        if (!localHost.isEmpty()) {
+            return localHost;
+        }
+        UpnpIOService localService = service;
+        if (localService != null) {
+            URL descriptorURL = localService.getDescriptorURL(this);
+            if (descriptorURL != null) {
+                return descriptorURL.getHost();
+            }
+        }
+        return "";
     }
 }
index eb60cbcec47ac7ec317d263a0dda91083a3e959b..5586bfe43f4c5554a6127777f8f3ed9872980e82 100644 (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.math.BigDecimal;
 import java.net.URL;
 import java.util.Collections;
 import java.util.Set;
@@ -43,10 +42,8 @@ import org.openhab.core.types.RefreshType;
 import org.openhab.core.types.State;
 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;
 
@@ -63,23 +60,15 @@ public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParti
 
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
 
-    private UpnpIOService service;
-    private WemoHttpCall wemoCall;
+    private final Object jobLock = new Object();
 
-    private @Nullable ScheduledFuture<?> refreshJob;
+    private @Nullable UpnpIOService service;
 
-    private final Runnable refreshRunnable = new Runnable() {
+    private WemoHttpCall wemoCall;
 
-        @Override
-        public void run() {
-            try {
-                updateWemoState();
-            } catch (Exception e) {
-                logger.debug("Exception during poll", e);
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-            }
-        }
-    };
+    private String host = "";
+
+    private @Nullable ScheduledFuture<?> pollingJob;
 
     public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
         super(thing, wemoHttpcaller);
@@ -94,11 +83,19 @@ public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParti
     public void initialize() {
         Configuration configuration = getConfig();
 
-        if (configuration.get("udn") != null) {
-            logger.debug("Initializing WemoMakerHandler for UDN '{}'", configuration.get("udn"));
-            onUpdate();
+        if (configuration.get(UDN) != null) {
+            logger.debug("Initializing WemoMakerHandler for UDN '{}'", configuration.get(UDN));
+            UpnpIOService localService = service;
+            if (localService != null) {
+                localService.registerParticipant(this);
+            }
+            host = getHost();
+            pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
+                    TimeUnit.SECONDS);
             updateStatus(ThingStatus.ONLINE);
         } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/config-status.error.missing-udn");
             logger.debug("Cannot initalize WemoMakerHandler. UDN not set.");
         }
     }
@@ -107,17 +104,59 @@ public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParti
     public void dispose() {
         logger.debug("WeMoMakerHandler disposed.");
 
-        ScheduledFuture<?> job = refreshJob;
+        ScheduledFuture<?> job = this.pollingJob;
         if (job != null && !job.isCancelled()) {
             job.cancel(true);
         }
-        refreshJob = null;
+        this.pollingJob = null;
+        UpnpIOService localService = service;
+        if (localService != null) {
+            localService.unregisterParticipant(this);
+        }
+    }
+
+    private void poll() {
+        synchronized (jobLock) {
+            if (pollingJob == null) {
+                return;
+            }
+            try {
+                logger.debug("Polling job");
+                host = getHost();
+                // Check if the Wemo device is set in the UPnP service registry
+                // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
+                if (!isUpnpDeviceRegistered()) {
+                    logger.debug("UPnP device {} not yet registered", getUDN());
+                    updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                            "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
+                    return;
+                }
+                updateStatus(ThingStatus.ONLINE);
+                updateWemoState();
+            } catch (Exception e) {
+                logger.debug("Exception during poll: {}", e.getMessage(), e);
+            }
+        }
     }
 
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.trace("Command '{}' received for channel '{}'", command, channelUID);
-
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String wemoURL = getWemoURL(localHost, BASICACTION);
+        if (wemoURL == null) {
+            logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
+                    getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         if (command instanceof RefreshType) {
             try {
                 updateWemoState();
@@ -127,27 +166,16 @@ public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParti
         } else if (channelUID.getId().equals(CHANNEL_RELAY)) {
             if (command instanceof OnOffType) {
                 try {
-                    String binaryState = null;
-
-                    if (command.equals(OnOffType.ON)) {
-                        binaryState = "1";
-                    } else if (command.equals(OnOffType.OFF)) {
-                        binaryState = "0";
-                    }
-
+                    boolean binaryState = OnOffType.ON.equals(command) ? true : false;
                     String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
-
-                    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:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">"
-                            + "<BinaryState>" + binaryState + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>"
-                            + "</s:Envelope>";
-
-                    URL descriptorURL = service.getDescriptorURL(this);
-                    String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
-                    if (wemoURL != null) {
-                        wemoCall.executeCall(wemoURL, soapHeader, content);
+                    String content = createBinaryStateContent(binaryState);
+                    String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+                    if (wemoCallResponse != null && logger.isTraceEnabled()) {
+                        logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                        logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                        logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                        logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
+                                getThing().getUID());
                     }
                 } catch (Exception e) {
                     logger.error("Failed to send command '{}' for device '{}' ", command, getThing().getUID(), e);
@@ -156,25 +184,12 @@ public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParti
         }
     }
 
-    @SuppressWarnings("unused")
-    private synchronized void onSubscription() {
-    }
-
-    @SuppressWarnings("unused")
-    private synchronized void removeSubscription() {
-    }
-
-    private synchronized void onUpdate() {
-        ScheduledFuture<?> job = refreshJob;
-        if (job == null || job.isCancelled()) {
-            Configuration config = getThing().getConfiguration();
-            int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
-            Object refreshConfig = config.get("refresh");
-            if (refreshConfig != null) {
-                refreshInterval = ((BigDecimal) refreshConfig).intValue();
-            }
-            refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
+    private boolean isUpnpDeviceRegistered() {
+        UpnpIOService localService = service;
+        if (localService != null) {
+            return localService.isRegistered(this);
         }
+        return false;
     }
 
     @Override
@@ -186,81 +201,91 @@ public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParti
      * The {@link updateWemoState} polls the actual state of a WeMo Maker.
      */
     protected void updateWemoState() {
-        String action = "GetAttributes";
-        String actionService = "deviceevent";
-
-        String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
-        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:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
-                + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+        String localHost = getHost();
+        if (localHost.isEmpty()) {
+            logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-ip");
+            return;
+        }
+        String actionService = DEVICEACTION;
+        String wemoURL = getWemoURL(localHost, actionService);
+        if (wemoURL == null) {
+            logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "@text/config-status.error.missing-url");
+            return;
+        }
         try {
-            URL descriptorURL = service.getDescriptorURL(this);
-            String wemoURL = getWemoURL(descriptorURL, actionService);
-
-            if (wemoURL != null) {
-                String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
-                if (wemoCallResponse != null) {
-                    try {
-                        String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
-                        logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
-                        logger.trace("'{}'", stringParser);
-
-                        // Due to Belkins bad response formatting, we need to run this twice.
-                        stringParser = unescapeXml(stringParser);
-                        stringParser = unescapeXml(stringParser);
-                        logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID());
-
-                        stringParser = "<data>" + stringParser + "</data>";
-
-                        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("attribute");
-
-                        // iterate the attributes
-                        for (int i = 0; i < nodes.getLength(); i++) {
-                            Element element = (Element) nodes.item(i);
-
-                            NodeList deviceIndex = element.getElementsByTagName("name");
-                            Element line = (Element) deviceIndex.item(0);
-                            String attributeName = getCharacterDataFromElement(line);
-                            logger.trace("attributeName: {}", attributeName);
-
-                            NodeList deviceID = element.getElementsByTagName("value");
-                            line = (Element) deviceID.item(0);
-                            String attributeValue = getCharacterDataFromElement(line);
-                            logger.trace("attributeValue: {}", attributeValue);
-
-                            switch (attributeName) {
-                                case "Switch":
-                                    State relayState = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
-                                    logger.debug("New relayState '{}' for device '{}' received", relayState,
-                                            getThing().getUID());
-                                    updateState(CHANNEL_RELAY, relayState);
-                                    break;
-                                case "Sensor":
-                                    State sensorState = attributeValue.equals("1") ? OnOffType.OFF : OnOffType.ON;
-                                    logger.debug("New sensorState '{}' for device '{}' received", sensorState,
-                                            getThing().getUID());
-                                    updateState(CHANNEL_SENSOR, sensorState);
-                                    break;
-                            }
+            String action = "GetAttributes";
+            String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
+            String content = createStateRequestContent(action, actionService);
+            String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+            if (wemoCallResponse != null) {
+                if (logger.isTraceEnabled()) {
+                    logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+                    logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+                    logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+                    logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+                }
+                try {
+                    String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
+                    logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
+                    logger.trace("'{}'", stringParser);
+
+                    // Due to Belkins bad response formatting, we need to run this twice.
+                    stringParser = unescapeXml(stringParser);
+                    stringParser = unescapeXml(stringParser);
+                    logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID());
+
+                    stringParser = "<data>" + stringParser + "</data>";
+
+                    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("attribute");
+
+                    // iterate the attributes
+                    for (int i = 0; i < nodes.getLength(); i++) {
+                        Element element = (Element) nodes.item(i);
+
+                        NodeList deviceIndex = element.getElementsByTagName("name");
+                        Element line = (Element) deviceIndex.item(0);
+                        String attributeName = getCharacterDataFromElement(line);
+                        logger.trace("attributeName: {}", attributeName);
+
+                        NodeList deviceID = element.getElementsByTagName("value");
+                        line = (Element) deviceID.item(0);
+                        String attributeValue = getCharacterDataFromElement(line);
+                        logger.trace("attributeValue: {}", attributeValue);
+
+                        switch (attributeName) {
+                            case "Switch":
+                                State relayState = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
+                                logger.debug("New relayState '{}' for device '{}' received", relayState,
+                                        getThing().getUID());
+                                updateState(CHANNEL_RELAY, relayState);
+                                break;
+                            case "Sensor":
+                                State sensorState = "1".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
+                                logger.debug("New sensorState '{}' for device '{}' received", sensorState,
+                                        getThing().getUID());
+                                updateState(CHANNEL_SENSOR, sensorState);
+                                break;
                         }
-                    } catch (Exception e) {
-                        logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
                     }
+                } catch (Exception e) {
+                    logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
                 }
             }
         } catch (Exception e) {
@@ -268,13 +293,19 @@ public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParti
         }
     }
 
-    public static String getCharacterDataFromElement(Element e) {
-        Node child = e.getFirstChild();
-        if (child instanceof CharacterData) {
-            CharacterData cd = (CharacterData) child;
-            return cd.getData();
+    public String getHost() {
+        String localHost = host;
+        if (!localHost.isEmpty()) {
+            return localHost;
+        }
+        UpnpIOService localService = service;
+        if (localService != null) {
+            URL descriptorURL = localService.getDescriptorURL(this);
+            if (descriptorURL != null) {
+                return descriptorURL.getHost();
+            }
         }
-        return "?";
+        return "";
     }
 
     @Override
index 92b9c22c56e1d73b0c905dc9b2bfe8cc289f1b51..62e0ef550626b8ed630a03215cea695f06c897fa 100644 (file)
@@ -124,3 +124,9 @@ channel-type.wemo.timespan.label = Usage Timespan (s)
 channel-type.wemo.timespan.description = Time used to measure average usage
 channel-type.wemo.waterLevelReached.label = WaterLevelReached
 channel-type.wemo.waterLevelReached.description = Indicates if the WeMo Coffee Maker needs to be refilled
+
+# Config status messages
+config-status.pending.device-not-registered = UPnP device is not registered yet.
+config-status.error.missing-udn = UDN of the WeMo device is missing.
+config-status.error.missing-ip = IP address of the WeMo device is missing.
+config-status.error.missing-url = URL for the WeMo device cannot be created.
index f60e7a507a1741260dce394c5899eec08444e4ca..91e749948123c059fb3e5e933e449f1d6f4203a2 100644 (file)
@@ -17,7 +17,6 @@
                                <label>Unique Device Name</label>
                                <description>The UDN identifies the WeMo Device</description>
                        </parameter>
-
                </config-description>
        </thing-type>
 
                                <label>Unique Device Name</label>
                                <description>The UDN identifies the WeMo Device</description>
                        </parameter>
+               </config-description>
+       </thing-type>
 
+       <thing-type id="Crockpot">
+               <label>Crock-Pot Slow Cooker</label>
+               <description>Crock-Pot Smart Slow Cooker with WeMo</description>
+
+               <channels>
+                       <channel id="cookMode" typeId="cookMode"/>
+                       <channel id="warmCookTime" typeId="warmCookTime"/>
+                       <channel id="lowCookTime" typeId="lowCookTime"/>
+                       <channel id="highCookTime" typeId="highCookTime"/>
+                       <channel id="cookedTime" typeId="cookedTime"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="udn" type="text">
+                               <label>Unique Device Name</label>
+                               <description>The UDN identifies the WeMo Device</description>
+                               <required>true</required>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="Purifier">
+               <label>Holmes Air Purifier</label>
+               <description>Holmes Smart Air Purifier with WeMo</description>
+
+               <channels>
+                       <channel id="purifierMode" typeId="purifierMode"/>
+                       <channel id="airQuality" typeId="airQuality"/>
+                       <channel id="ionizer" typeId="ionizer"/>
+                       <channel id="filterLife" typeId="filterLife"/>
+                       <channel id="expiredFilterTime" typeId="expiredFilterTime"/>
+                       <channel id="filterPresent" typeId="filterPresent"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="udn" type="text">
+                               <label>Unique Device Name</label>
+                               <description>The UDN identifies the WeMo Device</description>
+                               <required>true</required>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="Humidifier">
+               <label>Holmes Humidifier</label>
+               <description>Holmes Smart Humidifier with WeMo</description>
+
+               <channels>
+                       <channel id="humidifierMode" typeId="humidifierMode"/>
+                       <channel id="desiredHumidity" typeId="desiredHumidity"/>
+                       <channel id="currentHumidity" typeId="currentHumidity"/>
+                       <channel id="waterLEvel" typeId="waterLEvel"/>
+                       <channel id="filterLife" typeId="filterLife"/>
+                       <channel id="expiredFilterTime" typeId="expiredFilterTime"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="udn" type="text">
+                               <label>Unique Device Name</label>
+                               <description>The UDN identifies the WeMo Device</description>
+                               <required>true</required>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="Heater">
+               <label>Holmes Heater</label>
+               <description>Holmes Smart Heater with WeMo</description>
+
+               <channels>
+                       <channel id="heaterMode" typeId="heaterMode"/>
+                       <channel id="currentTemperature" typeId="currentTemperature"/>
+                       <channel id="targetTemperature" typeId="targetTemperature"/>
+                       <channel id="autoOffTime" typeId="autoOffTime"/>
+                       <channel id="heatingRemaining" typeId="heatingRemaining"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="udn" type="text">
+                               <label>Unique Device Name</label>
+                               <description>The UDN identifies the WeMo Device</description>
+                               <required>true</required>
+                       </parameter>
                </config-description>
        </thing-type>
 
                <description>Allows setting the brightness of Night Mode</description>
        </channel-type>
 
+       <channel-type id="cookMode">
+               <item-type>String</item-type>
+               <label>Cooking Mode</label>
+               <description>Shows the operation mode of a WeMo CrockPot</description>
+               <state readOnly="false">
+                       <options>
+                               <option value="OFF">Not cooking</option>
+                               <option value="WARM">Warming</option>
+                               <option value="LOW">Low cooking</option>
+                               <option value="HIGH">High cooking</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="warmCookTime">
+               <item-type>Number</item-type>
+               <label>WarmCookTime</label>
+               <description>Shows the timer settings for warm cooking mode</description>
+       </channel-type>
+
+       <channel-type id="lowCookTime">
+               <item-type>Number</item-type>
+               <label>LowCookTime</label>
+               <description>Shows the timer settings for low cooking mode</description>
+       </channel-type>
+
+       <channel-type id="highCookTime">
+               <item-type>Number</item-type>
+               <label>HighCookTime</label>
+               <description>Shows the timer settings for high cooking mode</description>
+       </channel-type>
+
+       <channel-type id="cookedTime">
+               <item-type>Number</item-type>
+               <label>CookedTime</label>
+               <description>Shows the elapsed cooking time</description>
+       </channel-type>
+
+       <channel-type id="purifierMode">
+               <item-type>String</item-type>
+               <label>Operation Mode</label>
+               <description>Shows the operation mode of a WeMo enabled Holmes Air Purifier</description>
+               <state readOnly="false">
+                       <options>
+                               <option value="OFF">Not Running</option>
+                               <option value="LOW">Running at low level</option>
+                               <option value="MED">Running at medium level</option>
+                               <option value="HIGH">Running at high level</option>
+                               <option value="AUTO">Running in auto mode</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="airQiality">
+               <item-type>String</item-type>
+               <label>Air Quality</label>
+               <description>Shows the air quality measured by a WeMo enabled Holmes Air Purifier</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="POOR"></option>
+                               <option value="MODERATE"></option>
+                               <option value="GOOD"></option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="ionizer">
+               <item-type>Switch</item-type>
+               <label>Ionizer</label>
+               <description>Switches ionization ON or OFF</description>
+       </channel-type>
+
+       <channel-type id="filterLife">
+               <item-type>Number</item-type>
+               <label>Filter Life</label>
+               <description>Shows the remaining lifetime percentage of the air filter</description>
+       </channel-type>
+
+       <channel-type id="filterExpired">
+               <item-type>Switch</item-type>
+               <label>Filter Time expired</label>
+               <description>Indicates whether the air Filter needs to be replaced</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="filterPresent">
+               <item-type>Switch</item-type>
+               <label>Filter is present</label>
+               <description>Indicates whether the air Filter is present</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="humidifierMode">
+               <item-type>String</item-type>
+               <label>Operation Mode</label>
+               <description>Shows the operation mode of a WeMo enabled Holmes Humidifier</description>
+               <state readOnly="false">
+                       <options>
+                               <option value="OFF">Not Running</option>
+                               <option value="MIN">Running at min level</option>
+                               <option value="LOW">Running at low level</option>
+                               <option value="MED">Running at medium level</option>
+                               <option value="HIGH">Running at high level</option>
+                               <option value="MAX">Running in max level</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="currentHumidity">
+               <item-type>Number</item-type>
+               <label>Current Humidity</label>
+               <description>Shows the current humidity of a WeMo enabled Holmes Humidifier</description>
+       </channel-type>
+
+       <channel-type id="desiredHumidity">
+               <item-type>Number</item-type>
+               <label>Target Humidity</label>
+               <description>Shows the target humidity of a WeMo enabled Holmes Humidifier</description>
+               <state readOnly="false">
+                       <options>
+                               <option value="45"></option>
+                               <option value="50"></option>
+                               <option value="55"></option>
+                               <option value="60"></option>
+                               <option value="100"></option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="waterLevel">
+               <item-type>String</item-type>
+               <label>Water Level</label>
+               <description>Shows the water levele of a WeMo enabled Holmes Humidifier</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="EMPTY"></option>
+                               <option value="LOW"></option>
+                               <option value="GOOD"></option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="heaterMode">
+               <item-type>String</item-type>
+               <label>Operation Mode</label>
+               <description>Shows the operation mode of a WeMo enabled Heater</description>
+               <state readOnly="false">
+                       <options>
+                               <option value="OFF">Not Running</option>
+                               <option value="FROSTPROTECT">Running at FrostProtect</option>
+                               <option value="HIGH">Running at high level</option>
+                               <option value="LOW">Running at low level</option>
+                               <option value="ECO">Running in Eco mode</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="currentTemperature">
+               <item-type>Number</item-type>
+               <label>Current Temperature</label>
+               <description>Shows the current temperature measured by a WeMo enabled Heater</description>
+       </channel-type>
+
+       <channel-type id="targetTemperature">
+               <item-type>Number</item-type>
+               <label>Target Temperature</label>
+               <description>Shows the target temperature for a WeMo enabled Heater</description>
+       </channel-type>
+
+       <channel-type id="autoOffTime">
+               <item-type>DateTime</item-type>
+               <label>Auto OFF Time</label>
+               <description>Time when a WeMo enabled Heater should switch off</description>
+               <state pattern="%1$tR" readOnly="false"/>
+       </channel-type>
+
+       <channel-type id="heatingRemaining">
+               <item-type>Number</item-type>
+               <label>Remaining heating time</label>
+               <description>Shows the target temperature for a WeMo enabled Heater</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
 </thing:thing-descriptions>
index 4bb4a595a666503ffab4c53b67e6232313c68224..cb03331bcc3ce68574cf0ff8a26cde0f6c579d9a 100644 (file)
@@ -170,6 +170,7 @@ public class WemoLightHandlerOSGiTest extends GenericWemoLightOSGiTestParent {
         ChannelUID channelUID = new ChannelUID(thingUID, channelID);
         ThingHandler handler = thing.getHandler();
         assertNotNull(handler);
+
         handler.handleCommand(channelUID, command);
 
         ArgumentCaptor<String> captur = ArgumentCaptor.forClass(String.class);
index 68db121b64908b4177ed5c3f128d39eeceebad85..1ab8cb64278462d7cbb7e5329dee0b73ae67c167 100644 (file)
@@ -86,6 +86,7 @@ public class WemoMakerHandlerOSGiTest extends GenericWemoOSGiTest {
         ChannelUID channelUID = new ChannelUID(thing.getUID(), DEFAULT_TEST_CHANNEL);
         ThingHandler handler = thing.getHandler();
         assertNotNull(handler);
+
         handler.handleCommand(channelUID, command);
 
         ArgumentCaptor<String> captur = ArgumentCaptor.forClass(String.class);
@@ -121,6 +122,7 @@ public class WemoMakerHandlerOSGiTest extends GenericWemoOSGiTest {
         ChannelUID channelUID = new ChannelUID(thing.getUID(), DEFAULT_TEST_CHANNEL);
         ThingHandler handler = thing.getHandler();
         assertNotNull(handler);
+
         handler.handleCommand(channelUID, command);
 
         ArgumentCaptor<String> captur = ArgumentCaptor.forClass(String.class);