finishing implementation of TelegramBuffer
This commit is contained in:
		
							parent
							
								
									f10032f701
								
							
						
					
					
						commit
						d990a316ad
					
				| @ -1,5 +1,6 @@ | ||||
| import asyncio | ||||
| import logging | ||||
| import re | ||||
| import serial | ||||
| import serial_asyncio | ||||
| 
 | ||||
| @ -136,3 +137,50 @@ class AsyncSerialReader(SerialReader): | ||||
|                     logger.warning('Failed to parse telegram: %s', e) | ||||
| 
 | ||||
|                 telegram = '' | ||||
| 
 | ||||
| 
 | ||||
| class TelegramBuffer(object): | ||||
| 
 | ||||
|     def __init__(self, callback): | ||||
|         self._callback = callback | ||||
|         self._buffer = '' | ||||
| 
 | ||||
|     def append(self, data): | ||||
|         """ | ||||
|         Add telegram data to buffer. The callback is called with a full telegram | ||||
|         when data is complete. | ||||
|         :param str data: chars or lines of telegram data | ||||
|         :return: | ||||
|         """ | ||||
|         self._buffer += data | ||||
| 
 | ||||
|         for telegram in self.find_telegrams(self._buffer): | ||||
|             self._callback(telegram) | ||||
|             self._remove(telegram) | ||||
| 
 | ||||
|     def _remove(self, telegram): | ||||
|         """ | ||||
|         Remove telegram from buffer and incomplete data preceding it. This | ||||
|         is easier than validating the data before adding it to the buffer. | ||||
|         :param str telegram: | ||||
|         :return: | ||||
|         """ | ||||
|         # Remove data leading up to the telegram and the telegram itself. | ||||
|         index = self._buffer.index(telegram) + len(telegram) | ||||
| 
 | ||||
|         self._buffer = self._buffer[index:] | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def find_telegrams(buffer): | ||||
|         """ | ||||
|         Find complete telegrams from buffer from  start ('/') till ending | ||||
|         checksum ('!AB12\r\n'). | ||||
|         :rtype: list | ||||
|         """ | ||||
|         # - Match all characters after start of telegram except for the start | ||||
|         # itself again '^\/]+', which eliminates incomplete preceding telegrams. | ||||
|         # - Do non greedy match using '?' so start is matched up to the first | ||||
|         # checksum that's found. | ||||
|         # - The checksum is optional '{0,4}' because not all telegram versions | ||||
|         # support it. | ||||
|         return re.findall(r'\/[^\/]+?\![A-F0-9]{0,4}\r\n', buffer, re.DOTALL) | ||||
|  | ||||
							
								
								
									
										62
									
								
								test/example_telegrams.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								test/example_telegrams.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| TELEGRAM_V2_2 = ( | ||||
|     '/ISk5\2MT382-1004\r\n' | ||||
|     '\r\n' | ||||
|     '0-0:96.1.1(00000000000000)\r\n' | ||||
|     '1-0:1.8.1(00001.001*kWh)\r\n' | ||||
|     '1-0:1.8.2(00001.001*kWh)\r\n' | ||||
|     '1-0:2.8.1(00001.001*kWh)\r\n' | ||||
|     '1-0:2.8.2(00001.001*kWh)\r\n' | ||||
|     '0-0:96.14.0(0001)\r\n' | ||||
|     '1-0:1.7.0(0001.01*kW)\r\n' | ||||
|     '1-0:2.7.0(0000.00*kW)\r\n' | ||||
|     '0-0:17.0.0(0999.00*kW)\r\n' | ||||
|     '0-0:96.3.10(1)\r\n' | ||||
|     '0-0:96.13.1()\r\n' | ||||
|     '0-0:96.13.0()\r\n' | ||||
|     '0-1:24.1.0(3)\r\n' | ||||
|     '0-1:96.1.0(000000000000)\r\n' | ||||
|     '0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n' | ||||
|     '(00001.001)\r\n' | ||||
|     '0-1:24.4.0(1)\r\n' | ||||
|     '!\r\n' | ||||
| ) | ||||
| 
 | ||||
