/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.binding.wemo.handler;
import static org.eclipse.smarthome.binding.wemo.WemoBindingConstants.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.binding.wemo.internal.http.WemoHttpCall;
import org.eclipse.smarthome.config.core.Configuration;
import org.eclipse.smarthome.config.discovery.DiscoveryListener;
import org.eclipse.smarthome.config.discovery.DiscoveryResult;
import org.eclipse.smarthome.config.discovery.DiscoveryService;
import org.eclipse.smarthome.core.library.types.DateTimeType;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.io.transport.upnp.UpnpIOParticipant;
import org.eclipse.smarthome.io.transport.upnp.UpnpIOService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Sets;
/**
* The {@link WemoHandler} is responsible for handling commands, which are
* sent to one of the channels and to update their states.
*
* @author Hans-Jörg Merk - Initial contribution; Added support for WeMo Insight energy measurement
* @author Kai Kreuzer - some refactoring for performance and simplification
* @author Stefan Bußweiler - Added new thing status handling
*/
public class WemoHandler extends BaseThingHandler implements UpnpIOParticipant, DiscoveryListener {
private final Logger logger = LoggerFactory.getLogger(WemoHandler.class);
public final static Set<ThingTypeUID> SUPPORTED_THING_TYPES = Sets.newHashSet(THING_TYPE_SOCKET, THING_TYPE_INSIGHT,
THING_TYPE_LIGHTSWITCH, THING_TYPE_MOTION);
private Map<String, Boolean> subscriptionState = new HashMap<String, Boolean>();
private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<String, String>());
protected final static int SUBSCRIPTION_DURATION = 600;
private UpnpIOService service;
/**
* The default refresh interval in Seconds.
*/
private int DEFAULT_REFRESH_INTERVAL = 120;
private 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);
}
}
};
public WemoHandler(Thing thing, UpnpIOService upnpIOService) {
super(thing);
logger.debug("Creating a WemoHandler for thing '{}'", getThing().getUID());
if (upnpIOService != null) {
this.service = upnpIOService;
} else {
logger.debug("upnpIOService not set.");
}
}
@Override
public void initialize() {
Configuration configuration = getConfig();
if (configuration.get("udn") != null) {
logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get("udn"));
onSubscription();
onUpdate();
super.initialize();
} else {
logger.debug("Cannot initalize WemoHandler. UDN not set.");
}
}
@Override
public void thingDiscovered(DiscoveryService source, DiscoveryResult result) {
if (result.getThingUID().equals(this.getThing().getUID())) {
if (getThing().getConfiguration().get(UDN).equals(result.getProperties().get(UDN))) {
logger.trace("Discovered UDN '{}' for thing '{}'", result.getProperties().get(UDN),
getThing().getUID());
updateStatus(ThingStatus.ONLINE);
onSubscription();
onUpdate();
}
}
}
@Override
public void thingRemoved(DiscoveryService source, ThingUID thingUID) {
if (thingUID.equals(this.getThing().getUID())) {
logger.trace("Setting status for thing '{}' to OFFLINE", getThing().getUID());
updateStatus(ThingStatus.OFFLINE);
}
}
@Override
public void dispose() {
logger.debug("WeMoHandler disposed.");
removeSubscription();
if (refreshJob != null && !refreshJob.isCancelled()) {
refreshJob.cancel(true);
refreshJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("Command '{}' received for channel '{}'", command, channelUID);
if (command instanceof RefreshType) {
try {
updateWemoState();
} catch (Exception e) {
logger.debug("Exception during poll : {}", e);
}
} else if (channelUID.getId().equals(CHANNEL_STATE)) {
if (command instanceof OnOffType) {
try {
String binaryState = null;
if (command.equals(OnOffType.ON)) {
binaryState = "1";
} else if (command.equals(OnOffType.OFF)) {
binaryState = "0";
}
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>";
String wemoURL = getWemoURL("basicevent");
if (wemoURL != null) {
WemoHttpCall.executeCall(wemoURL, soapHeader, content);
}
} catch (Exception e) {
logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
updateStatus(ThingStatus.ONLINE);
}
}
}
@Override
public void onServiceSubscribed(String service, boolean succeeded) {
logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
subscriptionState.put(service, succeeded);
}
@Override
public void onValueReceived(String variable, String value, String service) {
logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
new Object[] { variable, value, service, this.getThing().getUID() });
updateStatus(ThingStatus.ONLINE);
this.stateMap.put(variable, value);
if (getThing().getThingTypeUID().getId().equals("insight")) {
String insightParams = stateMap.get("InsightParams");
if (insightParams != null) {
String[] splitInsightParams = insightParams.split("\\|");
if (splitInsightParams[0] != null) {
OnOffType binaryState = null;
binaryState = splitInsightParams[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
if (binaryState != null) {
logger.trace("New InsightParam binaryState '{}' for device '{}' received", binaryState,
getThing().getUID());
updateState(CHANNEL_STATE, binaryState);
}
}
long lastChangedAt = 0;
try {
lastChangedAt = Long.parseLong(splitInsightParams[1]) * 1000; // convert s to ms
} catch (NumberFormatException e) {
logger.error("Unable to parse lastChangedAt value '{}' for device '{}'; expected long",
splitInsightParams[1], getThing().getUID());
}
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeInMillis(lastChangedAt);
State lastChangedAtState = new DateTimeType(cal);
if (lastChangedAt != 0) {
logger.trace("New InsightParam lastChangedAt '{}' for device '{}' received", lastChangedAtState,
getThing().getUID());
updateState(CHANNEL_LASTCHANGEDAT, lastChangedAtState);
}
State lastOnFor = DecimalType.valueOf(splitInsightParams[2]);
if (lastOnFor != null) {
logger.trace("New InsightParam lastOnFor '{}' for device '{}' received", lastOnFor,
getThing().getUID());
updateState(CHANNEL_LASTONFOR, lastOnFor);
}
State onToday = DecimalType.valueOf(splitInsightParams[3]);
if (onToday != null) {
logger.trace("New InsightParam onToday '{}' for device '{}' received", onToday,
getThing().getUID());
updateState(CHANNEL_ONTODAY, onToday);
}
State onTotal = DecimalType.valueOf(splitInsightParams[4]);
if (onTotal != null) {
logger.trace("New InsightParam onTotal '{}' for device '{}' received", onTotal,
getThing().getUID());
updateState(CHANNEL_ONTOTAL, onTotal);
}
State timespan = DecimalType.valueOf(splitInsightParams[5]);
if (timespan != null) {
logger.trace("New InsightParam timespan '{}' for device '{}' received", timespan,
getThing().getUID());
updateState(CHANNEL_TIMESPAN, timespan);
}
State averagePower = DecimalType.valueOf(splitInsightParams[6]); // natively given in W
if (averagePower != null) {
logger.trace("New InsightParam averagePower '{}' for device '{}' received", averagePower,
getThing().getUID());
updateState(CHANNEL_AVERAGEPOWER, averagePower);
}
BigDecimal currentMW = new BigDecimal(splitInsightParams[7]);
State currentPower = new DecimalType(currentMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
// mW to W
if (currentPower != null) {
logger.trace("New InsightParam currentPower '{}' for device '{}' received", currentPower,
getThing().getUID());
updateState(CHANNEL_CURRENTPOWER, currentPower);
}
BigDecimal energyTodayMWMin = new BigDecimal(splitInsightParams[8]);
// recalculate mW-mins to Wh
State energyToday = new DecimalType(
energyTodayMWMin.divide(new BigDecimal(60000), RoundingMode.HALF_UP));
if (energyToday != null) {
logger.trace("New InsightParam energyToday '{}' for device '{}' received", energyToday,
getThing().getUID());
updateState(CHANNEL_ENERGYTODAY, energyToday);
}
BigDecimal energyTotalMWMin = new BigDecimal(splitInsightParams[9]);
// recalculate mW-mins to Wh
State energyTotal = new DecimalType(
energyTotalMWMin.divide(new BigDecimal(60000), RoundingMode.HALF_UP));
if (energyTotal != null) {
logger.trace("New InsightParam energyTotal '{}' for device '{}' received", energyTotal,
getThing().getUID());
updateState(CHANNEL_ENERGYTOTAL, energyTotal);
}
BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
State standByLimit = new DecimalType(standByLimitMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
// mW to W
if (standByLimit != null) {
logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
getThing().getUID());
updateState(CHANNEL_STANDBYLIMIT, standByLimit);
}
}
} else {
State state = stateMap.get("BinaryState").equals("0") ? OnOffType.OFF : OnOffType.ON;
logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
if (state != null) {
if (getThing().getThingTypeUID().getId().equals("motion")) {
updateState(CHANNEL_MOTIONDETECTION, state);
if (state.equals(OnOffType.ON)) {
State lastMotionDetected = new DateTimeType();
updateState(CHANNEL_LASTMOTIONDETECTED, lastMotionDetected);
}
} 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 = null;
if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
subscription = "insight1";
} else {
subscription = "basicevent1";
}
if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
subscriptionState.put(subscription, true);
}
} 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 = null;
if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
subscription = "insight1";
} else {
subscription = "basicevent1";
}
if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
service.removeSubscription(this, subscription);
}
subscriptionState = new HashMap<String, Boolean>();
service.unregisterParticipant(this);
}
}
private synchronized void onUpdate() {
if (refreshJob == null || refreshJob.isCancelled()) {
Configuration config = getThing().getConfiguration();
int refreshInterval = DEFAULT_REFRESH_INTERVAL;
Object refreshConfig = config.get("refresh");
if (refreshConfig != null) {
refreshInterval = ((BigDecimal) refreshConfig).intValue();
}
refreshJob = scheduler.scheduleAtFixedRate(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
}
}
private boolean isUpnpDeviceRegistered() {
return service.isRegistered(this);
}
@Override
public String getUDN() {
return (String) this.getThing().getConfiguration().get(UDN);
}
/**
* 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 action = "GetBinaryState";
String variable = "BinaryState";
String actionService = "basicevent";
String value = null;
if (getThing().getThingTypeUID().getId().equals("insight")) {
action = "GetInsightParams";
variable = "InsightParams";
actionService = "insight";
}
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>";
try {
String wemoURL = getWemoURL(actionService);
if (wemoURL != null) {
String wemoCallResponse = WemoHttpCall.executeCall(wemoURL, soapHeader, content);
if (wemoCallResponse != null) {
logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
if (variable.equals("InsightParams")) {
value = StringUtils.substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
} else {
value = StringUtils.substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
}
if (value != null) {
logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
this.onValueReceived(variable, value, actionService + "1");
}
}
}
} catch (Exception e) {
logger.error("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
}
}
public String getWemoURL(String actionService) {
URL descriptorURL = service.getDescriptorURL(this);
String wemoURL = null;
if (descriptorURL != null) {
String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
return wemoURL;
}
return null;
}
@Override
public void onStatusChanged(boolean status) {
}
@Override
public Collection<ThingUID> removeOlderResults(DiscoveryService source, long timestamp,
Collection<ThingTypeUID> thingTypeUIDs) {
return null;
}
}