2 * Copyright (c) 2010-2024 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.Optional;
22 import java.util.regex.Pattern;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.openhab.binding.dsmr.internal.device.connector.DSMRErrorStatus;
26 import org.openhab.binding.dsmr.internal.device.cosem.CosemObject;
27 import org.openhab.binding.dsmr.internal.device.cosem.CosemObjectFactory;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
32 * The {@link P1TelegramParser} is a class that will read P1-port data create a full P1
35 * Data can be parsed in chunks. If a full P1 telegram is received, listeners are notified
37 * @author M. Volaart - Initial contribution
38 * @author Hilbrand Bouwkamp - Removed asynchronous call and some clean up
41 public class P1TelegramParser implements TelegramParser {
47 /** Wait for the '/' character */
49 /** '/' character seen */
51 /** Waiting for the header to end with a CR & LF */
53 /** Handling OBIS Identifier */
55 /** Parsing OBIS value */
57 /** OBIS value end seen ')' */
59 /** Parsing CRC value following '!' */
64 * Pattern for the CRC-code
66 private static final String CRC_PATTERN = "[0-9A-Z]{4}";
68 private final Logger logger = LoggerFactory.getLogger(P1TelegramParser.class);
70 /* internal state variables */
73 * current obisId buffer.
75 private final StringBuilder obisId = new StringBuilder();
78 * Current cosem object values buffer.
80 private final StringBuilder obisValue = new StringBuilder();
83 * In lenient mode store raw data and log when a complete message is received.
85 private final StringBuilder rawData = new StringBuilder();
88 * Current crc value read.
90 private final StringBuilder crcValue = new StringBuilder();
93 * CRC calculation helper
95 private final CRC16 crc;
98 * Current state of the P1 telegram parser
100 private volatile State state = State.WAIT_FOR_START;
103 * Work in lenient mode (more fault tolerant)
105 private volatile boolean lenientMode;
108 * Current telegram state
110 private volatile Optional<DSMRErrorStatus> telegramState = Optional.empty();
113 * CosemObjectFactory helper class
115 private final CosemObjectFactory factory;
118 * Received Cosem Objects in the P1Telegram that is currently received
120 private final List<Entry<String, String>> cosemObjects = new ArrayList<>();
123 * List of Cosem Object values that are not known to this binding.
125 private final List<Entry<String, String>> unknownCosemObjects = new ArrayList<>();
128 * Listener for new P1 telegrams
130 private final P1TelegramListener telegramListener;
133 * Enable in tests. Will throw an exception on CRC error.
135 private final boolean test;
138 * Creates a new P1TelegramParser
140 * @param telegramListener
142 public P1TelegramParser(final P1TelegramListener telegramListener) {
143 this(telegramListener, false);
146 public P1TelegramParser(final P1TelegramListener telegramListener, final boolean test) {
147 this.telegramListener = telegramListener;
150 factory = new CosemObjectFactory();
151 state = State.WAIT_FOR_START;
152 crc = new CRC16(CRC16.Polynom.CRC16_IBM);
153 telegramState = Optional.empty();
157 * Parses data. If a complete message is received the message will be passed to the telegramListener.
159 * @param data byte data to parse
160 * @param length number of bytes to parse
163 public void parse(final byte[] data, final int length) {
164 if (lenientMode || logger.isTraceEnabled()) {
165 final String rawBlock = new String(data, 0, length, StandardCharsets.UTF_8);
168 rawData.append(rawBlock);
170 if (logger.isTraceEnabled()) {
171 logger.trace("Raw data: {}, Parser state entering parseData: {}", rawBlock, state);
174 for (int i = 0; i < length; i++) {
175 final char c = (char) data[i];
180 setState(State.HEADER);
185 setState(State.CRLF);
189 if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
191 } else if (Character.isDigit(c)) {
192 setState(State.DATA_OBIS_ID);
194 handleUnexpectedCharacter(c);
196 setState(State.WAIT_FOR_START);
200 if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
202 } else if (Character.isDigit(c) || c == ':' || c == '-' || c == '.' || c == '*') { // NOPMD
204 } else if (c == '(') {
205 setState(State.DATA_OBIS_VALUE);
206 } else if (c == '!') {
207 handleUnexpectedCharacter(c);
209 // Clear current Obis Data (Keep already received data)
211 setState(State.CRC_VALUE);
213 setState(State.WAIT_FOR_START);
216 handleUnexpectedCharacter(c);
220 setState(State.DATA_OBIS_ID);
222 setState(State.WAIT_FOR_START);
226 case DATA_OBIS_VALUE:
228 setState(State.DATA_OBIS_VALUE_END);
231 case DATA_OBIS_VALUE_END:
232 if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
234 } else if (Character.isDigit(c)) {
235 setState(State.DATA_OBIS_ID);
236 } else if (c == '(') {
237 setState(State.DATA_OBIS_VALUE);
238 } else if (c == '!') {
239 setState(State.CRC_VALUE);
241 handleUnexpectedCharacter(c);
244 setState(State.WAIT_FOR_START);
245 } // Other wise try to recover in lenient mode
251 * Normally the P1 telegram ends with a \r\n sequence
252 * If we already see a '/' character we also assume the current
253 * P1 telegram is correctly finished
255 if (c == '\r' || c == '/') {
256 logger.trace("telegramState {}, crcValue to check 0x{}", telegramState, crcValue);
257 // Only perform CRC check if telegram is still ok
259 if (telegramState.isEmpty() && crcValue.length() > 0) {
260 telegramState = checkCRC();
266 * Immediately proceed to the next state (robust implementation for meter that do not follow
269 setState(State.HEADER);
277 logger.trace("State after parsing: {}", state);
280 private Optional<DSMRErrorStatus> checkCRC() {
281 final Optional<DSMRErrorStatus> telegramState;
283 if (Pattern.matches(CRC_PATTERN, crcValue)) {
284 final int crcP1Telegram = Integer.parseInt(crcValue.toString(), 16);
285 final int calculatedCRC = crc.getCurrentCRCCode();
287 if (logger.isDebugEnabled()) {
288 logger.trace("received CRC value: {}, calculated CRC value: 0x{}", crcValue,
289 String.format("%04X", calculatedCRC));
291 if (crcP1Telegram != calculatedCRC) {
293 throw new IllegalArgumentException(
294 String.format("Invalid CRC. Read: %s, expected: %04X", crcValue, calculatedCRC));
296 logger.trace("CRC value does not match, p1 Telegram failed");
298 telegramState = Optional.of(DSMRErrorStatus.TELEGRAM_CRC_ERROR);
300 telegramState = Optional.empty();
303 telegramState = Optional.of(DSMRErrorStatus.TELEGRAM_CRC_ERROR);
305 return telegramState;
308 private void processTelegram() {
309 telegramState.ifPresentOrElse(error -> telegramListener.onError(error, ""),
310 () -> telegramListener.telegramReceived(constructTelegram()));
313 private P1Telegram constructTelegram() {
314 final List<CosemObject> cosemObjectsCopy = new ArrayList<>();
316 cosemObjects.stream().forEach(e -> addCosemObject(cosemObjectsCopy, e));
318 return new P1Telegram(cosemObjectsCopy, rawData.toString(),
319 unknownCosemObjects.isEmpty() ? Collections.emptyList() : new ArrayList<>(unknownCosemObjects));
321 return new P1Telegram(cosemObjectsCopy);
325 private void addCosemObject(final List<CosemObject> objects, final Entry<String, String> cosemEntry) {
326 final String obisIdString = cosemEntry.getKey();
327 final String obisValueString = cosemEntry.getValue();
328 final CosemObject cosemObject = factory.getCosemObject(obisIdString, obisValueString);
330 if (cosemObject == null) {
332 unknownCosemObjects.add(new SimpleEntry<>(obisIdString, obisValueString));
335 logger.trace("Adding {} to list of Cosem Objects", cosemObject);
336 objects.add(cosemObject);
341 public void reset() {
342 setState(State.WAIT_FOR_START);
346 * Handles an unexpected character. The character will be logged and the current telegram is marked corrupted
348 * @param c the unexpected character
350 private void handleUnexpectedCharacter(final char c) {
351 logger.debug("Unexpected character '{}' in state: {}. This P1 telegram is marked as failed", c, state);
353 telegramState = Optional.of(DSMRErrorStatus.TELEGRAM_DATA_CORRUPTION);
354 telegramListener.onError(DSMRErrorStatus.TELEGRAM_DATA_CORRUPTION, "");
358 * Stores a single character
360 * @param c the character to process
362 private void handleCharacter(final char c) {
368 crc.processByte((byte) c);
371 crc.processByte((byte) c);
375 crc.processByte((byte) c);
377 case DATA_OBIS_VALUE:
379 crc.processByte((byte) c);
381 case DATA_OBIS_VALUE_END:
383 crc.processByte((byte) c);
387 crc.processByte((byte) c);
391 // CRC data is not part of received data
399 * Clears all internal state
401 private void clearInternalData() {
403 obisValue.setLength(0);
404 rawData.setLength(0);
405 crcValue.setLength(0);
407 cosemObjects.clear();
408 unknownCosemObjects.clear();
412 * Clears all the current OBIS data. I.e.
413 * - current OBIS identifier
414 * - current OBIS value
416 private void clearObisData() {
418 obisValue.setLength(0);
422 * Store the current CosemObject in the list of received cosem Objects
424 private void storeCurrentCosemObject() {
425 final String obisIdString = obisId.toString();
427 if (!obisIdString.isEmpty()) {
428 cosemObjects.add(new SimpleEntry<String, String>(obisIdString, obisValue.toString()));
434 * @param newState the new state to set
436 private void setState(final State newState) {
437 synchronized (state) {
440 // Clear CRC data and mark current telegram as OK
444 // Clears internal state data and mark current telegram as OK
446 telegramState = Optional.empty();
449 // If the current state is CRLF we are processing the header and don't have a cosem object yet
450 if (state != State.CRLF) {
451 storeCurrentCosemObject();
455 storeCurrentCosemObject();
465 public void setLenientMode(final boolean lenientMode) {
466 this.lenientMode = lenientMode;