From e12aff5c0d2dd7647313788aaa77ba3053b81a64 Mon Sep 17 00:00:00 2001 From: bremme Date: Sun, 27 Dec 2020 18:57:21 +0100 Subject: [PATCH 01/13] Add SocketReader for reading ipv4 tcp sockets --- dsmr_parser/clients/__init__.py | 1 + dsmr_parser/clients/socket_.py | 91 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 dsmr_parser/clients/socket_.py diff --git a/dsmr_parser/clients/__init__.py b/dsmr_parser/clients/__init__.py index 7323ecd..9563399 100644 --- a/dsmr_parser/clients/__init__.py +++ b/dsmr_parser/clients/__init__.py @@ -1,5 +1,6 @@ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader +from dsmr_parser.clients.socket_ import SocketReader from dsmr_parser.clients.protocol import create_dsmr_protocol, \ create_dsmr_reader, create_tcp_dsmr_reader diff --git a/dsmr_parser/clients/socket_.py b/dsmr_parser/clients/socket_.py new file mode 100644 index 0000000..7c13f02 --- /dev/null +++ b/dsmr_parser/clients/socket_.py @@ -0,0 +1,91 @@ +import logging +import socket + +from dsmr_parser.clients.telegram_buffer import TelegramBuffer +from dsmr_parser.exceptions import ParseError, InvalidChecksumError +from dsmr_parser.parsers import TelegramParser +from dsmr_parser.objects import Telegram + + +logger = logging.getLogger(__name__) + + +class SocketReader(object): + + BUFFER_SIZE = 256 + + def __init__(self, host, port, telegram_specification): + self.host = host + self.port = port + + self.telegram_parser = TelegramParser(telegram_specification) + self.telegram_buffer = TelegramBuffer() + self.telegram_specification = telegram_specification + + + def read(self): + """ + Read complete DSMR telegram's from remote interface and parse it + into CosemObject's and MbusObject's + + :rtype: generator + """ + buffer = b"" + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle: + + socket_handle.connect((self.host, self.port)) + + while True: + buffer += socket_handle.recv(self.BUFFER_SIZE) + + lines = buffer.splitlines(keepends=True) + + if len(lines) == 0: + continue + + for data in lines: + self.telegram_buffer.append(data.decode('ascii')) + + for telegram in self.telegram_buffer.get_all(): + try: + yield self.telegram_parser.parse(telegram) + except InvalidChecksumError as e: + logger.warning(str(e)) + except ParseError as e: + logger.error('Failed to parse telegram: %s', e) + + buffer = b"" + + def read_as_object(self): + """ + Read complete DSMR telegram's from remote and return a Telegram object. + + :rtype: generator + """ + buffer = b"" + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle: + + socket_handle.connect((self.host, self.port)) + + while True: + buffer += socket_handle.recv(self.BUFFER_SIZE) + + lines = buffer.splitlines(keepends=True) + + if len(lines) == 0: + continue + + for data in lines: + self.telegram_buffer.append(data.decode('ascii')) + + for telegram in self.telegram_buffer.get_all(): + try: + yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + except InvalidChecksumError as e: + logger.warning(str(e)) + except ParseError as e: + logger.error('Failed to parse telegram: %s', e) + + buffer = b"" \ No newline at end of file From 97786576cf4dc2aaee5d10c737f44eb30c6923ae Mon Sep 17 00:00:00 2001 From: bremme Date: Sun, 27 Dec 2020 18:58:14 +0100 Subject: [PATCH 02/13] Add SocketReader documentation --- README.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.rst b/README.rst index 03de485..b2d932f 100644 --- a/README.rst +++ b/README.rst @@ -43,6 +43,25 @@ process because the code is blocking (not asynchronous): To be documented. +**Socket client** + +Read a remote serial port (for example using ser2net) and work with the parsed telegrams. +It should be run in a separate process because the code is blocking (not asynchronous): + +.. code-block:: python + + from dsmr_parser import telegram_specifications + from dsmr_parser.clients import SocketReader + + socket_reader = SocketReader( + host='127.0.0.1', + port=2001, + telegram_specification=telegram_specifications.V4 + ) + + for telegram in socket_reader.read(): + print(telegram) # see 'Telegram object' docs below + Parsing module usage -------------------- From 28e3c51f0d5cfea4e8f5f4a2c5f9dbcf9bcc7e89 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 21 Nov 2021 16:08:47 +0100 Subject: [PATCH 03/13] fix pylama errors on PR92 + editorial fix --- dsmr_parser/clients/protocol.py | 2 +- test/example_telegrams.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index f46aea5..52f8383 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -113,7 +113,7 @@ class DSMRProtocol(asyncio.Protocol): self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): - # ensure actual telegram is ascii (7-bit) only (IEC 646 required in section 5.4 of IEC 62056-21) + # ensure actual telegram is ascii (7-bit) only (ISO 646:1991 IRV required in section 5.5 of IEC 62056-21) telegram = telegram.encode("latin1").decode("ascii") self.handle_telegram(telegram) diff --git a/test/example_telegrams.py b/test/example_telegrams.py index c59280c..1ccb8ce 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -136,7 +136,7 @@ TELEGRAM_V5 = ( # # last two lines are added by the COM-1 Ethernet Gateway -TELEGRAM_ESY5Q3DB1024_V304 = ( # Easymeter an Hauptstromzähler +TELEGRAM_ESY5Q3DB1024_V304 = ( '/ESY5Q3DB1024 V3.04\r\n' '\r\n' '1-0:0.0.0*255(0272031312565)\r\n' @@ -150,10 +150,11 @@ TELEGRAM_ESY5Q3DB1024_V304 = ( # Easymeter an Hauptstromzähler '0-0:96.1.255*255(1ESY1313002565)\r\n' '!\r\n' ' 25803103\r\n' - '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\r\n' + '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + '\xff\xff\xff\xff\xff\r\n' ) -TELEGRAM_ESY5Q3DA1004_V304 = ( # Easymeter an Wärmepumpe +TELEGRAM_ESY5Q3DA1004_V304 = ( '/ESY5Q3DA1004 V3.04\r\n' '\r\n' '1-0:0.0.0*255(1336001560)\r\n' From 83ef354c1285def5beecbd077670971e74b388e3 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sat, 27 Nov 2021 19:55:58 +0100 Subject: [PATCH 04/13] resolve name clash as reported in issue #95 --- dsmr_parser/clients/socket_.py | 3 +-- dsmr_parser/obis_name_mapping.py | 4 ---- dsmr_parser/obis_references.py | 12 +++++------- dsmr_parser/telegram_specifications.py | 8 ++++---- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/dsmr_parser/clients/socket_.py b/dsmr_parser/clients/socket_.py index 7c13f02..6727979 100644 --- a/dsmr_parser/clients/socket_.py +++ b/dsmr_parser/clients/socket_.py @@ -22,7 +22,6 @@ class SocketReader(object): self.telegram_buffer = TelegramBuffer() self.telegram_specification = telegram_specification - def read(self): """ Read complete DSMR telegram's from remote interface and parse it @@ -88,4 +87,4 @@ class SocketReader(object): except ParseError as e: logger.error('Failed to parse telegram: %s', e) - buffer = b"" \ No newline at end of file + buffer = b"" diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 87c720d..b224b7a 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -52,10 +52,6 @@ EN = { obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS', obis.BELGIUM_HOURLY_GAS_METER_READING: 'BELGIUM_HOURLY_GAS_METER_READING', obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER', - obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: 'LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL', - obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 'LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL', - obis.SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL: 'SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL', - obis.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 'SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL', obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE', obis.Q3D_EQUIPMENT_SERIALNUMBER: 'Q3D_EQUIPMENT_SERIALNUMBER', diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index ad3bd98..d4a4cbf 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -8,10 +8,8 @@ objects are introduced. """ P1_MESSAGE_HEADER = r'\d-\d:0\.2\.8.+?\r\n' P1_MESSAGE_TIMESTAMP = r'\d-\d:1\.0\.0.+?\r\n' -ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1\.8\.0.+?\r\n' ELECTRICITY_USED_TARIFF_1 = r'\d-\d:1\.8\.1.+?\r\n' ELECTRICITY_USED_TARIFF_2 = r'\d-\d:1\.8\.2.+?\r\n' -ELECTRICITY_EXPORTED_TOTAL = r'\d-\d:2\.8\.0.+?\r\n' ELECTRICITY_DELIVERED_TARIFF_1 = r'\d-\d:2\.8\.1.+?\r\n' ELECTRICITY_DELIVERED_TARIFF_2 = r'\d-\d:2\.8\.2.+?\r\n' ELECTRICITY_ACTIVE_TARIFF = r'\d-\d:96\.14\.0.+?\r\n' @@ -62,13 +60,13 @@ ELECTRICITY_DELIVERED_TARIFF_ALL = ( ELECTRICITY_DELIVERED_TARIFF_2 ) -# Alternate codes for foreign countries. +# International generalized additions +ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+) +ELECTRICITY_EXPORTED_TOTAL = r'\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-) + +# International non generalized additions (country specific) / risk for necessary refactoring BELGIUM_HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n' # Different code, same format. LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'\d-\d:42\.0\.0.+?\r\n' # Logical device name -LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+) -LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-) -SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+) -SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-) Q3D_EQUIPMENT_IDENTIFIER = r'\d-\d:0\.0\.0.+?\r\n' # Logical device name Q3D_EQUIPMENT_STATE = r'\d-\d:96\.5\.5.+?\r\n' # Device state (hexadecimal) Q3D_EQUIPMENT_SERIALNUMBER = r'\d-\d:96\.1\.255.+?\r\n' # Device Serialnumber diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 978a65c..c8f05a5 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -153,8 +153,8 @@ BELGIUM_FLUVIUS['objects'].update({ LUXEMBOURG_SMARTY = deepcopy(V5) LUXEMBOURG_SMARTY['objects'].update({ obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), - obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), }) # Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/ @@ -164,8 +164,8 @@ SWEDEN = { 'objects': { obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), - obis.SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), - obis.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), From f0e035c8ed39e821e2a155e3d62151d31f643a91 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 28 Nov 2021 01:12:32 +0100 Subject: [PATCH 05/13] fix newer version pylama errors --- dsmr_parser/objects.py | 2 +- dsmr_parser/profile_generic_specifications.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index c062d9d..af7d068 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -179,7 +179,7 @@ class ProfileGenericObject(DSMRObject): self._buffer_list = [] values_offset = 2 for i in range(self.buffer_length): - offset = values_offset + i*2 + offset = values_offset + i * 2 self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) return self._buffer_list diff --git a/dsmr_parser/profile_generic_specifications.py b/dsmr_parser/profile_generic_specifications.py index a52416c..e753c01 100644 --- a/dsmr_parser/profile_generic_specifications.py +++ b/dsmr_parser/profile_generic_specifications.py @@ -7,4 +7,4 @@ PG_HEAD_PARSERS = [ValueParser(int), ValueParser(str)] PG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)] BUFFER_TYPES = { PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)] - } +} From 3eed3654d4839b0cd2e6c2c98d0f6b6804d76fe1 Mon Sep 17 00:00:00 2001 From: Ronald Pijnacker Date: Thu, 30 Dec 2021 17:01:29 +0100 Subject: [PATCH 06/13] Wrap DSMR protocol in RFXtrx wrapper --- dsmr_parser/clients/protocol.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index fce549d..982fd81 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -142,3 +142,26 @@ class DSMRProtocol(asyncio.Protocol): async def wait_closed(self): """Wait until connection is closed.""" await self._closed.wait() + + +PACKETTYPE_DSMR = 0x62 +SUBTYPE_P1 = 0x01 + +class RFXtrxDSMRProtocol(DSMRProtocol): + + _data = b'' + + def data_received(self, data): + """Add incoming data to buffer.""" + + data = self._data + data + + while (len(data) > 0 and (packetlength := data[0]+1) <= len(data)): + packettype = data[1] + subtype = data[2] + if (packettype == PACKETTYPE_DSMR and subtype == SUBTYPE_P1): + dsmr_data = data[4:packetlength] + super().data_received(dsmr_data) + data = data[packetlength:] + + self._data = data From 8b64adb80c0699f6448e55f64be95052065f37e4 Mon Sep 17 00:00:00 2001 From: Ronald Pijnacker Date: Thu, 30 Dec 2021 17:23:31 +0100 Subject: [PATCH 07/13] Small rename --- dsmr_parser/clients/protocol.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 982fd81..567b0f4 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -149,12 +149,12 @@ SUBTYPE_P1 = 0x01 class RFXtrxDSMRProtocol(DSMRProtocol): - _data = b'' + remaining_data = b'' def data_received(self, data): """Add incoming data to buffer.""" - data = self._data + data + data = self.remaining_data + data while (len(data) > 0 and (packetlength := data[0]+1) <= len(data)): packettype = data[1] @@ -164,4 +164,4 @@ class RFXtrxDSMRProtocol(DSMRProtocol): super().data_received(dsmr_data) data = data[packetlength:] - self._data = data + self.remaining_data = data From c7ed4acb034c44ce47eaf944654b0fcc8a10a895 Mon Sep 17 00:00:00 2001 From: Ronald Pijnacker Date: Thu, 30 Dec 2021 20:23:58 +0100 Subject: [PATCH 08/13] Refactor into separate file --- dsmr_parser/clients/protocol.py | 32 ++++--------- dsmr_parser/clients/rfxtrx_protocol.py | 62 ++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 dsmr_parser/clients/rfxtrx_protocol.py diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 567b0f4..4e6d85d 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -16,6 +16,13 @@ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): """Creates a DSMR asyncio protocol.""" + protocol = _create_dsmr_protocol(dsmr_version, telegram_callback, + DSMRProtocol, loop, **kwargs) + return protocol + + +def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol loop=None, **kwargs): + """Creates a DSMR asyncio protocol.""" if dsmr_version == '2.2': specification = telegram_specifications.V2_2 @@ -39,7 +46,7 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) - protocol = partial(DSMRProtocol, loop, TelegramParser(specification), + protocol = partial(protocol, loop, TelegramParser(specification), telegram_callback=telegram_callback, **kwargs) return protocol, serial_settings @@ -142,26 +149,3 @@ class DSMRProtocol(asyncio.Protocol): async def wait_closed(self): """Wait until connection is closed.""" await self._closed.wait() - - -PACKETTYPE_DSMR = 0x62 -SUBTYPE_P1 = 0x01 - -class RFXtrxDSMRProtocol(DSMRProtocol): - - remaining_data = b'' - - def data_received(self, data): - """Add incoming data to buffer.""" - - data = self.remaining_data + data - - while (len(data) > 0 and (packetlength := data[0]+1) <= len(data)): - packettype = data[1] - subtype = data[2] - if (packettype == PACKETTYPE_DSMR and subtype == SUBTYPE_P1): - dsmr_data = data[4:packetlength] - super().data_received(dsmr_data) - data = data[packetlength:] - - self.remaining_data = data diff --git a/dsmr_parser/clients/rfxtrx_protocol.py b/dsmr_parser/clients/rfxtrx_protocol.py new file mode 100644 index 0000000..b8a347d --- /dev/null +++ b/dsmr_parser/clients/rfxtrx_protocol.py @@ -0,0 +1,62 @@ +"""Asyncio protocol implementation for handling telegrams over a RFXtrx connection .""" + +from functools import partial +import asyncio + +from serial_asyncio import create_serial_connection + +from .protocol import DSMRProtocol, _create_dsmr_protocol + + +def create_rfxtrx_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): + """Creates a DSMR asyncio protocol.""" + protocol = _create_dsmr_protocol(dsmr_version, telegram_callback, + RFXtrxDSMRProtocol, loop, **kwargs) + return protocol + + +def create_rfxtrx_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): + """Creates a DSMR asyncio protocol coroutine using a RFXtrx serial port.""" + protocol, serial_settings = create_rfxtrx_dsmr_protocol( + dsmr_version, telegram_callback, loop=None) + serial_settings['url'] = port + + conn = create_serial_connection(loop, protocol, **serial_settings) + return conn + + +def create_rfxtrx_tcp_dsmr_reader(host, port, dsmr_version, + telegram_callback, loop=None, + keep_alive_interval=None): + """Creates a DSMR asyncio protocol coroutine using a RFXtrx TCP connection.""" + if not loop: + loop = asyncio.get_event_loop() + protocol, _ = create_rfxtrx_dsmr_protocol( + dsmr_version, telegram_callback, loop=loop, + keep_alive_interval=keep_alive_interval) + conn = loop.create_connection(protocol, host, port) + return conn + + +PACKETTYPE_DSMR = 0x62 +SUBTYPE_P1 = 0x01 + + +class RFXtrxDSMRProtocol(DSMRProtocol): + + remaining_data = b'' + + def data_received(self, data): + """Add incoming data to buffer.""" + + data = self.remaining_data + data + + while (len(data) > 0 and (packetlength := data[0]+1) <= len(data)): + packettype = data[1] + subtype = data[2] + if (packettype == PACKETTYPE_DSMR and subtype == SUBTYPE_P1): + dsmr_data = data[4:packetlength] + super().data_received(dsmr_data) + data = data[packetlength:] + + self.remaining_data = data From 188cac52877ba702a6e8dcb623b0742187afc872 Mon Sep 17 00:00:00 2001 From: Ronald Pijnacker Date: Thu, 30 Dec 2021 20:32:44 +0100 Subject: [PATCH 09/13] Small update --- dsmr_parser/clients/protocol.py | 2 +- dsmr_parser/clients/rfxtrx_protocol.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 4e6d85d..e996423 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -21,7 +21,7 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): return protocol -def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol loop=None, **kwargs): +def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, **kwargs): """Creates a DSMR asyncio protocol.""" if dsmr_version == '2.2': diff --git a/dsmr_parser/clients/rfxtrx_protocol.py b/dsmr_parser/clients/rfxtrx_protocol.py index b8a347d..281f4c2 100644 --- a/dsmr_parser/clients/rfxtrx_protocol.py +++ b/dsmr_parser/clients/rfxtrx_protocol.py @@ -1,15 +1,13 @@ """Asyncio protocol implementation for handling telegrams over a RFXtrx connection .""" -from functools import partial import asyncio from serial_asyncio import create_serial_connection - from .protocol import DSMRProtocol, _create_dsmr_protocol def create_rfxtrx_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): - """Creates a DSMR asyncio protocol.""" + """Creates a RFXtrxDSMR asyncio protocol.""" protocol = _create_dsmr_protocol(dsmr_version, telegram_callback, RFXtrxDSMRProtocol, loop, **kwargs) return protocol From c082cf4868753c6335612274ff00605b5f00dd67 Mon Sep 17 00:00:00 2001 From: Ronald Pijnacker Date: Sat, 1 Jan 2022 20:19:24 +0100 Subject: [PATCH 10/13] Add test case --- test/test_rfxtrx_protocol.py | 79 ++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/test_rfxtrx_protocol.py diff --git a/test/test_rfxtrx_protocol.py b/test/test_rfxtrx_protocol.py new file mode 100644 index 0000000..14f54bc --- /dev/null +++ b/test/test_rfxtrx_protocol.py @@ -0,0 +1,79 @@ +from unittest.mock import Mock + +import unittest + +from dsmr_parser import obis_references as obis +from dsmr_parser import telegram_specifications +from dsmr_parser.parsers import TelegramParser +from dsmr_parser.clients.rfxtrx_protocol import create_rfxtrx_dsmr_protocol, PACKETTYPE_DSMR, SUBTYPE_P1 + + +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' +) + +OTHER_RF_PACKET = b'\x03\x01\x02\x03' + + +def encode_telegram_as_RF_packets(telegram): + data = b'' + + for line in telegram.split('\n'): + packet_data = (line + '\n').encode('ascii') + packet_header = bytes(bytearray([ + len(packet_data) + 3, # excluding length byte + PACKETTYPE_DSMR, + SUBTYPE_P1, + 0 # seq num (ignored) + ])) + + data += packet_header + packet_data + # other RF packets can pass by on the line + data += OTHER_RF_PACKET + + return data + + +class RFXtrxProtocolTest(unittest.TestCase): + + def setUp(self): + new_protocol, _ = create_rfxtrx_dsmr_protocol('2.2', + telegram_callback=Mock(), + keep_alive_interval=1) + self.protocol = new_protocol() + + def test_complete_packet(self): + """Protocol should assemble incoming lines into complete packet.""" + + data = encode_telegram_as_RF_packets(TELEGRAM_V2_2) + # send data broken up in two parts + self.protocol.data_received(data[0:200]) + self.protocol.data_received(data[200:]) + + telegram = self.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' From 7d28d0e3709a5ab581d754ff7075899cad516dc2 Mon Sep 17 00:00:00 2001 From: Ronald Pijnacker Date: Mon, 3 Jan 2022 21:25:19 +0100 Subject: [PATCH 11/13] Update according to coding style --- dsmr_parser/clients/rfxtrx_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/clients/rfxtrx_protocol.py b/dsmr_parser/clients/rfxtrx_protocol.py index 281f4c2..e4080a2 100644 --- a/dsmr_parser/clients/rfxtrx_protocol.py +++ b/dsmr_parser/clients/rfxtrx_protocol.py @@ -49,7 +49,7 @@ class RFXtrxDSMRProtocol(DSMRProtocol): data = self.remaining_data + data - while (len(data) > 0 and (packetlength := data[0]+1) <= len(data)): + while len(data) > 0 and (packetlength := data[0] + 1) <= len(data): packettype = data[1] subtype = data[2] if (packettype == PACKETTYPE_DSMR and subtype == SUBTYPE_P1): From dd6d26670eb703a0f5db70033c79a374f6432ce0 Mon Sep 17 00:00:00 2001 From: Ronald Pijnacker Date: Tue, 4 Jan 2022 09:49:11 +0100 Subject: [PATCH 12/13] Rewrite for compatibility with python 3.6 --- dsmr_parser/clients/rfxtrx_protocol.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/clients/rfxtrx_protocol.py b/dsmr_parser/clients/rfxtrx_protocol.py index e4080a2..848de71 100644 --- a/dsmr_parser/clients/rfxtrx_protocol.py +++ b/dsmr_parser/clients/rfxtrx_protocol.py @@ -49,12 +49,14 @@ class RFXtrxDSMRProtocol(DSMRProtocol): data = self.remaining_data + data - while len(data) > 0 and (packetlength := data[0] + 1) <= len(data): + packetlength = data[0] + 1 if len(data) > 0 else 1 + while packetlength <= len(data): packettype = data[1] subtype = data[2] if (packettype == PACKETTYPE_DSMR and subtype == SUBTYPE_P1): dsmr_data = data[4:packetlength] super().data_received(dsmr_data) data = data[packetlength:] + packetlength = data[0] + 1 if len(data) > 0 else 1 self.remaining_data = data From eace91b5910b85f54bf301cf5a646630533a9bec Mon Sep 17 00:00:00 2001 From: Ronald Pijnacker Date: Tue, 4 Jan 2022 09:53:49 +0100 Subject: [PATCH 13/13] Fix coding style issues --- dsmr_parser/clients/serial_.py | 1 - dsmr_parser/objects.py | 2 +- dsmr_parser/profile_generic_specifications.py | 2 +- dsmr_parser/telegram_specifications.py | 2 +- test/test_protocol.py | 2 -- test/test_rfxtrx_protocol.py | 2 -- 6 files changed, 3 insertions(+), 8 deletions(-) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index f63ff07..12d2245 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -1,4 +1,3 @@ -import asyncio import logging import serial import serial_asyncio diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index c062d9d..af7d068 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -179,7 +179,7 @@ class ProfileGenericObject(DSMRObject): self._buffer_list = [] values_offset = 2 for i in range(self.buffer_length): - offset = values_offset + i*2 + offset = values_offset + i * 2 self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) return self._buffer_list diff --git a/dsmr_parser/profile_generic_specifications.py b/dsmr_parser/profile_generic_specifications.py index a52416c..e753c01 100644 --- a/dsmr_parser/profile_generic_specifications.py +++ b/dsmr_parser/profile_generic_specifications.py @@ -7,4 +7,4 @@ PG_HEAD_PARSERS = [ValueParser(int), ValueParser(str)] PG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)] BUFFER_TYPES = { PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)] - } +} diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 4e59f51..5a06ce0 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -157,7 +157,7 @@ LUXEMBOURG_SMARTY['objects'].update({ obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), }) -# Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf +# Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf # noqa SWEDEN = { 'checksum_support': True, 'objects': { diff --git a/test/test_protocol.py b/test/test_protocol.py index c298d5c..d1393f3 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -3,8 +3,6 @@ from unittest.mock import Mock import unittest from dsmr_parser import obis_references as obis -from dsmr_parser import telegram_specifications -from dsmr_parser.parsers import TelegramParser from dsmr_parser.clients.protocol import create_dsmr_protocol diff --git a/test/test_rfxtrx_protocol.py b/test/test_rfxtrx_protocol.py index 14f54bc..7c79d22 100644 --- a/test/test_rfxtrx_protocol.py +++ b/test/test_rfxtrx_protocol.py @@ -3,8 +3,6 @@ from unittest.mock import Mock import unittest from dsmr_parser import obis_references as obis -from dsmr_parser import telegram_specifications -from dsmr_parser.parsers import TelegramParser from dsmr_parser.clients.rfxtrx_protocol import create_rfxtrx_dsmr_protocol, PACKETTYPE_DSMR, SUBTYPE_P1