/** * 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.autelis.internal; import java.io.StringReader; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.apache.commons.lang.StringUtils; import org.openhab.binding.autelis.AutelisBindingProvider; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.items.Item; import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.io.net.http.HttpUtil; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.InputSource; /** * Autelis Pool Control Binding * * Autelis controllers allow remote access to many common pool systems. This * binding allows openHAB to both monitor and control a pool system through these * controllers. * * @see <a href="http://Autelis.com">http://autelis.com</a> * * @author Dan Cunningham * @since 1.7.0 */ public class AutelisBinding extends AbstractActiveBinding<AutelisBindingProvider> { private static final Logger logger = LoggerFactory.getLogger(AutelisBinding.class); /** * Default port to use for connections to a Autelis controller */ static final int DEFAULT_PORT = 80; /** * Default timeout for http connections to a Autelis controller */ static final int TIMEOUT = 5000; /** * Dim command for lights */ static final String AUTELIS_CMD_DIM = "dim"; /** * UP command */ static final String AUTELIS_CMD_UP = "up"; /** * Down Command */ static final String AUTELIS_CMD_DOWN = "down"; /** * Value command */ static final String AUTELIS_CMD_VALUE = "value"; /** * Value command */ static final String AUTELIS_CMD_HEAT = "hval"; /** * Equipment status type */ static final String AUTELIS_TYPES_EQUIP = "equipment"; /** * General status type */ static final String AUTELIS_TYPES_STATUS = "status"; /** * Temperature status type */ static final String AUTELIS_TYPES_TEMP = "temp"; /** * Lights type */ static final String AUTELIS_TYPES_LIGHTS = "lightscmd"; /** * Chemistry status type */ static final String AUTELIS_TYPES_CHEMISTRY = "chem"; /** * Pumps status type. */ static final String AUTELIS_TYPES_PUMPS = "pumps"; /** * Setpoint */ static final String AUTELIS_SETPOINT = "sp"; /** * Heat point ? */ static final String AUTELIS_HEATPOINT = "hp"; /** * Heat type? */ static final String AUTELIS_HEATTYPE = "ht"; /** * Constructed URL consisting of host, port, username and password use to connect to a Autelis controller */ private String baseURL; /** * Regex expression to match XML responses from the Autelis, this is used to combine similar XML docs * into a single document, {@link XPath} is still used for XML querying */ private Pattern responsePattern = Pattern.compile("<response>(.+?)</response>", Pattern.DOTALL); /** * Commands can only be sent on equipment and temp configurations */ private Pattern commandPattern = Pattern.compile("^(equipment|temp)\\.(.*)"); /** * Autelis controllers will not update their XML immediately after we change a value. To compensate * we cache previous values for a {@link Item} using the item name as a key. After a polling * run has been executed we only update an item if the value is different then what's in the * cache. This cache is cleared after a fixed time period when commands are sent. */ private Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<String, State>()); /** * Clear our state every hour */ private static int NORMAL_CLEARTIME = 60 * 60; // one hour /** * Clear state after an command is sent */ private static int UPDATE_CLEARTIME = 60 * 2; // two minutes /** * Holds the next clear time in millis */ private long clearTime; /** * the refresh interval which is used to poll values from the Autelis * device (optional, defaults to 5000ms) */ private long refreshInterval = 5000; public AutelisBinding() { logger.debug("Autelis binding started"); } /** * 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) { configureBinding(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) { configureBinding(configuration); } /** * Configures binding for both bundle activation and modification. * * @param configuration */ private void configureBinding(final Map<String, Object> configuration) { String refreshIntervalString = (String) configuration.get("refresh"); if (StringUtils.isNotBlank(refreshIntervalString)) { refreshInterval = Long.parseLong(refreshIntervalString); } int port = DEFAULT_PORT; String host = (String) configuration.get("host"); String username = (String) configuration.get("username"); String password = (String) configuration.get("password"); if (StringUtils.isBlank(host)) { logger.error("Host config parameter is missing"); setProperlyConfigured(false); return; } String portString = (String) configuration.get("port"); if (StringUtils.isNotBlank(portString)) { port = Integer.parseInt(portString); } String prefix = ""; if (username != null) { prefix = username + ":" + password + "@"; } baseURL = "http://" + prefix + host + ":" + port; logger.debug("Autelis binding configured for host {}", host); setProperlyConfigured(true); } /** * @{inheritDoc} */ @Override protected long getRefreshInterval() { return refreshInterval; } /** * @{inheritDoc} */ @Override protected String getName() { return "autelis"; } /** * @{inheritDoc} */ @Override protected void execute() { logger.trace("Connecting to {}" + baseURL); clearState(); String xmlDoc = fetchStateFromController(); if (xmlDoc == null) { return; } for (AutelisBindingProvider provider : providers) { for (String itemName : provider.getItemNames()) { Item item = provider.getItem(itemName); String config = provider.getAutelisBindingConfigString(itemName); XPathFactory xpathFactory = XPathFactory.newInstance(); XPath xpath = xpathFactory.newXPath(); try { InputSource is = new InputSource(new StringReader(xmlDoc)); String value = xpath.evaluate("response/" + config.replace('.', '/'), is); State state = toState(item.getClass(), value); State oldState = stateMap.put(itemName, state); if (!state.equals(oldState)) { logger.debug("updating item {} with state {}", itemName, state); eventPublisher.postUpdate(itemName, state); } } catch (XPathExpressionException e) { logger.warn("could not parse xml", e); } } } } /** * @{inheritDoc} */ @Override protected void internalReceiveCommand(String itemName, Command command) { logger.trace("internalReceiveCommand({},{}) is called!", itemName, command); for (AutelisBindingProvider provider : providers) { Item item = provider.getItem(itemName); String config = provider.getAutelisBindingConfigString(itemName); Matcher m = commandPattern.matcher(config); if (m.find() && m.groupCount() > 1) { String type = m.group(1); String name = m.group(2); if (type.equals(AUTELIS_TYPES_EQUIP)) { String cmd = AUTELIS_CMD_VALUE; int value; if (command == OnOffType.OFF) { value = 0; } else if (command == OnOffType.ON) { value = 1; } else if (command instanceof DecimalType) { value = ((DecimalType) item.getStateAs(DecimalType.class)).intValue(); if (value >= 3) { // this is a dim type. not sure what 2 does cmd = AUTELIS_CMD_DIM; } } else { logger.error("Equipment commands must be of Decimal type not {}", command); break; } String response = HttpUtil.executeUrl("GET", baseURL + "/set.cgi?name=" + name + "&" + cmd + "=" + value, TIMEOUT); logger.trace("equipment set {} {} {} : result {}", name, cmd, value, response); } else if (type.equals(AUTELIS_TYPES_TEMP)) { String value; if (command == IncreaseDecreaseType.INCREASE) { value = AUTELIS_CMD_UP; } else if (command == IncreaseDecreaseType.DECREASE) { value = AUTELIS_CMD_DOWN; } else { value = command.toString(); } String cmd; // name ending in sp are setpoints, ht are heat types? if (name.endsWith(AUTELIS_SETPOINT)) { cmd = AUTELIS_TYPES_TEMP; } else if (name.endsWith(AUTELIS_HEATTYPE)) { cmd = AUTELIS_CMD_HEAT; } else { logger.error("Unknown temp type {}", name); break; } String response = HttpUtil.executeUrl("GET", baseURL + "/set.cgi?wait=1&name=" + name + "&" + cmd + "=" + value, TIMEOUT); logger.trace("temp set {} {} : result {}", cmd, value, response); } } else if (config.equals(AUTELIS_TYPES_LIGHTS)) { /* * lighting command * possible values, but we will let anything through. * alloff, allon, csync, cset, cswim, party, romance, caribbean, american, * sunset, royalty, blue, green, red, white, magenta, hold, recall */ String response = HttpUtil.executeUrl("GET", baseURL + "lights.cgi?val=" + command.toString(), TIMEOUT); logger.trace("lights set {} : result {}", command.toString(), response); } else { logger.error("Unsupported set config {}", config); } } scheduleClearTime(UPDATE_CLEARTIME); } /** * Fetches the XML string from a Autelis controller. * * @return */ private String fetchStateFromController() { // we will reconstruct the document with all the responses combined for // XPATH StringBuilder sb = new StringBuilder("<response>"); // pull down the three xml documents String[] statuses = { AUTELIS_TYPES_STATUS, AUTELIS_TYPES_CHEMISTRY, AUTELIS_TYPES_PUMPS }; for (String status : statuses) { String response = HttpUtil.executeUrl("GET", baseURL + "/" + status + ".xml", TIMEOUT); logger.trace(baseURL + "/" + status + ".xml \n {}", response); if (response == null) { logger.warn("No response from Autelis controller!"); return null; } // get the xml data between the response tags and append to our main // doc Matcher m = responsePattern.matcher(response); if (m.find()) { sb.append(m.group(1)); } } // finish our "new" XML Document sb.append("</response>"); /* * This xmlDoc will now contain the three XML documents we retrieved * wrapped in response tags for easier querying in XPath. */ return sb.toString(); } /** * Converts a {@link String} value to a {@link State} for a given {@link Item} * * @param itemType * @param value * @return {@link State} */ private State toState(Class<? extends Item> itemType, String value) { if (itemType.isAssignableFrom(NumberItem.class)) { return new DecimalType(value); } else if (itemType.isAssignableFrom(SwitchItem.class)) { return Integer.parseInt(value) > 0 ? OnOffType.ON : OnOffType.OFF; } else { return StringType.valueOf(value); } } /** * Clears our state if it is time */ private void clearState() { if (System.currentTimeMillis() >= clearTime) { stateMap.clear(); scheduleClearTime(NORMAL_CLEARTIME); } } /** * Schedule when our next clear cycle will be * * @param secs */ private void scheduleClearTime(int secs) { clearTime = System.currentTimeMillis() + (secs * 1000); } }