/** * 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.primare.internal; import java.util.Dictionary; import java.util.Enumeration; import java.util.EventObject; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openhab.binding.primare.PrimareBindingProvider; import org.openhab.binding.primare.internal.protocol.PrimareConnector; import org.openhab.binding.primare.internal.protocol.PrimareResponse; import org.openhab.binding.primare.internal.protocol.PrimareSerialConnector; import org.openhab.binding.primare.internal.protocol.PrimareTCPConnector; import org.openhab.binding.primare.internal.protocol.PrimareUtils; import org.openhab.core.binding.AbstractBinding; import org.openhab.core.binding.BindingChangeListener; import org.openhab.core.binding.BindingProvider; import org.openhab.core.items.Item; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Binding listening to openHAB event bus and send commands to Primare devices when certain * commands are received. * * @author Pauli Anttila, Veli-Pekka Juslin * @since 1.7.0 */ public class PrimareBinding extends AbstractBinding<PrimareBindingProvider> implements ManagedService, BindingChangeListener, PrimareEventListener { private static final Logger logger = LoggerFactory.getLogger(PrimareBinding.class); protected static final String WILDCARD_COMMAND_KEY = "*"; /** RegEx to validate a config <code>'^(.*?)\\.(host|port|serial|model)$'</code> */ private static final Pattern EXTRACT_CONFIG_PATTERN = Pattern.compile("^(.*?)\\.(host|port|serial|model)$"); /** Map table to store all available receivers configured by the user */ protected Map<String, DeviceConfig> deviceConfigCache = null; public PrimareBinding() { } @Override public void activate() { } @Override public void deactivate() { closeAllConnections(); } /** * {@inheritDoc} */ @Override public void bindingChanged(BindingProvider provider, String itemName) { logger.debug("bindingChanged for {}", itemName); initializeItem(itemName); } /** * @{inheritDoc} */ @Override protected void internalReceiveCommand(String itemName, Command command) { if (itemName == null) { logger.warn("ignore command {} - no item name", command); return; } logger.debug("Received command [item:{} command:{}, class:{}]", itemName, command.toString(), command.getClass().toString()); PrimareBindingProvider provider = findFirstMatchingBindingProvider(itemName, command.toString()); if (provider == null) { logger.warn("Ignore item:{} command:{} - no matching binding provider", itemName, command); return; } String tmp = provider.getDeviceCommand(itemName, command.toString()); if (tmp == null) { tmp = provider.getDeviceCommand(itemName, WILDCARD_COMMAND_KEY); } if (tmp == null) { logger.warn("Ignore item:{} command {} - no matching command", itemName, command); return; } String[] commandParts = tmp.split(":"); String deviceId = commandParts[0]; String deviceCmd = commandParts[1]; if (deviceId == null || deviceCmd == null) { logger.warn("Ignore item:{} command:{} - failed to find both device id and command in {}", itemName, command, tmp); return; } DeviceConfig deviceConfig = deviceConfigCache.get(deviceId); if (deviceConfig == null) { logger.warn("Ignore item:{} command:{} - no configuration found for device {}", itemName, command, deviceId); return; } PrimareConnector deviceConnector = deviceConfig.getInitializedConnector(); if (deviceConnector == null) { logger.warn("Ignore {} item:{} command:{} - no connector found", deviceConfig.toString(), itemName, command); return; } if (deviceConnector.isConnected()) { try { deviceConnector.sendCommand(command, deviceCmd); } catch (Exception e) { logger.warn("Ignore {} item:{} command:{} - message send error {}", deviceConfig.toString(), itemName, command, e.getMessage()); } } else { logger.warn("Ignore {} item:{} command:{} - device disconnected", deviceConfig.toString(), itemName, command); } } /** * Find the first matching {@link PrimareBindingProvider} according to * <code>itemName</code> and <code>command</code>. * * @param itemName * the item for which to find a provider * @param command * the openHAB command for which to find a provider * * @return the matching binding provider or <code>null</code> if no binding * provider could be found */ private PrimareBindingProvider findFirstMatchingBindingProvider(String itemName, String command) { PrimareBindingProvider firstMatchingProvider = null; for (PrimareBindingProvider provider : this.providers) { String tmp = provider.getDeviceCommand(itemName, command.toString()); if (tmp != null) { firstMatchingProvider = provider; break; } } if (firstMatchingProvider == null) { for (PrimareBindingProvider provider : this.providers) { String tmp = provider.getDeviceCommand(itemName, WILDCARD_COMMAND_KEY); if (tmp != null) { firstMatchingProvider = provider; break; } } } return firstMatchingProvider; } protected void addBindingProvider(PrimareBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(PrimareBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } /** * {@inheritDoc} */ @Override public void updated(Dictionary<String, ?> config) throws ConfigurationException { logger.debug("Configuration updated, valid configuration {}", (config != null ? "exists" : "does not exist")); if (config != null) { Enumeration<String> keys = config.keys(); if (deviceConfigCache == null) { deviceConfigCache = new HashMap<String, DeviceConfig>(); } while (keys.hasMoreElements()) { String key = keys.nextElement(); // the config-key enumeration contains additional keys that we // don't want to process here ... if ("service.pid".equals(key)) { continue; } Matcher matcher = EXTRACT_CONFIG_PATTERN.matcher(key); if (!matcher.matches()) { logger.debug("given config key '" + key + "' does not follow the expected pattern '<id>.<host|port|model>'"); continue; } matcher.reset(); matcher.find(); String deviceId = matcher.group(1); DeviceConfig deviceConfig = deviceConfigCache.get(deviceId); if (deviceConfig == null) { deviceConfig = new DeviceConfig(deviceId); deviceConfigCache.put(deviceId, deviceConfig); } String configKey = matcher.group(2); String value = (String) config.get(key); if ("host".equals(configKey)) { deviceConfig.host = value; } else if ("port".equals(configKey)) { deviceConfig.port = Integer.valueOf(value); } else if ("serial".equals(configKey)) { deviceConfig.serialport = value; } else if ("model".equals(configKey)) { String model = value.toUpperCase(); deviceConfig.model = value.toUpperCase(); } else { throw new ConfigurationException(configKey, "the given configKey '" + configKey + "' is unknown"); } } // open connection to all receivers for (String device : deviceConfigCache.keySet()) { PrimareConnector connector = deviceConfigCache.get(device).getInitializedConnector(); if (connector != null) { try { connector.connect(); connector.addEventListener(this); } catch (Exception e) { logger.warn("failed to connect to {} after configuration update", device.toString()); } } } for (PrimareBindingProvider provider : this.providers) { for (String itemName : provider.getItemNames()) { initializeItem(itemName); } } } } private void closeAllConnections() { if (deviceConfigCache != null) { for (String device : deviceConfigCache.keySet()) { PrimareConnector connector = deviceConfigCache.get(device).getConnector(); if (connector != null) { connector.disconnect(); connector.removeEventListener(this); } } deviceConfigCache = null; } } /** * {@inheritDoc} */ @Override public void statusUpdateReceived(EventObject event, String deviceId, byte[] data) { logger.trace("statusUpdateReceived for device {}: {}", deviceId, PrimareUtils.byteArrayToHex(data)); DeviceConfig deviceConfig = deviceConfigCache.get(deviceId); if (deviceConfig != null) { logger.trace("Received status update '{}' from device {}", PrimareUtils.byteArrayToHex(data), deviceConfig.toString()); PrimareResponse deviceResponse = null; deviceResponse = deviceConfig.connector.getResponseFactory().getResponse(data); for (PrimareBindingProvider provider : providers) { for (String itemName : provider.getItemNames()) { // Update all items which refer to command HashMap<String, String> values = provider.getDeviceCommands(itemName); for (String cmd : values.keySet()) { String[] commandParts = values.get(cmd).split(":"); String deviceCmd = commandParts[1]; boolean match = deviceResponse.isRelevantFor(deviceCmd); if (match) { Class<? extends Item> itemType = provider.getItemType(itemName); State v = deviceResponse.openHabState(itemType); logger.trace("Updating itemName {} to {} based on device command {}", itemName, v, deviceCmd); eventPublisher.postUpdate(itemName, v); break; } } } } } } /** * Initialize item value. Method sends a query to receiver if init query is * configured to binding item configuration * * @param itemType * */ private void initializeItem(String itemName) { logger.debug("Called initializeItem for {}, ignored", itemName); for (PrimareBindingProvider provider : providers) { String initCmd = provider.getItemInitCommand(itemName); if (initCmd == null) { logger.debug("No init command found for item {}", itemName); continue; } logger.debug("Initialize item {} with {}", itemName, initCmd); String[] commandParts = initCmd.split(":"); String deviceId = commandParts[0]; String deviceCmd = commandParts[1]; if (deviceId == null || deviceCmd == null) { logger.warn( "Initializing - ignore item:{} initCmd:{} - failed to find both device id and command in {}", itemName, initCmd, initCmd); continue; } DeviceConfig deviceConfig = deviceConfigCache.get(deviceId); if (deviceConfig == null) { logger.warn("Ignore item:{} initCmd:{} - no configuration found for device {}", itemName, initCmd, deviceId); continue; } PrimareConnector connector = deviceConfig.getInitializedConnector(); if (connector == null) { logger.warn("Ignore device:{} item:{} initCmd:{} - no connector (IP or serial) found for device {}", deviceId, itemName, initCmd, deviceId); continue; } if (!connector.isConnected()) { logger.warn("Ignore device:{} item:{} initCmd:{} - {} not connected", deviceId, itemName, initCmd, deviceConfig.toString()); continue; } try { // There are no arguments for a device init command connector.sendCommand(null, deviceCmd); } catch (Exception e) { logger.warn("Ignore device:{} item:{} initCmd:{} Message send error {}", deviceId, itemName, initCmd, e.getMessage()); } } return; } /** * Internal data structure which carries the connection details of one * device (there could be several) */ static class DeviceConfig { // TCP-to-serial converter connected to the Primare device private String host; // hostname or IP address private int port = 0; // port // Direct serial connection to Primare device private String serialport = null; // Serial port device or PTY, e.g. /dev/ttyS0, /dev/pts/5 // Primare model in uppercase, examples: "SP31.7", "SP31", "SPA20", "SPA21" private String model = null; // Primare model -dependent PrimareConnector subclass instance private PrimareConnector connector = null; // Device id used in config files private String deviceId; public DeviceConfig(String deviceId) { this.deviceId = deviceId; } @Override public String toString() { if (deviceId == null && model == null && serialport == null && host == null && port == 0) { return "Primare device [uninitialized]"; } else { return (serialport == null ? String.format("Primare device [id:%s model:%s host:%s port:%d]", deviceId, model, host, port) : String.format("Primare device [id:%s model:%s serial:%s]", deviceId, model, serialport)); } } // Initializes a connector based on openhab.cfg configuration. // See PrimareTCPConnector, PrimareSerialConnector if implementing // support for new Primare models public PrimareConnector getInitializedConnector() { if (connector == null) { connector = (serialport == null ? PrimareTCPConnector.newForModel(model, deviceId, host, port) : PrimareSerialConnector.newForModel(model, deviceId, serialport)); } return connector; } public PrimareConnector getConnector() { return connector; } } }