/** * 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.garadget.internal; import static org.apache.commons.lang.StringUtils.isNotBlank; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.httpclient.HttpStatus; import org.openhab.binding.garadget.GaradgetBindingProvider; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.StopMoveType; import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.UpDownType; import org.openhab.core.types.Command; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class polls the Garadget API and sends updates to the event bus * of configured items in openHAB * * @author John Cocula * @since 1.9.0 */ public class GaradgetBinding extends AbstractActiveBinding<GaradgetBindingProvider> { private final Logger logger = LoggerFactory.getLogger(GaradgetBinding.class); @SuppressWarnings("unused") private BundleContext bundleContext; private Connection connection = null; private Map<String, GaradgetDevice> devices = null; private static final String CONFIG_GRANULARITY = "granularity"; private static final String CONFIG_REFRESH = "refresh"; private static final String CONFIG_QUICKPOLL = "quickpoll"; private static final String CONFIG_TIMEOUT = "timeout"; private static final long DEFAULT_GRANULARITY = 5000; private static final long DEFAULT_REFRESH = 180000; private static final long DEFAULT_QUICKPOLL = 11000; private static final int DEFAULT_TIMEOUT = 5000; /** * the interval which is used to call the execute() method */ private long granularity = DEFAULT_GRANULARITY; /** * the quick refresh interval which is used to poll values from the Garadget after a function was called */ private long quickPollInterval = DEFAULT_QUICKPOLL; /** * the refresh interval which is used to poll values from the Garadget API. * (optional, defaults to 180000ms) */ private long refreshInterval = DEFAULT_REFRESH; /** * The next time to poll this instance. Initially 0 so pollTimeExpired() initially returns true. */ private final AtomicLong pollTime = new AtomicLong(0); /** * Called by the SCR to activate the component with its configuration read * from CAS * * @param bundleContext * BundleContext of the Bundle that defines this component * @param configuration * Configuration properties for this component obtained from the * ConfigAdmin service */ public void activate(final BundleContext bundleContext, final Map<String, Object> configuration) { this.bundleContext = bundleContext; modified(configuration); } /** * Called by the SCR when the configuration of a binding has been changed * through the ConfigAdmin service. * * @param configuration * Updated configuration properties */ public void modified(final Map<String, Object> configuration) { // to override the default granularity one has to add a // parameter to the .cfg like [garadget:]granularity=2000 String granularityString = Objects.toString(configuration.get(CONFIG_GRANULARITY), null); granularity = isNotBlank(granularityString) ? Long.parseLong(granularityString) : DEFAULT_GRANULARITY; // to override the default refresh interval one has to add a // parameter to .cfg like [garadget:]refresh=240000 String refreshIntervalString = Objects.toString(configuration.get(CONFIG_REFRESH), null); refreshInterval = isNotBlank(refreshIntervalString) ? Long.parseLong(refreshIntervalString) : DEFAULT_REFRESH; // to override the default quickPoll interval one has to add a // parameter to .cfg like [garadget:]quickpoll=4000 String quickPollIntervalString = Objects.toString(configuration.get(CONFIG_QUICKPOLL), null); quickPollInterval = isNotBlank(quickPollIntervalString) ? Long.parseLong(quickPollIntervalString) : DEFAULT_QUICKPOLL; // to override the default HTTP timeout one has to add a // parameter to .cfg like [garadget:]timeout=20000 String timeoutString = Objects.toString(configuration.get(CONFIG_TIMEOUT), null); int timeout = isNotBlank(timeoutString) ? Integer.parseInt(timeoutString) : DEFAULT_TIMEOUT; String username = Objects.toString(configuration.get("username"), null); String password = Objects.toString(configuration.get("password"), null); if (isNotBlank(username) && isNotBlank(password)) { connection = new Connection(username, password, timeout); connection.login(); // Poll at the earliest opportunity schedulePoll(0); setProperlyConfigured(true); } else { setProperlyConfigured(false); } } /** * Called by the SCR to deactivate the component when either the * configuration is removed or mandatory references are no longer satisfied * or the component has simply been stopped. * * @param reason * Reason code for the deactivation:<br> * <ul> * <li>0 – Unspecified * <li>1 – The component was disabled * <li>2 – A reference became unsatisfied * <li>3 – A configuration was changed * <li>4 – A configuration was deleted * <li>5 – The component was disposed * <li>6 – The bundle was stopped * </ul> */ public void deactivate(final int reason) { this.bundleContext = null; if (connection != null) { connection.logout(); connection = null; } } /** * {@inheritDoc} */ @Override protected String getName() { return "Garadget Refresh Service"; } /** * {@inheritDoc} */ @Override public long getRefreshInterval() { return granularity; } /** * {@inheritDoc} */ @Override public void execute() { if (!pollTimeExpired()) { return; } // schedule the next poll at the standard refresh interval schedulePoll(refreshInterval); logger.trace("Polling Garadget devices"); if (connection == null) { logger.debug("No connection defined; please check username and password."); return; } devices = connection.getDevices(); // Collect all the devices and variables for which we have in bindings Map<String, TreeSet<String>> pollMap = new HashMap<String, TreeSet<String>>(); for (GaradgetBindingProvider provider : providers) { for (String itemName : provider.getItemNames()) { GaradgetBindingConfig deviceConfig = getConfigForItemName(itemName); if (deviceConfig != null) { for (GaradgetSubscriber subscriber : deviceConfig.getSubscribers()) { TreeSet<String> varNames = pollMap.get(subscriber.getDeviceId()); if (varNames == null) { varNames = new TreeSet<String>(); pollMap.put(subscriber.getDeviceId(), varNames); } final String varName = subscriber.getVarName(); // Handle Garadget compound variables if only the "sub-variables" // are subscribed to. if (varName.startsWith(GaradgetDevice.DOOR_STATUS)) { varNames.add(GaradgetDevice.DOOR_STATUS); } else if (varName.startsWith(GaradgetDevice.DOOR_CONFIG)) { varNames.add(GaradgetDevice.DOOR_CONFIG); } else if (varName.startsWith(GaradgetDevice.NET_CONFIG)) { varNames.add(GaradgetDevice.NET_CONFIG); } else if (AbstractDevice.isVar(varName)) { varNames.add(varName); } } } } } // Retrieve variables for each in bound device if any were bound to items for (final String deviceId : pollMap.keySet()) { final GaradgetDevice device = devices.get(deviceId); if (device == null) { logger.warn("Unable to poll variables for deviceId {}; skipping.", deviceId); continue; } final TreeSet<String> varNames = pollMap.get(deviceId); for (final String varName : varNames) { connection.sendCommand(device, varName, null, new HttpResponseHandler() { @Override public void handleResponse(int statusCode, String responseBody) { if (statusCode == HttpStatus.SC_OK) { // save variable's value device.setVar(varName, responseBody); } } }); } } // Update all in-bound items for (final GaradgetBindingProvider provider : providers) { for (final String itemName : provider.getItemNames()) { GaradgetBindingConfig deviceConfig = getConfigForItemName(itemName); if (deviceConfig != null) { for (final GaradgetSubscriber subscriber : deviceConfig.getSubscribers()) { AbstractDevice device = devices.get(subscriber.getDeviceId()); if (device != null) { eventPublisher.postUpdate(itemName, device.getState(subscriber)); } } } } } } /** * @{inheritDoc} */ @Override public void internalReceiveCommand(String itemName, Command command) { logger.trace("Garadget binding received command '{}' for item '{}'", command, itemName); final GaradgetBindingConfig deviceConfig = getConfigForItemName(itemName); if (deviceConfig != null) { for (GaradgetPublisher publisher : deviceConfig.getPublishers()) { commandGaradget(itemName, publisher, command); } } else { logger.debug("Item '{}' has no garadget config; ignoring command '{}'", itemName, command); } } /** * Call a function in the Particle REST API using the property in the binding config. The argument to the Particle * device function is the String version of the command, with a special case for calling the setState function so * sending ON, OFF, UP, DOWN and STOP commands are translated as "open", "close" or "stopped". Upon successfully * calling the Particle device function, the state of the item is updated to the return value from the function * call. * * @param itemName * the item that is receiving the command * @param publisher * the binding config (deviceId,funcName) to send the command to * @param command * the command to send to the API */ private void commandGaradget(final String itemName, final GaradgetPublisher publisher, Command command) { if (connection == null) { logger.warn("Command '{}' not sent for item '{}'; no connection.", command, itemName); return; } try { final GaradgetDevice device = devices.get(publisher.getDeviceId()); if (device == null) { logger.warn("No device found with ID: {}", publisher.getDeviceId()); return; } // Handle the special cases for setState function if ("setState".equals(publisher.getFuncName())) { if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP) || command.equals(PercentType.ZERO)) { command = new StringType("open"); } else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN) || command.equals(PercentType.HUNDRED)) { command = new StringType("close"); } else if (command.equals(StopMoveType.STOP) || command instanceof PercentType) { command = new StringType("stopped"); } } // TODO: make JSON properly so special characters are escaped as needed String json = String.format("{ \"arg\": \"%s\" }\r\n", command.toString()); connection.sendCommand(device, publisher.getFuncName(), json, new HttpResponseHandler() { @Override public void handleResponse(int statusCode, String responseBody) { if (statusCode == HttpStatus.SC_OK) { logger.debug("Calling function '{}' returned '{}'", publisher.getFuncName(), responseBody); // A function was called successfully; poll soon schedulePoll(quickPollInterval); } else { logger.warn("Failed to call function '{}', status code={}", publisher.getFuncName(), statusCode); } } }); } catch (Exception ex) { logger.warn("Exception in commandGaradget:", ex); } } protected void addBindingProvider(GaradgetBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(GaradgetBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } private GaradgetBindingConfig getConfigForItemName(String itemName) { for (GaradgetBindingProvider provider : providers) { GaradgetBindingConfig bindingConfig = provider.getItemBindingConfig(itemName); if (bindingConfig != null) { return bindingConfig; } } return null; } /** * Return true if this instance is at or past the time to poll. * * @return if this instance is at or past the time to poll. */ private boolean pollTimeExpired() { return System.currentTimeMillis() >= pollTime.get(); } /** * Record the earliest time in the future at which we are allowed to poll this instance. * * @param future * the number of milliseconds in the future */ private void schedulePoll(long future) { this.pollTime.set(System.currentTimeMillis() + future); } }