diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoBindingConstants.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoBindingConstants.java index 16298b4dc..82a8a9a5e 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoBindingConstants.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoBindingConstants.java @@ -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 SUPPORTED_BRIDGE_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE); public static final Set SUPPORTED_LIGHT_THING_TYPES = Collections.singleton(THING_TYPE_MZ100); diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoHandlerFactory.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoHandlerFactory.java index 9e09b1214..56299260c 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoHandlerFactory.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoHandlerFactory.java @@ -64,7 +64,7 @@ public class WemoHandlerFactory extends BaseThingHandlerFactory { public static final Set 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)); diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoUtil.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoUtil.java index 2c0d7eb68..872048ac9 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoUtil.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/WemoUtil.java @@ -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 = "" + + "" + + "" + "" + "" + + binary + "" + "" + "" + ""; + return content; + } + + public static String createStateRequestContent(String action, String actionService) { + String content = "" + + "" + + "" + "" + "" + "" + ""; + return content; + } + + public static String getCharacterDataFromElement(Element e) { + Node child = e.getFirstChild(); + if (child instanceof CharacterData) { + CharacterData cd = (CharacterData) child; + return cd.getData(); + } + return "?"; + } } diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryParticipant.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryParticipant.java index 0db9798c2..f92e6cf5f 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryParticipant.java @@ -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 diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryService.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryService.java index f7eada0a2..6091b2ba5 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryService.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/discovery/WemoDiscoveryService.java @@ -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"); } diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoBridgeHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoBridgeHandler.java index 89795ad8a..701b4f1cb 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoBridgeHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoBridgeHandler.java @@ -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"); } } diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCoffeeHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCoffeeHandler.java index 399f2e960..cd3264867 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCoffeeHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCoffeeHandler.java @@ -16,7 +16,6 @@ import static org.openhab.binding.wemo.internal.WemoBindingConstants.*; import static org.openhab.binding.wemo.internal.WemoUtil.*; import java.io.StringReader; -import java.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 SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_COFFEE); - private Map 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 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 + "<attribute><name>Cleaning</name><value>NULL</value></attribute>" + "" + "" + ""; - 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); + 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 = "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); + 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); + 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); + } } - - 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); - } - 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 = "" - + "" - + "" + "" + "" + "" + ""; - + 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); + 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, "", ""); - if (wemoURL != null) { - String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content); - if (wemoCallResponse != null) { - try { - String stringParser = substringBetween(wemoCallResponse, "", ""); + // Due to Belkins bad response formatting, we need to run this twice. + stringParser = unescapeXml(stringParser); + stringParser = unescapeXml(stringParser); - // 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()); - logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser, - getThing().getUID()); + stringParser = "" + stringParser + ""; - stringParser = "" + stringParser + ""; + 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)); - 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"); - 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); - // 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 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); - 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 (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; - } + 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; } - return "?"; + UpnpIOService localService = service; + if (localService != null) { + URL descriptorURL = localService.getDescriptorURL(this); + if (descriptorURL != null) { + return descriptorURL.getHost(); + } + } + return ""; } @Override diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCrockpotHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCrockpotHandler.java index dcf6b7c07..047f82b8b 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCrockpotHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoCrockpotHandler.java @@ -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 SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_CROCKPOT); - private final Map subscriptionState = new HashMap<>(); + private final Object upnpLock = new Object(); + private final Object jobLock = new Object(); + private final Map 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 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 + "" + "" + "" + mode + "" + "" + "" + "" + ""; - - 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); + 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 (service.isRegistered(this)) { - String subscription = "basicevent1"; - - if (subscriptionState.get(subscription) != null) { - logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription); - service.removeSubscription(this, subscription); + 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 = "" - + "" - + "" + "" + "" + "" + ""; - + 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, "", ""); - String time = substringBetween(wemoCallResponse, ""); - String coockedTime = substringBetween(wemoCallResponse, "", ""); - - 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, "", ""); + String time = substringBetween(wemoCallResponse, ""); + String coockedTime = substringBetween(wemoCallResponse, "", ""); + + 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 ""; + } } diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoDimmerHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoDimmerHandler.java index a47171ef8..f864d4025 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoDimmerHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoDimmerHandler.java @@ -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 SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DIMMER); - private Map subscriptionState = new HashMap<>(); - private Map stateMap = Collections.synchronizedMap(new HashMap<>()); + private final Object upnpLock = new Object(); + private final Object jobLock = new Object(); + + private final Map stateMap = Collections.synchronizedMap(new HashMap<>()); + + private @Nullable UpnpIOService service; - private UpnpIOService service; private WemoHttpCall wemoCall; + private String host = ""; + + private Map 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 = "" + "" + "" + "" + "" + faderSeconds + ":" + timeStamp + ":" + faderEnabled + ":0:0" + ""; updateState(CHANNEL_STATE, OnOffType.ON); - } else if (command.equals(OnOffType.OFF)) { + } else if (OnOffType.OFF.equals(command)) { value = "" + "" + "" + "" + "" + faderSeconds + ":-1:" + faderEnabled + ":0:0" + ""; @@ -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 = "<startTime>0</startTime> \\n<nightMode>1</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>" + nightModeBrightness + "</nightModeBrightness> \\n"; - } else if (command.equals(OnOffType.OFF)) { + } else if (OnOffType.OFF.equals(command)) { value = "<startTime>0</startTime> \\n<nightMode>0</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>" + nightModeBrightness + "</nightModeBrightness> \\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); + 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); } - 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, 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 = "" - + "" - + "" + "" + "" + "" + ""; + 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, "", ""); - variable = "BinaryState"; - logger.trace("New state '{}' for device '{}' received", value, getThing().getUID()); - this.onValueReceived(variable, value, actionService + "1"); - value = substringBetween(wemoCallResponse, "", ""); - variable = "brightness"; - logger.trace("New brightness '{}' for device '{}' received", value, getThing().getUID()); - this.onValueReceived(variable, value, actionService + "1"); - value = substringBetween(wemoCallResponse, "", ""); - 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, "", ""); + variable = "BinaryState"; + this.onValueReceived(variable, value, actionService + "1"); + value = substringBetween(wemoCallResponse, "", ""); + variable = "brightness"; + this.onValueReceived(variable, value, actionService + "1"); + value = substringBetween(wemoCallResponse, "", ""); + 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 = "" - + "" - + "" + "" + "" + "" + ""; + 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, "", ""); - variable = "startTime"; - logger.trace("New startTime '{}' for device '{}' received", value, getThing().getUID()); - this.onValueReceived(variable, value, actionService + "1"); - value = substringBetween(wemoCallResponse, "", ""); - variable = "endTime"; - logger.trace("New endTime '{}' for device '{}' received", value, getThing().getUID()); - this.onValueReceived(variable, value, actionService + "1"); - value = substringBetween(wemoCallResponse, "", ""); - variable = "nightMode"; - logger.trace("New nightMode state '{}' for device '{}' received", value, getThing().getUID()); - this.onValueReceived(variable, value, actionService + "1"); - value = substringBetween(wemoCallResponse, "", ""); - 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, "", ""); + variable = "startTime"; + this.onValueReceived(variable, value, actionService + "1"); + value = substringBetween(wemoCallResponse, "", ""); + variable = "endTime"; + this.onValueReceived(variable, value, actionService + "1"); + value = substringBetween(wemoCallResponse, "", ""); + variable = "nightMode"; + this.onValueReceived(variable, value, actionService + "1"); + value = substringBetween(wemoCallResponse, "", ""); + 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 = "" @@ -575,11 +613,12 @@ public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOPart + "" + "" + "<" + argument + ">" + value + "" + "" + "" + ""; - 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 = "" + "" + "" + "" + value + "" + "" + ""; - - 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) { } diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHandler.java index 3e7bacbf0..23c2a99a4 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHandler.java @@ -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 subscriptionState = new HashMap<>(); + private final Object upnpLock = new Object(); + private final Object jobLock = new Object(); private final Map stateMap = Collections.synchronizedMap(new HashMap<>()); - protected UpnpIOService service; + private @Nullable UpnpIOService service; + private WemoHttpCall wemoCall; - private @Nullable ScheduledFuture refreshJob; + private Map subscriptionState = new HashMap<>(); - private final Runnable refreshRunnable = new Runnable() { + private @Nullable ScheduledFuture pollingJob; - @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 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 = "" - + "" - + "" + "" - + "" + binaryState + "" + "" + "" - + ""; - - 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); + 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 = "basicevent1"; + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + 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); + } - 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); + 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 (service.isRegistered(this)) { - ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - String subscription = "basicevent1"; + if (subscriptionState.get(subscription) != null) { + logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription); + localService.removeSubscription(this, subscription); + } - if (subscriptionState.get(subscription) != null) { - logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription); - service.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 = "" - + "" - + "" + "" + "" + "" + ""; - + 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, "", ""); - } else { - value = substringBetween(wemoCallResponse, "", ""); - } - 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, "", ""); + } else { + value = substringBetween(wemoCallResponse, "", ""); + } + if (value.length() != 0) { + logger.trace("New state '{}' for device '{}' received", value, getThing().getUID()); + this.onValueReceived(variable, value, actionService + "1"); } } } catch (Exception e) { diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHolmesHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHolmesHandler.java index 6840fc8e7..a97a99f41 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHolmesHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoHolmesHandler.java @@ -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 subscriptionState = new HashMap<>(); + + private final Object upnpLock = new Object(); + private final Object jobLock = new Object(); + private final Map 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 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 + "<attribute><name>" + attribute + "</name><value>" + value + "</value></attribute>" + "" + "" + ""; - - 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); + 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 (service.isRegistered(this)) { - String subscription = "basicevent1"; - - if (subscriptionState.get(subscription) != null) { - logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription); - service.removeSubscription(this, subscription); + 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 = "" - + "" - + "" + "" + "" + "" + ""; - + 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); + 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()); + } - 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, "", ""); - String stringParser = substringBetween(wemoCallResponse, "", ""); + // Due to Belkins bad response formatting, we need to run this twice. + stringParser = unescapeXml(stringParser); + stringParser = unescapeXml(stringParser); - // 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()); - logger.trace("AirPurifier response '{}' for device '{}' received", stringParser, - getThing().getUID()); + stringParser = "" + stringParser + ""; - stringParser = "" + stringParser + ""; + 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)); - 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"); - 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); - // 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 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); - 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": + 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; } - return "?"; + UpnpIOService localService = service; + if (localService != null) { + URL descriptorURL = localService.getDescriptorURL(this); + if (descriptorURL != null) { + return descriptorURL.getHost(); + } + } + return ""; } @Override diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoLightHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoLightHandler.java index af8d43bc9..1aad41ee3 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoLightHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoLightHandler.java @@ -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 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" encoding="UTF-8"?><DeviceStatus><DeviceID>" - + wemoLightID - + "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>" - + capability + "</CapabilityID><CapabilityValue>" + value - + "</CapabilityValue></DeviceStatus>" + "" - + "" + "" + ""; + if (capability != null && value != null) { + String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\""; + String content = "" + + "" + + "" + "" + + "" + + "<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><DeviceID>" + + wemoLightID + + "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>" + + capability + "</CapabilityID><CapabilityValue>" + value + + "</CapabilityValue></DeviceStatus>" + "" + + "" + "" + ""; - URL descriptorURL = service.getDescriptorURL(this); - String wemoURL = getWemoURL(descriptorURL, "bridge"); - - if (wemoURL != null && capability != null && value != null) { 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 = "" @@ -293,31 +348,32 @@ public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParti + "" + "" + "" + wemoLightID + "" + "" + "" + ""; - 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, "", ""); - 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, "", ""); + 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); + 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); - 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() { - if (service.isRegistered(this)) { - logger.debug("Removing WeMo GENA subscription for '{}'", this); + 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); - service.removeSubscription(this, SUBSCRIPTION); + 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() { - return service.isRegistered(this); + UpnpIOService localService = service; + if (localService != null) { + return localService.isRegistered(this); + } + return false; + } + + 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 ""; } } diff --git a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoMakerHandler.java b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoMakerHandler.java index eb60cbcec..5586bfe43 100644 --- a/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoMakerHandler.java +++ b/bundles/org.openhab.binding.wemo/src/main/java/org/openhab/binding/wemo/internal/handler/WemoMakerHandler.java @@ -16,7 +16,6 @@ import static org.openhab.binding.wemo.internal.WemoBindingConstants.*; import static org.openhab.binding.wemo.internal.WemoUtil.*; import java.io.StringReader; -import java.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 SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER); - private UpnpIOService service; + private final Object jobLock = new Object(); + + private @Nullable UpnpIOService service; + private WemoHttpCall wemoCall; - private @Nullable ScheduledFuture refreshJob; + private String host = ""; - private final Runnable refreshRunnable = new Runnable() { - - @Override - public void run() { - try { - updateWemoState(); - } catch (Exception e) { - logger.debug("Exception during poll", e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - } - } - }; + 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 = "" - + "" - + "" + "" - + "" + binaryState + "" + "" + "" - + ""; - - 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 = "" - + "" - + "" + "" + "" + "" + ""; - + 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); + 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, "", ""); + logger.trace("Escaped Maker response for device '{}' :", getThing().getUID()); + logger.trace("'{}'", stringParser); - if (wemoURL != null) { - String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content); - if (wemoCallResponse != null) { - try { - String stringParser = substringBetween(wemoCallResponse, "", ""); - 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()); - // 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 = "" + stringParser + ""; - stringParser = "" + stringParser + ""; + 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)); - 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"); - 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); - // 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 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); - 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; - } + 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; } - return "?"; + UpnpIOService localService = service; + if (localService != null) { + URL descriptorURL = localService.getDescriptorURL(this); + if (descriptorURL != null) { + return descriptorURL.getHost(); + } + } + return ""; } @Override diff --git a/bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/i18n/wemo.properties b/bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/i18n/wemo.properties index 92b9c22c5..62e0ef550 100644 --- a/bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/i18n/wemo.properties +++ b/bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/i18n/wemo.properties @@ -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. diff --git a/bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/thing/thing-types.xml index f60e7a507..91e749948 100644 --- a/bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.wemo/src/main/resources/OH-INF/thing/thing-types.xml @@ -17,7 +17,6 @@ The UDN identifies the WeMo Device - @@ -176,7 +175,92 @@ The UDN identifies the WeMo Device + + + + + Crock-Pot Smart Slow Cooker with WeMo + + + + + + + + + + + + + The UDN identifies the WeMo Device + true + + + + + + + Holmes Smart Air Purifier with WeMo + + + + + + + + + + + + + + The UDN identifies the WeMo Device + true + + + + + + + Holmes Smart Humidifier with WeMo + + + + + + + + + + + + + + The UDN identifies the WeMo Device + true + + + + + + + Holmes Smart Heater with WeMo + + + + + + + + + + + + + The UDN identifies the WeMo Device + true + @@ -418,4 +502,187 @@ Allows setting the brightness of Night Mode + + String + + Shows the operation mode of a WeMo CrockPot + + + + + + + + + + + + Number + + Shows the timer settings for warm cooking mode + + + + Number + + Shows the timer settings for low cooking mode + + + + Number + + Shows the timer settings for high cooking mode + + + + Number + + Shows the elapsed cooking time + + + + String + + Shows the operation mode of a WeMo enabled Holmes Air Purifier + + + + + + + + + + + + + String + + Shows the air quality measured by a WeMo enabled Holmes Air Purifier + + + + + + + + + + + Switch + + Switches ionization ON or OFF + + + + Number + + Shows the remaining lifetime percentage of the air filter + + + + Switch + + Indicates whether the air Filter needs to be replaced + + + + + Switch + + Indicates whether the air Filter is present + + + + + String + + Shows the operation mode of a WeMo enabled Holmes Humidifier + + + + + + + + + + + + + + Number + + Shows the current humidity of a WeMo enabled Holmes Humidifier + + + + Number + + Shows the target humidity of a WeMo enabled Holmes Humidifier + + + + + + + + + + + + + String + + Shows the water levele of a WeMo enabled Holmes Humidifier + + + + + + + + + + + String + + Shows the operation mode of a WeMo enabled Heater + + + + + + + + + + + + + Number + + Shows the current temperature measured by a WeMo enabled Heater + + + + Number + + Shows the target temperature for a WeMo enabled Heater + + + + DateTime + + Time when a WeMo enabled Heater should switch off + + + + + Number + + Shows the target temperature for a WeMo enabled Heater + + + diff --git a/itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoLightHandlerOSGiTest.java b/itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoLightHandlerOSGiTest.java index 4bb4a595a..cb03331bc 100644 --- a/itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoLightHandlerOSGiTest.java +++ b/itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoLightHandlerOSGiTest.java @@ -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 captur = ArgumentCaptor.forClass(String.class); diff --git a/itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoMakerHandlerOSGiTest.java b/itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoMakerHandlerOSGiTest.java index 68db121b6..1ab8cb642 100644 --- a/itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoMakerHandlerOSGiTest.java +++ b/itests/org.openhab.binding.wemo.tests/src/main/java/org/openhab/binding/wemo/internal/handler/test/WemoMakerHandlerOSGiTest.java @@ -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 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 captur = ArgumentCaptor.forClass(String.class);