public static final int LINK_DISCOVERY_SERVICE_INITIAL_DELAY = 5;
public static final String HTTP_CALL_CONTENT_HEADER = "text/xml; charset=utf-8";
+ public static final String BASICACTION = "basicevent";
+ public static final String BASICEVENT = "basicevent1";
+ public static final String BRIDGEACTION = "bridge";
+ public static final String BRIDGEEVENT = "bridge1";
+ public static final String DEVICEACTION = "deviceevent";
+ public static final String DEVICEEVENT = "deviceevent1";
+ public static final String INSIGHTACTION = "insight";
+ public static final String INSIGHTEVENT = "insight1";
+
public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
public static final Set<ThingTypeUID> SUPPORTED_LIGHT_THING_TYPES = Collections.singleton(THING_TYPE_MZ100);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = WemoBindingConstants.SUPPORTED_THING_TYPES;
- private UpnpIOService upnpIOService;
+ private final UpnpIOService upnpIOService;
private @Nullable WemoHttpCallFactory wemoHttpCallFactory;
@Override
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));
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;
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.
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);
entities.put("quot", "\"");
return entities;
}
+
+ public static String createBinaryStateContent(boolean binaryState) {
+ String binary = binaryState == true ? "1" : "0";
+ String content = "<?xml version=\"1.0\"?>"
+ + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
+ + "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<BinaryState>"
+ + binary + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>" + "</s:Envelope>";
+ return content;
+ }
+
+ public static String createStateRequestContent(String action, String actionService) {
+ String content = "<?xml version=\"1.0\"?>"
+ + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
+ + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
+ + action + ">" + "</s:Body>" + "</s:Envelope>";
+ return content;
+ }
+
+ public static String getCharacterDataFromElement(Element e) {
+ Node child = e.getFirstChild();
+ if (child instanceof CharacterData) {
+ CharacterData cd = (CharacterData) child;
+ return cd.getData();
+ }
+ return "?";
+ }
}
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;
/**
* 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
@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");
}
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");
}
}
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;
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;
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_COFFEE);
- private Map<String, Boolean> subscriptionState = new HashMap<>();
+ private final Object upnpLock = new Object();
+ private final Object jobLock = new Object();
- private UpnpIOService service;
+ private @Nullable UpnpIOService service;
private WemoHttpCall wemoCall;
- private @Nullable ScheduledFuture<?> refreshJob;
+ private String host = "";
- private final Runnable refreshRunnable = new Runnable() {
+ private Map<String, Boolean> subscriptionState = new HashMap<>();
- @Override
- public void run() {
- try {
- if (!isUpnpDeviceRegistered()) {
- logger.debug("WeMo UPnP device {} not yet registered", getUDN());
- }
-
- updateWemoState();
- onSubscription();
- } catch (Exception e) {
- logger.debug("Exception during poll", e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- }
- }
- };
+ private @Nullable ScheduledFuture<?> pollingJob;
public WemoCoffeeHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
super(thing, wemoHttpCaller);
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.");
}
}
@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();
+ "<attribute><name>Cleaning</name><value>NULL</value></attribute></attributeList>"
+ "</u:SetAttributes>" + "</s:Body>" + "</s:Envelope>";
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
- if (wemoURL != null) {
- String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
- if (wemoCallResponse != null) {
- updateState(CHANNEL_STATE, OnOffType.ON);
- State newMode = new StringType("Brewing");
- updateState(CHANNEL_COFFEEMODE, newMode);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null) {
+ updateState(CHANNEL_STATE, OnOffType.ON);
+ State newMode = new StringType("Brewing");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader,
+ getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content,
+ getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
+ getThing().getUID());
}
}
} catch (Exception e) {
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);
}
// We can subscribe to GENA events, but there is no usefull response right now.
}
- private synchronized void onSubscription() {
- if (service.isRegistered(this)) {
- logger.debug("Checking WeMo GENA subscription for '{}'", this);
-
- String subscription = "deviceevent1";
- if (subscriptionState.get(subscription) == null) {
- logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
- service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
- subscriptionState.put(subscription, true);
+ private synchronized void addSubscription() {
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
+
+ String subscription = DEVICEEVENT;
+ if (subscriptionState.get(subscription) == null) {
+ logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+ subscription);
+ localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+ subscriptionState.put(subscription, true);
+ }
+ } else {
+ logger.debug(
+ "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+ getThing().getUID());
+ }
}
- } else {
- logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
- this);
}
}
private synchronized void removeSubscription() {
- logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
- if (service.isRegistered(this)) {
- String subscription = "deviceevent1";
- if (subscriptionState.get(subscription) != null) {
- logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
- service.removeSubscription(this, subscription);
- }
-
- subscriptionState = new HashMap<>();
- service.unregisterParticipant(this);
- }
- }
-
- private synchronized void onUpdate() {
- ScheduledFuture<?> job = refreshJob;
- if (job == null || job.isCancelled()) {
- Configuration config = getThing().getConfiguration();
- int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
- Object refreshConfig = config.get("pollingInterval");
- if (refreshConfig != null) {
- refreshInterval = ((BigDecimal) refreshConfig).intValue();
- logger.debug("Setting WemoCoffeeHandler refreshInterval to '{}' seconds", refreshInterval);
+ logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ String subscription = DEVICEEVENT;
+ if (subscriptionState.get(subscription) != null) {
+ logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+ localService.removeSubscription(this, subscription);
+ }
+ subscriptionState = new HashMap<>();
+ localService.unregisterParticipant(this);
+ }
}
- refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
}
}
private boolean isUpnpDeviceRegistered() {
- return service.isRegistered(this);
+ UpnpIOService localService = service;
+ if (localService != null) {
+ return localService.isRegistered(this);
+ }
+ return false;
}
@Override
* The {@link updateWemoState} polls the actual state of a WeMo CoffeeMaker.
*/
protected void updateWemoState() {
- String action = "GetAttributes";
- String actionService = "deviceevent";
-
- String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
- String content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
- + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+ String localHost = getHost();
+ if (localHost.isEmpty()) {
+ logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
+ String actionService = DEVICEACTION;
+ String wemoURL = getWemoURL(host, actionService);
+ if (wemoURL == null) {
+ logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
+ }
try {
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, actionService);
-
- if (wemoURL != null) {
- String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
- if (wemoCallResponse != null) {
- try {
- String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
-
- // Due to Belkins bad response formatting, we need to run this twice.
- stringParser = unescapeXml(stringParser);
- stringParser = unescapeXml(stringParser);
-
- logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser,
- getThing().getUID());
-
- stringParser = "<data>" + stringParser + "</data>";
-
- DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
- // see
- // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
- dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
- dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
- dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
- dbf.setXIncludeAware(false);
- dbf.setExpandEntityReferences(false);
- DocumentBuilder db = dbf.newDocumentBuilder();
- InputSource is = new InputSource();
- is.setCharacterStream(new StringReader(stringParser));
-
- Document doc = db.parse(is);
- NodeList nodes = doc.getElementsByTagName("attribute");
-
- // iterate the attributes
- for (int i = 0; i < nodes.getLength(); i++) {
- Element element = (Element) nodes.item(i);
-
- NodeList deviceIndex = element.getElementsByTagName("name");
- Element line = (Element) deviceIndex.item(0);
- String attributeName = getCharacterDataFromElement(line);
- logger.trace("attributeName: {}", attributeName);
-
- NodeList deviceID = element.getElementsByTagName("value");
- line = (Element) deviceID.item(0);
- String attributeValue = getCharacterDataFromElement(line);
- logger.trace("attributeValue: {}", attributeValue);
-
- switch (attributeName) {
- case "Mode":
- State newMode = new StringType("Brewing");
- State newAttributeValue;
-
- switch (attributeValue) {
- case "0":
- updateState(CHANNEL_STATE, OnOffType.ON);
- newMode = new StringType("Refill");
- updateState(CHANNEL_COFFEEMODE, newMode);
- break;
- case "1":
- updateState(CHANNEL_STATE, OnOffType.OFF);
- newMode = new StringType("PlaceCarafe");
- updateState(CHANNEL_COFFEEMODE, newMode);
- break;
- case "2":
- updateState(CHANNEL_STATE, OnOffType.OFF);
- newMode = new StringType("RefillWater");
- updateState(CHANNEL_COFFEEMODE, newMode);
- break;
- case "3":
- updateState(CHANNEL_STATE, OnOffType.OFF);
- newMode = new StringType("Ready");
- updateState(CHANNEL_COFFEEMODE, newMode);
- break;
- case "4":
- updateState(CHANNEL_STATE, OnOffType.ON);
- newMode = new StringType("Brewing");
- updateState(CHANNEL_COFFEEMODE, newMode);
- break;
- case "5":
- updateState(CHANNEL_STATE, OnOffType.OFF);
- newMode = new StringType("Brewed");
- updateState(CHANNEL_COFFEEMODE, newMode);
- break;
- case "6":
- updateState(CHANNEL_STATE, OnOffType.OFF);
- newMode = new StringType("CleaningBrewing");
- updateState(CHANNEL_COFFEEMODE, newMode);
- break;
- case "7":
- updateState(CHANNEL_STATE, OnOffType.OFF);
- newMode = new StringType("CleaningSoaking");
- updateState(CHANNEL_COFFEEMODE, newMode);
- break;
- case "8":
- updateState(CHANNEL_STATE, OnOffType.OFF);
- newMode = new StringType("BrewFailCarafeRemoved");
- updateState(CHANNEL_COFFEEMODE, newMode);
- break;
- }
- break;
- case "ModeTime":
- newAttributeValue = new DecimalType(attributeValue);
- updateState(CHANNEL_MODETIME, newAttributeValue);
- break;
- case "TimeRemaining":
- newAttributeValue = new DecimalType(attributeValue);
- updateState(CHANNEL_TIMEREMAINING, newAttributeValue);
- break;
- case "WaterLevelReached":
- newAttributeValue = new DecimalType(attributeValue);
- updateState(CHANNEL_WATERLEVELREACHED, newAttributeValue);
- break;
- case "CleanAdvise":
- newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
- updateState(CHANNEL_CLEANADVISE, newAttributeValue);
- break;
- case "FilterAdvise":
- newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
- updateState(CHANNEL_FILTERADVISE, newAttributeValue);
- break;
- case "Brewed":
- newAttributeValue = getDateTimeState(attributeValue);
- if (newAttributeValue != null) {
- updateState(CHANNEL_BREWED, newAttributeValue);
- }
- break;
- case "LastCleaned":
- newAttributeValue = getDateTimeState(attributeValue);
- if (newAttributeValue != null) {
- updateState(CHANNEL_LASTCLEANED, newAttributeValue);
- }
- break;
- }
+ String action = "GetAttributes";
+ String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
+ String content = createStateRequestContent(action, actionService);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+ }
+ try {
+ String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
+
+ // Due to Belkins bad response formatting, we need to run this twice.
+ stringParser = unescapeXml(stringParser);
+ stringParser = unescapeXml(stringParser);
+
+ logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser,
+ getThing().getUID());
+
+ stringParser = "<data>" + stringParser + "</data>";
+
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ // see
+ // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
+ dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ dbf.setXIncludeAware(false);
+ dbf.setExpandEntityReferences(false);
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ InputSource is = new InputSource();
+ is.setCharacterStream(new StringReader(stringParser));
+
+ Document doc = db.parse(is);
+ NodeList nodes = doc.getElementsByTagName("attribute");
+
+ // iterate the attributes
+ for (int i = 0; i < nodes.getLength(); i++) {
+ Element element = (Element) nodes.item(i);
+
+ NodeList deviceIndex = element.getElementsByTagName("name");
+ Element line = (Element) deviceIndex.item(0);
+ String attributeName = getCharacterDataFromElement(line);
+ logger.trace("attributeName: {}", attributeName);
+
+ NodeList deviceID = element.getElementsByTagName("value");
+ line = (Element) deviceID.item(0);
+ String attributeValue = getCharacterDataFromElement(line);
+ logger.trace("attributeValue: {}", attributeValue);
+
+ switch (attributeName) {
+ case "Mode":
+ State newMode = new StringType("Brewing");
+ State newAttributeValue;
+
+ switch (attributeValue) {
+ case "0":
+ updateState(CHANNEL_STATE, OnOffType.ON);
+ newMode = new StringType("Refill");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ break;
+ case "1":
+ updateState(CHANNEL_STATE, OnOffType.OFF);
+ newMode = new StringType("PlaceCarafe");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ break;
+ case "2":
+ updateState(CHANNEL_STATE, OnOffType.OFF);
+ newMode = new StringType("RefillWater");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ break;
+ case "3":
+ updateState(CHANNEL_STATE, OnOffType.OFF);
+ newMode = new StringType("Ready");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ break;
+ case "4":
+ updateState(CHANNEL_STATE, OnOffType.ON);
+ newMode = new StringType("Brewing");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ break;
+ case "5":
+ updateState(CHANNEL_STATE, OnOffType.OFF);
+ newMode = new StringType("Brewed");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ break;
+ case "6":
+ updateState(CHANNEL_STATE, OnOffType.OFF);
+ newMode = new StringType("CleaningBrewing");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ break;
+ case "7":
+ updateState(CHANNEL_STATE, OnOffType.OFF);
+ newMode = new StringType("CleaningSoaking");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ break;
+ case "8":
+ updateState(CHANNEL_STATE, OnOffType.OFF);
+ newMode = new StringType("BrewFailCarafeRemoved");
+ updateState(CHANNEL_COFFEEMODE, newMode);
+ break;
+ }
+ break;
+ case "ModeTime":
+ newAttributeValue = new DecimalType(attributeValue);
+ updateState(CHANNEL_MODETIME, newAttributeValue);
+ break;
+ case "TimeRemaining":
+ newAttributeValue = new DecimalType(attributeValue);
+ updateState(CHANNEL_TIMEREMAINING, newAttributeValue);
+ break;
+ case "WaterLevelReached":
+ newAttributeValue = new DecimalType(attributeValue);
+ updateState(CHANNEL_WATERLEVELREACHED, newAttributeValue);
+ break;
+ case "CleanAdvise":
+ newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
+ updateState(CHANNEL_CLEANADVISE, newAttributeValue);
+ break;
+ case "FilterAdvise":
+ newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
+ updateState(CHANNEL_FILTERADVISE, newAttributeValue);
+ break;
+ case "Brewed":
+ newAttributeValue = getDateTimeState(attributeValue);
+ if (newAttributeValue != null) {
+ updateState(CHANNEL_BREWED, newAttributeValue);
+ }
+ break;
+ case "LastCleaned":
+ newAttributeValue = getDateTimeState(attributeValue);
+ if (newAttributeValue != null) {
+ updateState(CHANNEL_LASTCLEANED, newAttributeValue);
+ }
+ break;
}
- } catch (Exception e) {
- logger.error("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(),
- e);
}
+ } catch (Exception e) {
+ logger.error("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(), e);
}
}
} catch (Exception e) {
return dateTimeState;
}
- public static String getCharacterDataFromElement(Element e) {
- Node child = e.getFirstChild();
- if (child instanceof CharacterData) {
- CharacterData cd = (CharacterData) child;
- return cd.getData();
+ public String getHost() {
+ String localHost = host;
+ if (!localHost.isEmpty()) {
+ return localHost;
+ }
+ UpnpIOService localService = service;
+ if (localService != null) {
+ URL descriptorURL = localService.getDescriptorURL(this);
+ if (descriptorURL != null) {
+ return descriptorURL.getHost();
+ }
}
- return "?";
+ return "";
}
@Override
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;
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_CROCKPOT);
- private final Map<String, Boolean> subscriptionState = new HashMap<>();
+ private final Object upnpLock = new Object();
+ private final Object jobLock = new Object();
+
private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
- private UpnpIOService service;
+ private @Nullable UpnpIOService service;
private WemoHttpCall wemoCall;
- private @Nullable ScheduledFuture<?> refreshJob;
+ private String host = "";
- private final Runnable refreshRunnable = () -> {
- updateWemoState();
- if (!isUpnpDeviceRegistered()) {
- logger.debug("WeMo UPnP device {} not yet registered", getUDN());
- } else {
- onSubscription();
- }
- };
+ private Map<String, Boolean> subscriptionState = new HashMap<>();
+
+ private @Nullable ScheduledFuture<?> pollingJob;
public WemoCrockpotHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
super(thing, wemoHttpCaller);
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.");
}
}
@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;
+ "<s:Body>" + "<u:SetCrockpotState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<mode>"
+ mode + "</mode>" + "<time>" + time + "</time>" + "</u:SetCrockpotState>" + "</s:Body>"
+ "</s:Envelope>";
-
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
- if (wemoURL != null) {
- wemoCall.executeCall(wemoURL, soapHeader, content);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null && logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
}
} catch (RuntimeException e) {
logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
}
}
- private synchronized void onSubscription() {
- if (service.isRegistered(this)) {
- logger.debug("Checking WeMo GENA subscription for '{}'", this);
+ private synchronized void addSubscription() {
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
- String subscription = "basicevent1";
+ String subscription = BASICEVENT;
- if (subscriptionState.get(subscription) == null) {
- logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
- service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
- subscriptionState.put(subscription, true);
+ if (subscriptionState.get(subscription) == null) {
+ logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+ subscription);
+ localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+ subscriptionState.put(subscription, true);
+ }
+ } else {
+ logger.debug(
+ "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+ getThing().getUID());
+ }
}
-
- } else {
- logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
- this);
}
}
private synchronized void removeSubscription() {
- logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
- if (service.isRegistered(this)) {
- String subscription = "basicevent1";
-
- if (subscriptionState.get(subscription) != null) {
- logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
- service.removeSubscription(this, subscription);
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+ String subscription = BASICEVENT;
+
+ if (subscriptionState.get(subscription) != null) {
+ logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+ localService.removeSubscription(this, subscription);
+ }
+ subscriptionState.remove(subscription);
+ localService.unregisterParticipant(this);
+ }
}
-
- subscriptionState.remove(subscription);
- service.unregisterParticipant(this);
- }
- }
-
- private synchronized void onUpdate() {
- ScheduledFuture<?> job = refreshJob;
- if (job == null || job.isCancelled()) {
- Configuration config = getThing().getConfiguration();
- int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
- Object refreshConfig = config.get("refresh");
- refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVALL_SECONDS
- : ((BigDecimal) refreshConfig).intValue();
- refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
}
}
private boolean isUpnpDeviceRegistered() {
- return service.isRegistered(this);
+ UpnpIOService localService = service;
+ if (localService != null) {
+ return localService.isRegistered(this);
+ }
+ return false;
}
@Override
*
*/
protected void updateWemoState() {
- String action = "GetCrockpotState";
- String actionService = "basicevent";
-
- String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
- String content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
- + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+ String localHost = getHost();
+ if (localHost.isEmpty()) {
+ logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
+ String actionService = BASICEVENT;
+ String wemoURL = getWemoURL(localHost, actionService);
+ if (wemoURL == null) {
+ logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
+ }
try {
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, actionService);
-
- if (wemoURL != null) {
- String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
- if (wemoCallResponse != null) {
- logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
- String mode = substringBetween(wemoCallResponse, "<mode>", "</mode>");
- String time = substringBetween(wemoCallResponse, "<time>", "</time>");
- String coockedTime = substringBetween(wemoCallResponse, "<coockedTime>", "</coockedTime>");
-
- State newMode = new StringType(mode);
- State newCoockedTime = DecimalType.valueOf(coockedTime);
- switch (mode) {
- case "0":
- newMode = new StringType("OFF");
- break;
- case "50":
- newMode = new StringType("WARM");
- State warmTime = DecimalType.valueOf(time);
- updateState(CHANNEL_WARMCOOKTIME, warmTime);
- break;
- case "51":
- newMode = new StringType("LOW");
- State lowTime = DecimalType.valueOf(time);
- updateState(CHANNEL_LOWCOOKTIME, lowTime);
- break;
- case "52":
- newMode = new StringType("HIGH");
- State highTime = DecimalType.valueOf(time);
- updateState(CHANNEL_HIGHCOOKTIME, highTime);
- break;
- }
- updateState(CHANNEL_COOKMODE, newMode);
- updateState(CHANNEL_COOKEDTIME, newCoockedTime);
+ String action = "GetCrockpotState";
+ String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
+ String content = createStateRequestContent(action, actionService);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+ }
+ String mode = substringBetween(wemoCallResponse, "<mode>", "</mode>");
+ String time = substringBetween(wemoCallResponse, "<time>", "</time>");
+ String coockedTime = substringBetween(wemoCallResponse, "<coockedTime>", "</coockedTime>");
+
+ State newMode = new StringType(mode);
+ State newCoockedTime = DecimalType.valueOf(coockedTime);
+ switch (mode) {
+ case "0":
+ newMode = new StringType("OFF");
+ break;
+ case "50":
+ newMode = new StringType("WARM");
+ State warmTime = DecimalType.valueOf(time);
+ updateState(CHANNEL_WARMCOOKTIME, warmTime);
+ break;
+ case "51":
+ newMode = new StringType("LOW");
+ State lowTime = DecimalType.valueOf(time);
+ updateState(CHANNEL_LOWCOOKTIME, lowTime);
+ break;
+ case "52":
+ newMode = new StringType("HIGH");
+ State highTime = DecimalType.valueOf(time);
+ updateState(CHANNEL_HIGHCOOKTIME, highTime);
+ break;
}
+ updateState(CHANNEL_COOKMODE, newMode);
+ updateState(CHANNEL_COOKEDTIME, newCoockedTime);
}
} catch (RuntimeException e) {
logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage(), e);
@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 "";
+ }
}
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;
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DIMMER);
- private Map<String, Boolean> subscriptionState = new HashMap<>();
- private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
+ private final Object upnpLock = new Object();
+ private final Object jobLock = new Object();
+
+ private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
+
+ private @Nullable UpnpIOService service;
- private UpnpIOService service;
private WemoHttpCall wemoCall;
+ private String host = "";
+
+ private Map<String, Boolean> subscriptionState = new HashMap<>();
+
+ private @Nullable ScheduledFuture<?> pollingJob;
+
private int currentBrightness;
private int currentNightModeBrightness;
private @Nullable String currentNightModeState;
*/
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);
@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.");
}
}
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);
value = String.valueOf(newBrightness);
currentBrightness = newBrightness;
argument = "brightness";
- if (value.equals("0")) {
+ if ("0".equals(value)) {
value = "1";
argument = "brightness";
setBinaryState(action, argument, "1");
break;
}
argument = "brightness";
- if (value.equals("0")) {
+ if ("0".equals(value)) {
value = "1";
argument = "brightness";
setBinaryState(action, argument, "1");
}
}
if (faderSeconds != null && faderEnabled != null) {
- if (command.equals(OnOffType.ON)) {
+ if (OnOffType.ON.equals(command)) {
value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
+ "<brightness></brightness>" + "<fader>" + faderSeconds + ":" + timeStamp + ":"
+ faderEnabled + ":0:0</fader>" + "<UDN></UDN>";
updateState(CHANNEL_STATE, OnOffType.ON);
- } else if (command.equals(OnOffType.OFF)) {
+ } else if (OnOffType.OFF.equals(command)) {
value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
+ "<brightness></brightness>" + "<fader>" + faderSeconds + ":-1:" + faderEnabled
+ ":0:0</fader>" + "<UDN></UDN>";
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";
}
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)) {
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);
}
}
}
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);
updateState(CHANNEL_NIGHTMODEBRIGHTNESS, nightModeBrightnessState);
break;
}
-
}
}
- private synchronized void onSubscription() {
- if (service.isRegistered(this)) {
- logger.debug("Checking WeMo GENA subscription for '{}'", this);
- String subscription = "basicevent1";
- if (subscriptionState.get(subscription) == null) {
- logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
- service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
- subscriptionState.put(subscription, true);
+ private synchronized void addSubscription() {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
+ String subscription = BASICEVENT;
+ if (subscriptionState.get(subscription) == null) {
+ logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+ subscription);
+ localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+ subscriptionState.put(subscription, true);
+ }
+ } else {
+ logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+ getThing().getUID());
}
- } else {
- logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
- this);
}
}
private synchronized void removeSubscription() {
- logger.debug("Removing WeMo GENA subscription for '{}'", this);
- if (service.isRegistered(this)) {
- String subscription = "basicevent1";
- if (subscriptionState.get(subscription) != null) {
- logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
- service.removeSubscription(this, subscription);
- }
- subscriptionState = new HashMap<>();
- service.unregisterParticipant(this);
- }
- }
-
- private synchronized void onUpdate() {
- ScheduledFuture<?> job = refreshJob;
- if (job == null || job.isCancelled()) {
- Configuration config = getThing().getConfiguration();
- int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
- Object refreshConfig = config.get("refresh");
- if (refreshConfig != null) {
- refreshInterval = ((BigDecimal) refreshConfig).intValue();
+ logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ String subscription = BASICEVENT;
+ if (subscriptionState.get(subscription) != null) {
+ logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+ localService.removeSubscription(this, subscription);
+ }
+ subscriptionState = new HashMap<>();
+ localService.unregisterParticipant(this);
}
- refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 10, refreshInterval, TimeUnit.SECONDS);
}
}
private boolean isUpnpDeviceRegistered() {
- return service.isRegistered(this);
+ UpnpIOService localService = service;
+ if (localService != null) {
+ return localService.isRegistered(this);
+ }
+ return false;
}
@Override
*
*/
protected void updateWemoState() {
+ String localHost = getHost();
+ if (localHost.isEmpty()) {
+ logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
+ String wemoURL = getWemoURL(localHost, BASICACTION);
+ if (wemoURL == null) {
+ logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
+ }
String action = "GetBinaryState";
String variable = null;
- String actionService = "basicevent";
+ String actionService = BASICACTION;
String value = null;
String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
- String content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
- + action + ">" + "</s:Body>" + "</s:Envelope>";
+ String content = createStateRequestContent(action, actionService);
try {
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
- if (wemoURL != null) {
- String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
- if (wemoCallResponse != null) {
- logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
- value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
- variable = "BinaryState";
- logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
- this.onValueReceived(variable, value, actionService + "1");
- value = substringBetween(wemoCallResponse, "<brightness>", "</brightness>");
- variable = "brightness";
- logger.trace("New brightness '{}' for device '{}' received", value, getThing().getUID());
- this.onValueReceived(variable, value, actionService + "1");
- value = substringBetween(wemoCallResponse, "<fader>", "</fader>");
- variable = "fader";
- logger.trace("New fader value '{}' for device '{}' received", value, getThing().getUID());
- this.onValueReceived(variable, value, actionService + "1");
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
}
+ value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
+ variable = "BinaryState";
+ this.onValueReceived(variable, value, actionService + "1");
+ value = substringBetween(wemoCallResponse, "<brightness>", "</brightness>");
+ variable = "brightness";
+ this.onValueReceived(variable, value, actionService + "1");
+ value = substringBetween(wemoCallResponse, "<fader>", "</fader>");
+ variable = "fader";
+ this.onValueReceived(variable, value, actionService + "1");
+ updateStatus(ThingStatus.ONLINE);
}
} catch (Exception e) {
logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
- updateStatus(ThingStatus.ONLINE);
action = "GetNightModeConfiguration";
variable = null;
value = null;
soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
- content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
- + action + ">" + "</s:Body>" + "</s:Envelope>";
+ content = createStateRequestContent(action, actionService);
try {
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
- if (wemoURL != null) {
- String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
- if (wemoCallResponse != null) {
- logger.trace("GetNightModeConfiguration response '{}' for device '{}' received", wemoCallResponse,
- getThing().getUID());
- value = substringBetween(wemoCallResponse, "<startTime>", "</startTime>");
- variable = "startTime";
- logger.trace("New startTime '{}' for device '{}' received", value, getThing().getUID());
- this.onValueReceived(variable, value, actionService + "1");
- value = substringBetween(wemoCallResponse, "<endTime>", "</endTime>");
- variable = "endTime";
- logger.trace("New endTime '{}' for device '{}' received", value, getThing().getUID());
- this.onValueReceived(variable, value, actionService + "1");
- value = substringBetween(wemoCallResponse, "<nightMode>", "</nightMode>");
- variable = "nightMode";
- logger.trace("New nightMode state '{}' for device '{}' received", value, getThing().getUID());
- this.onValueReceived(variable, value, actionService + "1");
- value = substringBetween(wemoCallResponse, "<nightModeBrightness>", "</nightModeBrightness>");
- variable = "nightModeBrightness";
- logger.trace("New nightModeBrightness '{}' for device '{}' received", value, getThing().getUID());
- this.onValueReceived(variable, value, actionService + "1");
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
}
+ value = substringBetween(wemoCallResponse, "<startTime>", "</startTime>");
+ variable = "startTime";
+ this.onValueReceived(variable, value, actionService + "1");
+ value = substringBetween(wemoCallResponse, "<endTime>", "</endTime>");
+ variable = "endTime";
+ this.onValueReceived(variable, value, actionService + "1");
+ value = substringBetween(wemoCallResponse, "<nightMode>", "</nightMode>");
+ variable = "nightMode";
+ this.onValueReceived(variable, value, actionService + "1");
+ value = substringBetween(wemoCallResponse, "<nightModeBrightness>", "</nightModeBrightness>");
+ variable = "nightModeBrightness";
+ this.onValueReceived(variable, value, actionService + "1");
+ updateStatus(ThingStatus.ONLINE);
+
}
} catch (Exception e) {
logger.debug("Failed to get actual NightMode state for device '{}': {}", getThing().getUID(),
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
- updateStatus(ThingStatus.ONLINE);
}
public @Nullable State getDateTimeState(String attributeValue) {
}
public void setBinaryState(String action, String argument, String value) {
+ String localHost = getHost();
+ if (localHost.isEmpty()) {
+ logger.error("Failed to set binary state for device '{}': IP address missing", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
+ String wemoURL = getWemoURL(localHost, BASICACTION);
+ if (wemoURL == null) {
+ logger.error("Failed to set binary state for device '{}': URL cannot be created", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
+ }
try {
String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
String content = "<?xml version=\"1.0\"?>"
+ "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<" + argument
+ ">" + value + "</" + argument + ">" + "</u:" + action + ">" + "</s:Body>" + "</s:Envelope>";
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
- if (wemoURL != null) {
- wemoCall.executeCall(wemoURL, soapHeader, content);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null && logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
}
} catch (Exception e) {
logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
}
public void setTimerStart(String action, String argument, String value) {
+ String localHost = getHost();
+ if (localHost.isEmpty()) {
+ logger.error("Failed to set timerStart for device '{}': IP address missing", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
+ String wemoURL = getWemoURL(localHost, BASICACTION);
+ if (wemoURL == null) {
+ logger.error("Failed to set timerStart for device '{}': URL cannot be created", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
+ }
try {
String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
String content = "<?xml version=\"1.0\"?>"
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
+ "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + value
+ "</u:SetBinaryState>" + "</s:Body>" + "</s:Envelope>";
-
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
- if (wemoURL != null) {
- wemoCall.executeCall(wemoURL, soapHeader, content);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null && logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
}
} catch (Exception e) {
- logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
+ logger.debug("Failed to set timerStart '{}' for device '{}': {}", value, getThing().getUID(),
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
+ public String getHost() {
+ String localHost = host;
+ if (!localHost.isEmpty()) {
+ return localHost;
+ }
+ UpnpIOService localService = service;
+ if (localService != null) {
+ URL descriptorURL = localService.getDescriptorURL(this);
+ if (descriptorURL != null) {
+ return descriptorURL.getHost();
+ }
+ }
+ return "";
+ }
+
@Override
public void onStatusChanged(boolean status) {
}
.of(THING_TYPE_SOCKET, THING_TYPE_INSIGHT, THING_TYPE_LIGHTSWITCH, THING_TYPE_MOTION)
.collect(Collectors.toSet());
- private Map<String, Boolean> subscriptionState = new HashMap<>();
+ private final Object upnpLock = new Object();
+ private final Object jobLock = new Object();
private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
- protected UpnpIOService service;
- private WemoHttpCall wemoCall;
+ private @Nullable UpnpIOService service;
- private @Nullable ScheduledFuture<?> refreshJob;
+ private WemoHttpCall wemoCall;
- private final Runnable refreshRunnable = new Runnable() {
+ private Map<String, Boolean> subscriptionState = new HashMap<>();
- @Override
- public void run() {
- try {
- if (!isUpnpDeviceRegistered()) {
- logger.debug("WeMo UPnP device {} not yet registered", getUDN());
- }
+ private @Nullable ScheduledFuture<?> pollingJob;
- updateWemoState();
- onSubscription();
- } catch (Exception e) {
- logger.debug("Exception during poll", e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- }
- }
- };
+ private String host = "";
public WemoHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
super(thing, wemoHttpCaller);
public void initialize() {
Configuration configuration = getConfig();
- if (configuration.get("udn") != null) {
- logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get("udn"));
- service.registerParticipant(this);
- onSubscription();
- onUpdate();
+ if (configuration.get(UDN) != null) {
+ logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get(UDN));
+ UpnpIOService localService = service;
+ if (localService != null) {
+ localService.registerParticipant(this);
+ }
+ host = getHost();
+ pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
+ TimeUnit.SECONDS);
updateStatus(ThingStatus.ONLINE);
} else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/config-status.error.missing-udn");
logger.debug("Cannot initalize WemoHandler. UDN not set.");
}
}
@Override
public void dispose() {
- logger.debug("WeMoHandler disposed.");
+ logger.debug("WemoHandler disposed for thing {}", getThing().getUID());
- ScheduledFuture<?> job = refreshJob;
- if (job != null && !job.isCancelled()) {
+ ScheduledFuture<?> job = this.pollingJob;
+ if (job != null) {
job.cancel(true);
}
- refreshJob = null;
+ this.pollingJob = null;
removeSubscription();
}
+ private void poll() {
+ synchronized (jobLock) {
+ if (pollingJob == null) {
+ return;
+ }
+ try {
+ logger.debug("Polling job");
+ host = getHost();
+ // Check if the Wemo device is set in the UPnP service registry
+ // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
+ if (!isUpnpDeviceRegistered()) {
+ logger.debug("UPnP device {} not yet registered", getUDN());
+ updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+ "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
+ synchronized (upnpLock) {
+ subscriptionState = new HashMap<>();
+ }
+ return;
+ }
+ updateStatus(ThingStatus.ONLINE);
+ updateWemoState();
+ addSubscription();
+ } catch (Exception e) {
+ logger.debug("Exception during poll: {}", e.getMessage(), e);
+ }
+ }
+ }
+
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
- logger.trace("Command '{}' received for channel '{}'", command, channelUID);
-
+ String localHost = getHost();
+ if (localHost.isEmpty()) {
+ logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
+ getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
+ String wemoURL = getWemoURL(localHost, BASICACTION);
+ if (wemoURL == null) {
+ logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
+ getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
+ }
if (command instanceof RefreshType) {
try {
updateWemoState();
} catch (Exception e) {
logger.debug("Exception during poll", e);
}
- } else if (channelUID.getId().equals(CHANNEL_STATE)) {
+ } else if (CHANNEL_STATE.equals(channelUID.getId())) {
if (command instanceof OnOffType) {
try {
- String binaryState = null;
-
- if (command.equals(OnOffType.ON)) {
- binaryState = "1";
- } else if (command.equals(OnOffType.OFF)) {
- binaryState = "0";
- }
-
+ boolean binaryState = OnOffType.ON.equals(command) ? true : false;
String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
-
- String content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">"
- + "<BinaryState>" + binaryState + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>"
- + "</s:Envelope>";
-
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
- if (wemoURL != null) {
- wemoCall.executeCall(wemoURL, soapHeader, content);
+ String content = createBinaryStateContent(binaryState);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null && logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
+ getThing().getUID());
}
} catch (Exception e) {
logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
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);
getThing().getUID());
updateState(CHANNEL_ENERGYTOTAL, energyTotal);
- BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
- State standByLimit = new QuantityType<>(
- standByLimitMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP), Units.WATT); // recalculate
- // mW to W
- logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
- getThing().getUID());
- updateState(CHANNEL_STANDBYLIMIT, standByLimit);
+ if (splitInsightParams.length > 10 && splitInsightParams[10] != null) {
+ BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
+ State standByLimit = new QuantityType<>(
+ standByLimitMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP), Units.WATT); // recalculate
+ // mW to W
+ logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
+ getThing().getUID());
+ updateState(CHANNEL_STANDBYLIMIT, standByLimit);
- if (currentMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue() > standByLimitMW
- .divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue()) {
- updateState(CHANNEL_ONSTANDBY, OnOffType.OFF);
- } else {
- updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
+ if (currentMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue() > standByLimitMW
+ .divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue()) {
+ updateState(CHANNEL_ONSTANDBY, OnOffType.OFF);
+ } else {
+ updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
+ }
}
}
- } else {
+ } else if (value != null && value.length() == 1) {
String binaryState = stateMap.get("BinaryState");
if (binaryState != null) {
- State state = binaryState.equals("0") ? OnOffType.OFF : OnOffType.ON;
- logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
- if (getThing().getThingTypeUID().getId().equals("motion")) {
- updateState(CHANNEL_MOTIONDETECTION, state);
- if (state.equals(OnOffType.ON)) {
- State lastMotionDetected = new DateTimeType();
- updateState(CHANNEL_LASTMOTIONDETECTED, lastMotionDetected);
+ if (oldValue == null || !oldValue.equals(binaryState)) {
+ State state = "0".equals(binaryState) ? OnOffType.OFF : OnOffType.ON;
+ logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
+ if ("motion".equals(getThing().getThingTypeUID().getId())) {
+ updateState(CHANNEL_MOTIONDETECTION, state);
+ if (OnOffType.ON.equals(state)) {
+ State lastMotionDetected = new DateTimeType();
+ updateState(CHANNEL_LASTMOTIONDETECTED, lastMotionDetected);
+ }
+ } else {
+ updateState(CHANNEL_STATE, state);
}
- } else {
- updateState(CHANNEL_STATE, state);
}
}
}
}
- private synchronized void onSubscription() {
- if (service.isRegistered(this)) {
- logger.debug("Checking WeMo GENA subscription for '{}'", this);
-
- ThingTypeUID thingTypeUID = thing.getThingTypeUID();
- String subscription = "basicevent1";
-
- if (subscriptionState.get(subscription) == null) {
- logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
- service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
- subscriptionState.put(subscription, true);
- }
+ private synchronized void addSubscription() {
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
+
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ String subscription = BASICEVENT;
+
+ if (subscriptionState.get(subscription) == null) {
+ logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+ subscription);
+ localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+ subscriptionState.put(subscription, true);
+ }
- if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
- subscription = "insight1";
- if (subscriptionState.get(subscription) == null) {
- logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
- subscription);
- service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
- subscriptionState.put(subscription, true);
+ if (THING_TYPE_INSIGHT.equals(thingTypeUID)) {
+ subscription = INSIGHTEVENT;
+ if (subscriptionState.get(subscription) == null) {
+ logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+ subscription);
+ localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+ subscriptionState.put(subscription, true);
+ }
+ }
+ } else {
+ logger.debug(
+ "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+ getThing().getUID());
}
}
- } else {
- logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
- this);
}
}
private synchronized void removeSubscription() {
- logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
- if (service.isRegistered(this)) {
- ThingTypeUID thingTypeUID = thing.getThingTypeUID();
- String subscription = "basicevent1";
-
- if (subscriptionState.get(subscription) != null) {
- logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
- service.removeSubscription(this, subscription);
- }
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ String subscription = BASICEVENT;
+
+ if (subscriptionState.get(subscription) != null) {
+ logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+ localService.removeSubscription(this, subscription);
+ }
- if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
- subscription = "insight1";
- if (subscriptionState.get(subscription) != null) {
- logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
- service.removeSubscription(this, subscription);
+ if (THING_TYPE_INSIGHT.equals(thingTypeUID)) {
+ subscription = INSIGHTEVENT;
+ if (subscriptionState.get(subscription) != null) {
+ logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+ localService.removeSubscription(this, subscription);
+ }
+ }
+ subscriptionState = new HashMap<>();
+ localService.unregisterParticipant(this);
}
}
- subscriptionState = new HashMap<>();
- service.unregisterParticipant(this);
- }
- }
-
- private synchronized void onUpdate() {
- ScheduledFuture<?> job = refreshJob;
- if (job == null || job.isCancelled()) {
- Configuration config = getThing().getConfiguration();
- int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
- Object refreshConfig = config.get("refresh");
- if (refreshConfig != null) {
- refreshInterval = ((BigDecimal) refreshConfig).intValue();
- }
- refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
}
}
private boolean isUpnpDeviceRegistered() {
- return service.isRegistered(this);
+ UpnpIOService localService = service;
+ if (localService != null) {
+ return localService.isRegistered(this);
+ }
+ return false;
}
@Override
return (String) this.getThing().getConfiguration().get(UDN);
}
+ public String getHost() {
+ String localHost = host;
+ if (!localHost.isEmpty()) {
+ return localHost;
+ }
+ UpnpIOService localService = service;
+ if (localService != null) {
+ URL descriptorURL = localService.getDescriptorURL(this);
+ if (descriptorURL != null) {
+ return descriptorURL.getHost();
+ }
+ }
+ return "";
+ }
+
/**
* The {@link updateWemoState} polls the actual state of a WeMo device and
* calls {@link onValueReceived} to update the statemap and channels..
*
*/
protected void updateWemoState() {
+ String actionService = BASICACTION;
+ String localhost = getHost();
+ if (localhost.isEmpty()) {
+ logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
String action = "GetBinaryState";
String variable = "BinaryState";
- String actionService = "basicevent";
String value = null;
-
- if (getThing().getThingTypeUID().getId().equals("insight")) {
+ if ("insight".equals(getThing().getThingTypeUID().getId())) {
action = "GetInsightParams";
variable = "InsightParams";
- actionService = "insight";
+ actionService = INSIGHTACTION;
+ }
+ String wemoURL = getWemoURL(localhost, actionService);
+ if (wemoURL == null) {
+ logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
}
-
String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
- String content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
- + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+ String content = createStateRequestContent(action, actionService);
try {
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, actionService);
-
- if (wemoURL != null) {
- String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
- if (wemoCallResponse != null) {
- logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
- if (variable.equals("InsightParams")) {
- value = substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
- } else {
- value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
- }
- if (value.length() != 0) {
- logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
- this.onValueReceived(variable, value, actionService + "1");
- }
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+ }
+ if ("InsightParams".equals(variable)) {
+ value = substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
+ } else {
+ value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
+ }
+ if (value.length() != 0) {
+ logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
+ this.onValueReceived(variable, value, actionService + "1");
}
}
} catch (Exception e) {
import java.io.IOException;
import java.io.StringReader;
-import java.math.BigDecimal;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
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;
private static final int FILTER_LIFE_DAYS = 330;
private static final int FILTER_LIFE_MINS = FILTER_LIFE_DAYS * 24 * 60;
- private final Map<String, Boolean> subscriptionState = new HashMap<>();
+
+ private final Object upnpLock = new Object();
+ private final Object jobLock = new Object();
+
private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
- private UpnpIOService service;
+ private @Nullable UpnpIOService service;
+
private WemoHttpCall wemoCall;
- private @Nullable ScheduledFuture<?> refreshJob;
+ private String host = "";
- private final Runnable refreshRunnable = () -> {
- if (!isUpnpDeviceRegistered()) {
- logger.debug("WeMo UPnP device {} not yet registered", getUDN());
- } else {
- updateWemoState();
- onSubscription();
- }
- };
+ private Map<String, Boolean> subscriptionState = new HashMap<>();
+
+ private @Nullable ScheduledFuture<?> pollingJob;
public WemoHolmesHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
super(thing, wemoHttpCaller);
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.");
}
}
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;
+ "<attributeList><attribute><name>" + attribute + "</name><value>" + value
+ "</value></attribute></attributeList>" + "</u:SetAttributes>" + "</s:Body>"
+ "</s:Envelope>";
-
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "deviceevent");
-
- if (wemoURL != null) {
- wemoCall.executeCall(wemoURL, soapHeader, content);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null && logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
}
} catch (RuntimeException e) {
logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
}
}
- private synchronized void onSubscription() {
- if (service.isRegistered(this)) {
- logger.debug("Checking WeMo GENA subscription for '{}'", this);
+ private synchronized void addSubscription() {
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
- String subscription = "basicevent1";
+ String subscription = BASICEVENT;
- if (subscriptionState.get(subscription) == null) {
- logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
- service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
- subscriptionState.put(subscription, true);
+ if (subscriptionState.get(subscription) == null) {
+ logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+ subscription);
+ localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
+ subscriptionState.put(subscription, true);
+ }
+ } else {
+ logger.debug(
+ "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+ getThing().getUID());
+ }
}
-
- } else {
- logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
- this);
}
}
private synchronized void removeSubscription() {
- logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
- if (service.isRegistered(this)) {
- String subscription = "basicevent1";
-
- if (subscriptionState.get(subscription) != null) {
- logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
- service.removeSubscription(this, subscription);
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+ String subscription = BASICEVENT;
+
+ if (subscriptionState.get(subscription) != null) {
+ logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
+ localService.removeSubscription(this, subscription);
+ }
+ subscriptionState.remove(subscription);
+ localService.unregisterParticipant(this);
+ }
}
-
- subscriptionState.remove(subscription);
- service.unregisterParticipant(this);
- }
- }
-
- private synchronized void onUpdate() {
- ScheduledFuture<?> job = refreshJob;
- if (job == null || job.isCancelled()) {
- Configuration config = getThing().getConfiguration();
- int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
- Object refreshConfig = config.get("refresh");
- refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVALL_SECONDS
- : ((BigDecimal) refreshConfig).intValue();
- refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
}
}
private boolean isUpnpDeviceRegistered() {
- return service.isRegistered(this);
+ UpnpIOService localService = service;
+ if (localService != null) {
+ return localService.isRegistered(this);
+ }
+ return false;
}
@Override
*
*/
protected void updateWemoState() {
- String action = "GetAttributes";
- String actionService = "deviceevent";
-
- String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
- String content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
- + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+ String localHost = getHost();
+ if (localHost.isEmpty()) {
+ logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
+ String actionService = DEVICEACTION;
+ String wemoURL = getWemoURL(localHost, actionService);
+ if (wemoURL == null) {
+ logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
+ }
try {
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, actionService);
-
- if (wemoURL != null) {
- String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
- if (wemoCallResponse != null) {
- logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
-
- String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
-
- // Due to Belkins bad response formatting, we need to run this twice.
- stringParser = unescapeXml(stringParser);
- stringParser = unescapeXml(stringParser);
-
- logger.trace("AirPurifier response '{}' for device '{}' received", stringParser,
- getThing().getUID());
+ String action = "GetAttributes";
+ String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
+ String content = createStateRequestContent(action, actionService);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+ }
- stringParser = "<data>" + stringParser + "</data>";
-
- DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
- // see
- // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
- dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
- dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
- dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
- dbf.setXIncludeAware(false);
- dbf.setExpandEntityReferences(false);
- DocumentBuilder db = dbf.newDocumentBuilder();
- InputSource is = new InputSource();
- is.setCharacterStream(new StringReader(stringParser));
-
- Document doc = db.parse(is);
- NodeList nodes = doc.getElementsByTagName("attribute");
-
- // iterate the attributes
- for (int i = 0; i < nodes.getLength(); i++) {
- Element element = (Element) nodes.item(i);
-
- NodeList deviceIndex = element.getElementsByTagName("name");
- Element line = (Element) deviceIndex.item(0);
- String attributeName = getCharacterDataFromElement(line);
- logger.trace("attributeName: {}", attributeName);
-
- NodeList deviceID = element.getElementsByTagName("value");
- line = (Element) deviceID.item(0);
- String attributeValue = getCharacterDataFromElement(line);
- logger.trace("attributeValue: {}", attributeValue);
-
- State newMode = new StringType();
- switch (attributeName) {
- case "Mode":
- if ("purifier".equals(getThing().getThingTypeUID().getId())) {
- switch (attributeValue) {
- case "0":
- newMode = new StringType("OFF");
- break;
- case "1":
- newMode = new StringType("LOW");
- break;
- case "2":
- newMode = new StringType("MED");
- break;
- case "3":
- newMode = new StringType("HIGH");
- break;
- case "4":
- newMode = new StringType("AUTO");
- break;
- }
- updateState(CHANNEL_PURIFIERMODE, newMode);
- } else {
- switch (attributeValue) {
- case "0":
- newMode = new StringType("OFF");
- break;
- case "1":
- newMode = new StringType("FROSTPROTECT");
- break;
- case "2":
- newMode = new StringType("HIGH");
- break;
- case "3":
- newMode = new StringType("LOW");
- break;
- case "4":
- newMode = new StringType("ECO");
- break;
- }
- updateState(CHANNEL_HEATERMODE, newMode);
- }
- break;
- case "Ionizer":
- switch (attributeValue) {
- case "0":
- newMode = OnOffType.OFF;
- break;
- case "1":
- newMode = OnOffType.ON;
- break;
- }
- updateState(CHANNEL_IONIZER, newMode);
- break;
- case "AirQuality":
- switch (attributeValue) {
- case "0":
- newMode = new StringType("POOR");
- break;
- case "1":
- newMode = new StringType("MODERATE");
- break;
- case "2":
- newMode = new StringType("GOOD");
- break;
- }
- updateState(CHANNEL_AIRQUALITY, newMode);
- break;
- case "FilterLife":
- int filterLife = Integer.valueOf(attributeValue);
- if ("purifier".equals(getThing().getThingTypeUID().getId())) {
- filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
- } else {
- filterLife = Math.round((filterLife / 60480) * 100);
- }
- updateState(CHANNEL_FILTERLIFE, new PercentType(String.valueOf(filterLife)));
- break;
- case "ExpiredFilterTime":
- switch (attributeValue) {
- case "0":
- newMode = OnOffType.OFF;
- break;
- case "1":
- newMode = OnOffType.ON;
- break;
- }
- updateState(CHANNEL_EXPIREDFILTERTIME, newMode);
- break;
- case "FilterPresent":
- switch (attributeValue) {
- case "0":
- newMode = OnOffType.OFF;
- break;
- case "1":
- newMode = OnOffType.ON;
- break;
- }
- updateState(CHANNEL_FILTERPRESENT, newMode);
- break;
- case "FANMode":
+ String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
+
+ // Due to Belkins bad response formatting, we need to run this twice.
+ stringParser = unescapeXml(stringParser);
+ stringParser = unescapeXml(stringParser);
+
+ logger.trace("AirPurifier response '{}' for device '{}' received", stringParser, getThing().getUID());
+
+ stringParser = "<data>" + stringParser + "</data>";
+
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ // see
+ // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
+ dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ dbf.setXIncludeAware(false);
+ dbf.setExpandEntityReferences(false);
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ InputSource is = new InputSource();
+ is.setCharacterStream(new StringReader(stringParser));
+
+ Document doc = db.parse(is);
+ NodeList nodes = doc.getElementsByTagName("attribute");
+
+ // iterate the attributes
+ for (int i = 0; i < nodes.getLength(); i++) {
+ Element element = (Element) nodes.item(i);
+
+ NodeList deviceIndex = element.getElementsByTagName("name");
+ Element line = (Element) deviceIndex.item(0);
+ String attributeName = getCharacterDataFromElement(line);
+ logger.trace("attributeName: {}", attributeName);
+
+ NodeList deviceID = element.getElementsByTagName("value");
+ line = (Element) deviceID.item(0);
+ String attributeValue = getCharacterDataFromElement(line);
+ logger.trace("attributeValue: {}", attributeValue);
+
+ State newMode = new StringType();
+ switch (attributeName) {
+ case "Mode":
+ if ("purifier".equals(getThing().getThingTypeUID().getId())) {
switch (attributeValue) {
case "0":
newMode = new StringType("OFF");
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;
}
}
}
updateStatus(ThingStatus.ONLINE);
}
- public static String getCharacterDataFromElement(Element e) {
- Node child = e.getFirstChild();
- if (child instanceof CharacterData) {
- CharacterData cd = (CharacterData) child;
- return cd.getData();
+ public String getHost() {
+ String localHost = host;
+ if (!localHost.isEmpty()) {
+ return localHost;
+ }
+ UpnpIOService localService = service;
+ if (localService != null) {
+ URL descriptorURL = localService.getDescriptorURL(this);
+ if (descriptorURL != null) {
+ return descriptorURL.getHost();
+ }
}
- return "?";
+ return "";
}
@Override
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;
private Map<String, Boolean> subscriptionState = new HashMap<>();
- private UpnpIOService service;
- private WemoHttpCall wemoCall;
+ private final Object upnpLock = new Object();
+ private final Object jobLock = new Object();
private @Nullable WemoBridgeHandler wemoBridgeHandler;
+ private @Nullable UpnpIOService service;
+
+ private String host = "";
+
private @Nullable String wemoLightID;
private int currentBrightness;
+ private WemoHttpCall wemoCall;
+
/**
* Set dimming stepsize to 5%
*/
*/
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
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);
}
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;
}
}
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();
}
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();
break;
}
try {
- String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
- String content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
- + "<DeviceStatusList>"
- + "<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><DeviceID>"
- + wemoLightID
- + "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>"
- + capability + "</CapabilityID><CapabilityValue>" + value
- + "</CapabilityValue></DeviceStatus>" + "</DeviceStatusList>"
- + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
-
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "bridge");
-
- if (wemoURL != null && capability != null && value != null) {
+ if (capability != null && value != null) {
+ String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
+ String content = "<?xml version=\"1.0\"?>"
+ + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
+ + "<s:Body>" + "<u:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
+ + "<DeviceStatusList>"
+ + "<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><DeviceID>"
+ + wemoLightID
+ + "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>"
+ + capability + "</CapabilityID><CapabilityValue>" + value
+ + "</CapabilityValue></DeviceStatus>" + "</DeviceStatusList>"
+ + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
+
String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
if (wemoCallResponse != null) {
- if (capability.equals("10008")) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader,
+ getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
+ getThing().getUID());
+ }
+ if ("10008".equals(capability)) {
OnOffType binaryState = null;
- binaryState = value.equals("0") ? OnOffType.OFF : OnOffType.ON;
+ binaryState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
updateState(CHANNEL_STATE, binaryState);
}
}
* channel states.
*/
public void getDeviceState() {
+ String localHost = getHost();
+ if (localHost.isEmpty()) {
+ logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
logger.debug("Request actual state for LightID '{}'", wemoLightID);
+ String wemoURL = getWemoURL(localHost, BRIDGEACTION);
+ if (wemoURL == null) {
+ logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
+ }
try {
String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
String content = "<?xml version=\"1.0\"?>"
+ "<s:Body>" + "<u:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>"
+ wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "bridge");
-
- if (wemoURL != null) {
- String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
- if (wemoCallResponse != null) {
- wemoCallResponse = unescapeXml(wemoCallResponse);
- String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
- logger.trace("wemoNewLightState = {}", response);
- String[] splitResponse = response.split(",");
- if (splitResponse[0] != null) {
- OnOffType binaryState = null;
- binaryState = splitResponse[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
- updateState(CHANNEL_STATE, binaryState);
- }
- if (splitResponse[1] != null) {
- String splitBrightness[] = splitResponse[1].split(":");
- if (splitBrightness[0] != null) {
- int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
- int newBrightness = Math.round(newBrightnessValue * 100 / 255);
- logger.trace("newBrightness = {}", newBrightness);
- State newBrightnessState = new PercentType(newBrightness);
- updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
- currentBrightness = newBrightness;
- }
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+ }
+ wemoCallResponse = unescapeXml(wemoCallResponse);
+ String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
+ logger.trace("wemoNewLightState = {}", response);
+ String[] splitResponse = response.split(",");
+ if (splitResponse[0] != null) {
+ OnOffType binaryState = null;
+ binaryState = "0".equals(splitResponse[0]) ? OnOffType.OFF : OnOffType.ON;
+ updateState(CHANNEL_STATE, binaryState);
+ }
+ if (splitResponse[1] != null) {
+ String splitBrightness[] = splitResponse[1].split(":");
+ if (splitBrightness[0] != null) {
+ int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
+ int newBrightness = Math.round(newBrightnessValue * 100 / 255);
+ logger.trace("newBrightness = {}", newBrightness);
+ State newBrightnessState = new PercentType(newBrightness);
+ updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
+ currentBrightness = newBrightness;
}
}
}
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":
public void onStatusChanged(boolean status) {
}
- private synchronized void onSubscription() {
- if (service.isRegistered(this)) {
- logger.debug("Checking WeMo GENA subscription for '{}'", this);
-
- if (subscriptionState.get(SUBSCRIPTION) == null) {
- logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), SUBSCRIPTION);
- service.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION_SECONDS);
- subscriptionState.put(SUBSCRIPTION, true);
+ private synchronized void addSubscription() {
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
+
+ if (subscriptionState.get(SUBSCRIPTION) == null) {
+ logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
+ SUBSCRIPTION);
+ localService.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION_SECONDS);
+ subscriptionState.put(SUBSCRIPTION, true);
+ }
+ } else {
+ logger.debug(
+ "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
+ getThing().getUID());
+ }
}
- } else {
- logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
- this);
}
}
private synchronized void removeSubscription() {
- if (service.isRegistered(this)) {
- logger.debug("Removing WeMo GENA subscription for '{}'", this);
-
- if (subscriptionState.get(SUBSCRIPTION) != null) {
- logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
- service.removeSubscription(this, SUBSCRIPTION);
+ synchronized (upnpLock) {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ if (localService.isRegistered(this)) {
+ logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
+
+ if (subscriptionState.get(SUBSCRIPTION) != null) {
+ logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
+ localService.removeSubscription(this, SUBSCRIPTION);
+ }
+ subscriptionState = new HashMap<>();
+ localService.unregisterParticipant(this);
+ }
}
-
- subscriptionState = new HashMap<>();
- service.unregisterParticipant(this);
}
}
- private synchronized void onUpdate() {
- ScheduledFuture<?> job = refreshJob;
- if (job == null || job.isCancelled()) {
- Configuration config = getThing().getConfiguration();
- int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
- Object refreshConfig = config.get("refresh");
- if (refreshConfig != null) {
- refreshInterval = ((BigDecimal) refreshConfig).intValue();
- }
- logger.trace("Start polling job for LightID '{}'", wemoLightID);
- refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, DEFAULT_REFRESH_INITIAL_DELAY,
- refreshInterval, TimeUnit.SECONDS);
+ private boolean isUpnpDeviceRegistered() {
+ UpnpIOService localService = service;
+ if (localService != null) {
+ return localService.isRegistered(this);
}
+ return false;
}
- private boolean isUpnpDeviceRegistered() {
- return service.isRegistered(this);
+ public String getHost() {
+ String localHost = host;
+ if (!localHost.isEmpty()) {
+ return localHost;
+ }
+ UpnpIOService localService = service;
+ if (localService != null) {
+ URL descriptorURL = localService.getDescriptorURL(this);
+ if (descriptorURL != null) {
+ return descriptorURL.getHost();
+ }
+ }
+ return "";
}
}
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;
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;
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
- private UpnpIOService service;
- private WemoHttpCall wemoCall;
+ private final Object jobLock = new Object();
- private @Nullable ScheduledFuture<?> refreshJob;
+ private @Nullable UpnpIOService service;
- private final Runnable refreshRunnable = new Runnable() {
+ private WemoHttpCall wemoCall;
- @Override
- public void run() {
- try {
- updateWemoState();
- } catch (Exception e) {
- logger.debug("Exception during poll", e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- }
- }
- };
+ private String host = "";
+
+ private @Nullable ScheduledFuture<?> pollingJob;
public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
super(thing, wemoHttpcaller);
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.");
}
}
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();
} else if (channelUID.getId().equals(CHANNEL_RELAY)) {
if (command instanceof OnOffType) {
try {
- String binaryState = null;
-
- if (command.equals(OnOffType.ON)) {
- binaryState = "1";
- } else if (command.equals(OnOffType.OFF)) {
- binaryState = "0";
- }
-
+ boolean binaryState = OnOffType.ON.equals(command) ? true : false;
String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
-
- String content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">"
- + "<BinaryState>" + binaryState + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>"
- + "</s:Envelope>";
-
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, "basicevent");
-
- if (wemoURL != null) {
- wemoCall.executeCall(wemoURL, soapHeader, content);
+ String content = createBinaryStateContent(binaryState);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null && logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
+ getThing().getUID());
}
} catch (Exception e) {
logger.error("Failed to send command '{}' for device '{}' ", command, getThing().getUID(), e);
}
}
- @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
* The {@link updateWemoState} polls the actual state of a WeMo Maker.
*/
protected void updateWemoState() {
- String action = "GetAttributes";
- String actionService = "deviceevent";
-
- String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
- String content = "<?xml version=\"1.0\"?>"
- + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
- + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
- + action + ">" + "</s:Body>" + "</s:Envelope>";
-
+ String localHost = getHost();
+ if (localHost.isEmpty()) {
+ logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-ip");
+ return;
+ }
+ String actionService = DEVICEACTION;
+ String wemoURL = getWemoURL(localHost, actionService);
+ if (wemoURL == null) {
+ logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/config-status.error.missing-url");
+ return;
+ }
try {
- URL descriptorURL = service.getDescriptorURL(this);
- String wemoURL = getWemoURL(descriptorURL, actionService);
-
- if (wemoURL != null) {
- String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
- if (wemoCallResponse != null) {
- try {
- String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
- logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
- logger.trace("'{}'", stringParser);
-
- // Due to Belkins bad response formatting, we need to run this twice.
- stringParser = unescapeXml(stringParser);
- stringParser = unescapeXml(stringParser);
- logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID());
-
- stringParser = "<data>" + stringParser + "</data>";
-
- DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
- // see
- // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
- dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
- dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
- dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
- dbf.setXIncludeAware(false);
- dbf.setExpandEntityReferences(false);
- DocumentBuilder db = dbf.newDocumentBuilder();
- InputSource is = new InputSource();
- is.setCharacterStream(new StringReader(stringParser));
-
- Document doc = db.parse(is);
- NodeList nodes = doc.getElementsByTagName("attribute");
-
- // iterate the attributes
- for (int i = 0; i < nodes.getLength(); i++) {
- Element element = (Element) nodes.item(i);
-
- NodeList deviceIndex = element.getElementsByTagName("name");
- Element line = (Element) deviceIndex.item(0);
- String attributeName = getCharacterDataFromElement(line);
- logger.trace("attributeName: {}", attributeName);
-
- NodeList deviceID = element.getElementsByTagName("value");
- line = (Element) deviceID.item(0);
- String attributeValue = getCharacterDataFromElement(line);
- logger.trace("attributeValue: {}", attributeValue);
-
- switch (attributeName) {
- case "Switch":
- State relayState = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
- logger.debug("New relayState '{}' for device '{}' received", relayState,
- getThing().getUID());
- updateState(CHANNEL_RELAY, relayState);
- break;
- case "Sensor":
- State sensorState = attributeValue.equals("1") ? OnOffType.OFF : OnOffType.ON;
- logger.debug("New sensorState '{}' for device '{}' received", sensorState,
- getThing().getUID());
- updateState(CHANNEL_SENSOR, sensorState);
- break;
- }
+ String action = "GetAttributes";
+ String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
+ String content = createStateRequestContent(action, actionService);
+ String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
+ if (wemoCallResponse != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
+ logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
+ logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
+ logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
+ }
+ try {
+ String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
+ logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
+ logger.trace("'{}'", stringParser);
+
+ // Due to Belkins bad response formatting, we need to run this twice.
+ stringParser = unescapeXml(stringParser);
+ stringParser = unescapeXml(stringParser);
+ logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID());
+
+ stringParser = "<data>" + stringParser + "</data>";
+
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ // see
+ // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
+ dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ dbf.setXIncludeAware(false);
+ dbf.setExpandEntityReferences(false);
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ InputSource is = new InputSource();
+ is.setCharacterStream(new StringReader(stringParser));
+
+ Document doc = db.parse(is);
+ NodeList nodes = doc.getElementsByTagName("attribute");
+
+ // iterate the attributes
+ for (int i = 0; i < nodes.getLength(); i++) {
+ Element element = (Element) nodes.item(i);
+
+ NodeList deviceIndex = element.getElementsByTagName("name");
+ Element line = (Element) deviceIndex.item(0);
+ String attributeName = getCharacterDataFromElement(line);
+ logger.trace("attributeName: {}", attributeName);
+
+ NodeList deviceID = element.getElementsByTagName("value");
+ line = (Element) deviceID.item(0);
+ String attributeValue = getCharacterDataFromElement(line);
+ logger.trace("attributeValue: {}", attributeValue);
+
+ switch (attributeName) {
+ case "Switch":
+ State relayState = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
+ logger.debug("New relayState '{}' for device '{}' received", relayState,
+ getThing().getUID());
+ updateState(CHANNEL_RELAY, relayState);
+ break;
+ case "Sensor":
+ State sensorState = "1".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
+ logger.debug("New sensorState '{}' for device '{}' received", sensorState,
+ getThing().getUID());
+ updateState(CHANNEL_SENSOR, sensorState);
+ break;
}
- } catch (Exception e) {
- logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
}
+ } catch (Exception e) {
+ logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
}
}
} catch (Exception e) {
}
}
- public static String getCharacterDataFromElement(Element e) {
- Node child = e.getFirstChild();
- if (child instanceof CharacterData) {
- CharacterData cd = (CharacterData) child;
- return cd.getData();
+ public String getHost() {
+ String localHost = host;
+ if (!localHost.isEmpty()) {
+ return localHost;
+ }
+ UpnpIOService localService = service;
+ if (localService != null) {
+ URL descriptorURL = localService.getDescriptorURL(this);
+ if (descriptorURL != null) {
+ return descriptorURL.getHost();
+ }
}
- return "?";
+ return "";
}
@Override
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.
<label>Unique Device Name</label>
<description>The UDN identifies the WeMo Device</description>
</parameter>
-
</config-description>
</thing-type>
<label>Unique Device Name</label>
<description>The UDN identifies the WeMo Device</description>
</parameter>
+ </config-description>
+ </thing-type>
+ <thing-type id="Crockpot">
+ <label>Crock-Pot Slow Cooker</label>
+ <description>Crock-Pot Smart Slow Cooker with WeMo</description>
+
+ <channels>
+ <channel id="cookMode" typeId="cookMode"/>
+ <channel id="warmCookTime" typeId="warmCookTime"/>
+ <channel id="lowCookTime" typeId="lowCookTime"/>
+ <channel id="highCookTime" typeId="highCookTime"/>
+ <channel id="cookedTime" typeId="cookedTime"/>
+ </channels>
+
+ <config-description>
+ <parameter name="udn" type="text">
+ <label>Unique Device Name</label>
+ <description>The UDN identifies the WeMo Device</description>
+ <required>true</required>
+ </parameter>
+ </config-description>
+ </thing-type>
+
+ <thing-type id="Purifier">
+ <label>Holmes Air Purifier</label>
+ <description>Holmes Smart Air Purifier with WeMo</description>
+
+ <channels>
+ <channel id="purifierMode" typeId="purifierMode"/>
+ <channel id="airQuality" typeId="airQuality"/>
+ <channel id="ionizer" typeId="ionizer"/>
+ <channel id="filterLife" typeId="filterLife"/>
+ <channel id="expiredFilterTime" typeId="expiredFilterTime"/>
+ <channel id="filterPresent" typeId="filterPresent"/>
+ </channels>
+
+ <config-description>
+ <parameter name="udn" type="text">
+ <label>Unique Device Name</label>
+ <description>The UDN identifies the WeMo Device</description>
+ <required>true</required>
+ </parameter>
+ </config-description>
+ </thing-type>
+
+ <thing-type id="Humidifier">
+ <label>Holmes Humidifier</label>
+ <description>Holmes Smart Humidifier with WeMo</description>
+
+ <channels>
+ <channel id="humidifierMode" typeId="humidifierMode"/>
+ <channel id="desiredHumidity" typeId="desiredHumidity"/>
+ <channel id="currentHumidity" typeId="currentHumidity"/>
+ <channel id="waterLEvel" typeId="waterLEvel"/>
+ <channel id="filterLife" typeId="filterLife"/>
+ <channel id="expiredFilterTime" typeId="expiredFilterTime"/>
+ </channels>
+
+ <config-description>
+ <parameter name="udn" type="text">
+ <label>Unique Device Name</label>
+ <description>The UDN identifies the WeMo Device</description>
+ <required>true</required>
+ </parameter>
+ </config-description>
+ </thing-type>
+
+ <thing-type id="Heater">
+ <label>Holmes Heater</label>
+ <description>Holmes Smart Heater with WeMo</description>
+
+ <channels>
+ <channel id="heaterMode" typeId="heaterMode"/>
+ <channel id="currentTemperature" typeId="currentTemperature"/>
+ <channel id="targetTemperature" typeId="targetTemperature"/>
+ <channel id="autoOffTime" typeId="autoOffTime"/>
+ <channel id="heatingRemaining" typeId="heatingRemaining"/>
+ </channels>
+
+ <config-description>
+ <parameter name="udn" type="text">
+ <label>Unique Device Name</label>
+ <description>The UDN identifies the WeMo Device</description>
+ <required>true</required>
+ </parameter>
</config-description>
</thing-type>
<description>Allows setting the brightness of Night Mode</description>
</channel-type>
+ <channel-type id="cookMode">
+ <item-type>String</item-type>
+ <label>Cooking Mode</label>
+ <description>Shows the operation mode of a WeMo CrockPot</description>
+ <state readOnly="false">
+ <options>
+ <option value="OFF">Not cooking</option>
+ <option value="WARM">Warming</option>
+ <option value="LOW">Low cooking</option>
+ <option value="HIGH">High cooking</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="warmCookTime">
+ <item-type>Number</item-type>
+ <label>WarmCookTime</label>
+ <description>Shows the timer settings for warm cooking mode</description>
+ </channel-type>
+
+ <channel-type id="lowCookTime">
+ <item-type>Number</item-type>
+ <label>LowCookTime</label>
+ <description>Shows the timer settings for low cooking mode</description>
+ </channel-type>
+
+ <channel-type id="highCookTime">
+ <item-type>Number</item-type>
+ <label>HighCookTime</label>
+ <description>Shows the timer settings for high cooking mode</description>
+ </channel-type>
+
+ <channel-type id="cookedTime">
+ <item-type>Number</item-type>
+ <label>CookedTime</label>
+ <description>Shows the elapsed cooking time</description>
+ </channel-type>
+
+ <channel-type id="purifierMode">
+ <item-type>String</item-type>
+ <label>Operation Mode</label>
+ <description>Shows the operation mode of a WeMo enabled Holmes Air Purifier</description>
+ <state readOnly="false">
+ <options>
+ <option value="OFF">Not Running</option>
+ <option value="LOW">Running at low level</option>
+ <option value="MED">Running at medium level</option>
+ <option value="HIGH">Running at high level</option>
+ <option value="AUTO">Running in auto mode</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="airQiality">
+ <item-type>String</item-type>
+ <label>Air Quality</label>
+ <description>Shows the air quality measured by a WeMo enabled Holmes Air Purifier</description>
+ <state readOnly="true">
+ <options>
+ <option value="POOR"></option>
+ <option value="MODERATE"></option>
+ <option value="GOOD"></option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="ionizer">
+ <item-type>Switch</item-type>
+ <label>Ionizer</label>
+ <description>Switches ionization ON or OFF</description>
+ </channel-type>
+
+ <channel-type id="filterLife">
+ <item-type>Number</item-type>
+ <label>Filter Life</label>
+ <description>Shows the remaining lifetime percentage of the air filter</description>
+ </channel-type>
+
+ <channel-type id="filterExpired">
+ <item-type>Switch</item-type>
+ <label>Filter Time expired</label>
+ <description>Indicates whether the air Filter needs to be replaced</description>
+ <state readOnly="true"></state>
+ </channel-type>
+
+ <channel-type id="filterPresent">
+ <item-type>Switch</item-type>
+ <label>Filter is present</label>
+ <description>Indicates whether the air Filter is present</description>
+ <state readOnly="true"></state>
+ </channel-type>
+
+ <channel-type id="humidifierMode">
+ <item-type>String</item-type>
+ <label>Operation Mode</label>
+ <description>Shows the operation mode of a WeMo enabled Holmes Humidifier</description>
+ <state readOnly="false">
+ <options>
+ <option value="OFF">Not Running</option>
+ <option value="MIN">Running at min level</option>
+ <option value="LOW">Running at low level</option>
+ <option value="MED">Running at medium level</option>
+ <option value="HIGH">Running at high level</option>
+ <option value="MAX">Running in max level</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="currentHumidity">
+ <item-type>Number</item-type>
+ <label>Current Humidity</label>
+ <description>Shows the current humidity of a WeMo enabled Holmes Humidifier</description>
+ </channel-type>
+
+ <channel-type id="desiredHumidity">
+ <item-type>Number</item-type>
+ <label>Target Humidity</label>
+ <description>Shows the target humidity of a WeMo enabled Holmes Humidifier</description>
+ <state readOnly="false">
+ <options>
+ <option value="45"></option>
+ <option value="50"></option>
+ <option value="55"></option>
+ <option value="60"></option>
+ <option value="100"></option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="waterLevel">
+ <item-type>String</item-type>
+ <label>Water Level</label>
+ <description>Shows the water levele of a WeMo enabled Holmes Humidifier</description>
+ <state readOnly="true">
+ <options>
+ <option value="EMPTY"></option>
+ <option value="LOW"></option>
+ <option value="GOOD"></option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="heaterMode">
+ <item-type>String</item-type>
+ <label>Operation Mode</label>
+ <description>Shows the operation mode of a WeMo enabled Heater</description>
+ <state readOnly="false">
+ <options>
+ <option value="OFF">Not Running</option>
+ <option value="FROSTPROTECT">Running at FrostProtect</option>
+ <option value="HIGH">Running at high level</option>
+ <option value="LOW">Running at low level</option>
+ <option value="ECO">Running in Eco mode</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="currentTemperature">
+ <item-type>Number</item-type>
+ <label>Current Temperature</label>
+ <description>Shows the current temperature measured by a WeMo enabled Heater</description>
+ </channel-type>
+
+ <channel-type id="targetTemperature">
+ <item-type>Number</item-type>
+ <label>Target Temperature</label>
+ <description>Shows the target temperature for a WeMo enabled Heater</description>
+ </channel-type>
+
+ <channel-type id="autoOffTime">
+ <item-type>DateTime</item-type>
+ <label>Auto OFF Time</label>
+ <description>Time when a WeMo enabled Heater should switch off</description>
+ <state pattern="%1$tR" readOnly="false"/>
+ </channel-type>
+
+ <channel-type id="heatingRemaining">
+ <item-type>Number</item-type>
+ <label>Remaining heating time</label>
+ <description>Shows the target temperature for a WeMo enabled Heater</description>
+ <state readOnly="true"></state>
+ </channel-type>
+
</thing:thing-descriptions>
ChannelUID channelUID = new ChannelUID(thingUID, channelID);
ThingHandler handler = thing.getHandler();
assertNotNull(handler);
+
handler.handleCommand(channelUID, command);
ArgumentCaptor<String> captur = ArgumentCaptor.forClass(String.class);
ChannelUID channelUID = new ChannelUID(thing.getUID(), DEFAULT_TEST_CHANNEL);
ThingHandler handler = thing.getHandler();
assertNotNull(handler);
+
handler.handleCommand(channelUID, command);
ArgumentCaptor<String> captur = ArgumentCaptor.forClass(String.class);
ChannelUID channelUID = new ChannelUID(thing.getUID(), DEFAULT_TEST_CHANNEL);
ThingHandler handler = thing.getHandler();
assertNotNull(handler);
+
handler.handleCommand(channelUID, command);
ArgumentCaptor<String> captur = ArgumentCaptor.forClass(String.class);