/**
* 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.panstamp.internal;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.openhab.binding.panstamp.PanStampBindingConfig;
import org.openhab.binding.panstamp.PanStampBindingProvider;
import org.openhab.core.binding.AbstractBinding;
import org.openhab.core.binding.BindingChangeListener;
import org.openhab.core.binding.BindingProvider;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import me.legrange.panstamp.Endpoint;
import me.legrange.panstamp.EndpointListener;
import me.legrange.panstamp.EndpointNotFoundException;
import me.legrange.panstamp.ModemException;
import me.legrange.panstamp.Network;
import me.legrange.panstamp.NetworkException;
import me.legrange.panstamp.NetworkListener;
import me.legrange.panstamp.PanStamp;
import me.legrange.panstamp.PanStampListener;
import me.legrange.panstamp.Register;
import me.legrange.panstamp.RegisterListener;
import me.legrange.panstamp.definition.CompoundDeviceLibrary;
import me.legrange.panstamp.event.AbstractNetworkListener;
import me.legrange.panstamp.event.AbstractPanStampListener;
import me.legrange.panstamp.event.AbstractRegisterListener;
import me.legrange.panstamp.xml.ClassLoaderLibrary;
import me.legrange.panstamp.xml.FileLibrary;
import me.legrange.swap.ModemSetup;
import me.legrange.swap.SerialModem;
import me.legrange.swap.SwapException;
import me.legrange.swap.SwapModem;
import me.legrange.swap.tcp.TcpException;
import me.legrange.swap.tcp.TcpServer;
/**
* This binding connects to a panStamp network and handles traffic to and from it.
*
* @author Gideon le Grange
* @since 1.8.0
*/
public class PanStampBinding extends AbstractBinding<PanStampBindingProvider> {
private static final Logger logger = LoggerFactory.getLogger(PanStampBinding.class);
private PanStampBindingSettings cfg;
private SwapModem modem;
private Network network;
private ConfigDeviceStore store;
private TcpServer debugServer;
/**
* {@inheritDoc}
*/
public void activate(final BundleContext bundleContext, final Map<String, Object> configuration) {
logger.debug("activate()");
try {
startup(PanStampBindingSettings.parseConfig(configuration));
} catch (ValueException e) {
logger.error("Configuration error: {} ", e.getMessage());
}
}
protected void addBindingProvider(PanStampBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(PanStampBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
/**
* {@inheritDoc}
*/
public void modified(final Map<String, Object> configuration) {
logger.debug("modified()");
try {
PanStampBindingSettings newCfg = PanStampBindingSettings.parseConfig(configuration);
if (cfg == null) {
startup(newCfg);
} else if (cfg.equals(newCfg)) {
logger.info("Settings not changed, continuing");
} else {
// difficult case, cfg changed
// the serial config has changed
if (cfg.serialDiffers(newCfg)) {
logger.info("Serial configuration changed. Restarting panStamp binding");
stop();
startup(newCfg);
return;
}
if (cfg.networkDiffers(newCfg)) {
setupModem(newCfg);
} // the modem setup has changed
if (cfg.directoriesDiffers(newCfg)) {
setupDirectories(newCfg);
}
if (cfg.debugDiffers(newCfg)) {
stopDebug();
startDebug(newCfg);
}
}
cfg = newCfg;
} catch (ValueException e) {
logger.error("Configuration error: {}", e.getMessage());
}
}
/**
* @{inheritDoc
*/
public void deactivate(final int reason) {
// deallocate resources here that are no longer needed and
// should be reset when activating this binding again
logger.debug("deactivate()");
stop();
super.deactivate();
}
/**
* @{inheritDoc
*/
@SuppressWarnings("unchecked")
@Override
protected void internalReceiveCommand(String itemName, Command command) {
logger.debug("internalReceiveCommand({},{})", itemName, command);
List<PanStampBindingConfig<?>> confs = getConfigs(itemName);
for (PanStampBindingConfig<?> conf : confs) {
try {
PanStamp dev;
if (!network.hasDevice(conf.getAddress())) {
dev = new PanStamp(network, conf.getAddress());
network.addDevice(dev);
} else {
dev = network.getDevice(conf.getAddress());
}
if (dev.hasRegister(conf.getRegister())) {
Register reg = dev.getRegister(conf.getRegister());
if (reg.hasEndpoint(conf.getEndpoint())) {
Endpoint<Object> ep = reg.getEndpoint(conf.getEndpoint());
if (ep.isOutput()) {
Object val = null;
switch (ep.getType()) {
case BINARY:
val = PanStampConversions.toBoolean(command);
break;
case INTEGER:
val = PanStampConversions.toLong(command);
break;
case NUMBER:
val = PanStampConversions.toDouble(command);
break;
case STRING:
val = PanStampConversions.toString(command);
break;
default:
logger.error("Unsupported panStamp endpoint type '{}' for endpoint {}. BUG!",
ep.getType(), PanStampConversions.toString(ep));
}
if (val != null) {
if (!conf.getUnit().equals("")) {
ep.setValue(conf.getUnit(), val);
} else {
ep.setValue(val);
}
}
} else {
logger.error("Internal update received for input-only endpoint {}",
PanStampConversions.toString(ep));
}
} else {
logger.error("No endpoint '{}' found for register '{}' on panStamp '{}' linked to item '{}'",
conf.getEndpoint(), conf.getRegister(), conf.getAddress(), itemName);
}
} else {
logger.error("No register '{}' found on panStamp '{}' linked to item '{}'", conf.getRegister(),
conf.getAddress(), itemName);
}
} catch (EndpointNotFoundException e) {
logger.error(e.getMessage());
} catch (NetworkException e) {
logger.error(e.getMessage());
} catch (ValueException e) {
logger.error(e.getMessage());
}
}
}
private void startup(PanStampBindingSettings cfg) {
store = new ConfigDeviceStore();
for (PanStampBindingProvider prov : providers) {
prov.addBindingChangeListener(bindingChangeListener);
for (String itemName : prov.getItemNames()) {
PanStampBindingConfig<?> conf = prov.getConfig(itemName);
updateProductCode(conf);
logger.debug("found item {} on startup -> {}", itemName, conf);
}
}
startNetwork(cfg);
startDebug(cfg);
if (network != null) {
for (PanStampBindingProvider prov : providers) {
for (String itemName : prov.getItemNames()) {
PanStampBindingConfig<?> conf = prov.getConfig(itemName);
if (!network.hasDevice(conf.getAddress())) {
try {
network.addDevice(new PanStamp(network, conf.getAddress()));
logger.debug("Added device {} to network", conf.getAddress());
} catch (NetworkException e) {
logger.error(e.getMessage());
}
}
}
}
}
}
private void stop() {
stopDebug();
stopNetwork();
}
private void startNetwork(final PanStampBindingSettings cfg) {
try {
logger.debug("startNetwork()");
modem = new SerialModem(cfg.serialPort, cfg.serialSpeed);
modem.open();
logger.info("Opened panStamp network on serial port {} at {}bps", cfg.serialPort, cfg.serialSpeed);
setupModem(cfg);
network = Network.create(modem);
setupDirectories(cfg);
network.setDeviceStore(store);
logger.info("Configured binding product codes");
network.addListener(networkListener);
network.open();
logger.info("Opened network");
} catch (NetworkException e) {
logger.error("Error connecting to serial network on {}: {}", cfg.serialPort, e.getMessage());
} catch (SwapException e) {
logger.error("Error reading modem settings for serial network on {}: {}", cfg.serialPort, e.getMessage());
} catch (Throwable e) {
logger.error("Fatal error: {}", e.getMessage(), e);
}
}
private void stopNetwork() {
logger.debug("stopNetwork()");
try {
network.close();
network = null;
logger.info("Closed panStamp network");
} catch (ModemException e) {
logger.error("Error closing panStamp network: " + e.getMessage(), e);
}
}
private void startDebug(final PanStampBindingSettings cfg) {
if (cfg.debugEnabled) {
try {
debugServer = new TcpServer(network.getSWAPModem(), cfg.debugPort);
logger.info("TCP Debug enabled on port {}", cfg.debugPort);
} catch (TcpException e) {
logger.error("Error creating debug service: {}" + e.getMessage());
}
}
}
private void stopDebug() {
try {
if (debugServer != null) {
debugServer.close();
debugServer = null;
}
} catch (TcpException e) {
logger.error("Error closing debug port: " + e.getMessage(), e);
}
}
private void setupModem(PanStampBindingSettings cfg) {
try {
ModemSetup setup = modem.getSetup();
if (cfg.networkId != -1) {
setup.setNetworkID(cfg.networkId);
}
if (cfg.networkDeviceAddress != -1) {
setup.setDeviceAddress(cfg.networkDeviceAddress);
}
if (cfg.networkChannel != -1) {
setup.setChannel(cfg.networkChannel);
}
if ((cfg.networkId != -1) || (cfg.networkChannel != -1) || (cfg.networkDeviceAddress != -1)) {
modem.setSetup(setup);
logger.info("Modem setup updated to address {}, channel {}, and network ID {}",
setup.getDeviceAddress(), setup.getChannel(), setup.getNetworkID());
}
} catch (SwapException e) {
logger.error("Error configuring network: {}", e.getMessage());
}
}
private void setupDirectories(PanStampBindingSettings cfg) {
File xmlDir = new File(cfg.xmlDir);
if (xmlDir.exists() && xmlDir.isDirectory() && xmlDir.canRead()) {
File devFile = new File(xmlDir, "devices.xml");
if (devFile.exists() && devFile.canRead() && devFile.isFile()) {
network.setDeviceLibrary(new CompoundDeviceLibrary(new FileLibrary(xmlDir), new ClassLoaderLibrary()));
logger.info("Configured XML directory to {}", cfg.xmlDir);
} else {
logger.error("devices.xml in XML directory {} cannot be read", cfg.xmlDir);
}
} else {
logger.error("XML directory {} cannot be read", cfg.xmlDir);
}
}
private void updateProductCode(final PanStampBindingConfig<?> conf) {
store.addProductCode(conf.getAddress(), conf.getManufacturerId(), conf.getProductId());
logger.debug("Product code for {} updated to {}/{}", conf.getAddress(), conf.getManufacturerId(),
conf.getProductId());
}
/* Register a detected panStamp device with the binding */
private void addDevice(PanStamp dev) {
try {
logger.debug("addDevice({} ({}/{})", PanStampConversions.toString(dev), dev.getManufacturerId(),
dev.getProductId());
} catch (NetworkException e) {
logger.error(e.getMessage());
}
dev.addListener(panStampListener);
for (Register reg : dev.getRegisters()) {
if (!reg.isStandard()) {
addRegister(reg);
}
}
}
/* Unregiter a panStamp device from the binding */
private void removeDevice(PanStamp dev) {
try {
logger.debug("removeDevice({} ({}/{}))", PanStampConversions.toString(dev), dev.getManufacturerId(),
dev.getProductId());
} catch (NetworkException e) {
logger.error(e.getMessage());
}
dev.removeListener(panStampListener);
for (Register reg : dev.getRegisters()) {
if (!reg.isStandard()) {
removeRegister(reg);
}
}
}
private void addRegister(Register reg) {
logger.debug("addRegister({})", PanStampConversions.toString(reg));
reg.addListener(registerListener);
for (Endpoint<?> ep : reg.getEndpoints()) {
addEndpoint(ep);
}
}
private void removeRegister(Register reg) {
logger.debug("removeRegister({})", PanStampConversions.toString(reg));
reg.removeListener(registerListener);
for (Endpoint<?> ep : reg.getEndpoints()) {
removeEndpoint(ep);
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void addEndpoint(Endpoint ep) {
logger.debug("addEndpoint({})", PanStampConversions.toString(ep));
ep.addListener(endpointListener);
}
@SuppressWarnings("unchecked")
private void removeEndpoint(Endpoint<?> ep) {
logger.debug("removeEndpoint({})", PanStampConversions.toString(ep));
ep.removeListener(endpointListener);
}
/**
* Return all available binding configs for the given item name
*
* @param itemName
* The item name for which configs are requested.
* @return The configs found.
*/
private List<PanStampBindingConfig<?>> getConfigs(String itemName) {
List<PanStampBindingConfig<?>> confs = new ArrayList<PanStampBindingConfig<?>>();
for (PanStampBindingProvider prov : providers) {
PanStampBindingConfig<?> conf = prov.getConfig(itemName);
if (conf != null) {
confs.add(conf);
}
}
return confs;
}
/**
* Return all available binding configs for the given endpoint
*
* @param ep
* The endpoint for which configs are requested.
* @return The configs found.
*/
private List<PanStampBindingConfig<?>> getConfigs(Endpoint<?> ep) {
List<PanStampBindingConfig<?>> configs = new ArrayList<PanStampBindingConfig<?>>();
Register reg = ep.getRegister();
PanStamp ps = reg.getDevice();
for (PanStampBindingProvider prov : providers) {
for (String itemName : prov.getItemNames()) {
PanStampBindingConfig<?> conf = prov.getConfig(itemName);
if ((conf.getAddress() == ps.getAddress() && (conf.getRegister() == reg.getId())
&& conf.getEndpoint().equals(ep.getName()))) {
configs.add(conf);
}
}
}
return configs;
}
/**
* Update all items associated with an endpoint with its new value
*
* @param ep
* The endpoint for which a new value is available
* @param val
* The value.
*/
private void updateValue(Endpoint<?> ep, Object val) {
List<PanStampBindingConfig<?>> confs = getConfigs(ep);
for (PanStampBindingConfig<?> conf : confs) {
try {
State state;
if (!conf.getUnit().equals("")) {
state = PanStampConversions.toState(ep, ep.getValue(conf.getUnit()));
} else {
state = PanStampConversions.toState(ep, ep.getValue());
}
eventPublisher.postUpdate(conf.getItemName(), state);
} catch (ValueException e) {
logger.error(e.getMessage());
} catch (NetworkException e) {
logger.error(e.getMessage());
}
}
}
private final BindingChangeListener bindingChangeListener = new BindingChangeListener() {
@Override
public void bindingChanged(BindingProvider provider, String itemName) {
logger.debug("bindingChanged({}, {}", provider, itemName);
PanStampBindingConfig<?> c = ((PanStampBindingProvider) provider).getConfig(itemName);
if (c != null) {
updateProductCode(c);
}
logger.debug("{} => {}", itemName, c);
}
@Override
public void allBindingsChanged(BindingProvider provider) {
}
};
/**
* implement a network listener to receive events when devices are added to or removed from the network.
*/
private final NetworkListener networkListener = new AbstractNetworkListener() {
@Override
public void deviceDetected(Network gw, PanStamp dev) {
logger.debug("device detected: {}", PanStampConversions.toString(dev));
addDevice(dev);
}
@Override
public void deviceRemoved(Network gw, PanStamp dev) {
logger.debug("device removed: {}", PanStampConversions.toString(dev));
removeDevice(dev);
}
};
private final PanStampListener panStampListener = new AbstractPanStampListener() {
@Override
public void productCodeChange(PanStamp dev, int manufacturerId, int productId) {
logger.debug("product code changed: {} {}/{}", PanStampConversions.toString(dev), manufacturerId,
productId);
removeDevice(dev);
store.addProductCode(dev.getAddress(), manufacturerId, productId);
addDevice(dev);
}
@Override
public void registerDetected(PanStamp dev, Register reg) {
try {
logger.debug("register detected: {}", PanStampConversions.toString(reg));
addRegister(reg);
} catch (Throwable e) {
e.printStackTrace();
}
}
};
private final RegisterListener registerListener = new AbstractRegisterListener() {
@SuppressWarnings({ "rawtypes" })
@Override
public void endpointAdded(Register reg, Endpoint ep) {
logger.debug("endpoint added {}", PanStampConversions.toString(ep));
addEndpoint(ep);
}
};
@SuppressWarnings("rawtypes")
private final EndpointListener endpointListener = new EndpointListener() {
@Override
public void valueReceived(Endpoint ep, Object val) {
logger.debug("value received {} = {}", PanStampConversions.toString(ep), val);
updateValue(ep, val);
}
};
}