| TELEGRAM_V4_2 = ( | ||||
|     '/KFM5KAIFA-METER\r\n' | ||||
|     '\r\n' | ||||
|     '1-3:0.2.8(42)\r\n' | ||||
|     '0-0:1.0.0(161113205757W)\r\n' | ||||
|     '0-0:96.1.1(3960221976967177082151037881335713)\r\n' | ||||
|     '1-0:1.8.1(001581.123*kWh)\r\n' | ||||
|     '1-0:1.8.2(001435.706*kWh)\r\n' | ||||
|     '1-0:2.8.1(000000.000*kWh)\r\n' | ||||
|     '1-0:2.8.2(000000.000*kWh)\r\n' | ||||
|     '0-0:96.14.0(0002)\r\n' | ||||
|     '1-0:1.7.0(02.027*kW)\r\n' | ||||
|     '1-0:2.7.0(00.000*kW)\r\n' | ||||
|     '0-0:96.7.21(00015)\r\n' | ||||
|     '0-0:96.7.9(00007)\r\n' | ||||
|     '1-0:99.97.0(3)(0-0:96.7.19)(000104180320W)(0000237126*s)(000101000001W)' | ||||
|     '(2147583646*s)(000102000003W)(2317482647*s)\r\n' | ||||
|     '1-0:32.32.0(00000)\r\n' | ||||
|     '1-0:52.32.0(00000)\r\n' | ||||
|     '1-0:72.32.0(00000)\r\n' | ||||
|     '1-0:32.36.0(00000)\r\n' | ||||
|     '1-0:52.36.0(00000)\r\n' | ||||
|     '1-0:72.36.0(00000)\r\n' | ||||
|     '0-0:96.13.1()\r\n' | ||||
|     '0-0:96.13.0()\r\n' | ||||
|     '1-0:31.7.0(000*A)\r\n' | ||||
|     '1-0:51.7.0(006*A)\r\n' | ||||
|     '1-0:71.7.0(002*A)\r\n' | ||||
|     '1-0:21.7.0(00.170*kW)\r\n' | ||||
|     '1-0:22.7.0(00.000*kW)\r\n' | ||||
|     '1-0:41.7.0(01.247*kW)\r\n' | ||||
|     '1-0:42.7.0(00.000*kW)\r\n' | ||||
|     '1-0:61.7.0(00.209*kW)\r\n' | ||||
|     '1-0:62.7.0(00.000*kW)\r\n' | ||||
|     '0-1:24.1.0(003)\r\n' | ||||
|     '0-1:96.1.0(4819243993373755377509728609491464)\r\n' | ||||
|     '0-1:24.2.1(161129200000W)(00981.443*m3)\r\n' | ||||
|     '!6796\r\n' | ||||
| ) | ||||
							
								
								
									
										87
									
								
								test/telegram_buffer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								test/telegram_buffer.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| from unittest import mock, TestCase | ||||
| from unittest.mock import call | ||||
| 
 | ||||
| from dsmr_parser.serial import TelegramBuffer | ||||
| from test.example_telegrams import TELEGRAM_V2_2, TELEGRAM_V4_2 | ||||
| 
 | ||||
| 
 | ||||
| class TelegramBufferTest(TestCase): | ||||
| 
 | ||||
|     def setUp(self): | ||||
|         self.callback = mock.MagicMock() | ||||
|         self.telegram_buffer = TelegramBuffer(self.callback) | ||||
| 
 | ||||
|     def test_v22_telegram(self): | ||||
|         self.telegram_buffer.append(TELEGRAM_V2_2) | ||||
| 
 | ||||
|         self.callback.assert_called_once_with(TELEGRAM_V2_2) | ||||
|         self.assertEqual(self.telegram_buffer._buffer, '') | ||||
| 
 | ||||
|     def test_v42_telegram(self): | ||||
|         self.telegram_buffer.append(TELEGRAM_V4_2) | ||||
| 
 | ||||
|         self.callback.assert_called_once_with(TELEGRAM_V4_2) | ||||
|         self.assertEqual(self.telegram_buffer._buffer, '') | ||||
| 
 | ||||
|     def test_multiple_mixed_telegrams(self): | ||||
|         self.telegram_buffer.append( | ||||
|             ''.join((TELEGRAM_V2_2, TELEGRAM_V4_2, TELEGRAM_V2_2)) | ||||
|         ) | ||||
| 
 | ||||
