2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.dsmr.internal.device.p1telegram;
15 import java.nio.charset.StandardCharsets;
16 import java.util.AbstractMap.SimpleEntry;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.List;
20 import java.util.Map.Entry;
21 import java.util.regex.Pattern;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.openhab.binding.dsmr.internal.device.cosem.CosemObject;
25 import org.openhab.binding.dsmr.internal.device.cosem.CosemObjectFactory;
26 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram.TelegramState;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
31 * The {@link P1TelegramParser} is a class that will read P1-port data create a full P1
34 * Data can be parsed in chunks. If a full P1 telegram is received, listeners are notified
36 * @author M. Volaart - Initial contribution
37 * @author Hilbrand Bouwkamp - Removed asynchronous call and some clean up
40 public class P1TelegramParser implements TelegramParser {
46 /** Wait for the '/' character */
48 /** '/' character seen */
50 /** Waiting for the header to end with a CR & LF */
52 /** Handling OBIS Identifier */
54 /** Parsing OBIS value */
56 /** OBIS value end seen ')' */
58 /** Parsing CRC value following '!' */
63 * Pattern for the CRC-code
65 private static final String CRC_PATTERN = "[0-9A-Z]{4}";
67 private final Logger logger = LoggerFactory.getLogger(P1TelegramParser.class);
69 /* internal state variables */
72 * current obisId buffer.
74 private final StringBuilder obisId = new StringBuilder();
77 * Current cosem object values buffer.
79 private final StringBuilder obisValue = new StringBuilder();
82 * In lenient mode store raw data and log when a complete message is received.
84 private final StringBuilder rawData = new StringBuilder();
87 * Current crc value read.
89 private final StringBuilder crcValue = new StringBuilder();
92 * CRC calculation helper
94 private final CRC16 crc;
97 * Current state of the P1 telegram parser
99 private volatile State state = State.WAIT_FOR_START;
102 * Work in lenient mode (more fault tolerant)
104 private volatile boolean lenientMode;
107 * Current telegram state
109 private volatile TelegramState telegramState;
112 * CosemObjectFactory helper class
114 private final CosemObjectFactory factory;
117 * Received Cosem Objects in the P1Telegram that is currently received
119 private final List<CosemObject> cosemObjects = new ArrayList<>();
122 * List of Cosem Object values that are not known to this binding.
124 private final List<Entry<String, String>> unknownCosemObjects = new ArrayList<>();
127 * Listener for new P1 telegrams
129 private final P1TelegramListener telegramListener;
132 * Creates a new P1TelegramParser
134 * @param telegramListener
136 public P1TelegramParser(P1TelegramListener telegramListener) {
137 this.telegramListener = telegramListener;
139 factory = new CosemObjectFactory();
140 state = State.WAIT_FOR_START;
141 crc = new CRC16(CRC16.Polynom.CRC16_IBM);
142 telegramState = TelegramState.OK;
146 * Parses data. If a complete message is received the message will be passed to the telegramListener.
148 * @param data byte data to parse
149 * @param length number of bytes to parse
152 public void parse(byte[] data, int length) {
153 if (lenientMode || logger.isTraceEnabled()) {
154 String rawBlock = new String(data, 0, length, StandardCharsets.UTF_8);
157 rawData.append(rawBlock);
159 if (logger.isTraceEnabled()) {
160 logger.trace("Raw data: {}, Parser state entering parseData: {}", rawBlock, state);
163 for (int i = 0; i < length; i++) {
164 char c = (char) data[i];
169 setState(State.HEADER);
174 setState(State.CRLF);
178 if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
180 } else if (Character.isDigit(c)) {
181 setState(State.DATA_OBIS_ID);
183 handleUnexpectedCharacter(c);
185 setState(State.WAIT_FOR_START);
189 if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
191 } else if (Character.isDigit(c) || c == ':' || c == '-' || c == '.' || c == '*') { // NOPMD
193 } else if (c == '(') {
194 setState(State.DATA_OBIS_VALUE);
195 } else if (c == '!') {
196 handleUnexpectedCharacter(c);
198 // Clear current Obis Data (Keep already received data)
200 setState(State.CRC_VALUE);
202 setState(State.WAIT_FOR_START);
205 handleUnexpectedCharacter(c);
209 setState(State.DATA_OBIS_ID);
211 setState(State.WAIT_FOR_START);
215 case DATA_OBIS_VALUE:
217 setState(State.DATA_OBIS_VALUE_END);
220 case DATA_OBIS_VALUE_END:
221 if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
223 } else if (Character.isDigit(c)) {
224 setState(State.DATA_OBIS_ID);
225 } else if (c == '(') {
226 setState(State.DATA_OBIS_VALUE);
227 } else if (c == '!') {
228 setState(State.CRC_VALUE);
230 handleUnexpectedCharacter(c);
233 setState(State.WAIT_FOR_START);
234 } // Other wise try to recover in lenient mode
240 * Normally the P1 telegram ends with a \r\n sequence
241 * If we already see a '/' character we also assume the current
242 * P1 telegram is correctly finished
244 if (c == '\r' || c == '/') {
245 logger.trace("telegramState {}, crcValue to check 0x{}", telegramState, crcValue);
246 // Only perform CRC check if telegram is still ok
247 if (telegramState == TelegramState.OK && crcValue.length() > 0) {
248 if (Pattern.matches(CRC_PATTERN, crcValue)) {
249 int crcP1Telegram = Integer.parseInt(crcValue.toString(), 16);
250 int calculatedCRC = crc.getCurrentCRCCode();
252 if (logger.isDebugEnabled()) {
253 logger.trace("received CRC value: {}, calculated CRC value: 0x{}", crcValue,
254 String.format("%04X", calculatedCRC));
256 if (crcP1Telegram != calculatedCRC) {
257 logger.trace("CRC value does not match, p1 Telegram failed");
259 telegramState = TelegramState.CRC_ERROR;
262 telegramState = TelegramState.CRC_ERROR;
265 telegramListener.telegramReceived(constructTelegram());
269 * Immediately proceed to the next state (robust implementation for meter that do not follow
272 setState(State.HEADER);
280 logger.trace("State after parsing: {}", state);
283 private P1Telegram constructTelegram() {
284 final List<CosemObject> cosemObjectsCopy = new ArrayList<>(cosemObjects);
287 return new P1Telegram(cosemObjectsCopy, telegramState, rawData.toString(),
288 unknownCosemObjects.isEmpty() ? Collections.emptyList() : new ArrayList<>(unknownCosemObjects));
290 return new P1Telegram(cosemObjectsCopy, telegramState);
295 public void reset() {
296 setState(State.WAIT_FOR_START);
300 * Handles an unexpected character. The character will be logged and the current telegram is marked corrupted
302 * @param c the unexpected character
304 private void handleUnexpectedCharacter(char c) {
305 logger.debug("Unexpected character '{}' in state: {}. This P1 telegram is marked as failed", c, state);
307 telegramState = TelegramState.DATA_CORRUPTION;
311 * Stores a single character
313 * @param c the character to process
315 private void handleCharacter(char c) {
321 crc.processByte((byte) c);
324 crc.processByte((byte) c);
328 crc.processByte((byte) c);
330 case DATA_OBIS_VALUE:
332 crc.processByte((byte) c);
334 case DATA_OBIS_VALUE_END:
336 crc.processByte((byte) c);
340 crc.processByte((byte) c);
344 // CRC data is not part of received data
352 * Clears all internal state
354 private void clearInternalData() {
356 obisValue.setLength(0);
357 rawData.setLength(0);
358 crcValue.setLength(0);
360 cosemObjects.clear();
361 unknownCosemObjects.clear();
365 * Clears all the current OBIS data. I.e.
366 * - current OBIS identifier
367 * - current OBIS value
369 private void clearObisData() {
371 obisValue.setLength(0);
375 * Store the current CosemObject in the list of received cosem Objects
377 private void storeCurrentCosemObject() {
378 String obisIdString = obisId.toString();
380 if (!obisIdString.isEmpty()) {
381 final String obisValueString = obisValue.toString();
382 CosemObject cosemObject = factory.getCosemObject(obisIdString, obisValueString);
384 if (cosemObject == null) {
386 unknownCosemObjects.add(new SimpleEntry<>(obisIdString, obisValueString));
389 logger.trace("Adding {} to list of Cosem Objects", cosemObject);
390 cosemObjects.add(cosemObject);
397 * @param newState the new state to set
399 private void setState(State newState) {
400 synchronized (state) {
403 // Clear CRC data and mark current telegram as OK
407 // Clears internal state data and mark current telegram as OK
409 telegramState = TelegramState.OK;
412 // If the current state is CRLF we are processing the header and don't have a cosem object yet
413 if (state != State.CRLF) {
414 storeCurrentCosemObject();
418 storeCurrentCosemObject();
428 public void setLenientMode(boolean lenientMode) {
429 this.lenientMode = lenientMode;