2 * Copyright (c) 2010-2023 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 * Enable in tests. Will throw an exception on CRC error.
134 private final boolean test;
137 * Creates a new P1TelegramParser
139 * @param telegramListener
141 public P1TelegramParser(P1TelegramListener telegramListener) {
142 this(telegramListener, false);
145 public P1TelegramParser(P1TelegramListener telegramListener, boolean test) {
146 this.telegramListener = telegramListener;
149 factory = new CosemObjectFactory();
150 state = State.WAIT_FOR_START;
151 crc = new CRC16(CRC16.Polynom.CRC16_IBM);
152 telegramState = TelegramState.OK;
156 * Parses data. If a complete message is received the message will be passed to the telegramListener.
158 * @param data byte data to parse
159 * @param length number of bytes to parse
162 public void parse(byte[] data, int length) {
163 if (lenientMode || logger.isTraceEnabled()) {
164 final String rawBlock = new String(data, 0, length, StandardCharsets.UTF_8);
167 rawData.append(rawBlock);
169 if (logger.isTraceEnabled()) {
170 logger.trace("Raw data: {}, Parser state entering parseData: {}", rawBlock, state);
173 for (int i = 0; i < length; i++) {
174 final char c = (char) data[i];
179 setState(State.HEADER);
184 setState(State.CRLF);
188 if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
190 } else if (Character.isDigit(c)) {
191 setState(State.DATA_OBIS_ID);
193 handleUnexpectedCharacter(c);
195 setState(State.WAIT_FOR_START);
199 if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
201 } else if (Character.isDigit(c) || c == ':' || c == '-' || c == '.' || c == '*') { // NOPMD
203 } else if (c == '(') {
204 setState(State.DATA_OBIS_VALUE);
205 } else if (c == '!') {
206 handleUnexpectedCharacter(c);
208 // Clear current Obis Data (Keep already received data)
210 setState(State.CRC_VALUE);
212 setState(State.WAIT_FOR_START);
215 handleUnexpectedCharacter(c);
219 setState(State.DATA_OBIS_ID);
221 setState(State.WAIT_FOR_START);
225 case DATA_OBIS_VALUE:
227 setState(State.DATA_OBIS_VALUE_END);
230 case DATA_OBIS_VALUE_END:
231 if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
233 } else if (Character.isDigit(c)) {
234 setState(State.DATA_OBIS_ID);
235 } else if (c == '(') {
236 setState(State.DATA_OBIS_VALUE);
237 } else if (c == '!') {
238 setState(State.CRC_VALUE);
240 handleUnexpectedCharacter(c);
243 setState(State.WAIT_FOR_START);
244 } // Other wise try to recover in lenient mode
250 * Normally the P1 telegram ends with a \r\n sequence
251 * If we already see a '/' character we also assume the current
252 * P1 telegram is correctly finished
254 if (c == '\r' || c == '/') {
255 logger.trace("telegramState {}, crcValue to check 0x{}", telegramState, crcValue);
256 // Only perform CRC check if telegram is still ok
257 if (telegramState == TelegramState.OK && crcValue.length() > 0) {
258 telegramState = checkCRC(telegramState);
260 telegramListener.telegramReceived(constructTelegram());
264 * Immediately proceed to the next state (robust implementation for meter that do not follow
267 setState(State.HEADER);
275 logger.trace("State after parsing: {}", state);
278 private TelegramState checkCRC(TelegramState currentState) {
279 final TelegramState telegramState;
281 if (Pattern.matches(CRC_PATTERN, crcValue)) {
282 final int crcP1Telegram = Integer.parseInt(crcValue.toString(), 16);
283 final int calculatedCRC = crc.getCurrentCRCCode();
285 if (logger.isDebugEnabled()) {
286 logger.trace("received CRC value: {}, calculated CRC value: 0x{}", crcValue,
287 String.format("%04X", calculatedCRC));
289 if (crcP1Telegram != calculatedCRC) {
291 throw new IllegalArgumentException(
292 String.format("Invalid CRC. Read: %s, expected: %04X", crcValue, calculatedCRC));
294 logger.trace("CRC value does not match, p1 Telegram failed");
296 telegramState = TelegramState.CRC_ERROR;
298 telegramState = currentState;
301 telegramState = TelegramState.CRC_ERROR;
303 return telegramState;
306 private P1Telegram constructTelegram() {
307 final List<CosemObject> cosemObjectsCopy = new ArrayList<>(cosemObjects);
310 return new P1Telegram(cosemObjectsCopy, telegramState, rawData.toString(),
311 unknownCosemObjects.isEmpty() ? Collections.emptyList() : new ArrayList<>(unknownCosemObjects));
313 return new P1Telegram(cosemObjectsCopy, telegramState);
318 public void reset() {
319 setState(State.WAIT_FOR_START);
323 * Handles an unexpected character. The character will be logged and the current telegram is marked corrupted
325 * @param c the unexpected character
327 private void handleUnexpectedCharacter(char c) {
328 logger.debug("Unexpected character '{}' in state: {}. This P1 telegram is marked as failed", c, state);
330 telegramState = TelegramState.DATA_CORRUPTION;
334 * Stores a single character
336 * @param c the character to process
338 private void handleCharacter(char c) {
344 crc.processByte((byte) c);
347 crc.processByte((byte) c);
351 crc.processByte((byte) c);
353 case DATA_OBIS_VALUE:
355 crc.processByte((byte) c);
357 case DATA_OBIS_VALUE_END:
359 crc.processByte((byte) c);
363 crc.processByte((byte) c);
367 // CRC data is not part of received data
375 * Clears all internal state
377 private void clearInternalData() {
379 obisValue.setLength(0);
380 rawData.setLength(0);
381 crcValue.setLength(0);
383 cosemObjects.clear();
384 unknownCosemObjects.clear();
388 * Clears all the current OBIS data. I.e.
389 * - current OBIS identifier
390 * - current OBIS value
392 private void clearObisData() {
394 obisValue.setLength(0);
398 * Store the current CosemObject in the list of received cosem Objects
400 private void storeCurrentCosemObject() {
401 final String obisIdString = obisId.toString();
403 if (!obisIdString.isEmpty()) {
404 final String obisValueString = obisValue.toString();
405 final CosemObject cosemObject = factory.getCosemObject(obisIdString, obisValueString);
407 if (cosemObject == null) {
409 unknownCosemObjects.add(new SimpleEntry<>(obisIdString, obisValueString));
412 logger.trace("Adding {} to list of Cosem Objects", cosemObject);
413 cosemObjects.add(cosemObject);
420 * @param newState the new state to set
422 private void setState(State newState) {
423 synchronized (state) {
426 // Clear CRC data and mark current telegram as OK
430 // Clears internal state data and mark current telegram as OK
432 telegramState = TelegramState.OK;
435 // If the current state is CRLF we are processing the header and don't have a cosem object yet
436 if (state != State.CRLF) {
437 storeCurrentCosemObject();
441 storeCurrentCosemObject();
451 public void setLenientMode(boolean lenientMode) {
452 this.lenientMode = lenientMode;