org.openhab.binding.powerdoglocalapi.internal.PowerDogLocalApiBinding.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.powerdoglocalapi.internal.PowerDogLocalApiBinding.java

Source

/**
 * Copyright (c) 2010-2019 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.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;
    }
}