org.openhab.binding.homematic.internal.bus.HomematicBinding.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.homematic.internal.bus.HomematicBinding.java

Source

/**
 * Copyright (c) 2010-2014, 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.homematic.internal.bus;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;
import org.openhab.binding.homematic.HomematicBindingProvider;
import org.openhab.binding.homematic.internal.ccu.CCU;
import org.openhab.binding.homematic.internal.ccu.CCURF;
import org.openhab.binding.homematic.internal.config.AdminItem;
import org.openhab.binding.homematic.internal.config.ConfiguredDevice;
import org.openhab.binding.homematic.internal.config.DeviceConfigLocator;
import org.openhab.binding.homematic.internal.config.HomematicParameterAddress;
import org.openhab.binding.homematic.internal.converter.command.CommandConverter;
import org.openhab.binding.homematic.internal.converter.lookup.ConverterLookup;
import org.openhab.binding.homematic.internal.converter.lookup.StateConverterLookupByConfiguredDevices;
import org.openhab.binding.homematic.internal.converter.lookup.StateConverterLookupByCustomConverter;
import org.openhab.binding.homematic.internal.converter.lookup.StateConverterLookupByParameterId;
import org.openhab.binding.homematic.internal.converter.lookup.StateConverterLookupByParameterIdConfigurer;
import org.openhab.binding.homematic.internal.converter.state.StateConverter;
import org.openhab.binding.homematic.internal.device.ParameterKey;
import org.openhab.binding.homematic.internal.device.channel.HMChannel;
import org.openhab.binding.homematic.internal.device.physical.HMPhysicalDevice;
import org.openhab.binding.homematic.internal.device.physical.rf.DefaultHMRFDevice;
import org.openhab.binding.homematic.internal.xmlrpc.HomematicBindingException;
import org.openhab.binding.homematic.internal.xmlrpc.XmlRpcConnectionRF;
import org.openhab.binding.homematic.internal.xmlrpc.callback.CallbackHandler;
import org.openhab.binding.homematic.internal.xmlrpc.callback.CallbackReceiver;
import org.openhab.binding.homematic.internal.xmlrpc.callback.CallbackServer;
import org.openhab.binding.homematic.internal.xmlrpc.impl.Paramset;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.binding.BindingProvider;
import org.openhab.core.items.Item;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.event.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Homematic binding implementation.
 * 
 * @author Thomas Letsch (contact@thomas-letsch.de)
 * @since 1.2.0
 */
