/** * 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.io.StringReader; import java.math.BigDecimal; import java.net.URL; import java.util.Collection; import java.util.Collections; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; 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.config.discovery.DiscoveryListener; import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryService; 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.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 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; /** * The {@link WemoMakerHandler} 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 */ public class WemoMakerHandler extends BaseThingHandler implements UpnpIOParticipant, DiscoveryListener { private final Logger logger = LoggerFactory.getLogger(WemoMakerHandler.class); public final static Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER); private UpnpIOService service; /** * The default refresh interval in Seconds. */ private int DEFAULT_REFRESH_INTERVAL = 15; private ScheduledFuture<?> refreshJob; private Runnable refreshRunnable = new Runnable() { @Override public void run() { try { updateWemoState(); } catch (Exception e) { logger.debug("Exception during poll : {}", e); } } }; public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService) { super(thing); logger.debug("Creating a WemoMakerHandler 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 WemoMakerHandler for UDN '{}'", configuration.get("udn")); onUpdate(); super.initialize(); } else { logger.debug("Cannot initalize WemoMakerHandler. 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); 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("WeMoMakerHandler disposed."); 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_RELAY)) { 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) { @SuppressWarnings("unused") String wemoCallResponse = WemoHttpCall.executeCall(wemoURL, soapHeader, content); } } 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() { if (service.isRegistered(this)) { 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); } } } @Override public String getUDN() { return (String) this.getThing().getConfiguration().get(UDN); } /** * 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>"; try { String wemoURL = getWemoURL(actionService); if (wemoURL != null) { String wemoCallResponse = WemoHttpCall.executeCall(wemoURL, soapHeader, content); if (wemoCallResponse != null) { try { String stringParser = StringUtils.substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>"); // Due to Belkins bad response formatting, we need to run this twice. stringParser = StringEscapeUtils.unescapeXml(stringParser); stringParser = StringEscapeUtils.unescapeXml(stringParser); logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID()); stringParser = "<data>" + stringParser + "</data>"; DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 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; if (relayState != null) { 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; if (sensorState != null) { 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 get attributes for device '{}'", getThing().getUID(), e); } } 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; } public static String getCharacterDataFromElement(Element e) { Node child = e.getFirstChild(); if (child instanceof CharacterData) { CharacterData cd = (CharacterData) child; return cd.getData(); } return "?"; } @Override public void onStatusChanged(boolean status) { } @Override public Collection<ThingUID> removeOlderResults(DiscoveryService source, long timestamp, Collection<ThingTypeUID> thingTypeUIDs) { return null; } @Override public void onServiceSubscribed(String service, boolean succeeded) { } @Override public void onValueReceived(String variable, String value, String service) { } }