/**
* 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.dsmr.internal.p1telegram;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.openhab.binding.dsmr.internal.messages.OBISMessage;
import org.openhab.binding.dsmr.internal.messages.OBISMsgFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The P1TelegramParser is a class that will read P1-port data create a full P1
* telegram.
*
* This class parses data via the public method parseData. It will parse the
* received data. If a complete P1 telegram is received the OBIS messages are
* returned. Otherwise it will continue parsing with the next call op parseData.
*
* @author mvolaart
* @since 1.7.0
*/
public class P1TelegramParser {
// Logger
public static final Logger logger = LoggerFactory.getLogger(P1TelegramParser.class);
// Helper classers
private OBISMsgFactory factory;
// Reader state
private P1TelegramParserState parserState;
// Current P1Telegram values
private LinkedList<OBISDataLine> obisDataLines;
private OBISDataLine currentLine = null;
/**
* Creates a new P1TelegramParser
*
* @param factory
* OBISMsgFactory object
*/
public P1TelegramParser(OBISMsgFactory factory) {
this.factory = factory;
obisDataLines = new LinkedList<OBISDataLine>();
parserState = new P1TelegramParserState();
}
/**
* Parses data. If parsing is not ready yet nothing will be returned. If
* parsing fails completely nothing will be returned. If parsing succeeds
* (partial) the received OBIS messages will be returned.
*
* @param data
* byte data
* @param offset
* offset tot start in the data buffer
* @param length
* number of bytes to parse
* @return List of {@link OBISMessage} if a full P1 telegram is received,
* empty list if parsing is not ready or failed completely.
*/
public List<OBISMessage> parseData(byte[] data, int offset, int length) {
List<OBISMessage> receivedMessages = new LinkedList<OBISMessage>();
if (logger.isTraceEnabled()) {
logger.trace("Data: {}, state before parsing: {}", new String(Arrays.copyOfRange(data, offset, length)),
parserState);
}
for (int i = offset; i < (offset + length); i++) {
char c = (char) data[i];
switch (parserState.getState()) {
case WAIT_FOR_START:
if (c == '/') {
parserState.setState(P1TelegramParserState.State.STARTED);
handleNewP1Telegram();
}
break;
case STARTED:
parserState.setState(P1TelegramParserState.State.HEADER);
break;
case HEADER:
if (c == '\r') {
parserState.setState(P1TelegramParserState.State.CRLF);
}
break;
case CRLF:
if (Character.isWhitespace(c)) {
// do nothing
} else if (Character.isDigit(c)) {
parserState.setState(P1TelegramParserState.State.DATA_OBIS_ID);
finishObisLine();
} else {
handleUnexpectedCharacter(c);
parserState.setState(P1TelegramParserState.State.WAIT_FOR_START);
}
break;
case DATA_OBIS_ID:
if (Character.isWhitespace(c)) {
// ignore
} else if (Character.isDigit(c) || c == ':' || c == '-' || c == '.' || c == '*') {
// do nothing
} else if (c == '(') {
parserState.setState(P1TelegramParserState.State.DATA_OBIS_VALUE_START);
} else if (c == '!') {
logger.warn("Unexpected character '!'. Going to state: {}",
P1TelegramParserState.State.CRC_VALUE);
parserState.setState(P1TelegramParserState.State.CRC_VALUE);
} else {
handleUnexpectedCharacter(c);
parserState.setState(P1TelegramParserState.State.WAIT_FOR_START);
}
break;
case DATA_OBIS_VALUE_START:
if (c == ')') {
handleObisValueReady();
parserState.setState(P1TelegramParserState.State.DATA_OBIS_VALUE_END);
} else {
parserState.setState(P1TelegramParserState.State.DATA_OBIS_VALUE);
}
break;
case DATA_OBIS_VALUE:
if (c == ')') {
handleObisValueReady();
parserState.setState(P1TelegramParserState.State.DATA_OBIS_VALUE_END);
}
break;
case DATA_OBIS_VALUE_END:
if (Character.isWhitespace(c)) {
// ignore
} else if (Character.isDigit(c)) {
finishObisLine();
parserState.setState(P1TelegramParserState.State.DATA_OBIS_ID);
} else if (c == '(') {
parserState.setState(P1TelegramParserState.State.DATA_OBIS_VALUE_START);
} else if (c == '!') {
parserState.setState(P1TelegramParserState.State.CRC_VALUE);
} else {
handleUnexpectedCharacter(c);
parserState.setState(P1TelegramParserState.State.WAIT_FOR_START);
}
break;
case CRC_VALUE:
if (c == '\r') {
if (parserState.getCrcValue().length() > 0) {
// TODO: Handle CRC here
}
receivedMessages.addAll(handleEndP1Telegram());
parserState.setState(P1TelegramParserState.State.WAIT_FOR_START);
}
break;
}
parserState.handleCharacter(c);
}
logger.trace("State after parsing: {}", parserState);
return receivedMessages;
}
/**
* Handles the start of a new P1 telegram This method will clear internal
* state
*/
private void handleNewP1Telegram() {
obisDataLines.clear();
currentLine = null;
}
/**
* Handles the end of a P1 telegram. This method will parse the raw data to
* a list of {@link OBISMessage}
*/
private List<OBISMessage> handleEndP1Telegram() {
finishObisLine();
List<OBISMessage> obisMessages = new LinkedList<OBISMessage>();
for (OBISDataLine obisDataLine : obisDataLines) {
OBISMessage obisMessage = factory.getMessage(obisDataLine.obisId, obisDataLine.obisStringValues);
logger.debug("Parsed: {}, to: {}", obisDataLine, obisMessage);
if (obisMessage != null) {
obisMessages.add(obisMessage);
} else {
logger.warn("Failed to parse: {}", obisDataLine);
}
}
return obisMessages;
}
/**
* Handles an unexpected character. The character will be logged and the P1
* telegram will be finished.
*
* @param c
* the unexpected character
*/
private void handleUnexpectedCharacter(char c) {
logger.warn("Unexpected character '{}' in state: {}. Publishing partial P1Telegram and wait for new P1Telegram",
c, parserState);
handleEndP1Telegram();
}
/**
* Handle if a full OBIS value is parsed. This method will store the OBIS
* value in the current OBISDataLine
*/
private void handleObisValueReady() {
if (currentLine == null) {
currentLine = new OBISDataLine(parserState.getObisId());
}
currentLine.obisStringValues.add(parserState.getObisValue());
}
/**
* Handle the end of a OBIS Line (i.e. all values are parsed)
*/
private void finishObisLine() {
if (currentLine != null) {
obisDataLines.add(currentLine);
currentLine = null;
}
}
/**
* Class representing a OBISDataLine, e.g. '0-0:96.3.10(1)'
*
* @author mvolaart
* @since 1.7.0
*/
class OBISDataLine {
// OBIS identifier
final String obisId;
// List of OBIS values
final LinkedList<String> obisStringValues;
/**
* Creates a new OBISDataLine
*
* @param obisId
* OBISIdentifer
*/
OBISDataLine(String obisId) {
this.obisId = obisId;
obisStringValues = new LinkedList<String>();
}
@Override
public String toString() {
return "OBISDataLine [OBIS id:" + obisId + ", obis values:" + obisStringValues;
}
}
}