/**
* 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.plclogo.internal;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Objects;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.plclogo.PLCLogoBindingConfig;
import org.openhab.binding.plclogo.PLCLogoBindingProvider;
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.NumberItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.model.item.binding.BindingConfigParseException;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import Moka7.S7;
import Moka7.S7Client;
/**
* Implement this class if you are going create an actively polling service
* like querying a Website/Device.
*
* @author Lehane Kellett
* @author Vladimir Grebenschikov
* @author Alexander Falkenstern
* @since 1.9.0
*/
public class PLCLogoBinding extends AbstractActiveBinding<PLCLogoBindingProvider> implements ManagedService {
private final ReentrantLock lock = new ReentrantLock();
private static final Logger logger = LoggerFactory.getLogger(PLCLogoBinding.class);
private static final Pattern EXTRACT_CONFIG_PATTERN = Pattern.compile("^(.*?)\\.(.*?)$");
/**
* the refresh interval which is used to poll values from the PlcLogo
* server (optional, defaults to 500ms)
*/
private long refreshInterval = 5000;
/**
* Buffer for read/write operations
*/
private byte data[] = new byte[2048];
private static Map<String, PLCLogoConfig> controllers = new HashMap<String, PLCLogoConfig>();
private int ReadLogoDBArea(S7Client client, int size) {
int result = 0;
int offset = 0;
final int bufSize = 1024;
// read first portion directly to data, to avoid extra copy
result = client.ReadArea(S7.S7AreaDB, 1, 0, bufSize, data);
offset = offset + bufSize;
while ((result == 0) && (offset < size)) {
byte buffer[] = new byte[Math.min(size - offset, bufSize)];
result = client.ReadArea(S7.S7AreaDB, 1, offset, buffer.length, buffer);
System.arraycopy(buffer, 0, data, offset, buffer.length);
offset = offset + buffer.length;
}
return result;
}
private void ReconnectLogo(S7Client client) {
// try and reconnect
client.Disconnect();
client.Connect();
if (client.Connected) {
logger.warn("Reconnect successful");
}
}
public PLCLogoBinding() {
logger.info("PLCLogoBinding constuctor");
}
@Override
public void activate() {
}
@Override
public void deactivate() {
for (PLCLogoBindingProvider provider : providers) {
provider.removeBindingChangeListener(this);
}
providers.clear();
Iterator<Entry<String, PLCLogoConfig>> entries = controllers.entrySet().iterator();
while (entries.hasNext()) {
Entry<String, PLCLogoConfig> thisEntry = entries.next();
PLCLogoConfig logoConfig = thisEntry.getValue();
S7Client LogoS7Client = logoConfig.getS7Client();
if (LogoS7Client != null) {
LogoS7Client.Disconnect();
}
}
controllers.clear();
}
protected void addBindingProvider(PLCLogoBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(PLCLogoBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
@Override
protected String getName() {
return "PLCLogo Polling Service";
}
@Override
protected void execute() {
if (!bindingsExist()) {
logger.debug("There is no existing plclogo binding configuration => refresh cycle aborted!");
return;
}
Iterator<Entry<String, PLCLogoConfig>> entries = controllers.entrySet().iterator();
while (entries.hasNext()) {
Entry<String, PLCLogoConfig> thisEntry = entries.next();
String controller = thisEntry.getKey();
PLCLogoConfig logoConfig = thisEntry.getValue();
S7Client LogoS7Client = logoConfig.getS7Client();
if (LogoS7Client == null) {
logger.debug("No S7client for {} found", controller);
} else {
lock.lock();
int result = ReadLogoDBArea(LogoS7Client, logoConfig.getMemorySize());
lock.unlock();
if (result != 0) {
logger.warn("Failed to read memory: {}. Reconnecting...", S7Client.ErrorText(result));
ReconnectLogo(LogoS7Client);
return;
}
// Now have the LOGO! memory (note: not suitable for S7) - more efficient than multiple reads (test
// shows <14mS to read all)
// iterate through bindings to see what has changed - this approach assumes a small number (< 100)of
// bindings
// otherwise might see what has changed in memory and map to binding
}
for (PLCLogoBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
PLCLogoBindingConfig config = provider.getBindingConfig(itemName);
if (config.getController().equals(controller)) {
// it is for our currently selected controller
PLCLogoMemoryConfig rd = config.getRD();
int address = -1;
try {
address = rd.getAddress(logoConfig.getModel());
} catch (BindingConfigParseException exception) {
logger.error("Invalid address for block {} on {}", rd.getBlockName(), controller);
continue;
}
int currentValue;
if (rd.isDigital()) {
int bit = -1;
try {
bit = rd.getBit(logoConfig.getModel());
} catch (BindingConfigParseException exception) {
logger.error("Invalid bit for block {} on {}", rd.getBlockName(), controller);
continue;
}
currentValue = S7.GetBitAt(data, address, bit) ? 1 : 0;
} else {
/*
* After the data transfer from a LOGO! Base Module to LOGO!Soft Comfort,
* you can view only analog values within the range of -32768 to 32767 on LOGO!Soft Comfort.
* If an analog value exceeds the value range,
* then only the nearest upper limit (32767) or lower limit (-32768) can be displayed.
*/
currentValue = S7.GetShortAt(data, address);
}
if (config.isSet()) {
if (currentValue == config.getLastValue()) {
continue;
}
int delta = Math.abs(config.getLastValue() - currentValue);
if (!rd.isDigital() && (delta < config.getAnalogDelta())) {
continue;
}
}
boolean isValid = false;
Item item = provider.getItem(itemName);
switch (rd.getKind()) {
case I:
case NI: {
isValid = item instanceof ContactItem;
break;
}
case Q:
case NQ: {
isValid = item instanceof SwitchItem;
break;
}
case M:
case VB:
case VW: {
isValid = item instanceof ContactItem || item instanceof SwitchItem;
break;
}
default: {
break;
}
}
if (item instanceof NumberItem || isValid) {
eventPublisher.postUpdate(itemName, createState(item, currentValue));
config.setLastValue(currentValue);
} else {
String block = rd.getBlockName();
logger.warn("Block {} is incompatible with item {} on {}", block, item.getName(),
controller);
}
}
}
}
}
}
@Override
protected void internalReceiveCommand(String itemName, Command command) {
// the code being executed when a command was sent on the openHAB
// event bus goes here. This method is only called if one of the
// BindingProviders provide a binding for the given 'itemName'.
// Note itemname is the item name not the controller name/instance!
//
super.internalReceiveCommand(itemName, command);
logger.debug("internalReceiveCommand() is called!");
for (PLCLogoBindingProvider provider : providers) {
if (!provider.providesBindingFor(itemName)) {
continue;
}
PLCLogoBindingConfig config = provider.getBindingConfig(itemName);
if (!controllers.containsKey(config.getController())) {
logger.warn("Invalid write requested for controller {}", config.getController());
continue;
}
PLCLogoConfig controller = controllers.get(config.getController());
PLCLogoMemoryConfig wr = config.getWR();
int address = -1;
try {
address = wr.getAddress(controller.getModel());
} catch (BindingConfigParseException exception) {
logger.error("Invalid address for block {} on {}", wr.getBlockName(), controller);
continue;
}
int bit = -1;
try {
bit = wr.getBit(controller.getModel());
} catch (BindingConfigParseException exception) {
logger.error("Invalid bit for block {} on {}", wr.getBlockName(), controller);
continue;
}
if (!wr.isInRange(controller.getModel())) {
logger.warn("Invalid write request for block {} at address {}", wr.getBlockName(), address);
continue;
}
// Send command to the LOGO! controller memory
S7Client LogoS7Client = controller.getS7Client();
if (LogoS7Client == null) {
logger.debug("No S7client for controller {} found", config.getController());
continue;
}
final byte buffer[] = new byte[2];
int size = wr.isDigital() ? 1 : 2;
lock.lock();
int result = LogoS7Client.ReadArea(S7.S7AreaDB, 1, address, size, buffer);
logger.debug("Read word from logo memory: at {} {} bytes, result = {}", address, size, result);
if (result == 0) {
Item item = config.getItem();
if (item instanceof NumberItem && !wr.isDigital()) {
if (command instanceof DecimalType) {
int oldValue = S7.GetShortAt(buffer, 0);
int newValue = ((DecimalType) command).intValue();
S7.SetWordAt(buffer, 0, newValue);
logger.debug("Changed word at {} from {} to {}", address, oldValue, newValue);
result = LogoS7Client.WriteArea(S7.S7AreaDB, 1, address, size, buffer);
logger.debug("Wrote to memory at {} two bytes: [{}, {}]", address, buffer[0], buffer[1]);
}
} else if (item instanceof SwitchItem && wr.isDigital()) {
if (command instanceof OnOffType) {
boolean oldValue = S7.GetBitAt(buffer, 0, bit) ? true : false;
boolean newValue = command == OnOffType.ON ? true : false;
S7.SetBitAt(buffer, 0, bit, newValue);
logger.debug("Changed bit {}.{} from {} to {}", address, bit, oldValue, newValue);
result = LogoS7Client.WriteArea(S7.S7AreaDB, 1, address, size, buffer);
logger.debug("Wrote to memory at {} one byte: [{}]", address, buffer[0]);
}
}
// If nothing was written and read was ok, nothing will happen here
if (result != 0) {
logger.warn("Failed to write memory: {}. Reconnecting...", S7Client.ErrorText(result));
ReconnectLogo(LogoS7Client);
}
} else {
logger.warn("Failed to read memory: {}. Reconnecting...", S7Client.ErrorText(result));
ReconnectLogo(LogoS7Client);
}
lock.unlock();
}
}
@Override
public void updated(Dictionary<String, ?> config) throws ConfigurationException {
Boolean configured = false;
if (config != null) {
String refreshIntervalString = Objects.toString(config.get("refresh"), null);
if (StringUtils.isNotBlank(refreshIntervalString)) {
refreshInterval = Long.parseLong(refreshIntervalString);
}
if (controllers == null) {
controllers = new HashMap<String, PLCLogoConfig>();
}
Enumeration<String> keys = config.keys();
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()) {
continue;
}
matcher.reset();
matcher.find();
String controllerName = matcher.group(1);
PLCLogoConfig deviceConfig = controllers.get(controllerName);
if (deviceConfig == null) {
deviceConfig = new PLCLogoConfig();
controllers.put(controllerName, deviceConfig);
logger.info("Create new config for {}", controllerName);
}
if (matcher.group(2).equals("host")) {
String ip = config.get(key).toString();
deviceConfig.setIP(ip);
logger.info("Set host of {}: {}", controllerName, ip);
configured = true;
}
if (matcher.group(2).equals("remoteTSAP")) {
String tsap = config.get(key).toString();
deviceConfig.setRemoteTSAP(Integer.decode(tsap));
logger.info("Set remote TSAP for {}: {}", controllerName, tsap);
}
if (matcher.group(2).equals("localTSAP")) {
String tsap = config.get(key).toString();
deviceConfig.setLocalTSAP(Integer.decode(tsap));
logger.info("Set local TSAP for {}: {}", controllerName, tsap);
}
if (matcher.group(2).equals("model")) {
PLCLogoModel model = null;
String modelName = config.get(key).toString();
if (modelName.equalsIgnoreCase("0BA7")) {
model = PLCLogoModel.LOGO_MODEL_0BA7;
} else if (modelName.equalsIgnoreCase("0BA8")) {
model = PLCLogoModel.LOGO_MODEL_0BA8;
} else {
logger.info("Found unknown model for {}: {}", controllerName, modelName);
}
if (model != null) {
deviceConfig.setModel(model);
logger.info("Set model for {}: {}", controllerName, modelName);
}
}
} // while
Iterator<Entry<String, PLCLogoConfig>> entries = controllers.entrySet().iterator();
while (entries.hasNext()) {
Entry<String, PLCLogoConfig> thisEntry = entries.next();
String controllerName = thisEntry.getKey();
PLCLogoConfig deviceConfig = thisEntry.getValue();
S7Client LogoS7Client = deviceConfig.getS7Client();
if (LogoS7Client == null) {
LogoS7Client = new Moka7.S7Client();
} else {
LogoS7Client.Disconnect();
}
LogoS7Client.SetConnectionParams(deviceConfig.getlogoIP(), deviceConfig.getlocalTSAP(),
deviceConfig.getremoteTSAP());
logger.info("About to connect to {}", controllerName);
if ((LogoS7Client.Connect() == 0) && LogoS7Client.Connected) {
logger.info("Connected to PLC LOGO! device {}", controllerName);
} else {
logger.error("Could not connect to PLC LOGO! device {} : {}", controllerName,
S7Client.ErrorText(LogoS7Client.LastError));
throw new ConfigurationException("Could not connect to PLC LOGO! device ",
controllerName + " " + deviceConfig.getlogoIP());
}
deviceConfig.setS7Client(LogoS7Client);
}
setProperlyConfigured(configured);
} else {
logger.info("No configuration for PLCLogoBinding");
}
}
private State createState(Item item, Object value) {
DecimalType number = null;
if (value instanceof Number) {
number = new DecimalType(value.toString());
}
State state = null;
if (item instanceof StringType) {
state = new StringType((String) value);
} else if (item instanceof NumberItem) {
if (number != null) {
return number;
} else if (value instanceof String) {
state = new DecimalType(((String) value).replaceAll("[^\\d|.]", ""));
}
} else if (item instanceof SwitchItem && (number != null)) {
state = (number.intValue() > 0) ? OnOffType.ON : OnOffType.OFF;
} else if (item instanceof ContactItem && (number != null)) {
state = (number.intValue() > 0) ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
}
return state;
}
/**
* Class which represents a LOGO! online controller/PLC connection parameters
* and current instance - there may be multiple PLC's
*
* @author Lehane Kellett
* @since 1.9.0
*/
private class PLCLogoConfig {
private String logoIP;
private PLCLogoModel logoModel = PLCLogoModel.LOGO_MODEL_0BA7;
private int localTSAP = 0x0300;
private int remoteTSAP = 0x0200;
private S7Client LogoS7Client;
public PLCLogoConfig() {
}
public void setIP(String logoIP) {
this.logoIP = logoIP;
}
public void setLocalTSAP(int localTSAP) {
this.localTSAP = localTSAP;
}
public void setRemoteTSAP(int remoteTSAP) {
this.remoteTSAP = remoteTSAP;
}
public void setS7Client(S7Client LogoS7Client) {
this.LogoS7Client = LogoS7Client;
}
public String getlogoIP() {
return logoIP;
}
public int getlocalTSAP() {
return localTSAP;
}
public int getremoteTSAP() {
return remoteTSAP;
}
public void setModel(PLCLogoModel model) {
this.logoModel = model;
}
public PLCLogoModel getModel() {
return logoModel;
}
public int getMemorySize() {
return (logoModel == PLCLogoModel.LOGO_MODEL_0BA8) ? 1470 : 1024;
}
public S7Client getS7Client() {
return LogoS7Client;
}
}
}