/*
* Copyright 2011-16 Fraunhofer ISE
*
* This file is part of OpenMUC.
* For more information visit http://www.openmuc.org
*
* OpenMUC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenMUC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenMUC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openmuc.framework.driver.iec61850;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.openmuc.framework.config.ChannelScanInfo;
import org.openmuc.framework.data.BooleanValue;
import org.openmuc.framework.data.ByteArrayValue;
import org.openmuc.framework.data.DoubleValue;
import org.openmuc.framework.data.Flag;
import org.openmuc.framework.data.FloatValue;
import org.openmuc.framework.data.LongValue;
import org.openmuc.framework.data.Record;
import org.openmuc.framework.data.StringValue;
import org.openmuc.framework.data.ValueType;
import org.openmuc.framework.driver.spi.ChannelRecordContainer;
import org.openmuc.framework.driver.spi.ChannelValueContainer;
import org.openmuc.framework.driver.spi.Connection;
import org.openmuc.framework.driver.spi.ConnectionException;
import org.openmuc.framework.driver.spi.RecordsReceivedListener;
import org.openmuc.openiec61850.BasicDataAttribute;
import org.openmuc.openiec61850.BdaBitString;
import org.openmuc.openiec61850.BdaBoolean;
import org.openmuc.openiec61850.BdaEntryTime;
import org.openmuc.openiec61850.BdaFloat32;
import org.openmuc.openiec61850.BdaFloat64;
import org.openmuc.openiec61850.BdaInt16;
import org.openmuc.openiec61850.BdaInt16U;
import org.openmuc.openiec61850.BdaInt32;
import org.openmuc.openiec61850.BdaInt32U;
import org.openmuc.openiec61850.BdaInt64;
import org.openmuc.openiec61850.BdaInt8;
import org.openmuc.openiec61850.BdaInt8U;
import org.openmuc.openiec61850.BdaOctetString;
import org.openmuc.openiec61850.BdaTimestamp;
import org.openmuc.openiec61850.BdaUnicodeString;
import org.openmuc.openiec61850.BdaVisibleString;
import org.openmuc.openiec61850.ClientAssociation;
import org.openmuc.openiec61850.Fc;
import org.openmuc.openiec61850.FcModelNode;
import org.openmuc.openiec61850.ModelNode;
import org.openmuc.openiec61850.ServerModel;
import org.openmuc.openiec61850.ServiceError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class Iec61850Connection implements Connection {
private final static Logger logger = LoggerFactory.getLogger(Iec61850Connection.class);
private final static String STRING_SEPARATOR = ",";
private final ClientAssociation clientAssociation;
private final ServerModel serverModel;
public Iec61850Connection(ClientAssociation clientAssociation, ServerModel serverModel) {
this.clientAssociation = clientAssociation;
this.serverModel = serverModel;
}
@Override
public List<ChannelScanInfo> scanForChannels(String settings)
throws UnsupportedOperationException, ConnectionException {
List<BasicDataAttribute> bdas = serverModel.getBasicDataAttributes();
List<ChannelScanInfo> scanInfos = new ArrayList<>(bdas.size());
for (BasicDataAttribute bda : bdas) {
String channelAddress = bda.getReference() + ":" + bda.getFc();
switch (bda.getBasicType()) {
case CHECK:
case DOUBLE_BIT_POS:
case OPTFLDS:
case QUALITY:
case REASON_FOR_INCLUSION:
case TAP_COMMAND:
case TRIGGER_CONDITIONS:
case ENTRY_TIME:
case OCTET_STRING:
case VISIBLE_STRING:
case UNICODE_STRING:
bda.setDefault();
scanInfos.add(new ChannelScanInfo(channelAddress, "", ValueType.BYTE_ARRAY,
((BdaBitString) bda).getValue().length));
break;
case TIMESTAMP:
scanInfos.add(new ChannelScanInfo(channelAddress, "", ValueType.LONG, null));
break;
case BOOLEAN:
scanInfos.add(new ChannelScanInfo(channelAddress, "", ValueType.BOOLEAN, null));
break;
case FLOAT32:
scanInfos.add(new ChannelScanInfo(channelAddress, "", ValueType.FLOAT, null));
break;
case FLOAT64:
scanInfos.add(new ChannelScanInfo(channelAddress, "", ValueType.DOUBLE, null));
break;
case INT8:
scanInfos.add(new ChannelScanInfo(channelAddress, "", ValueType.BYTE, null));
break;
case INT8U:
case INT16:
scanInfos.add(new ChannelScanInfo(channelAddress, "", ValueType.SHORT, null));
break;
case INT16U:
case INT32:
scanInfos.add(new ChannelScanInfo(channelAddress, "", ValueType.INTEGER, null));
break;
case INT32U:
case INT64:
scanInfos.add(new ChannelScanInfo(channelAddress, "", ValueType.LONG, null));
break;
default:
throw new IllegalStateException("unknown BasicType received: " + bda.getBasicType());
}
}
return scanInfos;
}
@Override
public Object read(List<ChannelRecordContainer> containers, Object containerListHandle, String samplingGroup)
throws UnsupportedOperationException, ConnectionException {
for (ChannelRecordContainer container : containers) {
if (container.getChannelHandle() == null) {
String[] args = container.getChannelAddress().split(":", 3);
if (args.length != 2) {
logger.debug("Wrong channel address syntax: {}", container.getChannelAddress());
container.setRecord(new Record(Flag.DRIVER_ERROR_CHANNEL_WITH_THIS_ADDRESS_NOT_FOUND));
continue;
}
ModelNode modelNode = serverModel.findModelNode(args[0], Fc.fromString(args[1]));
if (modelNode == null) {
logger.debug("No Basic Data Attribute for the channel address {} was found in the server model.",
container.getChannelAddress());
container.setRecord(new Record(Flag.DRIVER_ERROR_CHANNEL_WITH_THIS_ADDRESS_NOT_FOUND));
continue;
}
FcModelNode fcModelNode;
try {
fcModelNode = (FcModelNode) modelNode;
} catch (ClassCastException e) {
logger.debug(
"ModelNode with object reference {} was found in the server model but is not a Basic Data Attribute.",
container.getChannelAddress());
container.setRecord(new Record(Flag.DRIVER_ERROR_CHANNEL_WITH_THIS_ADDRESS_NOT_FOUND));
continue;
}
container.setChannelHandle(fcModelNode);
}
}
if (!samplingGroup.isEmpty()) {
FcModelNode fcModelNode;
if (containerListHandle != null) {
fcModelNode = (FcModelNode) containerListHandle;
}
else {
String[] args = samplingGroup.split(":", 3);
if (args.length != 2) {
logger.debug("Wrong sampling group syntax: {}", samplingGroup);
for (ChannelRecordContainer container : containers) {
container.setRecord(new Record(Flag.DRIVER_ERROR_SAMPLING_GROUP_NOT_FOUND));
}
return null;
}
ModelNode modelNode = serverModel.findModelNode(args[0], Fc.fromString(args[1]));
if (modelNode == null) {
logger.debug(
"Error reading sampling group: no FCDO/DA or DataSet with object reference {} was not found in the server model.",
samplingGroup);
for (ChannelRecordContainer container : containers) {
container.setRecord(new Record(Flag.DRIVER_ERROR_SAMPLING_GROUP_NOT_FOUND));
}
return null;
}
try {
fcModelNode = (FcModelNode) modelNode;
} catch (ClassCastException e) {
logger.debug(
"Error reading channel: ModelNode with sampling group reference {} was found in the server model but is not a FcModelNode.",
samplingGroup);
for (ChannelRecordContainer container : containers) {
container.setRecord(new Record(Flag.DRIVER_ERROR_SAMPLING_GROUP_NOT_FOUND));
}
return null;
}
}
try {
clientAssociation.getDataValues(fcModelNode);
} catch (ServiceError e) {
logger.debug("Error reading sampling group: service error calling getDataValues on {}: {}",
samplingGroup, e);
for (ChannelRecordContainer container : containers) {
container.setRecord(new Record(Flag.DRIVER_ERROR_SAMPLING_GROUP_NOT_ACCESSIBLE));
}
return fcModelNode;
} catch (IOException e) {
throw new ConnectionException(e);
}
long receiveTime = System.currentTimeMillis();
for (ChannelRecordContainer container : containers) {
if (container.getChannelHandle() != null) {
setRecord(container, (BasicDataAttribute) container.getChannelHandle(), receiveTime);
}
else {
container.setRecord(new Record(Flag.DRIVER_ERROR_CHANNEL_NOT_PART_OF_SAMPLING_GROUP));
}
}
return fcModelNode;
}
// sampling group is empty
else {
for (ChannelRecordContainer container : containers) {
if (container.getChannelHandle() == null) {
continue;
}
FcModelNode fcModelNode = (FcModelNode) container.getChannelHandle();
try {
clientAssociation.getDataValues(fcModelNode);
} catch (ServiceError e) {
logger.debug("Error reading channel: service error calling getDataValues on {}: {}",
container.getChannelAddress(), e);
container.setRecord(new Record(Flag.DRIVER_ERROR_CHANNEL_NOT_ACCESSIBLE));
continue;
} catch (IOException e) {
throw new ConnectionException(e);
}
if (fcModelNode instanceof BasicDataAttribute) {
long receiveTime = System.currentTimeMillis();
setRecord(container, (BasicDataAttribute) fcModelNode, receiveTime);
}
else {
StringBuilder sb = new StringBuilder("");
for (BasicDataAttribute bda : fcModelNode.getBasicDataAttributes()) {
sb.append(bda2String(bda) + STRING_SEPARATOR);
}
sb.delete(sb.length() - 1, sb.length());// remove last separator
long receiveTime = System.currentTimeMillis();
setRecord(container, sb.toString(), receiveTime);
}
}
return null;
}
}
private String bda2String(BasicDataAttribute bda) {
String result;
switch (bda.getBasicType()) {
case CHECK:
case DOUBLE_BIT_POS:
case OPTFLDS:
case QUALITY:
case REASON_FOR_INCLUSION:
case TAP_COMMAND:
case TRIGGER_CONDITIONS:
case ENTRY_TIME:
case OCTET_STRING:
case VISIBLE_STRING:
case UNICODE_STRING:
result = new String(((BdaBitString) bda).getValue());
break;
case TIMESTAMP:
Date date = ((BdaTimestamp) bda).getDate();
result = date == null ? "<invalid date>" : ("" + date.getTime());
break;
case BOOLEAN:
result = String.valueOf(((BdaBoolean) bda).getValue());
break;
case FLOAT32:
result = String.valueOf(((BdaFloat32) bda).getFloat());
break;
case FLOAT64:
result = String.valueOf(((BdaFloat64) bda).getDouble());
break;
case INT8:
result = String.valueOf(((BdaInt8) bda).getValue());
break;
case INT8U:
result = String.valueOf(((BdaInt8U) bda).getValue());
break;
case INT16:
result = String.valueOf(((BdaInt16) bda).getValue());
break;
case INT16U:
result = String.valueOf(((BdaInt16U) bda).getValue());
break;
case INT32:
result = String.valueOf(((BdaInt32) bda).getValue());
break;
case INT32U:
result = String.valueOf(((BdaInt32U) bda).getValue());
break;
case INT64:
result = String.valueOf(((BdaInt64) bda).getValue());
break;
default:
throw new IllegalStateException("unknown BasicType received: " + bda.getBasicType());
}
return result;
}
private void setRecord(ChannelRecordContainer container, String stringValue, long receiveTime) {
container.setRecord(new Record(new ByteArrayValue(stringValue.getBytes(), true), receiveTime));
}
private void setRecord(ChannelRecordContainer container, BasicDataAttribute bda, long receiveTime) {
switch (bda.getBasicType()) {
case CHECK:
case DOUBLE_BIT_POS:
case OPTFLDS:
case QUALITY:
case REASON_FOR_INCLUSION:
case TAP_COMMAND:
case TRIGGER_CONDITIONS:
container.setRecord(new Record(new ByteArrayValue(((BdaBitString) bda).getValue(), true), receiveTime));
break;
case ENTRY_TIME:
container.setRecord(new Record(new ByteArrayValue(((BdaEntryTime) bda).getValue(), true), receiveTime));
break;
case OCTET_STRING:
container.setRecord(new Record(new ByteArrayValue(((BdaOctetString) bda).getValue(), true), receiveTime));
break;
case VISIBLE_STRING:
container.setRecord(new Record(new StringValue(((BdaVisibleString) bda).getStringValue()), receiveTime));
break;
case UNICODE_STRING:
container.setRecord(new Record(new ByteArrayValue(((BdaUnicodeString) bda).getValue(), true), receiveTime));
break;
case TIMESTAMP:
Date date = ((BdaTimestamp) bda).getDate();
if (date == null) {
container.setRecord(new Record(new LongValue(-1l), receiveTime));
}
else {
container.setRecord(new Record(new LongValue(date.getTime()), receiveTime));
}
break;
case BOOLEAN:
container.setRecord(new Record(new BooleanValue(((BdaBoolean) bda).getValue()), receiveTime));
break;
case FLOAT32:
container.setRecord(new Record(new FloatValue(((BdaFloat32) bda).getFloat()), receiveTime));
break;
case FLOAT64:
container.setRecord(new Record(new DoubleValue(((BdaFloat64) bda).getDouble()), receiveTime));
break;
case INT8:
container.setRecord(new Record(new DoubleValue(((BdaInt8) bda).getValue()), receiveTime));
break;
case INT8U:
container.setRecord(new Record(new DoubleValue(((BdaInt8U) bda).getValue()), receiveTime));
break;
case INT16:
container.setRecord(new Record(new DoubleValue(((BdaInt16) bda).getValue()), receiveTime));
break;
case INT16U:
container.setRecord(new Record(new DoubleValue(((BdaInt16U) bda).getValue()), receiveTime));
break;
case INT32:
container.setRecord(new Record(new DoubleValue(((BdaInt32) bda).getValue()), receiveTime));
break;
case INT32U:
container.setRecord(new Record(new DoubleValue(((BdaInt32U) bda).getValue()), receiveTime));
break;
case INT64:
container.setRecord(new Record(new DoubleValue(((BdaInt64) bda).getValue()), receiveTime));
break;
default:
throw new IllegalStateException("unknown BasicType received: " + bda.getBasicType());
}
}
@Override
public void startListening(List<ChannelRecordContainer> containers, RecordsReceivedListener listener)
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
@Override
public Object write(List<ChannelValueContainer> containers, Object containerListHandle)
throws UnsupportedOperationException, ConnectionException {
List<FcModelNode> modelNodesToBeWritten = new ArrayList<>(containers.size());
for (ChannelValueContainer container : containers) {
if (container.getChannelHandle() != null) {
modelNodesToBeWritten.add((FcModelNode) container.getChannelHandle());
setFcModelNode(container, (FcModelNode) container.getChannelHandle());
}
else {
String[] args = container.getChannelAddress().split(":", 3);
if (args.length != 2) {
logger.debug("Wrong channel address syntax: {}", container.getChannelAddress());
container.setFlag(Flag.DRIVER_ERROR_CHANNEL_WITH_THIS_ADDRESS_NOT_FOUND);
continue;
}
ModelNode modelNode = serverModel.findModelNode(args[0], Fc.fromString(args[1]));
if (modelNode == null) {
logger.debug("No Basic Data Attribute for the channel address {} was found in the server model.",
container.getChannelAddress());
container.setFlag(Flag.DRIVER_ERROR_CHANNEL_WITH_THIS_ADDRESS_NOT_FOUND);
continue;
}
FcModelNode fcModelNode;
try {
fcModelNode = (FcModelNode) modelNode;
} catch (ClassCastException e) {
logger.debug(
"ModelNode with object reference {} was found in the server model but is not a Basic Data Attribute.",
container.getChannelAddress());
container.setFlag(Flag.DRIVER_ERROR_CHANNEL_WITH_THIS_ADDRESS_NOT_FOUND);
continue;
}
container.setChannelHandle(fcModelNode);
modelNodesToBeWritten.add(fcModelNode);
setFcModelNode(container, fcModelNode);
}
}
// TODO
// first check all datasets if the are some that contain only requested channels
// then check all remaining model nodes
List<FcModelNode> fcNodesToBeRequested = new ArrayList<>();
while (modelNodesToBeWritten.size() > 0) {
fillRequestedNodes(fcNodesToBeRequested, modelNodesToBeWritten, serverModel);
}
for (FcModelNode fcModelNode : fcNodesToBeRequested) {
try {
clientAssociation.setDataValues(fcModelNode);
} catch (ServiceError e) {
logger.debug("Error writing to channel: service error calling setDataValues on {}: {}",
fcModelNode.getReference(), e);
for (BasicDataAttribute bda : fcModelNode.getBasicDataAttributes()) {
for (ChannelValueContainer valueContainer : containers) {
if (valueContainer.getChannelHandle() == bda) {
valueContainer.setFlag(Flag.DRIVER_ERROR_CHANNEL_NOT_ACCESSIBLE);
}
}
}
return null;
} catch (IOException e) {
throw new ConnectionException(e);
}
for (BasicDataAttribute bda : fcModelNode.getBasicDataAttributes()) {
for (ChannelValueContainer valueContainer : containers) {
if (valueContainer.getChannelHandle() == bda) {
valueContainer.setFlag(Flag.VALID);
}
}
}
}
return null;
}
void fillRequestedNodes(List<FcModelNode> fcNodesToBeRequested, List<FcModelNode> remainingFcModelNodes,
ServerModel serverModel) {
FcModelNode currentFcModelNode = remainingFcModelNodes.get(0);
if (!checkParent(currentFcModelNode, fcNodesToBeRequested, remainingFcModelNodes, serverModel)) {
remainingFcModelNodes.remove(currentFcModelNode);
fcNodesToBeRequested.add(currentFcModelNode);
}
}
boolean checkParent(ModelNode modelNode, List<FcModelNode> fcNodesToBeRequested,
List<FcModelNode> remainingModelNodes, ServerModel serverModel) {
if (!(modelNode instanceof FcModelNode)) {
return false;
}
FcModelNode fcModelNode = (FcModelNode) modelNode;
ModelNode parentNode = serverModel;
for (int i = 0; i < fcModelNode.getReference().size() - 1; i++) {
parentNode = parentNode.getChild(fcModelNode.getReference().get(i), fcModelNode.getFc());
}
List<BasicDataAttribute> basicDataAttributes = parentNode.getBasicDataAttributes();
for (BasicDataAttribute bda : basicDataAttributes) {
if (!remainingModelNodes.contains(bda)) {
return false;
}
}
if (!checkParent(parentNode, fcNodesToBeRequested, remainingModelNodes, serverModel)) {
for (BasicDataAttribute bda : basicDataAttributes) {
remainingModelNodes.remove(bda);
}
fcNodesToBeRequested.add((FcModelNode) parentNode);
}
return true;
}
private void setFcModelNode(ChannelValueContainer container, FcModelNode fcModelNode) {
if (fcModelNode instanceof BasicDataAttribute) {
setBda(container, (BasicDataAttribute) fcModelNode);
}
else {
List<BasicDataAttribute> bdas = fcModelNode.getBasicDataAttributes();
String valueString = container.getValue().toString();
String[] bdaValues = valueString.split(STRING_SEPARATOR);
if (bdaValues.length != bdas.size()) {
throw new IllegalStateException("attempt to write array " + valueString + " into fcModelNode "
+ fcModelNode.getName() + " failed as the dimensions don't fit.");
}
for (int i = 0; i < bdaValues.length; i++) {
setBda(bdaValues[i], bdas.get(i));
}
}
}
private void setBda(String bdaValueString, BasicDataAttribute bda) {
switch (bda.getBasicType()) {
case CHECK:
case DOUBLE_BIT_POS:
case OPTFLDS:
case QUALITY:
case REASON_FOR_INCLUSION:
case TAP_COMMAND:
case TRIGGER_CONDITIONS:
((BdaBitString) bda).setValue(bdaValueString.getBytes());
break;
case ENTRY_TIME:
((BdaEntryTime) bda).setValue(bdaValueString.getBytes());
break;
case OCTET_STRING:
((BdaOctetString) bda).setValue(bdaValueString.getBytes());
break;
case VISIBLE_STRING:
((BdaVisibleString) bda).setValue(bdaValueString);
break;
case UNICODE_STRING:
((BdaUnicodeString) bda).setValue(bdaValueString.getBytes());
break;
case TIMESTAMP:
((BdaTimestamp) bda).setDate(new Date(Long.parseLong(bdaValueString)));
break;
case BOOLEAN:
((BdaBoolean) bda).setValue(Boolean.parseBoolean(bdaValueString));
break;
case FLOAT32:
((BdaFloat32) bda).setFloat(Float.parseFloat(bdaValueString));
break;
case FLOAT64:
((BdaFloat64) bda).setDouble(Double.parseDouble(bdaValueString));
break;
case INT8:
((BdaInt8) bda).setValue(Byte.parseByte(bdaValueString));
break;
case INT8U:
((BdaInt8U) bda).setValue(Short.parseShort(bdaValueString));
break;
case INT16:
((BdaInt16) bda).setValue(Short.parseShort(bdaValueString));
break;
case INT16U:
((BdaInt16U) bda).setValue(Integer.parseInt(bdaValueString));
break;
case INT32:
((BdaInt32) bda).setValue(Integer.parseInt(bdaValueString));
break;
case INT32U:
((BdaInt32U) bda).setValue(Long.parseLong(bdaValueString));
break;
case INT64:
((BdaInt64) bda).setValue(Long.parseLong(bdaValueString));
break;
default:
throw new IllegalStateException("unknown BasicType received: " + bda.getBasicType());
}
}
private void setBda(ChannelValueContainer container, BasicDataAttribute bda) {
switch (bda.getBasicType()) {
case CHECK:
case DOUBLE_BIT_POS:
case OPTFLDS:
case QUALITY:
case REASON_FOR_INCLUSION:
case TAP_COMMAND:
case TRIGGER_CONDITIONS:
((BdaBitString) bda).setValue(container.getValue().asByteArray());
break;
case ENTRY_TIME:
((BdaEntryTime) bda).setValue(container.getValue().asByteArray());
break;
case OCTET_STRING:
((BdaOctetString) bda).setValue(container.getValue().asByteArray());
break;
case VISIBLE_STRING:
((BdaVisibleString) bda).setValue(container.getValue().asString());
break;
case UNICODE_STRING:
((BdaUnicodeString) bda).setValue(container.getValue().asByteArray());
break;
case TIMESTAMP:
((BdaTimestamp) bda).setDate(new Date(container.getValue().asLong()));
break;
case BOOLEAN:
((BdaBoolean) bda).setValue(container.getValue().asBoolean());
break;
case FLOAT32:
((BdaFloat32) bda).setFloat(container.getValue().asFloat());
break;
case FLOAT64:
((BdaFloat64) bda).setDouble(container.getValue().asDouble());
break;
case INT8:
((BdaInt8) bda).setValue(container.getValue().asByte());
break;
case INT8U:
((BdaInt8U) bda).setValue(container.getValue().asShort());
break;
case INT16:
((BdaInt16) bda).setValue(container.getValue().asShort());
break;
case INT16U:
((BdaInt16U) bda).setValue(container.getValue().asInt());
break;
case INT32:
((BdaInt32) bda).setValue(container.getValue().asInt());
break;
case INT32U:
((BdaInt32U) bda).setValue(container.getValue().asLong());
break;
case INT64:
((BdaInt64) bda).setValue(container.getValue().asLong());
break;
default:
throw new IllegalStateException("unknown BasicType received: " + bda.getBasicType());
}
}
@Override
public void disconnect() {
clientAssociation.close();
}
}