public class HomematicBinding extends AbstractActiveBinding<HomematicBindingProvider>
        implements ManagedService, CallbackReceiver {

    private static final Logger logger = LoggerFactory.getLogger(HomematicBinding.class);

    private static final String CONFIG_KEY_CCU_HOST = "host";
    private static final String CONFIG_KEY_CALLBACK_HOST = "callback.host";
    private static final String CONFIG_KEY_CALLBACK_PORT = "callback.port";
    private static final Integer DEFAULT_CALLBACK_PORT = 9123;
    private static final String CONFIG_KEY_CONNECTION_REFRESH_INTERVALL = "connection.refresh.ms";
    private static final long DEFAULT_INTERVALL_5_MINUTES = TimeUnit.MINUTES.toMillis(5);

    private ConverterLookup converterLookup = new ConverterLookup();

    private CCU<?> ccu;
    private Integer callbackPort;
    private String ccuHost;
    private String callbackHost;
    private CallbackServer cbServer;
    private long checkAlifeIntervallMS;
    private long lastEventTime = 0;

    private StateConverterLookupByConfiguredDevices converterLookupByConfiguredDevices;
    private StateConverterLookupByCustomConverter converterLookupByCustomConverter;
    private StateConverterLookupByParameterId converterLookupByParameterId;

    private Map<String, State> itemStates = new HashMap<String, State>();

    public HomematicBinding() {

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                removeCallbackHandler();
            }
        });

        converterLookupByConfiguredDevices = new StateConverterLookupByConfiguredDevices();
        DeviceConfigLocator locator = new DeviceConfigLocator("HM-CC-RT-DN.xml", "HM-LC-Dim1L-Pl.xml",
                "HM-LC-Bl1PBU-FM.xml", "HM-LC-Bl1-FM.xml", "HM-LC-Dim2L-SM.xml", "HM-LC-Dim2L-CV.xml",
                "HM-LC-Dim1L-CV.xml", "HM-LC-Dim1T-Pl.xml", "HM-LC-Dim1T-CV.xml", "HM-LC-Dim2T-SM.xml",
                "HM-PB-4DIS-WM.xml", "HM-Sec-SD.xml", "HM-Sec-SC.xml", "HM-Sec-RHS.xml");
        List<ConfiguredDevice> configuredDevices = locator.findAll();
        converterLookupByConfiguredDevices.addConfiguredDevices(configuredDevices);
        converterLookup.setConverterLookupByConfiguredDevices(converterLookupByConfiguredDevices);
        converterLookupByCustomConverter = new StateConverterLookupByCustomConverter();
        converterLookup.setConverterLookupByCustomConverter(converterLookupByCustomConverter);
        converterLookupByParameterId = new StateConverterLookupByParameterId();
        new StateConverterLookupByParameterIdConfigurer().configure(converterLookupByParameterId);
        converterLookup.setConverterLookupByParameterId(converterLookupByParameterId);
    }

    @Override
    public void activate() {
        logger.debug("activate");
        super.activate();
        if (isCCUInitialized() && !isCallbackServerInitialized()) {
            registerCallbackHandler();
        }
    }

    @Override
    public void deactivate() {
        logger.debug("deactivate");
        super.deactivate();
        if (isCallbackServerInitialized()) {
            removeCallbackHandler();
        }
    }

    @Override
    protected void internalReceiveCommand(String itemName, Command command) {
        logger.debug("Received command {} for item {}", command, itemName);
        for (HomematicBindingProvider provider : providers) {
            logger.debug("Checking provider with names {}", provider.getItemNames());
            if (provider.isAdminItem(itemName)) {
                handleAdminCommand(provider.getAdminItem(itemName), command);
            } else {
                HomematicParameterAddress parameterAddress = provider.getParameterAddress(itemName);
                State actualState = itemStates.get(itemName);
                if (actualState == null) {
                    actualState = UnDefType.NULL;
                }
                String parameterKey = parameterAddress.getParameterId();
                State newState;

                CommandConverter<?, ?> commandConverter = converterLookup
                        .getCommandToBindingValueConverter(itemName, command.getClass());
                if (commandConverter == null) {
                    if (command instanceof State) {
                        logger.debug("CommandConverter not found, using state instead.");
                        State state = (State) command;
                        newState = state;
                    } else {
                        logger.warn("No command converter found for {}. No command will be executed.",
                                parameterAddress);
                        return;
                    }
                } else {
                    newState = commandConverter.convertFrom(actualState, command);
                }
                logger.debug("Setting new state " + newState + " on item " + itemName);
                if (command instanceof StopMoveType && parameterKey.equals(ParameterKey.LEVEL.name())) {
                    // Roller shutter workaround: StopMove commands go to STOP
                    // parameterKey
                    parameterAddress = HomematicParameterAddress.from(parameterAddress.getAddress(),
                            ParameterKey.STOP.name());
                }
                setStateOnDevice(newState, parameterAddress, itemName);
            }
        }
    }

    @Override
    public void handleEvent(Event event) {
        super.handleEvent(event);
    }

    @Override
    protected void internalReceiveUpdate(String itemName, State newState) {
        itemStates.put(itemName, newState);
        for (HomematicBindingProvider provider : providers) {
            logger.debug("Checking provider with names {}", provider.getItemNames());
            HomematicParameterAddress parameterAddress = provider.getParameterAddress(itemName);
            setStateOnDevice(newState, parameterAddress, itemName);
        }
    }

    @Override
    public Integer event(String interfaceId, String address, String parameterKey, Object valueObject) {
        HomematicParameterAddress parameterAddress = HomematicParameterAddress.from(address, parameterKey);
        logger.debug("Received new value {} for device at {}", valueObject, parameterAddress);
        lastEventTime = System.currentTimeMillis();
        String itemName = getItemNameForParameter(parameterAddress);
        if (itemName != null) {
            StateConverter<?, ?> converter = converterLookup.getBindingValueToStateConverter(itemName);
            if (converter == null) {
                logger.warn("No converter found for " + parameterAddress + " - doing nothing.");
                return null;
            }
            State value = converter.convertTo(valueObject);
            logger.debug("Received new value {} for item {}", value, itemName);
            if (ParameterKey.PRESS_SHORT.name().equals(parameterAddress.getParameterId())) {
                // Workaround for button short not sending an OFF command
                postUpdate(itemName, OnOffType.ON);
                postUpdate(itemName, OnOffType.OFF);
            } else {
                postUpdate(itemName, value);
            }
        }
        return null;
    }

    private void postUpdate(String itemName, State value) {
        itemStates.put(itemName, value);
        eventPublisher.postUpdate(itemName, value);
    }

    @SuppressWarnings("unused")
    private void postCommand(String itemName, Command value) {
        if (value instanceof State) {
            State state = (State) value;
            itemStates.put(itemName, state);
        }
        eventPublisher.postCommand(itemName, value);
    }

    private void setStateOnDevice(State newState, HomematicParameterAddress parameterAddress, String itemName) {
        HMPhysicalDevice device = ccu.getPhysicalDevice(parameterAddress.getDeviceId());
        HMChannel channel = device.getChannel(parameterAddress.getChannelNumber());
        String parameterKey = parameterAddress.getParameterId();
        StateConverter<?, ?> converter = converterLookup.getStateToBindingValueConverter(itemName,
                newState.getClass());
        if (converter == null) {
            logger.warn("No converter found for " + parameterAddress + "!");
            return;
        }
        Object value = converter.convertFrom(newState);
        logger.debug("Setting new value " + value + " on parameter " + parameterAddress);
        channel.setValue(parameterKey, value);
    }

    @Override
    public void updated(Dictionary<String, ?> config) throws ConfigurationException {
        logger.debug("updated config={}", config);
        if (config == null) {
            return;
        }
        String checkAliveIntervallStr = (String) config.get(CONFIG_KEY_CONNECTION_REFRESH_INTERVALL);
        if (StringUtils.isBlank(checkAliveIntervallStr)) {
            checkAlifeIntervallMS = DEFAULT_INTERVALL_5_MINUTES;
        } else {
            checkAlifeIntervallMS = Integer.valueOf(checkAliveIntervallStr);
        }
        ccuHost = (String) config.get(CONFIG_KEY_CCU_HOST);
        ccu = new CCURF(new XmlRpcConnectionRF(ccuHost));
        converterLookupByConfiguredDevices.setCcu(ccu);
        String callbackPortStr = (String) config.get(CONFIG_KEY_CALLBACK_PORT);
        if (StringUtils.isBlank(callbackPortStr)) {
            callbackPort = DEFAULT_CALLBACK_PORT;
        } else {
            callbackPort = Integer.valueOf(callbackPortStr);
        }
        callbackHost = (String) config.get(CONFIG_KEY_CALLBACK_HOST);
        if (StringUtils.isBlank(callbackHost)) {
            callbackHost = LocalNetworkInterface.getLocalNetworkInterface();
        }
        if (isCallbackServerInitialized()) {
            removeCallbackHandler();
        }
        registerCallbackHandler();
        setProperlyConfigured(true);
        for (HomematicBindingProvider provider : providers) {
            queryAndSendAllActualStates(provider);
        }
    }

    @Override
    public void allBindingsChanged(BindingProvider provider) {
        logger.debug("allBindingsChanged provider={}", provider);
        if (provider instanceof HomematicBindingProvider) {
            HomematicBindingProvider homematicBindingProvider = (HomematicBindingProvider) provider;
            queryAndSendAllActualStates(homematicBindingProvider);
        }
    }

    @Override
    public void bindingChanged(BindingProvider provider, String itemName) {
        logger.debug("bindingChanged provider={}, itemName={}", provider, itemName);
        if (provider instanceof HomematicBindingProvider) {
            HomematicBindingProvider homematicBindingProvider = (HomematicBindingProvider) provider;
            initializeDeviceAndParameters(homematicBindingProvider, itemName);
        }
    }

    private void handleAdminCommand(AdminItem adminItem, Type type) {
        AdminCommand command = AdminCommand.valueOf(adminItem.getCommand());
        switch (command) {
        case DUMP_UNCONFIGURED_DEVICES:
            dumpUnconfiguredDevices();
            break;
        case DUMP_UNSUPPORTED_DEVICES:
            dumpUnsupportedDevices();
            break;
        }
    }

    private void dumpUnsupportedDevices() {
        // TODO Auto-generated method stub

    }

    @SuppressWarnings("unchecked")
    private void dumpUnconfiguredDevices() {
        logger.info("Dumping unconfigured devices:");
        Collection<String> configuredDeviceAddresses = new ArrayList<String>();
        for (HomematicBindingProvider provider : providers) {
            for (String itemName : provider.getItemNames()) {
                if (!provider.isAdminItem(itemName)) {
                    configuredDeviceAddresses.add(provider.getParameterAddress(itemName).getAddress());
                }
            }
        }
        Set<DefaultHMRFDevice> physicalDevices = (Set<DefaultHMRFDevice>) ccu.getPhysicalDevices();
        for (DefaultHMRFDevice device : physicalDevices) {
            if (isNonCCUDevice(device) && !configuredDeviceAddresses.contains(device.getAddress())) {
                dumpDeviceItemLine(device);
            }
        }

    }

    private void dumpDeviceItemLine(DefaultHMRFDevice device) {
        logger.info("Device with physical address " + device.getAddress() + " of type "
                + device.getDeviceDescription().getType());
        for (HMChannel channel : device.getChannels()) {
            String channelNum = channel.getAddress().split(":")[1];
            logger.info("  Channel " + channelNum + " with values " + channel.getValuesDescription());
        }
    }

    private boolean isNonCCUDevice(DefaultHMRFDevice device) {
        String parent = device.getDeviceDescription().getParent();
        return StringUtils.isBlank(parent) && !device.getAddress().equals("BidCoS-RF");
    }

    private void queryAndSendAllActualStates(HomematicBindingProvider provider) {
        logger.debug("Updating item state for items {}", provider.getItemNames());
        for (String itemName : provider.getItemNames()) {
            initializeDeviceAndParameters(provider, itemName);
        }
    }

    private void initializeDeviceAndParameters(HomematicBindingProvider provider, String itemName) {
        if (!isCCUInitialized()) {
            return;
        }
        if (provider.isAdminItem(itemName)) {
            return;
        }
        HomematicParameterAddress parameterAddress = provider.getParameterAddress(itemName);
        Item item = provider.getItem(itemName);
        if (item == null) {
            logger.warn("No item found for " + parameterAddress + " - doing nothing.");
            return;
        }
        configureConverterForItem(provider, itemName, parameterAddress, item);
        State value = getValueFromDevice(parameterAddress, item);
        postUpdate(itemName, value);
    }

    public void configureConverterForItem(HomematicBindingProvider provider, String itemName,
            HomematicParameterAddress parameterAddress, Item item) {
        if (provider.getConverter(itemName) != null) {
            converterLookupByCustomConverter.addCustomConverter(itemName, provider.getConverter(itemName));
        }
        converterLookup.configureItem(item, parameterAddress);
    }

    @SuppressWarnings("rawtypes")
    void setCCU(CCU ccu) {
        this.ccu = ccu;
    }

    private String getItemNameForParameter(HomematicParameterAddress parameterAddress) {
        for (HomematicBindingProvider provider : providers) {
            for (String itemName : provider.getItemNames()) {
                if (parameterAddress.equals(provider.getParameterAddress(itemName))) {
                    return itemName;
                }
            }
        }
        return null;
    }

    private State getValueFromDevice(HomematicParameterAddress parameterAddress, Item item) {
        HMPhysicalDevice physicalDevice = ccu.getPhysicalDevice(parameterAddress.getDeviceId());
        if (physicalDevice == null) {
            logger.warn("Physical device not found for item " + item.getName() + " with address " + parameterAddress
                    + " - no state updated.");
            return null;
        }
        HMChannel channel = physicalDevice.getChannel(parameterAddress.getChannelNumber());
        if (channel == null) {
            logger.warn("Channel not found for " + parameterAddress + " - doing nothing.");
            return null;
        }
        Paramset values = channel.getValues();
        if (values == null) {
            logger.warn("Values not found for " + parameterAddress + " - doing nothing.");
            return null;
        }
        Object valueObject = values.getValue(parameterAddress.getParameterId());
        StateConverter<?, ?> converter = converterLookup.getBindingValueToStateConverter(item.getName());
        if (converter == null) {
            logger.warn("No converter found for " + parameterAddress + " - doing nothing.");
            return null;
        }
        State value = converter.convertTo(valueObject);
        logger.debug("Found device at {} with value {}", parameterAddress, value);
        return value;
    }

    @Override
    public Object[] listDevices(String interfaceId) {
        return null;
    }

    @Override
    public Integer newDevices(String interfaceId, Object[] deviceDescriptions) {
        return null;
    }

    @Override
    public Integer deleteDevices(String interfaceId, Object[] addresses) {
        return null;
    }

    @Override
    public Integer updateDevice(String interfaceId, String address, Integer hint) {
        return null;
    }

    private synchronized void registerCallbackHandler() {
        if (isCallbackServerInitialized()) {
            return;
        }
        logger.debug("Registering callback handler.");
        CallbackHandler handler = new CallbackHandler();
        handler.registerCallbackReceiver(ccu);
        handler.registerCallbackReceiver(this);

        try {
            cbServer = new CallbackServer(InetAddress.getByName(callbackHost), callbackPort, handler);
        } catch (UnknownHostException e) {
            throw new HomematicBindingException("Could not create CallbackServer", e);
        }
        cbServer.start();
        ccu.getConnection().init("http://" + callbackHost + ":" + callbackPort + "/xmlrpc", createHomematicId());
        lastEventTime = System.currentTimeMillis();
        logger.debug("Callback handler registered.");
    }

    private synchronized void removeCallbackHandler() {
        if (!isCallbackServerInitialized()) {
            return;
        }
        logger.debug("Removing callback handler.");
        try {
            ccu.getConnection().init("", createHomematicId());
            cbServer.stop();
        } catch (Exception e) {
            logger.debug("Error while unregistering callback server. Will be ignored.");
        }
        cbServer = null;
    }

    private String createHomematicId() {
        return callbackHost + ":" + callbackPort + "/OPENHAB";
    }

    public StateConverterLookupByConfiguredDevices getConverterLookupByConfiguredDevices() {
        return converterLookupByConfiguredDevices;
    }

    @Override
    protected void execute() {
        long timeSinceLastEvent = System.currentTimeMillis() - lastEventTime;
        if (timeSinceLastEvent <= checkAlifeIntervallMS) {
            logger.debug("Last event was only " + timeSinceLastEvent + "ms ago. No need to refresh connection.");
            return;
        }
        logger.info("Check alife timeout reached. Refreshing CCU connection.");
        removeCallbackHandler();
        registerCallbackHandler();
    }

    @Override
    protected long getRefreshInterval() {
        return checkAlifeIntervallMS;
    }

    @Override
    protected String getName() {
        return "Homematic Connection Refresh Thread";
    }

    private boolean isCallbackServerInitialized() {
        return cbServer != null;
    }

    private boolean isCCUInitialized() {
        return ccu != null;
    }

    public void updateItemState(String itemName, State state) {
        itemStates.put(itemName, state);
    }

}