commit
						6f3c74ce7c
					
				| @ -1,6 +1,8 @@ | |||||||
| import argparse | import argparse | ||||||
| from dsmr_parser.serial import SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, SerialReader | import asyncio | ||||||
| from dsmr_parser import telegram_specifications | import logging | ||||||
|  | 
 | ||||||
|  | from .protocol import create_dsmr_reader | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def console(): | def console(): | ||||||
| @ -11,22 +13,26 @@ def console(): | |||||||
|                         help='port to read DSMR data from') |                         help='port to read DSMR data from') | ||||||
|     parser.add_argument('--version', default='2.2', choices=['2.2', '4'], |     parser.add_argument('--version', default='2.2', choices=['2.2', '4'], | ||||||
|                         help='DSMR version (2.2, 4)') |                         help='DSMR version (2.2, 4)') | ||||||
|  |     parser.add_argument('--verbose', '-v', action='count') | ||||||
| 
 | 
 | ||||||
|     args = parser.parse_args() |     args = parser.parse_args() | ||||||
| 
 | 
 | ||||||
|     settings = { |     if args.verbose: | ||||||
|         '2.2': (SERIAL_SETTINGS_V2_2, telegram_specifications.V2_2), |         level = logging.DEBUG | ||||||
|         '4': (SERIAL_SETTINGS_V4, telegram_specifications.V4), |     else: | ||||||
|     } |         level = logging.ERROR | ||||||
|  |     logging.basicConfig(level=level) | ||||||
| 
 | 
 | ||||||
|     serial_reader = SerialReader( |     loop = asyncio.get_event_loop() | ||||||
|         device=args.device, |  | ||||||
|         serial_settings=settings[args.version][0], |  | ||||||
|         telegram_specification=settings[args.version][1], |  | ||||||
|     ) |  | ||||||
| 
 | 
 | ||||||
|     for telegram in serial_reader.read(): |     def print_callback(telegram): | ||||||
|  |         """Callback that prints telegram values.""" | ||||||
|         for obiref, obj in telegram.items(): |         for obiref, obj in telegram.items(): | ||||||
|             if obj: |             if obj: | ||||||
|                 print(obj.value, obj.unit) |                 print(obj.value, obj.unit) | ||||||
|         print() |         print() | ||||||
|  | 
 | ||||||
|  |     conn = create_dsmr_reader(args.device, args.version, print_callback, loop=loop) | ||||||
|  | 
 | ||||||
|  |     loop.create_task(conn) | ||||||
|  |     loop.run_forever() | ||||||
|  | |||||||
							
								
								
									
										97
									
								
								dsmr_parser/protocol.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								dsmr_parser/protocol.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | |||||||
|  | """Asyncio protocol implementation for handling telegrams.""" | ||||||
|  | 
 | ||||||
|  | import asyncio | ||||||
|  | import logging | ||||||
|  | from functools import partial | ||||||
|  | 
 | ||||||
|  | from serial_asyncio import create_serial_connection | ||||||
|  | 
 | ||||||
|  | from . import telegram_specifications | ||||||
|  | from .exceptions import ParseError | ||||||
|  | from .parsers import TelegramParser, TelegramParserV2_2 | ||||||
|  | from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, | ||||||
|  |                      is_end_of_telegram, is_start_of_telegram) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): | ||||||
|  |     """Creates a DSMR asyncio protocol coroutine.""" | ||||||
|  | 
 | ||||||
|  |     if dsmr_version == '2.2': | ||||||
|  |         specifications = telegram_specifications.V2_2 | ||||||
|  |         telegram_parser = TelegramParserV2_2 | ||||||
|  |         serial_settings = SERIAL_SETTINGS_V2_2 | ||||||
|  |     elif dsmr_version == '4': | ||||||
|  |         specifications = telegram_specifications.V4 | ||||||
|  |         telegram_parser = TelegramParser | ||||||
|  |         serial_settings = SERIAL_SETTINGS_V4 | ||||||
|  | 
 | ||||||
|  |     serial_settings['url'] = port | ||||||
|  | 
 | ||||||
|  |     protocol = partial(DSMRProtocol, loop, telegram_parser(specifications), | ||||||
|  |                        telegram_callback=telegram_callback) | ||||||
|  | 
 | ||||||
|  |     conn = create_serial_connection(loop, protocol, **serial_settings) | ||||||
|  | 
 | ||||||
|  |     return conn | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DSMRProtocol(asyncio.Protocol): | ||||||
|  |     """Assemble and handle incoming data into complete DSM telegrams.""" | ||||||
|  | 
 | ||||||
|  |     transport = None | ||||||
|  |     telegram_callback = None | ||||||
|  | 
 | ||||||
