/** * Copyright (c) 2010-2016 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.openhab.binding.wemo.internal; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MulticastSocket; import java.net.SocketTimeoutException; import java.nio.charset.Charset; import java.util.Dictionary; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Map; import java.util.Properties; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.openhab.binding.wemo.WemoBindingProvider; import org.openhab.binding.wemo.internal.WemoGenericBindingProvider.WemoChannelType; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.io.net.http.HttpUtil; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This binding allows you to switch your Belkin WeMo devices On or Off, shows energy measurement of Insight Switches * and refreshs itemState by polling every 30 Seconds. * The Binding does a discovery at startup to find all your WeMo Devices in your installations and stores their * UDN and location (IP-Address) in a internal map. * If location of a found device changes due to a dhcp lease renewal, rediscovery is started to find the new location. * * @author Hans-Jörg Merk * @since 1.6.0 */ public class WemoBinding extends AbstractActiveBinding<WemoBindingProvider>implements ManagedService { private static final Logger logger = LoggerFactory.getLogger(WemoBinding.class); // wemoConfigMap stores the values wemoFriendlyName and their according location (IP-Address:Port) found during // discovery. protected Map<String, String> wemoConfigMap = new HashMap<String, String>(); private static String getInsightParamsXML; private static String getRequestXML; private static String setRequestXML; static { try { getInsightParamsXML = IOUtils.toString( WemoBinding.class.getResourceAsStream("/org/openhab/binding/wemo/internal/GetInsightParams.xml")); getRequestXML = IOUtils.toString( WemoBinding.class.getResourceAsStream("/org/openhab/binding/wemo/internal/GetRequest.xml")); setRequestXML = IOUtils.toString( WemoBinding.class.getResourceAsStream("/org/openhab/binding/wemo/internal/SetRequest.xml")); } catch (Exception e) { LoggerFactory.getLogger(WemoBinding.class).error("Cannot read XML files!", e); } } /** * the refresh interval which is used to poll values from the WeMo-Devices */ private long refreshInterval = 60000; public InetAddress address; @Override public void activate() { // Start device discovery, each time the binding starts. wemoDiscovery(); } @Override public void deactivate() { } /** * @{inheritDoc} */ @Override protected long getRefreshInterval() { return refreshInterval; } /** * @{inheritDoc} */ @Override protected String getName() { return "Wemo Refresh Service"; } /** * @{inheritDoc} */ @Override protected void execute() { logger.debug("execute() method is called!"); for (WemoBindingProvider provider : providers) { for (String itemName : provider.getItemNames()) { logger.debug("Wemo item '{}' state will be updated", itemName); try { if (provider.getUDN(itemName).toLowerCase().contains("insight")) { String insightParams = getInsightParams(itemName); if (insightParams != null) { String[] splitInsightParams = insightParams.split("\\|"); if (splitInsightParams[0] != null) { if (provider.getChannelType(itemName).equals(WemoChannelType.state)) { OnOffType binaryState = null; binaryState = splitInsightParams[0].equals("0") ? OnOffType.OFF : OnOffType.ON; if (binaryState != null) { logger.trace("New InsightParam binaryState '{}' for device '{}' received", binaryState, itemName); eventPublisher.postUpdate(itemName, binaryState); } } if (provider.getChannelType(itemName).equals(WemoChannelType.lastChangedAt)) { 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], itemName); } GregorianCalendar cal = new GregorianCalendar(); cal.setTimeInMillis(lastChangedAt); State lastChangedAtState = new DateTimeType(cal); if (lastChangedAt != 0) { logger.trace("New InsightParam lastChangedAt '{}' for device '{}' received", lastChangedAtState, itemName); eventPublisher.postUpdate(itemName, lastChangedAtState); } } if (provider.getChannelType(itemName).equals(WemoChannelType.lastOnFor)) { State lastOnFor = DecimalType.valueOf(splitInsightParams[2]); if (lastOnFor != null) { logger.trace("New InsightParam lastOnFor '{}' for device '{}' received", lastOnFor, itemName); eventPublisher.postUpdate(itemName, lastOnFor); } } if (provider.getChannelType(itemName).equals(WemoChannelType.onToday)) { State onToday = DecimalType.valueOf(splitInsightParams[3]); if (onToday != null) { logger.trace("New InsightParam onToday '{}' for device '{}' received", onToday, itemName); eventPublisher.postUpdate(itemName, onToday); } } if (provider.getChannelType(itemName).equals(WemoChannelType.onTotal)) { State onTotal = DecimalType.valueOf(splitInsightParams[4]); if (onTotal != null) { logger.trace("New InsightParam onTotal '{}' for device '{}' received", onTotal, itemName); eventPublisher.postUpdate(itemName, onTotal); } } if (provider.getChannelType(itemName).equals(WemoChannelType.timespan)) { State timespan = DecimalType.valueOf(splitInsightParams[5]); if (timespan != null) { logger.trace("New InsightParam timespan '{}' for device '{}' received", timespan, itemName); eventPublisher.postUpdate(itemName, timespan); } } if (provider.getChannelType(itemName).equals(WemoChannelType.averagePower)) { State averagePower = DecimalType.valueOf(splitInsightParams[6]); // natively given // in W if (averagePower != null) { logger.trace("New InsightParam averagePower '{}' for device '{}' received", averagePower, itemName); eventPublisher.postUpdate(itemName, averagePower); } } if (provider.getChannelType(itemName).equals(WemoChannelType.currentPower)) { 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, itemName); eventPublisher.postUpdate(itemName, currentPower); } } if (provider.getChannelType(itemName).equals(WemoChannelType.energyToday)) { 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, itemName); eventPublisher.postUpdate(itemName, energyToday); } } if (provider.getChannelType(itemName).equals(WemoChannelType.energyTotal)) { 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, itemName); eventPublisher.postUpdate(itemName, energyTotal); } } if (provider.getChannelType(itemName).equals(WemoChannelType.standbyLimit)) { BigDecimal standbyLimitMW = new BigDecimal(splitInsightParams[10]); // recalculate mW to W State standbyLimit = new DecimalType( standbyLimitMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); if (standbyLimit != null) { logger.trace("New InsightParam standbyLimit '{}' for device '{}' received", standbyLimit, itemName); eventPublisher.postUpdate(itemName, standbyLimit); } } } } } else { String state = getWemoState(itemName); if (state != null) { if (provider.getUDN(itemName).toLowerCase().contains("motion")) { State newState = state.equals("0") ? OpenClosedType.OPEN : OpenClosedType.CLOSED; eventPublisher.postUpdate(itemName, newState); } else { State itemState = state.equals("0") ? OnOffType.OFF : OnOffType.ON; eventPublisher.postUpdate(itemName, itemState); } } } } catch (Exception e) { logger.error("Error in execute method: " + e.getMessage(), e); } } } } /** * @{inheritDoc} */ @Override protected void internalReceiveCommand(String itemName, Command command) { logger.debug("internalReceiveCommand() is called!"); for (WemoBindingProvider provider : providers) { try { String udn = provider.getUDN(itemName); logger.trace("item '{}' has UDN '{}'", itemName, udn); logger.trace("Command '{}' is about to be sent to item '{}'", command, itemName); sendCommand(itemName, command); } catch (Exception e) { logger.error("Failed to send {} command", command, e); } } } public void wemoDiscovery() { logger.debug("wemoDiscovery() is called!"); try { final int SSDP_PORT = 1900; final int SSDP_SEARCH_PORT = 1901; // Broadcast address final String SSDP_IP = "239.255.255.250"; // Connection timeout int TIMEOUT = 1000; // Send from localhost:1901 InetAddress localhost = InetAddress.getLocalHost(); InetSocketAddress srcAddress = new InetSocketAddress(localhost, SSDP_SEARCH_PORT); // Send to 239.255.255.250:1900 InetSocketAddress dstAddress = new InetSocketAddress(InetAddress.getByName(SSDP_IP), SSDP_PORT); // Request-Packet-Constructor StringBuffer discoveryMessage = new StringBuffer(); discoveryMessage.append("M-SEARCH * HTTP/1.1\r\n"); discoveryMessage.append("HOST: " + SSDP_IP + ":" + SSDP_PORT + "\r\n"); discoveryMessage.append("MAN: \"ssdp:discover\"\r\n"); discoveryMessage.append("MX: 5\r\n"); discoveryMessage.append("ST: urn:Belkin:service:basicevent:1\r\n"); discoveryMessage.append("\r\n"); logger.trace("Request: {}", discoveryMessage.toString()); byte[] discoveryMessageBytes = discoveryMessage.toString().getBytes(); DatagramPacket discoveryPacket = new DatagramPacket(discoveryMessageBytes, discoveryMessageBytes.length, dstAddress); // Send multi-cast packet MulticastSocket multicast = null; try { multicast = new MulticastSocket(null); multicast.bind(srcAddress); logger.trace("Source-Address = '{}'", srcAddress); multicast.setTimeToLive(5); logger.trace("Send multicast request."); multicast.send(discoveryPacket); } finally { logger.trace("Multicast ends. Close connection."); multicast.disconnect(); multicast.close(); } // Response-Listener MulticastSocket wemoReceiveSocket = null; DatagramPacket receivePacket = null; try { wemoReceiveSocket = new MulticastSocket(SSDP_SEARCH_PORT); wemoReceiveSocket.setTimeToLive(10); wemoReceiveSocket.setSoTimeout(TIMEOUT); logger.debug("Send datagram packet."); wemoReceiveSocket.send(discoveryPacket); while (true) { try { logger.debug("Receive SSDP Message."); receivePacket = new DatagramPacket(new byte[2048], 2048); wemoReceiveSocket.receive(receivePacket); final String message = new String(receivePacket.getData()); if (message.contains("Belkin")) { logger.trace("Received message: {}", message); } new Thread(new Runnable() { @Override public void run() { if (message != null) { String location = StringUtils.substringBetween(message, "LOCATION: ", "/setup.xml"); String udn = StringUtils.substringBetween(message, "USN: uuid:", "::urn:Belkin"); if (udn != null) { logger.trace("Save location '{}' for WeMo device with UDN '{}'", location, udn); wemoConfigMap.put(udn, location); logger.info("Wemo Device with UDN '{}' discovered", udn); } } } }).start(); } catch (SocketTimeoutException e) { logger.debug("Message receive timed out."); for (String name : wemoConfigMap.keySet()) { logger.trace(name + ":" + wemoConfigMap.get(name)); } break; } } } finally { if (wemoReceiveSocket != null) { wemoReceiveSocket.disconnect(); wemoReceiveSocket.close(); } } } catch (Exception e) { logger.error("Could not start wemo device discovery", e); } } public void sendCommand(String itemName, Command command) throws IOException { boolean onOff = OnOffType.ON.equals(command); logger.trace("command '{}' transformed to '{}'", command, onOff); String wemoCallResponse = wemoCall(itemName, "urn:Belkin:service:basicevent:1#SetBinaryState", setRequestXML.replace("{{state}}", onOff ? "1" : "0")); logger.trace("setOn ={}", wemoCallResponse); } private String wemoCall(String itemName, String soapMethod, String content) { try { for (WemoBindingProvider provider : providers) { String soapHeader = "SOAPACTION: \"" + soapMethod + "\""; String contentHeader = "Content-Type: text/xml; charset=\"utf-8\""; String endpoint = "/upnp/control/basicevent1"; if (soapMethod.contains("insight")) { endpoint = "/upnp/control/insight1"; } String wemoUDN = provider.getUDN(itemName); if (wemoUDN == null) { return null; } logger.trace("Calling WeMo item '{}' with configuration :", itemName); logger.trace(" UDN = '{}'", provider.getUDN(itemName)); logger.trace("ChannelType = '{}'", provider.getChannelType(itemName)); String wemoLocation = wemoConfigMap.get(wemoUDN); if (wemoLocation != null) { logger.trace(" Location = '{}'", wemoLocation); logger.trace(" EndPoint = '{}'", endpoint); String wemoURL = wemoLocation + endpoint; Properties wemoHeaders = new Properties(); wemoHeaders.setProperty(soapHeader, contentHeader); InputStream wemoContent = new ByteArrayInputStream(content.getBytes(Charset.forName("UTF-8"))); String wemoCallResponse = HttpUtil.executeUrl("POST", wemoURL, wemoHeaders, wemoContent, "text/xml", 2000); logger.trace("wemoresp: {}", wemoCallResponse); return wemoCallResponse; } else { logger.debug("No Location found for item '{}', start new discovery ", itemName); wemoDiscovery(); String wemoCallResponse = ""; return wemoCallResponse; } } } catch (Exception e) { wemoDiscovery(); throw new RuntimeException("Could not call Wemo, did rediscovery", e); } return null; } private String getWemoState(String itemName) { String stateRequest = null; String returnState = null; try { stateRequest = wemoCall(itemName, "urn:Belkin:service:basicevent:1#GetBinaryState", getRequestXML); if (stateRequest != null) { returnState = StringUtils.substringBetween(stateRequest, "<BinaryState>", "</BinaryState>"); logger.debug("New binary state '{}' for item '{}' received", returnState, itemName); } } catch (Exception e) { logger.error("Failed to get binary state for item '{}'", itemName, e); } if (returnState != null) { return returnState; } else { return null; } } private String getInsightParams(String itemName) { String insightParamsRequest = null; String returnInsightParams = null; try { insightParamsRequest = wemoCall(itemName, "urn:Belkin:service:insight:1#GetInsightParams", getInsightParamsXML); if (insightParamsRequest != null) { logger.trace("insightParamsRequestResponse :"); logger.trace("{}", insightParamsRequest); returnInsightParams = StringUtils.substringBetween(insightParamsRequest, "<InsightParams>", "</InsightParams>"); logger.debug("New raw InsightParams '{}' for device '{}' received", returnInsightParams, itemName); return returnInsightParams; } } catch (Exception e) { logger.error("Failed to get InsightParams for device '{}'", itemName, e); } return null; } /** * @{inheritDoc} */ @Override public void updated(Dictionary<String, ?> config) throws ConfigurationException { setProperlyConfigured(true); if (config != null) { String refreshIntervalString = (String) config.get("refresh"); if (StringUtils.isNotBlank(refreshIntervalString)) { refreshInterval = Long.parseLong(refreshIntervalString); } } } }