|         self.callback.assert_has_calls([ | ||||
|             call(TELEGRAM_V2_2), | ||||
|             call(TELEGRAM_V4_2), | ||||
|             call(TELEGRAM_V2_2), | ||||
|         ]) | ||||
|         self.assertEqual(self.telegram_buffer._buffer, '') | ||||
| 
 | ||||
|     def test_v42_telegram_preceded_with_unclosed_telegram(self): | ||||
|         # There are unclosed telegrams at the start of the buffer. | ||||
|         incomplete_telegram = TELEGRAM_V4_2[:-1] | ||||
| 
 | ||||
|         self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2) | ||||
| 
 | ||||
|         self.callback.assert_called_once_with(TELEGRAM_V4_2) | ||||
|         self.assertEqual(self.telegram_buffer._buffer, '') | ||||
| 
 | ||||
|     def test_v42_telegram_preceded_with_unopened_telegram(self): | ||||
|         # There is unopened telegrams at the start of the buffer indicating that | ||||
|         # the buffer was being filled while the telegram was outputted halfway. | ||||
|         incomplete_telegram = TELEGRAM_V4_2[1:] | ||||
| 
 | ||||
|         self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2) | ||||
| 
 | ||||
|         self.callback.assert_called_once_with(TELEGRAM_V4_2) | ||||
|         self.assertEqual(self.telegram_buffer._buffer, '') | ||||
| 
 | ||||
|     def test_v42_telegram_trailed_by_unclosed_telegram(self): | ||||
|         incomplete_telegram = TELEGRAM_V4_2[:-1] | ||||
| 
 | ||||
|         self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram) | ||||
| 
 | ||||
|         self.callback.assert_called_once_with(TELEGRAM_V4_2) | ||||
|         self.assertEqual(self.telegram_buffer._buffer, incomplete_telegram) | ||||
| 
 | ||||
|     def test_v42_telegram_trailed_by_unopened_telegram(self): | ||||
|         incomplete_telegram = TELEGRAM_V4_2[1:] | ||||
| 
 | ||||
|         self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram) | ||||
| 
 | ||||
|         self.callback.assert_called_once_with(TELEGRAM_V4_2) | ||||
|         self.assertEqual(self.telegram_buffer._buffer, incomplete_telegram) | ||||
| 
 | ||||
|     def test_v42_telegram_adding_line_by_line(self): | ||||
| 
 | ||||
|         for line in TELEGRAM_V4_2.splitlines(keepends=True): | ||||
|             self.telegram_buffer.append(line) | ||||
| 
 | ||||
|         self.callback.assert_called_once_with(TELEGRAM_V4_2) | ||||
|         self.assertEqual(self.telegram_buffer._buffer, '') | ||||
| 
 | ||||
|     def test_v42_telegram_adding_char_by_char(self): | ||||
| 
 | ||||
|         for char in TELEGRAM_V4_2: | ||||
|             self.telegram_buffer.append(char) | ||||
| 
 | ||||
|         self.callback.assert_called_once_with(TELEGRAM_V4_2) | ||||
|         self.assertEqual(self.telegram_buffer._buffer, '') | ||||
| @ -1,32 +1,10 @@ | ||||
| import unittest | ||||
| 
 | ||||
| from .example_telegrams import TELEGRAM_V2_2 | ||||
| from dsmr_parser.parsers import TelegramParserV2_2 | ||||
| from dsmr_parser import telegram_specifications | ||||
| from dsmr_parser import obis_references as obis | ||||
| 
 | ||||
