/** * 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.net.URL; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.StringEscapeUtils; 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.core.library.types.IncreaseDecreaseType; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.PercentType; import org.eclipse.smarthome.core.thing.Bridge; 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.ThingStatusInfo; import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.eclipse.smarthome.core.thing.binding.ThingHandler; 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; /** * {@link WemoLightHandler} is the handler for a WeMo light, responsible for handling commands and state updates for the * different channels of a WeMo light. * * @author Hans-Jörg Merk - Initial contribution */ public class WemoLightHandler extends BaseThingHandler implements UpnpIOParticipant { private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class); private Map<String, Boolean> subscriptionState = new HashMap<String, Boolean>(); private UpnpIOService service; private WemoBridgeHandler wemoBridgeHandler; private String wemoLightID; private int currentBrightness; /** * Set dimming stepsize to 5% */ private static final int DIM_STEPSIZE = 5; protected final static int SUBSCRIPTION_DURATION = 600; /** * The default refresh interval in Seconds. */ private int DEFAULT_REFRESH_INTERVAL = 60; /** * The default refresh initial delay in Seconds. */ private static int DEFAULT_REFRESH_INITIAL_DELAY = 15; private ScheduledFuture<?> refreshJob; private 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); } } }; public WemoLightHandler(Thing thing, UpnpIOService upnpIOService) { super(thing); if (upnpIOService != null) { logger.debug("UPnPIOService '{}'", upnpIOService); this.service = upnpIOService; } else { logger.debug("upnpIOService not set."); } } @Override public void initialize() { // initialize() is only called if the required parameter 'deviceID' is available wemoLightID = (String) getConfig().get(DEVICE_ID); if (getBridge() != null) { logger.debug("Initializing WemoLightHandler for LightID '{}'", wemoLightID); if (getBridge().getStatus() == ThingStatus.ONLINE) { updateStatus(ThingStatus.ONLINE); onSubscription(); onUpdate(); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE); } } else { updateStatus(ThingStatus.OFFLINE); } } @Override public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) { updateStatus(ThingStatus.ONLINE); onSubscription(); onUpdate(); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE); if (refreshJob != null && !refreshJob.isCancelled()) { refreshJob.cancel(true); refreshJob = null; } } } @Override public void dispose() { logger.debug("WeMoLightHandler disposed."); removeSubscription(); if (refreshJob != null && !refreshJob.isCancelled()) { refreshJob.cancel(true); refreshJob = null; } } private synchronized WemoBridgeHandler getWemoBridgeHandler() { if (this.wemoBridgeHandler == null) { Bridge bridge = getBridge(); if (bridge == null) { logger.error("Required bridge not defined for device {}.", wemoLightID); return null; } ThingHandler handler = bridge.getHandler(); if (handler instanceof WemoBridgeHandler) { this.wemoBridgeHandler = (WemoBridgeHandler) handler; } else { logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID()); return null; } } return this.wemoBridgeHandler; } @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { try { getDeviceState(); } catch (Exception e) { logger.debug("Exception during poll : {}", e); } } else { Configuration configuration = getConfig(); configuration.get(DEVICE_ID); WemoBridgeHandler wemoBridge = getWemoBridgeHandler(); if (wemoBridge == null) { logger.debug("wemoBridgeHandler not found, cannot handle command"); return; } String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString(); logger.trace("WeMo Bridge to send command to : {}", devUDN); String value = null; String capability = null; switch (channelUID.getId()) { case CHANNEL_BRIGHTNESS: capability = "10008"; if (command instanceof PercentType) { int newBrightness = ((PercentType) command).intValue(); logger.trace("wemoLight received Value {}", newBrightness); int value1 = Math.round(newBrightness * 255 / 100); value = value1 + ":0"; currentBrightness = newBrightness; } else if (command instanceof OnOffType) { switch (command.toString()) { case "ON": value = "255:0"; break; case "OFF": value = "0:0"; break; } } else if (command instanceof IncreaseDecreaseType) { int newBrightness; switch (command.toString()) { case "INCREASE": currentBrightness = currentBrightness + DIM_STEPSIZE; newBrightness = Math.round(currentBrightness * 255 / 100); if (newBrightness > 255) { newBrightness = 255; } value = newBrightness + ":0"; break; case "DECREASE": currentBrightness = currentBrightness - DIM_STEPSIZE; newBrightness = Math.round(currentBrightness * 255 / 100); if (newBrightness < 0) { newBrightness = 0; } value = newBrightness + ":0"; break; } } break; case CHANNEL_STATE: capability = "10006"; switch (command.toString()) { case "ON": value = "1"; break; case "OFF": value = "0"; break; } 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>"; String wemoURL = getWemoURL(); if (wemoURL != null && capability != null && value != null) { String wemoCallResponse = WemoHttpCall.executeCall(wemoURL, soapHeader, content); if (wemoCallResponse != null) { if (capability != null && capability.equals("10008") && value != null) { OnOffType binaryState = null; binaryState = value.equals("0") ? OnOffType.OFF : OnOffType.ON; if (binaryState != null) { updateState(CHANNEL_STATE, binaryState); } } } } } catch (Exception e) { throw new RuntimeException("Could not send command to WeMo Bridge", e); } } } @Override public String getUDN() { WemoBridgeHandler wemoBridge = getWemoBridgeHandler(); if (wemoBridge == null) { logger.debug("wemoBridgeHandler not found"); return null; } return (String) wemoBridge.getThing().getConfiguration().get(UDN); } /** * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according * channel states. */ public void getDeviceState() { logger.debug("Request actual state for LightID '{}'", wemoLightID); try { String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\""; 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:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>" + wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>"; String wemoURL = getWemoURL(); if (wemoURL != null) { String wemoCallResponse = WemoHttpCall.executeCall(wemoURL, soapHeader, content); if (wemoCallResponse != null) { wemoCallResponse = StringEscapeUtils.unescapeXml(wemoCallResponse); String response = StringUtils.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; if (binaryState != null) { 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; } } } } } catch (Exception e) { throw new RuntimeException("Could not retrieve new Wemo light state", e); } } @Override public void onServiceSubscribed(String service, boolean succeeded) { } @Override public void onValueReceived(String variable, String value, String service) { logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'", new Object[] { variable, value, service, this.getThing().getUID() }); String capabilityId = StringUtils.substringBetween(value, "<CapabilityId>", "</CapabilityId>"); String newValue = StringUtils.substringBetween(value, "<Value>", "</Value>"); switch (capabilityId) { case "10006": OnOffType binaryState = null; binaryState = newValue.equals("0") ? OnOffType.OFF : OnOffType.ON; if (binaryState != null) { updateState(CHANNEL_STATE, binaryState); } break; case "10008": String splitValue[] = newValue.split(":"); if (splitValue[0] != null) { int newBrightnessValue = Integer.valueOf(splitValue[0]); int newBrightness = Math.round(newBrightnessValue * 100 / 255); State newBrightnessState = new PercentType(newBrightness); updateState(CHANNEL_BRIGHTNESS, newBrightnessState); currentBrightness = newBrightness; } break; } } @Override public void onStatusChanged(boolean status) { } private synchronized void onSubscription() { if (service.isRegistered(this)) { logger.debug("Checking WeMo GENA subscription for '{}'", this); String subscription = "bridge1"; 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)) { String subscription = null; if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) { logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription); service.removeSubscription(this, "bridge1"); } 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(); } logger.trace("Start polling job for LightID '{}'", wemoLightID); refreshJob = scheduler.scheduleAtFixedRate(refreshRunnable, DEFAULT_REFRESH_INITIAL_DELAY, refreshInterval, TimeUnit.SECONDS); } } private boolean isUpnpDeviceRegistered() { return service.isRegistered(this); } public String getWemoURL() { URL descriptorURL = service.getDescriptorURL(this); String wemoURL = null; if (descriptorURL != null) { String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml"); wemoURL = deviceURL + "/upnp/control/bridge1"; return wemoURL; } return null; } }