]> git.basschouten.com Git - openhab-addons.git/blob
c8e040e9809c20d339cb60078959f59d19345c2b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.dsmr.internal.device.p1telegram;
14
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;
22
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;
29
30 /**
31  * The {@link P1TelegramParser} is a class that will read P1-port data create a full P1
32  * telegram.
33  *
34  * Data can be parsed in chunks. If a full P1 telegram is received, listeners are notified
35  *
36  * @author M. Volaart - Initial contribution
37  * @author Hilbrand Bouwkamp - Removed asynchronous call and some clean up
38  */
39 @NonNullByDefault
40 public class P1TelegramParser implements TelegramParser {
41
42     /**
43      * State of the parser
44      */
45     private enum State {
46         /** Wait for the '/' character */
47         WAIT_FOR_START,
48         /** '/' character seen */
49         HEADER,
50         /** Waiting for the header to end with a CR & LF */
51         CRLF,
52         /** Handling OBIS Identifier */
53         DATA_OBIS_ID,
54         /** Parsing OBIS value */
55         DATA_OBIS_VALUE,
56         /** OBIS value end seen ')' */
57         DATA_OBIS_VALUE_END,
58         /** Parsing CRC value following '!' */
59         CRC_VALUE
60     }
61
62     /**
63      * Pattern for the CRC-code
64      */
65     private static final String CRC_PATTERN = "[0-9A-Z]{4}";
66
67     private final Logger logger = LoggerFactory.getLogger(P1TelegramParser.class);
68
69     /* internal state variables */
70
71     /**
72      * current obisId buffer.
73      */
74     private final StringBuilder obisId = new StringBuilder();
75
76     /**
77      * Current cosem object values buffer.
78      */
79     private final StringBuilder obisValue = new StringBuilder();
80
81     /**
82      * In lenient mode store raw data and log when a complete message is received.
83      */
84     private final StringBuilder rawData = new StringBuilder();
85
86     /**
87      * Current crc value read.
88      */
89     private final StringBuilder crcValue = new StringBuilder();
90
91     /**
92      * CRC calculation helper
93      */
94     private final CRC16 crc;
95
96     /**
97      * Current state of the P1 telegram parser
98      */
99     private volatile State state = State.WAIT_FOR_START;
100
101     /**
102      * Work in lenient mode (more fault tolerant)
103      */
104     private volatile boolean lenientMode;
105
106     /**
107      * Current telegram state
108      */
109     private volatile TelegramState telegramState;
110
111     /**
112      * CosemObjectFactory helper class
113      */
114     private final CosemObjectFactory factory;
115
116     /**
117      * Received Cosem Objects in the P1Telegram that is currently received
118      */
119     private final List<CosemObject> cosemObjects = new ArrayList<>();
120
121     /**
122      * List of Cosem Object values that are not known to this binding.
123      */
124     private final List<Entry<String, String>> unknownCosemObjects = new ArrayList<>();
125
126     /**
127      * Listener for new P1 telegrams
128      */
129     private final P1TelegramListener telegramListener;
130
131     /**
132      * Enable in tests. Will throw an exception on CRC error.
133      */
134     private final boolean test;
135
136     /**
137      * Creates a new P1TelegramParser
138      *
139      * @param telegramListener
140      */
141     public P1TelegramParser(P1TelegramListener telegramListener) {
142         this(telegramListener, false);
143     }
144
145     public P1TelegramParser(P1TelegramListener telegramListener, boolean test) {
146         this.telegramListener = telegramListener;
147         this.test = test;
148
149         factory = new CosemObjectFactory();
150         state = State.WAIT_FOR_START;
151         crc = new CRC16(CRC16.Polynom.CRC16_IBM);
152         telegramState = TelegramState.OK;
153     }
154
155     /**
156      * Parses data. If a complete message is received the message will be passed to the telegramListener.
157      *
158      * @param data byte data to parse
159      * @param length number of bytes to parse
160      */
161     @Override
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);
165
166             if (lenientMode) {
167                 rawData.append(rawBlock);
168             }
169             if (logger.isTraceEnabled()) {
170                 logger.trace("Raw data: {}, Parser state entering parseData: {}", rawBlock, state);
171             }
172         }
173         for (int i = 0; i < length; i++) {
174             final char c = (char) data[i];
175
176             switch (state) {
177                 case WAIT_FOR_START:
178                     if (c == '/') {
179                         setState(State.HEADER);
180                     }
181                     break;
182                 case HEADER:
183                     if (c == '\r') {
184                         setState(State.CRLF);
185                     }
186                     break;
187                 case CRLF:
188                     if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
189                         // do nothing
190                     } else if (Character.isDigit(c)) {
191                         setState(State.DATA_OBIS_ID);
192                     } else {
193                         handleUnexpectedCharacter(c);
194
195                         setState(State.WAIT_FOR_START);
196                     }
197                     break;
198                 case DATA_OBIS_ID:
199                     if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
200                         // ignore
201                     } else if (Character.isDigit(c) || c == ':' || c == '-' || c == '.' || c == '*') { // NOPMD
202                         // do nothing
203                     } else if (c == '(') {
204                         setState(State.DATA_OBIS_VALUE);
205                     } else if (c == '!') {
206                         handleUnexpectedCharacter(c);
207                         if (lenientMode) {
208                             // Clear current Obis Data (Keep already received data)
209                             clearObisData();
210                             setState(State.CRC_VALUE);
211                         } else {
212                             setState(State.WAIT_FOR_START);
213                         }
214                     } else {
215                         handleUnexpectedCharacter(c);
216
217                         if (lenientMode) {
218                             clearObisData();
219                             setState(State.DATA_OBIS_ID);
220                         } else {
221                             setState(State.WAIT_FOR_START);
222                         }
223                     }
224                     break;
225                 case DATA_OBIS_VALUE:
226                     if (c == ')') {
227                         setState(State.DATA_OBIS_VALUE_END);
228                     }
229                     break;
230                 case DATA_OBIS_VALUE_END:
231                     if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
232                         // ignore
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);
239                     } else {
240                         handleUnexpectedCharacter(c);
241
242                         if (!lenientMode) {
243                             setState(State.WAIT_FOR_START);
244                         } // Other wise try to recover in lenient mode
245                     }
246                     break;
247
248                 case CRC_VALUE:
249                     /*
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
253                      */
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);
259                         }
260                         telegramListener.telegramReceived(constructTelegram());
261                         reset();
262                         if (c == '/') {
263                             /*
264                              * Immediately proceed to the next state (robust implementation for meter that do not follow
265                              * the specification
266                              */
267                             setState(State.HEADER);
268                         }
269                     }
270                     break;
271             }
272
273             handleCharacter(c);
274         }
275         logger.trace("State after parsing: {}", state);
276     }
277
278     private TelegramState checkCRC(TelegramState currentState) {
279         final TelegramState telegramState;
280
281         if (Pattern.matches(CRC_PATTERN, crcValue)) {
282             final int crcP1Telegram = Integer.parseInt(crcValue.toString(), 16);
283             final int calculatedCRC = crc.getCurrentCRCCode();
284
285             if (logger.isDebugEnabled()) {
286                 logger.trace("received CRC value: {}, calculated CRC value: 0x{}", crcValue,
287                         String.format("%04X", calculatedCRC));
288             }
289             if (crcP1Telegram != calculatedCRC) {
290                 if (test) {
291                     throw new IllegalArgumentException(
292                             String.format("Invalid CRC. Read: %s, expected: %04X", crcValue, calculatedCRC));
293                 }
294                 logger.trace("CRC value does not match, p1 Telegram failed");
295
296                 telegramState = TelegramState.CRC_ERROR;
297             } else {
298                 telegramState = currentState;
299             }
300         } else {
301             telegramState = TelegramState.CRC_ERROR;
302         }
303         return telegramState;
304     }
305
306     private P1Telegram constructTelegram() {
307         final List<CosemObject> cosemObjectsCopy = new ArrayList<>(cosemObjects);
308
309         if (lenientMode) {
310             return new P1Telegram(cosemObjectsCopy, telegramState, rawData.toString(),
311                     unknownCosemObjects.isEmpty() ? Collections.emptyList() : new ArrayList<>(unknownCosemObjects));
312         } else {
313             return new P1Telegram(cosemObjectsCopy, telegramState);
314         }
315     }
316
317     @Override
318     public void reset() {
319         setState(State.WAIT_FOR_START);
320     }
321
322     /**
323      * Handles an unexpected character. The character will be logged and the current telegram is marked corrupted
324      *
325      * @param c the unexpected character
326      */
327     private void handleUnexpectedCharacter(char c) {
328         logger.debug("Unexpected character '{}' in state: {}. This P1 telegram is marked as failed", c, state);
329
330         telegramState = TelegramState.DATA_CORRUPTION;
331     }
332
333     /**
334      * Stores a single character
335      *
336      * @param c the character to process
337      */
338     private void handleCharacter(char c) {
339         switch (state) {
340             case WAIT_FOR_START:
341                 // ignore the data
342                 break;
343             case HEADER:
344                 crc.processByte((byte) c);
345                 break;
346             case CRLF:
347                 crc.processByte((byte) c);
348                 break;
349             case DATA_OBIS_ID:
350                 obisId.append(c);
351                 crc.processByte((byte) c);
352                 break;
353             case DATA_OBIS_VALUE:
354                 obisValue.append(c);
355                 crc.processByte((byte) c);
356                 break;
357             case DATA_OBIS_VALUE_END:
358                 obisValue.append(c);
359                 crc.processByte((byte) c);
360                 break;
361             case CRC_VALUE:
362                 if (c == '!') {
363                     crc.processByte((byte) c);
364                 } else {
365                     crcValue.append(c);
366                 }
367                 // CRC data is not part of received data
368                 break;
369             default:
370                 break;
371         }
372     }
373
374     /**
375      * Clears all internal state
376      */
377     private void clearInternalData() {
378         obisId.setLength(0);
379         obisValue.setLength(0);
380         rawData.setLength(0);
381         crcValue.setLength(0);
382         crc.initialize();
383         cosemObjects.clear();
384         unknownCosemObjects.clear();
385     }
386
387     /**
388      * Clears all the current OBIS data. I.e.
389      * - current OBIS identifier
390      * - current OBIS value
391      */
392     private void clearObisData() {
393         obisId.setLength(0);
394         obisValue.setLength(0);
395     }
396
397     /**
398      * Store the current CosemObject in the list of received cosem Objects
399      */
400     private void storeCurrentCosemObject() {
401         final String obisIdString = obisId.toString();
402
403         if (!obisIdString.isEmpty()) {
404             final String obisValueString = obisValue.toString();
405             final CosemObject cosemObject = factory.getCosemObject(obisIdString, obisValueString);
406
407             if (cosemObject == null) {
408                 if (lenientMode) {
409                     unknownCosemObjects.add(new SimpleEntry<>(obisIdString, obisValueString));
410                 }
411             } else {
412                 logger.trace("Adding {} to list of Cosem Objects", cosemObject);
413                 cosemObjects.add(cosemObject);
414             }
415         }
416         clearObisData();
417     }
418
419     /**
420      * @param newState the new state to set
421      */
422     private void setState(State newState) {
423         synchronized (state) {
424             switch (newState) {
425                 case HEADER:
426                     // Clear CRC data and mark current telegram as OK
427                     crc.initialize();
428                     break;
429                 case WAIT_FOR_START:
430                     // Clears internal state data and mark current telegram as OK
431                     clearInternalData();
432                     telegramState = TelegramState.OK;
433                     break;
434                 case DATA_OBIS_ID:
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();
438                     }
439                     break;
440                 case CRC_VALUE:
441                     storeCurrentCosemObject();
442                     break;
443                 default:
444                     break;
445             }
446             state = newState;
447         }
448     }
449
450     @Override
451     public void setLenientMode(boolean lenientMode) {
452         this.lenientMode = lenientMode;
453     }
454 }