| TELEGRAM_V2_2 = ( | ||||
|     '/ISk5\2MT382-1004\r\n' | ||||
|     '\r\n' | ||||
|     '0-0:96.1.1(00000000000000)\r\n' | ||||
|     '1-0:1.8.1(00001.001*kWh)\r\n' | ||||
|     '1-0:1.8.2(00001.001*kWh)\r\n' | ||||
|     '1-0:2.8.1(00001.001*kWh)\r\n' | ||||
|     '1-0:2.8.2(00001.001*kWh)\r\n' | ||||
|     '0-0:96.14.0(0001)\r\n' | ||||
|     '1-0:1.7.0(0001.01*kW)\r\n' | ||||
|     '1-0:2.7.0(0000.00*kW)\r\n' | ||||
|     '0-0:17.0.0(0999.00*kW)\r\n' | ||||
|     '0-0:96.3.10(1)\r\n' | ||||
|     '0-0:96.13.1()\r\n' | ||||
|     '0-0:96.13.0()\r\n' | ||||
|     '0-1:24.1.0(3)\r\n' | ||||
|     '0-1:96.1.0(000000000000)\r\n' | ||||
|     '0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n' | ||||
|     '(00001.001)\r\n' | ||||
|     '0-1:24.4.0(1)\r\n' | ||||
|     '!\r\n' | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| class TelegramParserV2_2Test(unittest.TestCase): | ||||
|     """ Test parsing of a DSMR v2.2 telegram. """ | ||||
|  | ||||
| @ -4,52 +4,13 @@ import unittest | ||||
| 
 | ||||
| import pytz | ||||
| 
 | ||||
| from .example_telegrams import TELEGRAM_V4_2 | ||||
| from dsmr_parser import obis_references as obis | ||||
| from dsmr_parser import telegram_specifications | ||||
| from dsmr_parser.exceptions import InvalidChecksumError, ParseError | ||||
| from dsmr_parser.objects import CosemObject, MBusObject | ||||
| from dsmr_parser.parsers import TelegramParser, TelegramParserV4 | ||||
| 
 | ||||
| TELEGRAM_V4_2 = ( | ||||
|     '/KFM5KAIFA-METER\r\n' | ||||
|     '\r\n' | ||||
|     '1-3:0.2.8(42)\r\n' | ||||
|     '0-0:1.0.0(161113205757W)\r\n' | ||||
|     '0-0:96.1.1(3960221976967177082151037881335713)\r\n' | ||||
|     '1-0:1.8.1(001581.123*kWh)\r\n' | ||||
|     '1-0:1.8.2(001435.706*kWh)\r\n' | ||||
|     '1-0:2.8.1(000000.000*kWh)\r\n' | ||||
|     '1-0:2.8.2(000000.000*kWh)\r\n' | ||||
|     '0-0:96.14.0(0002)\r\n' | ||||
|     '1-0:1.7.0(02.027*kW)\r\n' | ||||
|     '1-0:2.7.0(00.000*kW)\r\n' | ||||
|     '0-0:96.7.21(00015)\r\n' | ||||
|     '0-0:96.7.9(00007)\r\n' | ||||
|     '1-0:99.97.0(3)(0-0:96.7.19)(000104180320W)(0000237126*s)(000101000001W)' | ||||
|     '(2147583646*s)(000102000003W)(2317482647*s)\r\n' | ||||
|     '1-0:32.32.0(00000)\r\n' | ||||
|     '1-0:52.32.0(00000)\r\n' | ||||
|     '1-0:72.32.0(00000)\r\n' | ||||
|     '1-0:32.36.0(00000)\r\n' | ||||
|     '1-0:52.36.0(00000)\r\n' | ||||
|     '1-0:72.36.0(00000)\r\n' | ||||
|     '0-0:96.13.1()\r\n' | ||||
|     '0-0:96.13.0()\r\n' | ||||
|     '1-0:31.7.0(000*A)\r\n' | ||||
|     '1-0:51.7.0(006*A)\r\n' | ||||
|     '1-0:71.7.0(002*A)\r\n' | ||||
|     '1-0:21.7.0(00.170*kW)\r\n' | ||||
|     '1-0:22.7.0(00.000*kW)\r\n' | ||||
|     '1-0:41.7.0(01.247*kW)\r\n' | ||||
|     '1-0:42.7.0(00.000*kW)\r\n' | ||||
|     '1-0:61.7.0(00.209*kW)\r\n' | ||||
|     '1-0:62.7.0(00.000*kW)\r\n' | ||||
|     '0-1:24.1.0(003)\r\n' | ||||
|     '0-1:96.1.0(4819243993373755377509728609491464)\r\n' | ||||
|     '0-1:24.2.1(161129200000W)(00981.443*m3)\r\n' | ||||
|     '!6796\r\n' | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| class TelegramParserV4_2Test(unittest.TestCase): | ||||
|     """ Test parsing of a DSMR v4.2 telegram. """ | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user