|  |     def __init__(self, loop, telegram_parser, telegram_callback=None): | ||||||
|  |         """Initialize class.""" | ||||||
|  |         self.loop = loop | ||||||
|  |         self.log = logging.getLogger(__name__) | ||||||
|  |         self.telegram_parser = telegram_parser | ||||||
|  |         # callback to call on complete telegram | ||||||
|  |         self.telegram_callback = telegram_callback | ||||||
|  |         # buffer to keep incoming telegram lines | ||||||
|  |         self.telegram = [] | ||||||
|  |         # buffer to keep incomplete incoming data | ||||||
|  |         self.buffer = '' | ||||||
|  | 
 | ||||||
|  |     def connection_made(self, transport): | ||||||
|  |         """Just logging for now.""" | ||||||
|  |         self.transport = transport | ||||||
|  |         self.log.debug('connected') | ||||||
|  | 
 | ||||||
|  |     def data_received(self, data): | ||||||
|  |         """Add incoming data to buffer.""" | ||||||
|  |         data = data.decode() | ||||||
|  |         self.log.debug('received data: %s', data.strip()) | ||||||
|  |         self.buffer += data | ||||||
|  |         self.handle_lines() | ||||||
|  | 
 | ||||||
|  |     def handle_lines(self): | ||||||
|  |         """Assemble incoming data into single lines.""" | ||||||
|  |         while "\r\n" in self.buffer: | ||||||
|  |             line, self.buffer = self.buffer.split("\r\n", 1) | ||||||
|  |             self.log.debug('got line: %s', line) | ||||||
|  | 
 | ||||||
|  |             # Telegrams need to be complete because the values belong to a | ||||||
|  |             # particular reading and can also be related to eachother. | ||||||
|  |             if not self.telegram and not is_start_of_telegram(line): | ||||||
|  |                 continue | ||||||
|  | 
 | ||||||
|  |             self.telegram.append(line) | ||||||
|  |             if is_end_of_telegram(line): | ||||||
|  |                 try: | ||||||
|  |                     parsed_telegram = self.telegram_parser.parse(self.telegram) | ||||||
|  |                     self.handle_telegram(parsed_telegram) | ||||||
|  |                 except ParseError: | ||||||
|  |                     self.log.exception("failed to parse telegram") | ||||||
|  |                 self.telegram = [] | ||||||
|  | 
 | ||||||
|  |     def connection_lost(self, exc): | ||||||
|  |         """Stop when connection is lost.""" | ||||||
|  |         self.log.error('disconnected') | ||||||
|  | 
 | ||||||
|  |     def handle_telegram(self, telegram): | ||||||
|  |         """Send off parsed telegram to handling callback.""" | ||||||
|  |         self.log.debug('got telegram: %s', telegram) | ||||||
|  | 
 | ||||||
|  |         if self.telegram_callback: | ||||||
|  |             self.telegram_callback(telegram) | ||||||
							
								
								
									
										39
									
								
								test/test_protocol.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								test/test_protocol.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | """Test DSMR serial protocol.""" | ||||||
|  | 
 | ||||||
|  | from unittest.mock import Mock | ||||||
|  | 
 | ||||||
|  | import pytest | ||||||
|  | from dsmr_parser import obis_references as obis | ||||||
|  | from dsmr_parser import telegram_specifications | ||||||
|  | from dsmr_parser.parsers import TelegramParserV2_2 | ||||||
|  | from dsmr_parser.protocol import DSMRProtocol | ||||||
|  | 
 | ||||||
|  | from .test_parse_v2_2 import TELEGRAM_V2_2 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.fixture | ||||||
|  | def protocol(): | ||||||
|  |     """DSMRprotocol instance with mocked telegram_callback.""" | ||||||
|  | 
 | ||||||
|  |     parser = TelegramParserV2_2 | ||||||
|  |     specification = telegram_specifications.V2_2 | ||||||
|  | 
 | ||||||
|  |     telegram_parser = parser(specification) | ||||||
|  |     return DSMRProtocol(None, telegram_parser, | ||||||
|  |                         telegram_callback=Mock()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_complete_packet(protocol): | ||||||
|  |     """Protocol should assemble incoming lines into complete packet.""" | ||||||
|  | 
 | ||||||
|  |     for line in TELEGRAM_V2_2: | ||||||
|  |         protocol.data_received(bytes(line + '\r\n', 'ascii')) | ||||||
|  | 
 | ||||||
|  |     telegram = protocol.telegram_callback.call_args_list[0][0][0] | ||||||
|  |     assert isinstance(telegram, dict) | ||||||
|  | 
 | ||||||
|  |     assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01 | ||||||
|  |     assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' | ||||||
|  | 
 | ||||||
|  |     assert float(telegram[obis.GAS_METER_READING].value) == 1.001 | ||||||
|  |     assert telegram[obis.GAS_METER_READING].unit == 'm3' | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user