From 3eed3654d4839b0cd2e6c2c98d0f6b6804d76fe1 Mon Sep 17 00:00:00 2001 From: Ronald Pijnacker Date: Thu, 30 Dec 2021 17:01:29 +0100 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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