]> git.basschouten.com Git - openhab-addons.git/blob
7ec8933f7ab19200b865ed60feaf691db4c0961b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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      * Creates a new P1TelegramParser
133      *
134      * @param telegramListener
135      */
136     public P1TelegramParser(P1TelegramListener telegramListener) {
137         this.telegramListener = telegramListener;
138
139         factory = new CosemObjectFactory();
140         state = State.WAIT_FOR_START;
141         crc = new CRC16(CRC16.Polynom.CRC16_IBM);
142         telegramState = TelegramState.OK;
143     }
144
145     /**
146      * Parses data. If a complete message is received the message will be passed to the telegramListener.
147      *
148      * @param data byte data to parse
149      * @param length number of bytes to parse
150      */
151     @Override
152     public void parse(byte[] data, int length) {
153         if (lenientMode || logger.isTraceEnabled()) {
154             String rawBlock = new String(data, 0, length, StandardCharsets.UTF_8);
155
156             if (lenientMode) {
157                 rawData.append(rawBlock);
158             }
159             if (logger.isTraceEnabled()) {
160                 logger.trace("Raw data: {}, Parser state entering parseData: {}", rawBlock, state);
161             }
162         }
163         for (int i = 0; i < length; i++) {
164             char c = (char) data[i];
165
166             switch (state) {
167                 case WAIT_FOR_START:
168                     if (c == '/') {
169                         setState(State.HEADER);
170                     }
171                     break;
172                 case HEADER:
173                     if (c == '\r') {
174                         setState(State.CRLF);
175                     }
176                     break;
177                 case CRLF:
178                     if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
179                         // do nothing
180                     } else if (Character.isDigit(c)) {
181                         setState(State.DATA_OBIS_ID);
182                     } else {
183                         handleUnexpectedCharacter(c);
184
185                         setState(State.WAIT_FOR_START);
186                     }
187                     break;
188                 case DATA_OBIS_ID:
189                     if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
190                         // ignore
191                     } else if (Character.isDigit(c) || c == ':' || c == '-' || c == '.' || c == '*') { // NOPMD
192                         // do nothing
193                     } else if (c == '(') {
194                         setState(State.DATA_OBIS_VALUE);
195                     } else if (c == '!') {
196                         handleUnexpectedCharacter(c);
197                         if (lenientMode) {
198                             // Clear current Obis Data (Keep already received data)
199                             clearObisData();
200                             setState(State.CRC_VALUE);
201                         } else {
202                             setState(State.WAIT_FOR_START);
203                         }
204                     } else {
205                         handleUnexpectedCharacter(c);
206
207                         if (lenientMode) {
208                             clearObisData();
209                             setState(State.DATA_OBIS_ID);
210                         } else {
211                             setState(State.WAIT_FOR_START);
212                         }
213                     }
214                     break;
215                 case DATA_OBIS_VALUE:
216                     if (c == ')') {
217                         setState(State.DATA_OBIS_VALUE_END);
218                     }
219                     break;
220                 case DATA_OBIS_VALUE_END:
221                     if (Character.isWhitespace(c)) { // NOPMD EmptyIfStmt
222                         // ignore
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);
229                     } else {
230                         handleUnexpectedCharacter(c);
231
232                         if (!lenientMode) {
233                             setState(State.WAIT_FOR_START);
234                         } // Other wise try to recover in lenient mode
235                     }
236                     break;
237
238                 case CRC_VALUE:
239                     /*
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
243                      */
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();
251
252                                 if (logger.isDebugEnabled()) {
253                                     logger.trace("received CRC value: {}, calculated CRC value: 0x{}", crcValue,
254                                             String.format("%04X", calculatedCRC));
255                                 }
256                                 if (crcP1Telegram != calculatedCRC) {
257                                     logger.trace("CRC value does not match, p1 Telegram failed");
258
259                                     telegramState = TelegramState.CRC_ERROR;
260                                 }
261                             } else {
262                                 telegramState = TelegramState.CRC_ERROR;
263                             }
264                         }
265                         telegramListener.telegramReceived(constructTelegram());
266                         reset();
267                         if (c == '/') {
268                             /*
269                              * Immediately proceed to the next state (robust implementation for meter that do not follow
270                              * the specification
271                              */
272                             setState(State.HEADER);
273                         }
274                     }
275                     break;
276             }
277
278             handleCharacter(c);
279         }
280         logger.trace("State after parsing: {}", state);
281     }
282
283     private P1Telegram constructTelegram() {
284         final List<CosemObject> cosemObjectsCopy = new ArrayList<>(cosemObjects);
285
286         if (lenientMode) {
287             return new P1Telegram(cosemObjectsCopy, telegramState, rawData.toString(),
288                     unknownCosemObjects.isEmpty() ? Collections.emptyList() : new ArrayList<>(unknownCosemObjects));
289         } else {
290             return new P1Telegram(cosemObjectsCopy, telegramState);
291         }
292     }
293
294     @Override
295     public void reset() {
296         setState(State.WAIT_FOR_START);
297     }
298
299     /**
300      * Handles an unexpected character. The character will be logged and the current telegram is marked corrupted
301      *
302      * @param c the unexpected character
303      */
304     private void handleUnexpectedCharacter(char c) {
305         logger.debug("Unexpected character '{}' in state: {}. This P1 telegram is marked as failed", c, state);
306
307         telegramState = TelegramState.DATA_CORRUPTION;
308     }
309
310     /**
311      * Stores a single character
312      *
313      * @param c the character to process
314      */
315     private void handleCharacter(char c) {
316         switch (state) {
317             case WAIT_FOR_START:
318                 // ignore the data
319                 break;
320             case HEADER:
321                 crc.processByte((byte) c);
322                 break;
323             case CRLF:
324                 crc.processByte((byte) c);
325                 break;
326             case DATA_OBIS_ID:
327                 obisId.append(c);
328                 crc.processByte((byte) c);
329                 break;
330             case DATA_OBIS_VALUE:
331                 obisValue.append(c);
332                 crc.processByte((byte) c);
333                 break;
334             case DATA_OBIS_VALUE_END:
335                 obisValue.append(c);
336                 crc.processByte((byte) c);
337                 break;
338             case CRC_VALUE:
339                 if (c == '!') {
340                     crc.processByte((byte) c);
341                 } else {
342                     crcValue.append(c);
343                 }
344                 // CRC data is not part of received data
345                 break;
346             default:
347                 break;
348         }
349     }
350
351     /**
352      * Clears all internal state
353      */
354     private void clearInternalData() {
355         obisId.setLength(0);
356         obisValue.setLength(0);
357         rawData.setLength(0);
358         crcValue.setLength(0);
359         crc.initialize();
360         cosemObjects.clear();
361         unknownCosemObjects.clear();
362     }
363
364     /**
365      * Clears all the current OBIS data. I.e.
366      * - current OBIS identifier
367      * - current OBIS value
368      */
369     private void clearObisData() {
370         obisId.setLength(0);
371         obisValue.setLength(0);
372     }
373
374     /**
375      * Store the current CosemObject in the list of received cosem Objects
376      */
377     private void storeCurrentCosemObject() {
378         String obisIdString = obisId.toString();
379
380         if (!obisIdString.isEmpty()) {
381             final String obisValueString = obisValue.toString();
382             CosemObject cosemObject = factory.getCosemObject(obisIdString, obisValueString);
383
384             if (cosemObject == null) {
385                 if (lenientMode) {
386                     unknownCosemObjects.add(new SimpleEntry<>(obisIdString, obisValueString));
387                 }
388             } else {
389                 logger.trace("Adding {} to list of Cosem Objects", cosemObject);
390                 cosemObjects.add(cosemObject);
391             }
392         }
393         clearObisData();
394     }
395
396     /**
397      * @param newState the new state to set
398      */
399     private void setState(State newState) {
400         synchronized (state) {
401             switch (newState) {
402                 case HEADER:
403                     // Clear CRC data and mark current telegram as OK
404                     crc.initialize();
405                     break;
406                 case WAIT_FOR_START:
407                     // Clears internal state data and mark current telegram as OK
408                     clearInternalData();
409                     telegramState = TelegramState.OK;
410                     break;
411                 case DATA_OBIS_ID:
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();
415                     }
416                     break;
417                 case CRC_VALUE:
418                     storeCurrentCosemObject();
419                     break;
420                 default:
421                     break;
422             }
423             state = newState;
424         }
425     }
426
427     @Override
428     public void setLenientMode(boolean lenientMode) {
429         this.lenientMode = lenientMode;
430     }
431 }