/** * Copyright (c) 2010-2016, openHAB.org and others. * * 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.powerdoglocalapi.internal; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.items.Item; import org.openhab.core.library.items.ContactItem; import org.openhab.core.library.items.DimmerItem; 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.PercentType; import org.openhab.core.library.types.StringType; 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.binding.powerdoglocalapi.PowerDogLocalApiBindingProvider; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redstone.xmlrpc.XmlRpcFault; import redstone.xmlrpc.XmlRpcProxy; import redstone.xmlrpc.XmlRpcStruct; /** * Queries eco-data PowerDog * * @author wuellueb * @since 1.9.0 */ public class PowerDogLocalApiBinding extends AbstractActiveBinding<PowerDogLocalApiBindingProvider> { private static final Logger logger = LoggerFactory.getLogger(PowerDogLocalApiBinding.class); /** * The BundleContext. This is only valid when the bundle is ACTIVE. It is * set in the activate() method and must not be accessed anymore once the * deactivate() method was called or before activate() was called. */ private BundleContext bundleContext; /** * the refresh interval which is used as minimum sample time to poll values * from the PowerDogLocalApi for all servers (optional, defaults to * 300000ms) */ private long refreshInterval = 300000; /** * RegEx to validate a config <code>'^(.*?)\\.(host|port)$'</code> */ private static final Pattern EXTRACT_CONFIG_PATTERN = Pattern.compile("^(.*?)\\.(.*?)$"); /** * Mapping of items to lastUpdate */ private Map<String, Long> lastUpdateMap = new HashMap<String, Long>(); /** * Mapping from serverId to PowerDog configuration structure */ private Map<String, PowerDogLocalApiServerConfig> serverList = new HashMap<String, PowerDogLocalApiServerConfig>(); public PowerDogLocalApiBinding() { } /** * 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; logger.debug("activate() method is called!"); // the configuration is guaranteed not to be null, because the component // definition has the configuration-policy set to require. If set to // 'optional' then the configuration may be null this.bundleContext = bundleContext; // to override the default refresh interval for all powerdogs one has to // add a parameter to openhab.cfg like // <bindingName>:refresh=<intervalInMs> String refreshIntervalString = (String) configuration.get("refresh"); if (StringUtils.isNotBlank(refreshIntervalString)) { refreshInterval = Long.parseLong(refreshIntervalString); } parseConfiguration(configuration); setProperlyConfigured(true); } /** * 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) { // update the internal configuration accordingly logger.debug("modified() method is called!"); parseConfiguration(configuration); setProperlyConfigured(true); } /** * 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; } /** * @{inheritDoc */ @Override protected long getRefreshInterval() { return refreshInterval; } /** * @{inheritDoc */ @Override protected String getName() { return "PowerDogLocalApi Refresh Service"; } /** * @{inheritDoc */ @Override protected void execute() { logger.debug("execute() method is called!"); // cycle over all available powerdogs for (PowerDogLocalApiBindingProvider provider : providers) { // cycle over all available in-bindings /* * TODO: check if reading of out-bindings is useful - currently it * seems to be applicable only for openhab init (but it works * without, so I give it a try) and in case several clients are * writing to the same PowerAPI, which is risky due to concurrency */ for (String itemName : provider.getInBindingItemNames()) { // get item specific refresh interval int refreshInterval = provider.getRefreshInterval(itemName); // check if item needs update Long lastUpdateTimeStamp = lastUpdateMap.get(itemName); if (lastUpdateTimeStamp == null) { lastUpdateTimeStamp = 0L; } long age = System.currentTimeMillis() - lastUpdateTimeStamp; boolean itemNeedsUpdate = (age >= refreshInterval); if (itemNeedsUpdate) { logger.debug("Item '{}' is about to be refreshed now", itemName); // Get the unit serverId from the binding, and relate that // to the config String unit = provider.getServerId(itemName); PowerDogLocalApiServerConfig server = serverList.get(unit); // get the server specific update time and check if it needs // an update boolean serverNeedsUpdate = false; if (server == null) { serverNeedsUpdate = false; logger.error("Unknown PowerDog server referenced: {}", unit); continue; } else { age = System.currentTimeMillis() - server.lastUpdate; serverNeedsUpdate = (age >= server.refresh); } XmlRpcStruct response = loadPowerDogResponse(serverNeedsUpdate, server); // update item state if (response != null) { String value = getVariable(response, provider.getValueId(itemName), provider.getName(itemName)); if (value != null) { Class<? extends Item> itemType = provider.getItemType(itemName); State state = createState(itemType, value); eventPublisher.postUpdate(itemName, state); lastUpdateMap.put(itemName, System.currentTimeMillis()); } } } } } logger.debug("execute() method is finished!"); } private XmlRpcStruct loadPowerDogResponse(boolean needsUpdate, PowerDogLocalApiServerConfig server) { // Get all current linear values from the powerdog in case of an update XmlRpcStruct response = null; if (needsUpdate == true) { try { logger.debug("PowerDogLocalApi querying PowerDog"); // perform XML RPC call and store response PowerDog powerdog = (PowerDog) XmlRpcProxy.createProxy(server.url(), "", new Class[] { PowerDog.class }, false); response = powerdog.getAllCurrentLinearValues(server.password); server.cache = response; server.lastUpdate = System.currentTimeMillis(); logger.debug("PowerDog.getAllCurrentLinearValues() result: {}", response.toString()); } catch (Exception e) { logger.warn("PowerDogLocalApi querying PowerDog failed"); logger.warn(e.getMessage()); } } else { logger.debug("Using PowerDogLocalApi cache"); response = server.cache; } return response; } /** * Parse PowerDog xmlrpc response to getAllCurrentLinearValues * * @param response * PowerDog Response * @param valueId * Value ID of PowerDog Item * @param name * Parameter name to be updated * @return */ private String getVariable(XmlRpcStruct response, String valueId, String name) { try { XmlRpcStruct reply = response.getStruct("Reply"); XmlRpcStruct item = reply.getStruct(valueId); String value = item.getString(name); return value; } catch (Exception e) { return null; } } /** * @{inheritDoc */ @Override protected void internalReceiveCommand(String itemName, Command command) { logger.debug("internalReceiveCommand({},{}) is called!", itemName, command); State newState = null; // cast Interfaces if (command instanceof OnOffType) { newState = (OnOffType) command; } else if (command instanceof OpenClosedType) { newState = (OpenClosedType) command; } else if (command instanceof PercentType) { newState = (PercentType) command; } else if (command instanceof DecimalType) { newState = (DecimalType) command; } if (newState != null) { eventPublisher.postUpdate(itemName, newState); } } /** * @{inheritDoc */ @Override protected void internalReceiveUpdate(String itemName, State newState) { logger.debug("internalReceiveUpdate({},{}) is called!", itemName, newState); // cycle on all available powerdogs for (PowerDogLocalApiBindingProvider provider : providers) { if (!provider.providesBindingFor(itemName)) { continue; } // only in case of an outbinding, this need to be handled if (provider.getOutBindingItemNames().contains(itemName)) { // check if item may send update already now // time indicated in config is the minimum time between two // updates Long lastUpdateTimeStamp = lastUpdateMap.get(itemName); if (lastUpdateTimeStamp == null) { lastUpdateTimeStamp = 0L; } long age = System.currentTimeMillis() - lastUpdateTimeStamp; boolean itemMayUpdate = (age >= provider.getRefreshInterval(itemName)); if (itemMayUpdate) { // Convert new State to PowerDog set Current_Value string String value = "0"; if (newState instanceof OnOffType) { if (newState == OnOffType.ON) { value = "1"; } } // C-like Not-Zero is True, Zero is false; Powerdog does // not offer boolean for PowerAPI, so this might not be // the best solution, but it is sufficient else if (newState instanceof OpenClosedType) { if (newState == OpenClosedType.OPEN) { value = "1"; } } // see comment above else if (newState instanceof PercentType) { value = newState.toString(); } else if (newState instanceof DecimalType) { value = newState.toString(); } // Get the unit serverId from the binding, and relate that // to the config String unit = provider.getServerId(itemName); PowerDogLocalApiServerConfig server = serverList.get(unit); try { logger.debug("PowerDogLocalApi sending to PowerDog"); // Perform XML RPC call PowerDog powerdog = (PowerDog) XmlRpcProxy.createProxy(server.url(), "", new Class[] { PowerDog.class }, false); XmlRpcStruct response = powerdog.setLinearSensorDevice(server.password, provider.getValueId(itemName), value); lastUpdateMap.put(itemName, System.currentTimeMillis()); logger.debug("PowerDog.setLinearSensorDevice() result: {}", response.toString()); } catch (Exception e) { logger.warn("PowerDogLocalApi sending to PowerDog failed"); logger.warn(e.getMessage()); } } } } } /** * Returns a {@link State} which is inherited from the {@link Item}s * accepted DataTypes. The call is delegated to the {@link TypeParser}. If * <code>item</code> is <code>null</code> the {@link StringType} is used. * * PowerDog supports in the PowerAPI the following types for In-Bindings, * which should be mapped to the following items: V, A, �C, W, l, m/s, km/h * --> Number, String % --> Number, Switch, Dimmer, Contact* (in case of * Switch, 100% will be mapped to ON; in case of Contact, 100% is mapped to * OPEN) String (from PowerDog API output) --> String * * @param itemType * @param transformedResponse * * @return a {@link State} which type is inherited by the {@link TypeParser} * or a {@link StringType} if <code>item</code> is <code>null</code> */ private State createState(Class<? extends Item> itemType, String transformedResponse) { try { // Assign according to output type or cast to output type directly if (itemType.isAssignableFrom(SwitchItem.class)) { int value = Math.round(Float.parseFloat(transformedResponse)); if (value > 0) { return OnOffType.ON; } else { return OnOffType.OFF; } } else if (itemType.isAssignableFrom(DimmerItem.class)) { return new PercentType(Math.round(Float.parseFloat(transformedResponse))); } else if (itemType.isAssignableFrom(ContactItem.class)) { int value = Math.round(Float.parseFloat(transformedResponse)); if (value > 0) { return OpenClosedType.OPEN; } else { return OpenClosedType.CLOSED; } } else if (itemType.isAssignableFrom(NumberItem.class)) { return DecimalType.valueOf(transformedResponse); } else { return StringType.valueOf(transformedResponse); } } catch (Exception e) { logger.debug("Couldn't create state of type '{}' for value '{}'", itemType, transformedResponse); return StringType.valueOf(transformedResponse); } } /** * Parse PowerDog Openhab configuration * * @param config * PowerDog configuration string */ private void parseConfiguration(Map<String, Object> config) { logger.debug("PowerDogLocalApi:parseConfiguration() method is called!"); if (config != null) { Set<String> keyset = config.keySet(); // create server list of not yet available if (serverList == null) { serverList = new HashMap<String, PowerDogLocalApiServerConfig>(); } // check keys of config set for (Iterator<String>keys = keyset.iterator(); keys.hasNext();) { String key = keys.next(); // the config-key enumeration contains additional keys that we // don't want to process here ... if ("service.pid".equals(key)) { continue; } else if ("event.topics".equals(key)) { continue; } else if ("component.name".equals(key)) { continue; } else if ("component.id".equals(key)) { continue; } else if ("objectClass".equals(key)) { continue; } // check if key matches powerdog-regex Matcher matcher = EXTRACT_CONFIG_PATTERN.matcher(key); if (!matcher.matches()) { continue; } matcher.reset(); matcher.find(); // get serverId as first item String serverId = matcher.group(1); // create config item for this specific powerdog unit PowerDogLocalApiServerConfig deviceConfig = serverList.get(serverId); if (deviceConfig == null) { deviceConfig = new PowerDogLocalApiServerConfig(); serverList.put(serverId, deviceConfig); } // extract values for host, port, password or refresh String configKey = matcher.group(2); String value = (String) config.get(key); if ("host".equals(configKey)) { deviceConfig.host = value; logger.debug("value: {}", value); } else if ("port".equals(configKey)) { if (StringUtils.isNotBlank(value)) { deviceConfig.port = (int) Long.parseLong(value); logger.debug("value: {}", value); } } else if ("password".equals(configKey)) { deviceConfig.password = value; } else if ("refresh".equals(configKey)) { if (StringUtils.isNotBlank(value)) { // refresh cannot be lower than refresh interval deviceConfig.refresh = (int) Math.max(Long.parseLong(value), refreshInterval); logger.debug("value: {}", value); } } else { // cannot throw new ConfigurationException(configKey, // "The given PowerDogLocalApi configKey '" + configKey + // "' is unknown"); logger.warn("The given PowerDogLocalApi configKey '{}' is unknown", configKey); } logger.debug("New Server config: {}", deviceConfig.toString()); } setProperlyConfigured(true); logger.debug("PowerDogLocalApi:parseConfiguration() method is terminated"); } } static class PowerDogLocalApiServerConfig { public String host; // IP adress or DNS entry public int port; // port number public String password; // password public int refresh; // refresh rate in ms public Long lastUpdate; // saves last update time when xmlrpc was read public XmlRpcStruct cache; PowerDogLocalApiServerConfig() { lastUpdate = (long) 0; // set defaults refresh = 300000; // 5 min is default password = ""; // empty password will normally not be accepted by // PowerDog, needs to be configured port = 20000; // port 20000 is default for PowerDog host = "powerdog"; // local DNS in router might resolve this one } @Override public String toString() { String displayPassword = "[not set]"; if (StringUtils.isNotBlank(password)) { displayPassword = "[set]*****"; } return "PowerDogLocalApiServerCache [host=" + host + ", password=" + displayPassword + ", lastUpdate=" + lastUpdate + ", cache=" + cache + "]"; } public URL url() throws MalformedURLException { return new URL("http", host, port, ""); } } /*- * PowerAPI Local Device API 0.b (15.02.2013) * * PowerDog supports via the PowerAPI Local Device API live communication * with your PowerDog device. The API is accessible using XMLRPC. * * This interface defines the possible RPC communication with the PowerDog. * This interface is according to PowerDog's RPC interface as per the * publicly available document * http://api.power-dog.eu/documentation/DOCUMENATION/PowerAPI%20Local%20Device%20API%20Description_v0.b.pdf * * Tested against PowerDog Firmware V1.84 (High Velocity) 2013-09-19 * * VariantMap getPowerDogInfo(String password); * VariantMap getSensors(String password); * VariantMap getCounters(String password); * VariantMap getRegulations(String password); * VariantMap getLinearDevices(String password); * VariantMap getAllCurrentLinearValues(String password); * VariantMap getCurrentLinearValues(String password, String comma_seperated_list_of_keys); * VariantMap setLinearSensorDevice(String password, String key, String current_value); * VariantMap setLinearCounterDevice(String password, String key, String current_value, String countup_meter_reading); * * Remark: PowerDog also supports a web API with non-live data using the web * service available at http://power-dog.eu - this interface is different * and neither used nor supported by the PowerDogLocalApiBinding. * * @author Wuellueb * */ static interface PowerDog { public XmlRpcStruct getPowerDogInfo(String password) throws XmlRpcFault; public XmlRpcStruct getSensors(String password) throws XmlRpcFault; public XmlRpcStruct getCounters(String password) throws XmlRpcFault; public XmlRpcStruct getRegulations(String password) throws XmlRpcFault; public XmlRpcStruct getLinearDevices(String password) throws XmlRpcFault; public XmlRpcStruct getAllCurrentLinearValues(String password) throws XmlRpcFault; public XmlRpcStruct getCurrentLinearValues(String password, String comma_seperated_list_of_keys) throws XmlRpcFault; public XmlRpcStruct setLinearSensorDevice(String password, String key, String current_value) throws XmlRpcFault; public XmlRpcStruct setLinearCounterDevice(String password, String key, String current_value, String countup_meter_reading) throws XmlRpcFault; } }