From de5c884d8d4c8cc789155fd37d03caf31d997452 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 27 Feb 2017 20:44:18 +0100 Subject: [PATCH 001/226] Small comment improvement --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 749b118..7b53ee4 100644 --- a/README.rst +++ b/README.rst @@ -54,6 +54,7 @@ into a dictionary. from dsmr_parser import telegram_specifications from dsmr_parser.parsers import TelegramParser + # String is formatted in separate lines for readability. telegram_str = ( '/ISk5\2MT382-1000\r\n' '\r\n' From efc09df71f3a5480f68b2f3127175f47912b7a5c Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Fri, 12 May 2017 20:30:53 +0200 Subject: [PATCH 002/226] Add DSMR5 option to protocol. --- dsmr_parser/clients/protocol.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 8f55376..d63a3fc 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -23,6 +23,9 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): elif dsmr_version == '4': specification = telegram_specifications.V4 serial_settings = SERIAL_SETTINGS_V4 + elif dsmr_version == '5': + specification = telegram_specifications.V5 + serial_settings = SERIAL_SETTINGS_V4 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) From 9b0a85e84b2bdce88c1925985e7c9b9846e8b58f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Fri, 12 May 2017 21:37:54 +0200 Subject: [PATCH 003/226] updated version --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d34527..3617fb4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.9** (2017-05-12) + +- added DSMR v5 serial settings + **0.8** (2017-01-26) - added support for DSMR v3 diff --git a/setup.py b/setup.py index a4b5fdd..b8dc651 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.8', + version='0.9', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From dcb59fddb1a6f5efe791ff94cd0d6d9de12621d2 Mon Sep 17 00:00:00 2001 From: Alex Mekkering Date: Mon, 5 Jun 2017 09:07:47 +0200 Subject: [PATCH 004/226] Support optional telegram signatures --- dsmr_parser/parsers.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 6a9f465..c38dcf0 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -51,12 +51,10 @@ def parse(self, telegram_data): for signature, parser in self.telegram_specification['objects'].items(): match = re.search(signature, telegram_data, re.DOTALL) - # All telegram specification lines/signatures are expected to be - # present. - if not match: - raise ParseError('Telegram specification does not match ' - 'telegram data') - telegram[signature] = parser.parse(match.group(0)) + # Some signatures are optional and may not be present, + # so only parse lines that match + if match: + telegram[signature] = parser.parse(match.group(0)) return telegram From d6e28db116d4394cd80a70cec15ad2e532af0ae9 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 5 Jun 2017 21:02:59 +0200 Subject: [PATCH 005/226] log checksum errors as warning; dont force full telegram signatures; removed unused code for automatic telegram version detection; --- CHANGELOG.rst | 6 +++++ dsmr_parser/clients/protocol.py | 2 ++ dsmr_parser/clients/serial_.py | 2 ++ dsmr_parser/parsers.py | 32 +++-------------------- setup.py | 2 +- test/test_match_telegram_specification.py | 24 ----------------- 6 files changed, 15 insertions(+), 53 deletions(-) delete mode 100644 test/test_match_telegram_specification.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3617fb4..0da194d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Change Log ---------- +**0.10** (2017-06-05) + +- bugix: don't force full telegram signatures (`pull request #25 `_) +- removed unused code for automatic telegram detection as this needs reworking after the fix mentioned above +- InvalidChecksumError's are logged as warning instead of error + **0.9** (2017-05-12) - added DSMR v5 serial settings diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index d63a3fc..68f2434 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -101,6 +101,8 @@ def handle_telegram(self, telegram): try: parsed_telegram = self.telegram_parser.parse(telegram) + except InvalidChecksumError as e: + self.log.warning(str(e)) except ParseError: self.log.exception("failed to parse telegram") else: diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index d69cac3..ddc0a14 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -36,6 +36,8 @@ def read(self): 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) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index c38dcf0..087d9e0 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -92,31 +92,6 @@ def validate_checksum(telegram): ) -def match_telegram_specification(telegram_data): - """ - Find telegram specification that matches the telegram data by trying all - specifications. - - Could be further optimized to check the actual 0.2.8 OBIS reference which - is available for DSMR version 4 and up. - - :param str telegram_data: full telegram from start ('/') to checksum - ('!ABCD') including line endings in between the telegram's lines - :return: telegram specification - :rtype: dict - """ - # Prevent circular import - from dsmr_parser import telegram_specifications - - for specification in telegram_specifications.ALL: - try: - TelegramParser(specification).parse(telegram_data) - except ParseError: - pass - else: - return specification - - class DSMRObjectParser(object): """ Parses an object (can also be see as a 'line') from a telegram. @@ -174,10 +149,11 @@ class CosemParser(DSMRObjectParser): 1 23 45 1) OBIS Reduced ID-code - 2) Separator “(“, ASCII 28h + 2) Separator "(", ASCII 28h 3) COSEM object attribute value - 4) Unit of measurement values (Unit of capture objects attribute) – only if applicable - 5) Separator “)”, ASCII 29h + 4) Unit of measurement values (Unit of capture objects attribute) - only if + applicable + 5) Separator ")", ASCII 29h """ def parse(self, line): diff --git a/setup.py b/setup.py index b8dc651..176233c 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.9', + version='0.10', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', diff --git a/test/test_match_telegram_specification.py b/test/test_match_telegram_specification.py deleted file mode 100644 index 357495a..0000000 --- a/test/test_match_telegram_specification.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from dsmr_parser.parsers import match_telegram_specification -from dsmr_parser import telegram_specifications -from test import example_telegrams - - -class MatchTelegramSpecificationTest(unittest.TestCase): - - def test_v2_2(self): - assert match_telegram_specification(example_telegrams.TELEGRAM_V2_2) \ - == telegram_specifications.V2_2 - - def test_v3(self): - assert match_telegram_specification(example_telegrams.TELEGRAM_V3) \ - == telegram_specifications.V3 - - def test_v4_2(self): - assert match_telegram_specification(example_telegrams.TELEGRAM_V4_2) \ - == telegram_specifications.V4 - - def test_v5(self): - assert match_telegram_specification(example_telegrams.TELEGRAM_V5) \ - == telegram_specifications.V5 From c78ebe3e2d2dcad1512ff11aab3a50bef2646ff2 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 5 Jun 2017 21:06:48 +0200 Subject: [PATCH 006/226] fixed import errors --- dsmr_parser/clients/protocol.py | 2 +- dsmr_parser/clients/serial_.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 68f2434..0834051 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -8,7 +8,7 @@ from dsmr_parser import telegram_specifications from dsmr_parser.clients.telegram_buffer import TelegramBuffer -from dsmr_parser.exceptions import ParseError +from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.parsers import TelegramParser from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ SERIAL_SETTINGS_V4 diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index ddc0a14..1d7be89 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -4,7 +4,7 @@ import serial_asyncio from dsmr_parser.clients.telegram_buffer import TelegramBuffer -from dsmr_parser.exceptions import ParseError +from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.parsers import TelegramParser From e3203a5334a3dc95160f45f3ca3aaddc971d0b1f Mon Sep 17 00:00:00 2001 From: Vincent van den Braken Date: Fri, 15 Sep 2017 13:48:10 +0200 Subject: [PATCH 007/226] Optional NUL after checksum My smart meter returns 00 0D 0A after the checksum, not just 0D 0A. --- dsmr_parser/clients/telegram_buffer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/clients/telegram_buffer.py b/dsmr_parser/clients/telegram_buffer.py index 78a98eb..5933296 100644 --- a/dsmr_parser/clients/telegram_buffer.py +++ b/dsmr_parser/clients/telegram_buffer.py @@ -51,7 +51,7 @@ def _find_telegrams(self): # - The checksum is optional '{0,4}' because not all telegram versions # support it. return re.findall( - r'\/[^\/]+?\![A-F0-9]{0,4}\r\n', + r'\/[^\/]+?\![A-F0-9]{0,4}\0?\r\n', self._buffer, re.DOTALL ) From d94bc8de03586b53959b5b320528606f78971a9a Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 18 Sep 2017 12:02:36 +0200 Subject: [PATCH 008/226] updated version --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0da194d..10d1169 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.11** (2017-09-18) + +- NULL value fix in checksum (`pull request #26 `_) + **0.10** (2017-06-05) - bugix: don't force full telegram signatures (`pull request #25 `_) diff --git a/setup.py b/setup.py index 176233c..63fc719 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.10', + version='0.11', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From d534b1d8b064f62257feda6828dc9f810a85389d Mon Sep 17 00:00:00 2001 From: jk-5 Date: Thu, 6 Sep 2018 14:53:30 +0200 Subject: [PATCH 009/226] Added missing values for 3-phase sagemcom TD210-D meter --- dsmr_parser/obis_references.py | 7 +++++++ dsmr_parser/telegram_specifications.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 6791f1e..14f696b 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -17,6 +17,7 @@ CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n' LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n' +SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n' POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n' VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n' VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n' @@ -24,6 +25,12 @@ VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n' VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n' VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n' TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n' TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n' DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n' diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 5222786..19b0028 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -96,6 +96,7 @@ obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), + obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), @@ -103,6 +104,12 @@ obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(int)), obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), obis.DEVICE_TYPE: CosemParser(ValueParser(int)), obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), From 472c54968e72c630707d3c54d58a6f13f4de3f68 Mon Sep 17 00:00:00 2001 From: jk-5 Date: Thu, 6 Sep 2018 15:40:02 +0200 Subject: [PATCH 010/226] Corrected meter types --- dsmr_parser/telegram_specifications.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 19b0028..1e28fce 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -104,12 +104,12 @@ obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), obis.DEVICE_TYPE: CosemParser(ValueParser(int)), obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), From 3327c78c0ee4f19f909e5951cbb5710dc811ffb0 Mon Sep 17 00:00:00 2001 From: jk-5 Date: Thu, 6 Sep 2018 15:49:11 +0200 Subject: [PATCH 011/226] Updated unittests --- test/test_parse_v5.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index 550fde9..e9cfbc1 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -87,6 +87,12 @@ def test_parse(self): assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int) assert result[obis.LONG_POWER_FAILURE_COUNT].value == 0 + # SHORT_POWER_FAILURE_COUNT (1-0:96.7.21) + assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject) + assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None + assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int) + assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 13 + # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0) assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject) assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None @@ -123,6 +129,42 @@ def test_parse(self): assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int) assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0 + # INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0) + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject) + assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V' + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal) + assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('230.0') + + # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0) + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject) + assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V' + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal) + assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('230.0') + + # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0) + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject) + assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V' + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal) + assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('229.0') + + # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.48') + + # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.44') + + # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.86') + # TEXT_MESSAGE (0-0:96.13.0) assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) assert result[obis.TEXT_MESSAGE].unit is None From ad6ab304f5c28279bd8c22d760883775c939adce Mon Sep 17 00:00:00 2001 From: Thomas Neele Date: Sat, 15 Sep 2018 21:18:36 +0200 Subject: [PATCH 012/226] Added serial settings for DSMR v5.0 --- dsmr_parser/clients/__init__.py | 2 +- dsmr_parser/clients/settings.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/clients/__init__.py b/dsmr_parser/clients/__init__.py index 2a3b1fd..7323ecd 100644 --- a/dsmr_parser/clients/__init__.py +++ b/dsmr_parser/clients/__init__.py @@ -1,5 +1,5 @@ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ - SERIAL_SETTINGS_V4 + SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader from dsmr_parser.clients.protocol import create_dsmr_protocol, \ create_dsmr_reader, create_tcp_dsmr_reader diff --git a/dsmr_parser/clients/settings.py b/dsmr_parser/clients/settings.py index 2c2677c..26502d0 100644 --- a/dsmr_parser/clients/settings.py +++ b/dsmr_parser/clients/settings.py @@ -20,3 +20,13 @@ 'rtscts': 0, 'timeout': 20 } + +SERIAL_SETTINGS_V5 = { + 'baudrate': 115200, + 'bytesize': serial.EIGHTBITS, + 'parity': serial.PARITY_NONE, + 'stopbits': serial.STOPBITS_ONE, + 'xonxoff': 0, + 'rtscts': 0, + 'timeout': 20 +} From 887dd3a2aa820ed4b33a776a3805c5d86dae9b86 Mon Sep 17 00:00:00 2001 From: bossjl Date: Fri, 21 Sep 2018 18:37:32 +0200 Subject: [PATCH 013/226] Lux-creos-obis-1.8.0 --- dsmr_parser/obis_references.py | 1 + dsmr_parser/telegram_specifications.py | 1 + 2 files changed, 2 insertions(+) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 14f696b..5050f43 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -8,6 +8,7 @@ """ 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_DELIVERED_TARIFF_1 = r'\d-\d:2\.8\.1.+?\r\n' diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 1e28fce..e2c0bf5 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -88,6 +88,7 @@ obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), From 59811dbf9fe11483c5827e9088e4f1728d31a11b Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 23 Sep 2018 12:59:02 +0200 Subject: [PATCH 014/226] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 63fc719..aa2b577 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.11', + version='0.12', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 41f6a7ac168a5b306f4cb41a0e562aaf06a92504 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 23 Sep 2018 13:01:04 +0200 Subject: [PATCH 015/226] Update CHANGELOG.rst --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 10d1169..234c994 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.12** (2018-09-23) +- Add serial settings for DSMR v5.0 (`pull request #31 `_). +- Lux-creos-obis-1.8.0 (`pull request #32 `_). + **0.11** (2017-09-18) - NULL value fix in checksum (`pull request #26 `_) From 48783acc00dceecf35a01447a04d186f9d796e75 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 23 Sep 2018 13:01:22 +0200 Subject: [PATCH 016/226] Update CHANGELOG.rst --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 234c994..32629c4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,7 @@ Change Log ---------- **0.12** (2018-09-23) + - Add serial settings for DSMR v5.0 (`pull request #31 `_). - Lux-creos-obis-1.8.0 (`pull request #32 `_). From 85c67464a1a9ff4e606e155bc5ff707140c23702 Mon Sep 17 00:00:00 2001 From: Mark Leenaerts Date: Sat, 19 Jan 2019 16:11:12 +0100 Subject: [PATCH 017/226] Fix DSMR v5.0 serial settings which were not used While analysing some CRC check errors I encounter within the home-assistant plugin (which uses this component) I encountered this oversight in the code. --- dsmr_parser/clients/protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 0834051..141a389 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -11,7 +11,7 @@ from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.parsers import TelegramParser from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ - SERIAL_SETTINGS_V4 + SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): @@ -25,7 +25,7 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): serial_settings = SERIAL_SETTINGS_V4 elif dsmr_version == '5': specification = telegram_specifications.V5 - serial_settings = SERIAL_SETTINGS_V4 + serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) From c04b0a5add58ce70153eede1a87ca171876b61c7 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 4 Mar 2019 20:31:51 +0100 Subject: [PATCH 018/226] Updated version number --- CHANGELOG.rst | 4 ++++ setup.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 32629c4..f7d822c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.13** (2019-03-04) + +- Fix DSMR v5.0 serial settings which were not used (`pull request #33 `_). + **0.12** (2018-09-23) - Add serial settings for DSMR v5.0 (`pull request #31 `_). diff --git a/setup.py b/setup.py index aa2b577..e375858 100644 --- a/setup.py +++ b/setup.py @@ -4,9 +4,9 @@ name='dsmr-parser', description='Library to parse Dutch Smart Meter Requirements (DSMR)', author='Nigel Dokter', - author_email='nigeldokter@gmail.com', + author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.12', + version='0.13', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 8bdf77c78d938281bbe29b6c4d785877435dcdf6 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sat, 6 Apr 2019 12:56:27 +0200 Subject: [PATCH 019/226] ensure build and dist directories are not synced --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1da5fee..20de282 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /.project /.pydevproject /.coverage +build/ +dist/ From c36f68a88423e9e656658db0afdd46fd7b350ff6 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Thu, 6 Jun 2019 05:41:55 +0200 Subject: [PATCH 020/226] working version of the Telegram object --- dsmr_parser/clients/serial_.py | 20 ++++++++++++ dsmr_parser/obis_name_mapping.py | 54 ++++++++++++++++++++++++++++++++ dsmr_parser/objects.py | 54 ++++++++++++++++++++++++++++++++ dsmr_parser/parsers.py | 2 +- test/test_telegram.py | 30 ++++++++++++++++++ tox.ini | 2 +- 6 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 dsmr_parser/obis_name_mapping.py create mode 100644 test/test_telegram.py diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 1d7be89..c252f6f 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -6,6 +6,7 @@ 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__) @@ -41,6 +42,25 @@ def read(self): except ParseError as e: logger.error('Failed to parse telegram: %s', e) + def read_as_object(self): + """ + Read complete DSMR telegram's from the serial interface and return a Telegram object. + + :rtype: generator + """ + with serial.Serial(**self.serial_settings) as serial_handle: + while True: + data = serial_handle.readline() + self.telegram_buffer.append(data.decode('ascii')) + + for telegram in self.telegram_buffer.get_all(): + try: + yield Telegram(telegram, telegram_parser, telegram_specification) + except InvalidChecksumError as e: + logger.warning(str(e)) + except ParseError as e: + logger.error('Failed to parse telegram: %s', e) + class AsyncSerialReader(SerialReader): """Serial reader using asyncio pyserial.""" diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py new file mode 100644 index 0000000..8f72654 --- /dev/null +++ b/dsmr_parser/obis_name_mapping.py @@ -0,0 +1,54 @@ +from dsmr_parser import obis_references as obis + +""" +dsmr_parser.obis_name_mapping +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module contains a mapping of obis references to names. +""" + +EN = { + obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', + obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', + obis.ELECTRICITY_IMPORTED_TOTAL : 'ELECTRICITY_IMPORTED_TOTAL', + obis.ELECTRICITY_USED_TARIFF_1 : 'ELECTRICITY_USED_TARIFF_1', + obis.ELECTRICITY_USED_TARIFF_2 : 'ELECTRICITY_USED_TARIFF_2', + obis.ELECTRICITY_DELIVERED_TARIFF_1 : 'ELECTRICITY_DELIVERED_TARIFF_1', + obis.ELECTRICITY_DELIVERED_TARIFF_2 : 'ELECTRICITY_DELIVERED_TARIFF_2', + obis.ELECTRICITY_ACTIVE_TARIFF : 'ELECTRICITY_ACTIVE_TARIFF', + obis.EQUIPMENT_IDENTIFIER : 'EQUIPMENT_IDENTIFIER', + obis.CURRENT_ELECTRICITY_USAGE : 'CURRENT_ELECTRICITY_USAGE', + obis.CURRENT_ELECTRICITY_DELIVERY : 'CURRENT_ELECTRICITY_DELIVERY', + obis.LONG_POWER_FAILURE_COUNT : 'LONG_POWER_FAILURE_COUNT', + obis.SHORT_POWER_FAILURE_COUNT : 'SHORT_POWER_FAILURE_COUNT', + obis.POWER_EVENT_FAILURE_LOG : 'POWER_EVENT_FAILURE_LOG', + obis.VOLTAGE_SAG_L1_COUNT : 'VOLTAGE_SAG_L1_COUNT', + obis.VOLTAGE_SAG_L2_COUNT : 'VOLTAGE_SAG_L2_COUNT', + obis.VOLTAGE_SAG_L3_COUNT : 'VOLTAGE_SAG_L3_COUNT', + obis.VOLTAGE_SWELL_L1_COUNT : 'VOLTAGE_SWELL_L1_COUNT', + obis.VOLTAGE_SWELL_L2_COUNT : 'VOLTAGE_SWELL_L2_COUNT', + obis.VOLTAGE_SWELL_L3_COUNT : 'VOLTAGE_SWELL_L3_COUNT', + obis.INSTANTANEOUS_VOLTAGE_L1 : 'INSTANTANEOUS_VOLTAGE_L1', + obis.INSTANTANEOUS_VOLTAGE_L2 : 'INSTANTANEOUS_VOLTAGE_L2', + obis.INSTANTANEOUS_VOLTAGE_L3 : 'INSTANTANEOUS_VOLTAGE_L3', + obis.INSTANTANEOUS_CURRENT_L1 : 'INSTANTANEOUS_CURRENT_L1', + obis.INSTANTANEOUS_CURRENT_L2 : 'INSTANTANEOUS_CURRENT_L2', + obis.INSTANTANEOUS_CURRENT_L3 : 'INSTANTANEOUS_CURRENT_L3', + obis.TEXT_MESSAGE_CODE : 'TEXT_MESSAGE_CODE', + obis.TEXT_MESSAGE : 'TEXT_MESSAGE', + obis.DEVICE_TYPE : 'DEVICE_TYPE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + obis.EQUIPMENT_IDENTIFIER_GAS : 'EQUIPMENT_IDENTIFIER_GAS', + obis.HOURLY_GAS_METER_READING : 'HOURLY_GAS_METER_READING', + obis.GAS_METER_READING : 'GAS_METER_READING', + obis.ACTUAL_TRESHOLD_ELECTRICITY : 'ACTUAL_TRESHOLD_ELECTRICITY', + obis.ACTUAL_SWITCH_POSITION : 'ACTUAL_SWITCH_POSITION', + obis.VALVE_POSITION_GAS : 'VALVE_POSITION_GAS' +} + +REVERSE_EN = dict([ (v,k) for k,v in EN.items()]) \ No newline at end of file diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index e6706c4..940318a 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,3 +1,57 @@ +import dsmr_parser.obis_name_mapping + +class Telegram(object): + """ + Container for raw and parsed telegram data. + Initializing: + from dsmr_parser import telegram_specifications + from dsmr_parser.exceptions import InvalidChecksumError, ParseError + from dsmr_parser.objects import CosemObject, MBusObject, Telegram + from dsmr_parser.parsers import TelegramParser + from test.example_telegrams import TELEGRAM_V4_2 + parser = TelegramParser(telegram_specifications.V4) + telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) + + Attributes can be accessed on a telegram object by addressing by their english name, for example: + telegram.ELECTRICITY_USED_TARIFF_1 + + All attributes in a telegram can be iterated over, for example: + [k for k,v in telegram] + yields: + ['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...] + """ + def __init__(self, telegram_data, telegram_parser, telegram_specification): + self._telegram_data = telegram_data + self._telegram_specification = telegram_specification + self._telegram_parser = telegram_parser + self._obis_name_mapping = dsmr_parser.obis_name_mapping.EN + self._reverse_obis_name_mapping = dsmr_parser.obis_name_mapping.REVERSE_EN + self._dictionary = self._telegram_parser.parse(telegram_data) + self._item_names = self._get_item_names() + + def __getattr__(self, name): + ''' will only get called for undefined attributes ''' + obis_reference = self._reverse_obis_name_mapping[name] + value = self._dictionary[obis_reference] + setattr(self, name, value) + return value + + def _get_item_names(self): + return [self._obis_name_mapping[k] for k, v in self._dictionary.items()] + + def __iter__(self): + for attr in self._item_names: + value = getattr(self, attr) + yield attr, value + + def __str__(self): + output = "" + for attr,value in self: + output += " " if not output == "" else "" + output += "{}: \t {} \t[{}]\n".format(attr,str(value.value),str(value.unit)) + return output + + class DSMRObject(object): """ Represents all data from a single telegram line. diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 087d9e0..4b415f6 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,7 +3,7 @@ from PyCRC.CRC16 import CRC16 -from dsmr_parser.objects import MBusObject, CosemObject +from dsmr_parser.objects import MBusObject, CosemObject, Telegram from dsmr_parser.exceptions import ParseError, InvalidChecksumError logger = logging.getLogger(__name__) diff --git a/test/test_telegram.py b/test/test_telegram.py new file mode 100644 index 0000000..d0e1042 --- /dev/null +++ b/test/test_telegram.py @@ -0,0 +1,30 @@ +from decimal import Decimal + +import datetime +import unittest + +import pytz + +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, Telegram +from dsmr_parser.parsers import TelegramParser +from test.example_telegrams import TELEGRAM_V4_2 + +class TelegramTest(unittest.TestCase): + """ Test instantiation of Telegram object """ + + def test_instantiate(self): + parser = TelegramParser(telegram_specifications.V4) + #result = parser.parse(TELEGRAM_V4_2) + telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) + + + + + # P1_MESSAGE_HEADER (1-3:0.2.8) + #assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject) + #assert result[obis.P1_MESSAGE_HEADER].unit is None + #assert isinstance(result[obis.P1_MESSAGE_HEADER].value, str) + #assert result[obis.P1_MESSAGE_HEADER].value == '50' diff --git a/tox.ini b/tox.ini index 95660fe..23fe214 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ commands= pylama dsmr_parser test [pylama:dsmr_parser/clients/__init__.py] -ignore = W0611 +ignore = W0611,W0605 [pylama:pylint] max_line_length = 100 From 83886247216b439af43160841ae69221d146cf49 Mon Sep 17 00:00:00 2001 From: Thomas Neele Date: Mon, 22 Jul 2019 21:33:15 +0200 Subject: [PATCH 021/226] Read more data from serial port at once A telegram can contain dozens of lines. Reading them one by one is somewhat inefficient. With this change, the client tries to read all data that is available. This significantly reduced CPU load for me. --- dsmr_parser/clients/serial_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 1d7be89..9939194 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -30,7 +30,7 @@ def read(self): """ with serial.Serial(**self.serial_settings) as serial_handle: while True: - data = serial_handle.readline() + data = serial_handle.read(max(1, min(1024, serial_handle.in_waiting))) self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): From f9e9e70771598f4316e6b64b6d6016925f50c3e4 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Thu, 3 Oct 2019 21:33:11 +0200 Subject: [PATCH 022/226] experimentation file --- test/experiment_telegram.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test/experiment_telegram.py diff --git a/test/experiment_telegram.py b/test/experiment_telegram.py new file mode 100644 index 0000000..3450a05 --- /dev/null +++ b/test/experiment_telegram.py @@ -0,0 +1,14 @@ +from decimal import Decimal +import datetime +import unittest +import pytz +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, Telegram +from dsmr_parser.parsers import TelegramParser +from test.example_telegrams import TELEGRAM_V4_2 +parser = TelegramParser(telegram_specifications.V4) +telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) + +print(telegram) \ No newline at end of file From b31bcd61fc918fe5b5bc3c0e1cff78fa6947ab3a Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 8 Oct 2019 22:13:58 +0200 Subject: [PATCH 023/226] Updated version number --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f7d822c..5fac8e7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.14** (2019-10-08) + +- Changed serial reading to reduce CPU usage (`pull request #37 `_). + **0.13** (2019-03-04) - Fix DSMR v5.0 serial settings which were not used (`pull request #33 `_). diff --git a/setup.py b/setup.py index e375858..b20a2a8 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.13', + version='0.14', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From f7ba363b93598c98ce46230994c4fcfb619a559d Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 25 Nov 2019 01:37:48 +0100 Subject: [PATCH 024/226] small fixes --- dsmr_parser/clients/serial_.py | 2 +- test/experiment_telegram.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index c252f6f..319becc 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -55,7 +55,7 @@ def read_as_object(self): for telegram in self.telegram_buffer.get_all(): try: - yield Telegram(telegram, telegram_parser, telegram_specification) + yield Telegram(telegram, self.telegram_parser, self.telegram_specification) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: diff --git a/test/experiment_telegram.py b/test/experiment_telegram.py index 3450a05..2649f51 100644 --- a/test/experiment_telegram.py +++ b/test/experiment_telegram.py @@ -7,7 +7,7 @@ from dsmr_parser.exceptions import InvalidChecksumError, ParseError from dsmr_parser.objects import CosemObject, MBusObject, Telegram from dsmr_parser.parsers import TelegramParser -from test.example_telegrams import TELEGRAM_V4_2 +from example_telegrams import TELEGRAM_V4_2 parser = TelegramParser(telegram_specifications.V4) telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) From 8ba400800b7f5b663e000919219f506b30ca8cc0 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 25 Nov 2019 01:53:54 +0100 Subject: [PATCH 025/226] small fixes --- dsmr_parser/clients/serial_.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 319becc..265cedd 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -21,6 +21,7 @@ def __init__(self, device, serial_settings, telegram_specification): self.telegram_parser = TelegramParser(telegram_specification) self.telegram_buffer = TelegramBuffer() + self.telegram_specification = telegram_specification def read(self): """ From d712d468ac5c3eee28d03e3fa893b0a2fa00fc75 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 25 Nov 2019 20:44:25 +0100 Subject: [PATCH 026/226] remove useless space --- dsmr_parser/objects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 940318a..e7374ab 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -47,7 +47,6 @@ def __iter__(self): def __str__(self): output = "" for attr,value in self: - output += " " if not output == "" else "" output += "{}: \t {} \t[{}]\n".format(attr,str(value.value),str(value.unit)) return output From a137ef0e02fa97dd11a1e28a25a0318cc9a4dc1b Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 1 Dec 2019 17:47:22 +0100 Subject: [PATCH 027/226] add some documentation for the use of the telegram as an object --- README.rst | 114 ++++++++++++++++++++++++++++++++++++++++- dsmr_parser/objects.py | 2 +- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7b53ee4..5f0d7d6 100644 --- a/README.rst +++ b/README.rst @@ -85,8 +85,8 @@ into a dictionary. telegram = parser.parse(telegram_str) print(telegram) # see 'Telegram object' docs below -Telegram object ---------------- +Telegram dictionary +------------------- A dictionary of which the key indicates the field type. These regex values correspond to one of dsmr_parser.obis_reference constants. @@ -138,6 +138,116 @@ Example to get some of the values: # See dsmr_reader.obis_references for all readable telegram values. # Note that the avilable values differ per DSMR version. +Telegram as an Object +--------------------- +An object version of the telegram is available as well. + + +.. code-block:: python + + # DSMR v4.2 p1 using dsmr_parser and telegram objects + + from dsmr_parser import telegram_specifications + from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5 + from dsmr_parser.objects import CosemObject, MBusObject, Telegram + from dsmr_parser.parsers import TelegramParser + import os + + serial_reader = SerialReader( + device='/dev/ttyUSB0', + serial_settings=SERIAL_SETTINGS_V5, + telegram_specification=telegram_specifications.V4 + ) + + # telegram = next(serial_reader.read_as_object()) + # print(telegram) + + for telegram in serial_reader.read_as_object(): + os.system('clear') + print(telegram) + +Example of output of print of the telegram object: + +.. code-block:: console + + P1_MESSAGE_HEADER: 42 [None] + P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None] + EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None] + ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh] + ELECTRICITY_USED_TARIFF_2: 1435.706 [kWh] + ELECTRICITY_DELIVERED_TARIFF_1: 0.000 [kWh] + ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh] + ELECTRICITY_ACTIVE_TARIFF: 0002 [None] + CURRENT_ELECTRICITY_USAGE: 2.027 [kW] + CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW] + LONG_POWER_FAILURE_COUNT: 7 [None] + VOLTAGE_SAG_L1_COUNT: 0 [None] + VOLTAGE_SAG_L2_COUNT: 0 [None] + VOLTAGE_SAG_L3_COUNT: 0 [None] + VOLTAGE_SWELL_L1_COUNT: 0 [None] + VOLTAGE_SWELL_L2_COUNT: 0 [None] + VOLTAGE_SWELL_L3_COUNT: 0 [None] + TEXT_MESSAGE_CODE: None [None] + TEXT_MESSAGE: None [None] + DEVICE_TYPE: 3 [None] + INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.170 [kW] + INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 1.247 [kW] + INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.209 [kW] + INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW] + INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW] + INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW] + EQUIPMENT_IDENTIFIER_GAS: 4819243993373755377509728609491464 [None] + HOURLY_GAS_METER_READING: 981.443 [m3] + +Accessing the telegrams information as attributes directly: + +.. code-block:: python + + telegram + Out[3]: + telegram.CURRENT_ELECTRICITY_USAGE.value + Out[4]: Decimal('2.027') + telegram.CURRENT_ELECTRICITY_USAGE.unit + Out[5]: 'kW' + +The telegram object has an iterator, can be used to find all the elements in the current telegram: + +.. code-block:: python + + for attr, value in telegram: + print(attr) + + Out[7]: + P1_MESSAGE_HEADER + P1_MESSAGE_TIMESTAMP + EQUIPMENT_IDENTIFIER + ELECTRICITY_USED_TARIFF_1 + ELECTRICITY_USED_TARIFF_2 + ELECTRICITY_DELIVERED_TARIFF_1 + ELECTRICITY_DELIVERED_TARIFF_2 + ELECTRICITY_ACTIVE_TARIFF + CURRENT_ELECTRICITY_USAGE + CURRENT_ELECTRICITY_DELIVERY + LONG_POWER_FAILURE_COUNT + VOLTAGE_SAG_L1_COUNT + VOLTAGE_SAG_L2_COUNT + VOLTAGE_SAG_L3_COUNT + VOLTAGE_SWELL_L1_COUNT + VOLTAGE_SWELL_L2_COUNT + VOLTAGE_SWELL_L3_COUNT + TEXT_MESSAGE_CODE + TEXT_MESSAGE + DEVICE_TYPE + INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE + INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE + INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE + INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE + INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE + INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE + EQUIPMENT_IDENTIFIER_GAS + HOURLY_GAS_METER_READING + + Installation ------------ diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index e7374ab..07d576d 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -46,7 +46,7 @@ def __iter__(self): def __str__(self): output = "" - for attr,value in self: + for attr, value in self: output += "{}: \t {} \t[{}]\n".format(attr,str(value.value),str(value.unit)) return output From 1b522fc7f088f22a8b54df5605dada8fc79cb5f7 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 1 Dec 2019 18:34:21 +0100 Subject: [PATCH 028/226] add some documentation for the use of the telegram as an object --- README.rst | 65 ++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/README.rst b/README.rst index 5f0d7d6..11ce600 100644 --- a/README.rst +++ b/README.rst @@ -210,43 +210,40 @@ Accessing the telegrams information as attributes directly: telegram.CURRENT_ELECTRICITY_USAGE.unit Out[5]: 'kW' -The telegram object has an iterator, can be used to find all the elements in the current telegram: +The telegram object has an iterator, can be used to find all the information elements in the current telegram: .. code-block:: python - for attr, value in telegram: - print(attr) - - Out[7]: - P1_MESSAGE_HEADER - P1_MESSAGE_TIMESTAMP - EQUIPMENT_IDENTIFIER - ELECTRICITY_USED_TARIFF_1 - ELECTRICITY_USED_TARIFF_2 - ELECTRICITY_DELIVERED_TARIFF_1 - ELECTRICITY_DELIVERED_TARIFF_2 - ELECTRICITY_ACTIVE_TARIFF - CURRENT_ELECTRICITY_USAGE - CURRENT_ELECTRICITY_DELIVERY - LONG_POWER_FAILURE_COUNT - VOLTAGE_SAG_L1_COUNT - VOLTAGE_SAG_L2_COUNT - VOLTAGE_SAG_L3_COUNT - VOLTAGE_SWELL_L1_COUNT - VOLTAGE_SWELL_L2_COUNT - VOLTAGE_SWELL_L3_COUNT - TEXT_MESSAGE_CODE - TEXT_MESSAGE - DEVICE_TYPE - INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE - INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE - INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE - INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE - INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE - INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE - EQUIPMENT_IDENTIFIER_GAS - HOURLY_GAS_METER_READING - + [attr for attr, value in telegram] + Out[11]: + ['P1_MESSAGE_HEADER', + 'P1_MESSAGE_TIMESTAMP', + 'EQUIPMENT_IDENTIFIER', + 'ELECTRICITY_USED_TARIFF_1', + 'ELECTRICITY_USED_TARIFF_2', + 'ELECTRICITY_DELIVERED_TARIFF_1', + 'ELECTRICITY_DELIVERED_TARIFF_2', + 'ELECTRICITY_ACTIVE_TARIFF', + 'CURRENT_ELECTRICITY_USAGE', + 'CURRENT_ELECTRICITY_DELIVERY', + 'LONG_POWER_FAILURE_COUNT', + 'VOLTAGE_SAG_L1_COUNT', + 'VOLTAGE_SAG_L2_COUNT', + 'VOLTAGE_SAG_L3_COUNT', + 'VOLTAGE_SWELL_L1_COUNT', + 'VOLTAGE_SWELL_L2_COUNT', + 'VOLTAGE_SWELL_L3_COUNT', + 'TEXT_MESSAGE_CODE', + 'TEXT_MESSAGE', + 'DEVICE_TYPE', + 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + 'EQUIPMENT_IDENTIFIER_GAS', + 'HOURLY_GAS_METER_READING'] Installation From ea804d485ebbf5f8bb98b5902e2a32ae69f21040 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 1 Dec 2019 18:39:21 +0100 Subject: [PATCH 029/226] add some documentation for the use of the telegram as an object --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 11ce600..47fc884 100644 --- a/README.rst +++ b/README.rst @@ -205,10 +205,12 @@ Accessing the telegrams information as attributes directly: telegram Out[3]: + telegram.CURRENT_ELECTRICITY_USAGE + Out[4]: telegram.CURRENT_ELECTRICITY_USAGE.value - Out[4]: Decimal('2.027') + Out[5]: Decimal('2.027') telegram.CURRENT_ELECTRICITY_USAGE.unit - Out[5]: 'kW' + Out[6]: 'kW' The telegram object has an iterator, can be used to find all the information elements in the current telegram: From aa4f9fb96751bcd2d210e347376ad71498afb6da Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 1 Dec 2019 18:55:53 +0100 Subject: [PATCH 030/226] update version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b20a2a8..673267f 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.14', + version='0.15', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 5313edd6cb84ace9a2549a0e482103d738338ace Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 9 Dec 2019 22:31:40 +0100 Subject: [PATCH 031/226] add file reader --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 20de282..4dfc343 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ /.coverage build/ dist/ +*.*~ +*~ \ No newline at end of file From 3c78b0b6c4f17246fa4a6a1d0e9db095ce987cbc Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 9 Dec 2019 22:36:46 +0100 Subject: [PATCH 032/226] add file reader --- dsmr_parser/clients/filereader.py | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 dsmr_parser/clients/filereader.py diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py new file mode 100644 index 0000000..ee1fc90 --- /dev/null +++ b/dsmr_parser/clients/filereader.py @@ -0,0 +1,35 @@ +from dsmr_parser import telegram_specifications +from dsmr_parser.clients.telegram_buffer import TelegramBuffer +from dsmr_parser.exceptions import ParseError, InvalidChecksumError +from dsmr_parser.objects import CosemObject, MBusObject, Telegram +from dsmr_parser.parsers import TelegramParser +import os + +logger = logging.getLogger(__name__) + +class FileReader(object): + + def __init__(self, file, telegram_specification): + self._file = file + self.telegram_parser = TelegramParser(telegram_specification) + self.telegram_buffer = TelegramBuffer() + self.telegram_specification = telegram_specification + + def read_as_object(self): + """ + Read complete DSMR telegram's from a file and return a Telegram object. + :rtype: generator + """ + with open(self._file,"rb") as file_handle: + while True: + data = file_handle.readline() + str = data.decode() + self.telegram_buffer.append(str) + + 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) From a65949823d9303055ff31dfe6cd4083c146721d7 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 9 Dec 2019 23:27:57 +0100 Subject: [PATCH 033/226] fix filereader --- dsmr_parser/clients/filereader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index ee1fc90..30396d9 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -1,9 +1,9 @@ -from dsmr_parser import telegram_specifications +import logging + from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError -from dsmr_parser.objects import CosemObject, MBusObject, Telegram +from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser -import os logger = logging.getLogger(__name__) From c1d7ba151d7bb7f3a58b38d44f8f0c880da0f365 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 9 Dec 2019 23:48:48 +0100 Subject: [PATCH 034/226] add some documentation for the file reader --- dsmr_parser/clients/filereader.py | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index 30396d9..dae492d 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -8,6 +8,44 @@ logger = logging.getLogger(__name__) class FileReader(object): + """ + Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects + for each read telegram. + Usage: + from dsmr_parser import telegram_specifications + from dsmr_parser.clients.filereader import FileReader + + if __name__== "__main__": + + infile = '/data/smartmeter/readings.txt' + + file_reader = FileReader( + file = infile, + telegram_specification = telegram_specifications.V4 + ) + + for telegram in file_reader.read_as_object(): + print(telegram) + + The file can be created like: + from dsmr_parser import telegram_specifications + from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5 + + if __name__== "__main__": + + outfile = '/data/smartmeter/readings.txt' + + serial_reader = SerialReader( + device='/dev/ttyUSB0', + serial_settings=SERIAL_SETTINGS_V5, + telegram_specification=telegram_specifications.V4 + ) + + for telegram in serial_reader.read_as_object(): + f=open(outfile,"ab+") + f.write(telegram._telegram_data.encode()) + f.close() + """ def __init__(self, file, telegram_specification): self._file = file From 7c9c59308e2284a9807e1a1e21cc93bc00bbf138 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 12 Dec 2019 22:20:16 +0100 Subject: [PATCH 035/226] `create_tcp_dsmr_reader` accepts `loop=None` but always expects a loop. Fixes #36 --- dsmr_parser/clients/protocol.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 141a389..2c9650e 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -49,12 +49,13 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): def create_tcp_dsmr_reader(host, port, dsmr_version, telegram_callback, loop=None): """Creates a DSMR asyncio protocol coroutine using TCP connection.""" + if not loop: + loop = asyncio.get_event_loop() protocol, _ = create_dsmr_protocol( - dsmr_version, telegram_callback, loop=None) + dsmr_version, telegram_callback, loop=loop) conn = loop.create_connection(protocol, host, port) return conn - class DSMRProtocol(asyncio.Protocol): """Assemble and handle incoming data into complete DSM telegrams.""" From 12aa003799ee3240f8c523e9133101dcd37778a1 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 12 Dec 2019 22:29:18 +0100 Subject: [PATCH 036/226] Updated version number --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5fac8e7..3b334c9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.15** (2019-12-12) + +- Fixed asyncio loop issue (`pull request #37 `_). + **0.14** (2019-10-08) - Changed serial reading to reduce CPU usage (`pull request #37 `_). diff --git a/setup.py b/setup.py index b20a2a8..673267f 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.14', + version='0.15', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 3a8b4d24582d299751df0645ad4f5de7411a9baa Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 12 Dec 2019 22:33:14 +0100 Subject: [PATCH 037/226] Corrected changelog --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b334c9..584c593 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,7 @@ Change Log **0.15** (2019-12-12) -- Fixed asyncio loop issue (`pull request #37 `_). +- Fixed asyncio loop issue (`pull request #43 `_). **0.14** (2019-10-08) From 50cef2646b18171ec6b50e4abde75438813815b5 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 16 Dec 2019 15:30:55 +0100 Subject: [PATCH 038/226] add fileinputreader --- dsmr_parser/clients/filereader.py | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index dae492d..c2a35cb 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -71,3 +71,47 @@ def read_as_object(self): logger.warning(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e) + +class FileInputReader(object): + """ + Filereader to read and parse raw telegram strings from stdin or files specified at the commandline + and instantiate Telegram objects for each read telegram. + Usage: + from dsmr_parser import telegram_specifications + from dsmr_parser.clients.filereader import FileInputReader + + if __name__== "__main__": + + fileinput_reader = FileReader( + file = infile, + telegram_specification = telegram_specifications.V4 + ) + + for telegram in fileinput_reader.read_as_object(): + print(telegram) + """ + + def __init__(self, telegram_specification): + self.telegram_parser = TelegramParser(telegram_specification) + self.telegram_buffer = TelegramBuffer() + self.telegram_specification = telegram_specification + + def read_as_object(self): + """ + Read complete DSMR telegram's from stdin of filearguments specified on teh command line + and return a Telegram object. + :rtype: generator + """ + with fileinput.input(mode='rb') as file_handle: + while True: + data = file_handle.readline() + str = data.decode() + self.telegram_buffer.append(str) + + 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) From 43500e6bc25e596ef985b39481a86adedcfa541d Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 16 Dec 2019 15:36:13 +0100 Subject: [PATCH 039/226] fix failing import --- dsmr_parser/clients/filereader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index c2a35cb..f4ed6fd 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -1,4 +1,5 @@ import logging +import fileinput from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError From 2d4b0d8e7266acf4f5c938157d1cb203ebe6a426 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 16 Dec 2019 15:41:24 +0100 Subject: [PATCH 040/226] fix documentation FileInputReader --- dsmr_parser/clients/filereader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index f4ed6fd..e6eeb59 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -77,7 +77,7 @@ class FileInputReader(object): """ Filereader to read and parse raw telegram strings from stdin or files specified at the commandline and instantiate Telegram objects for each read telegram. - Usage: + Usage python script "syphon_smartmeter_readings_stdin.py": from dsmr_parser import telegram_specifications from dsmr_parser.clients.filereader import FileInputReader @@ -90,6 +90,10 @@ class FileInputReader(object): for telegram in fileinput_reader.read_as_object(): print(telegram) + + Command line: + tail -f /data/smartmeter/readings.txt | python3 syphon_smartmeter_readings_stdin.py + """ def __init__(self, telegram_specification): From 3bfb555d0ea94cb623bdd64ce1c1ad5f47eeb9e1 Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Thu, 19 Dec 2019 04:19:29 +0100 Subject: [PATCH 041/226] Add support for Belgian and Smarty meters --- dsmr_parser/clients/protocol.py | 3 +++ dsmr_parser/obis_references.py | 5 +++++ dsmr_parser/telegram_specifications.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 2c9650e..7e8e260 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -26,6 +26,9 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): elif dsmr_version == '5': specification = telegram_specifications.V5 serial_settings = SERIAL_SETTINGS_V5 + elif dsmr_version == '5B': + specification = telegram_specifications.BELGIUM_FLUVIUS + serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 5050f43..cb7b158 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -60,3 +60,8 @@ ELECTRICITY_DELIVERED_TARIFF_1, ELECTRICITY_DELIVERED_TARIFF_2 ) + +# Alternate codes for foreign countries. +BELGIUM_HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n' # Different code, same format. +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-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index e2c0bf5..a42806f 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -1,4 +1,5 @@ from decimal import Decimal +from copy import deepcopy from dsmr_parser import obis_references as obis from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser @@ -128,3 +129,18 @@ } ALL = (V2_2, V3, V4, V5) + + +BELGIUM_FLUVIUS = deepcopy(V5) +BELGIUM_FLUVIUS['objects'].update({ + obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ) +}) + +LUXEMBOURG_SMARTY = deepcopy(V5) +LUXEMBOURG_SMARTY['objects'].update({ + obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), + obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), +}) From a01e67364630c10da635b9d43eaa535f05feff63 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 21 Dec 2019 15:02:13 +0100 Subject: [PATCH 042/226] updated changelog for new release --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 584c593..babab7c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.16** (2019-12-21) + +- Add support for Belgian and Smarty meters (`pull request #44 `_). + **0.15** (2019-12-12) - Fixed asyncio loop issue (`pull request #43 `_). diff --git a/setup.py b/setup.py index 673267f..fe434f6 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.15', + version='0.16', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From eac9681e0792294cd5fa384fc8f858cda6b77dc0 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 21 Dec 2019 17:37:50 +0100 Subject: [PATCH 043/226] updated changelog --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index babab7c..fe4d4ea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.17** (2019-12-21) + +- Add a true telegram object (`pull request #40 `_). + **0.16** (2019-12-21) - Add support for Belgian and Smarty meters (`pull request #44 `_). From 8c2485d70f5fe9743be05d6d7b9f72b339e6707c Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Tue, 24 Dec 2019 00:57:44 +0100 Subject: [PATCH 044/226] added file tail reader --- dsmr_parser/clients/filereader.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index e6eeb59..12164f2 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -1,5 +1,6 @@ import logging import fileinput +import tailer from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError @@ -120,3 +121,31 @@ def read_as_object(self): logger.warning(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e) + + +class FileTailReader(object): + + def __init__(self, file, telegram_specification): + self._file = file + self.telegram_parser = TelegramParser(telegram_specification) + self.telegram_buffer = TelegramBuffer() + self.telegram_specification = telegram_specification + + def read_as_object(self): + """ + Read complete DSMR telegram's from a files tail and return a Telegram object. + :rtype: generator + """ + with open(self._file,"rb") as file_handle: + for data in tailer.follow(file_handle): + str = data.decode() + self.telegram_buffer.append(str) + + for telegram in self.telegram_buffer.get_all(): + try: + yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + except dsmr_parser.exceptions.InvalidChecksumError as e: + logger.warning(str(e)) + except dsmr_parser.exceptions.ParseError as e: + logger.error('Failed to parse telegram: %s', e) + From e6625df4a7ff66da22cab1c74e80f393a9e4cd02 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Fri, 27 Dec 2019 15:18:35 +0100 Subject: [PATCH 045/226] add documentation to FileTailReader --- dsmr_parser/clients/filereader.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index 12164f2..3869a8a 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -124,6 +124,25 @@ def read_as_object(self): class FileTailReader(object): + """ + Filereader to read and parse raw telegram strings from the tail of a + given file and instantiate Telegram objects for each read telegram. + Usage python script "syphon_smartmeter_readings_stdin.py": + from dsmr_parser import telegram_specifications + from dsmr_parser.clients.filereader import FileTailReader + + if __name__== "__main__": + + infile = '/data/smartmeter/readings.txt' + + filetail_reader = FileTailReader( + file = infile, + telegram_specification = telegram_specifications.V5 + ) + + for telegram in filetail_reader.read_as_object(): + print(telegram) + """ def __init__(self, file, telegram_specification): self._file = file From 0675a6e2e7af1f626d90abc3cf37341ae26233dd Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Fri, 27 Dec 2019 16:22:10 +0100 Subject: [PATCH 046/226] add tailer dependency --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e1f0139..0c6015d 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,8 @@ 'pyserial>=3,<4', 'pyserial-asyncio<1', 'pytz', - 'PyCRC>=1.2,<2' + 'PyCRC>=1.2,<2', + 'Tailer==0.4.1' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] From d714528c5a8ec8e4286176c5711398100bc2c23c Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Tue, 28 Jan 2020 17:26:45 +0100 Subject: [PATCH 047/226] Include needed PyCRC code --- dsmr_parser/parsers.py | 27 +++++++++++++++++++++++++-- setup.py | 1 - tox.ini | 1 - 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 4b415f6..4609287 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -1,7 +1,7 @@ import logging import re -from PyCRC.CRC16 import CRC16 +from ctypes import c_ushort from dsmr_parser.objects import MBusObject, CosemObject, Telegram from dsmr_parser.exceptions import ParseError, InvalidChecksumError @@ -79,7 +79,7 @@ def validate_checksum(telegram): 'incomplete. The checksum and/or content values are missing.' ) - calculated_crc = CRC16().calculate(checksum_contents.group(0)) + calculated_crc = TelegramParser.crc16(checksum_contents.group(0)) expected_crc = int(checksum_hex.group(0), base=16) if calculated_crc != expected_crc: @@ -91,6 +91,29 @@ def validate_checksum(telegram): ) ) + @staticmethod + def crc16(telegram): + crc16_tab = [] + + for i in range(0, 256): + crc = c_ushort(i).value + for j in range(0, 8): + if (crc & 0x0001): + crc = c_ushort(crc >> 1).value ^ 0xA001 + else: + crc = c_ushort(crc >> 1).value + crc16_tab.append(hex(crc)) + + crcValue = 0x0000 + + for c in telegram: + d = ord(c) + tmp = crcValue ^ d + rotated = c_ushort(crcValue >> 8).value + crcValue = rotated ^ int(crc16_tab[(tmp & 0x00ff)], 0) + + return crcValue + class DSMRObjectParser(object): """ diff --git a/setup.py b/setup.py index e1f0139..7c54d73 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,6 @@ 'pyserial>=3,<4', 'pyserial-asyncio<1', 'pytz', - 'PyCRC>=1.2,<2' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] diff --git a/tox.ini b/tox.ini index 23fe214..3efa213 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ deps= pytest-asyncio pytest-catchlog pytest-mock - PyCRC commands= py.test --cov=dsmr_parser test {posargs} pylama dsmr_parser test From dc6c35a0b6dc5a96e15d8863933125dba61bf1d2 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 28 Jan 2020 19:40:27 +0100 Subject: [PATCH 048/226] Updated changelog --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fe4d4ea..0fda0e3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.18** (2020-01-28) + +- PyCRC replacement (`pull request #48 `_). + **0.17** (2019-12-21) - Add a true telegram object (`pull request #40 `_). diff --git a/setup.py b/setup.py index 7c54d73..78eba62 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.17', + version='0.18', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From fee3f696c46a062c1ca717773372e777bc797d0e Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 2 Feb 2020 17:26:47 +0100 Subject: [PATCH 049/226] merged upstream 0.18 version and resolved conflict --- CHANGELOG.rst | 8 ++++++++ dsmr_parser/parsers.py | 27 +++++++++++++++++++++++++-- setup.py | 3 +-- tox.ini | 1 - 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index babab7c..0fda0e3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Change Log ---------- +**0.18** (2020-01-28) + +- PyCRC replacement (`pull request #48 `_). + +**0.17** (2019-12-21) + +- Add a true telegram object (`pull request #40 `_). + **0.16** (2019-12-21) - Add support for Belgian and Smarty meters (`pull request #44 `_). diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 4b415f6..4609287 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -1,7 +1,7 @@ import logging import re -from PyCRC.CRC16 import CRC16 +from ctypes import c_ushort from dsmr_parser.objects import MBusObject, CosemObject, Telegram from dsmr_parser.exceptions import ParseError, InvalidChecksumError @@ -79,7 +79,7 @@ def validate_checksum(telegram): 'incomplete. The checksum and/or content values are missing.' ) - calculated_crc = CRC16().calculate(checksum_contents.group(0)) + calculated_crc = TelegramParser.crc16(checksum_contents.group(0)) expected_crc = int(checksum_hex.group(0), base=16) if calculated_crc != expected_crc: @@ -91,6 +91,29 @@ def validate_checksum(telegram): ) ) + @staticmethod + def crc16(telegram): + crc16_tab = [] + + for i in range(0, 256): + crc = c_ushort(i).value + for j in range(0, 8): + if (crc & 0x0001): + crc = c_ushort(crc >> 1).value ^ 0xA001 + else: + crc = c_ushort(crc >> 1).value + crc16_tab.append(hex(crc)) + + crcValue = 0x0000 + + for c in telegram: + d = ord(c) + tmp = crcValue ^ d + rotated = c_ushort(crcValue >> 8).value + crcValue = rotated ^ int(crc16_tab[(tmp & 0x00ff)], 0) + + return crcValue + class DSMRObjectParser(object): """ diff --git a/setup.py b/setup.py index 0c6015d..beb8e57 100644 --- a/setup.py +++ b/setup.py @@ -6,13 +6,12 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.17', + version='0.19', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', 'pyserial-asyncio<1', 'pytz', - 'PyCRC>=1.2,<2', 'Tailer==0.4.1' ], entry_points={ diff --git a/tox.ini b/tox.ini index 23fe214..3efa213 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ deps= pytest-asyncio pytest-catchlog pytest-mock - PyCRC commands= py.test --cov=dsmr_parser test {posargs} pylama dsmr_parser test From b6537678a70961ecabfc618273a46f5bc1dabd15 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 2 Feb 2020 22:12:25 +0100 Subject: [PATCH 050/226] cleaned up based on pylama complaints / pinpointed to coverage version 4.5.4 as next version is incompatible --- dsmr_parser/clients/filereader.py | 11 +++-- dsmr_parser/clients/protocol.py | 1 + dsmr_parser/obis_name_mapping.py | 80 +++++++++++++++---------------- dsmr_parser/objects.py | 3 +- dsmr_parser/parsers.py | 2 +- test/experiment_telegram.py | 10 +--- test/test_telegram.py | 23 +++------ tox.ini | 12 +++-- 8 files changed, 67 insertions(+), 75 deletions(-) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index 3869a8a..061eda7 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -9,6 +9,7 @@ logger = logging.getLogger(__name__) + class FileReader(object): """ Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects @@ -60,7 +61,7 @@ def read_as_object(self): Read complete DSMR telegram's from a file and return a Telegram object. :rtype: generator """ - with open(self._file,"rb") as file_handle: + with open(self._file, "rb") as file_handle: while True: data = file_handle.readline() str = data.decode() @@ -74,6 +75,7 @@ def read_as_object(self): except ParseError as e: logger.error('Failed to parse telegram: %s', e) + class FileInputReader(object): """ Filereader to read and parse raw telegram strings from stdin or files specified at the commandline @@ -155,7 +157,7 @@ def read_as_object(self): Read complete DSMR telegram's from a files tail and return a Telegram object. :rtype: generator """ - with open(self._file,"rb") as file_handle: + with open(self._file, "rb") as file_handle: for data in tailer.follow(file_handle): str = data.decode() self.telegram_buffer.append(str) @@ -163,8 +165,7 @@ def read_as_object(self): for telegram in self.telegram_buffer.get_all(): try: yield Telegram(telegram, self.telegram_parser, self.telegram_specification) - except dsmr_parser.exceptions.InvalidChecksumError as e: + except InvalidChecksumError as e: logger.warning(str(e)) - except dsmr_parser.exceptions.ParseError as e: + except ParseError as e: logger.error('Failed to parse telegram: %s', e) - diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 7e8e260..e43e230 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -59,6 +59,7 @@ def create_tcp_dsmr_reader(host, port, dsmr_version, conn = loop.create_connection(protocol, host, port) return conn + class DSMRProtocol(asyncio.Protocol): """Assemble and handle incoming data into complete DSM telegrams.""" diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 8f72654..0401f5e 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -10,45 +10,45 @@ EN = { obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', - obis.ELECTRICITY_IMPORTED_TOTAL : 'ELECTRICITY_IMPORTED_TOTAL', - obis.ELECTRICITY_USED_TARIFF_1 : 'ELECTRICITY_USED_TARIFF_1', - obis.ELECTRICITY_USED_TARIFF_2 : 'ELECTRICITY_USED_TARIFF_2', - obis.ELECTRICITY_DELIVERED_TARIFF_1 : 'ELECTRICITY_DELIVERED_TARIFF_1', - obis.ELECTRICITY_DELIVERED_TARIFF_2 : 'ELECTRICITY_DELIVERED_TARIFF_2', - obis.ELECTRICITY_ACTIVE_TARIFF : 'ELECTRICITY_ACTIVE_TARIFF', - obis.EQUIPMENT_IDENTIFIER : 'EQUIPMENT_IDENTIFIER', - obis.CURRENT_ELECTRICITY_USAGE : 'CURRENT_ELECTRICITY_USAGE', - obis.CURRENT_ELECTRICITY_DELIVERY : 'CURRENT_ELECTRICITY_DELIVERY', - obis.LONG_POWER_FAILURE_COUNT : 'LONG_POWER_FAILURE_COUNT', - obis.SHORT_POWER_FAILURE_COUNT : 'SHORT_POWER_FAILURE_COUNT', - obis.POWER_EVENT_FAILURE_LOG : 'POWER_EVENT_FAILURE_LOG', - obis.VOLTAGE_SAG_L1_COUNT : 'VOLTAGE_SAG_L1_COUNT', - obis.VOLTAGE_SAG_L2_COUNT : 'VOLTAGE_SAG_L2_COUNT', - obis.VOLTAGE_SAG_L3_COUNT : 'VOLTAGE_SAG_L3_COUNT', - obis.VOLTAGE_SWELL_L1_COUNT : 'VOLTAGE_SWELL_L1_COUNT', - obis.VOLTAGE_SWELL_L2_COUNT : 'VOLTAGE_SWELL_L2_COUNT', - obis.VOLTAGE_SWELL_L3_COUNT : 'VOLTAGE_SWELL_L3_COUNT', - obis.INSTANTANEOUS_VOLTAGE_L1 : 'INSTANTANEOUS_VOLTAGE_L1', - obis.INSTANTANEOUS_VOLTAGE_L2 : 'INSTANTANEOUS_VOLTAGE_L2', - obis.INSTANTANEOUS_VOLTAGE_L3 : 'INSTANTANEOUS_VOLTAGE_L3', - obis.INSTANTANEOUS_CURRENT_L1 : 'INSTANTANEOUS_CURRENT_L1', - obis.INSTANTANEOUS_CURRENT_L2 : 'INSTANTANEOUS_CURRENT_L2', - obis.INSTANTANEOUS_CURRENT_L3 : 'INSTANTANEOUS_CURRENT_L3', - obis.TEXT_MESSAGE_CODE : 'TEXT_MESSAGE_CODE', - obis.TEXT_MESSAGE : 'TEXT_MESSAGE', - obis.DEVICE_TYPE : 'DEVICE_TYPE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', - obis.EQUIPMENT_IDENTIFIER_GAS : 'EQUIPMENT_IDENTIFIER_GAS', - obis.HOURLY_GAS_METER_READING : 'HOURLY_GAS_METER_READING', - obis.GAS_METER_READING : 'GAS_METER_READING', - obis.ACTUAL_TRESHOLD_ELECTRICITY : 'ACTUAL_TRESHOLD_ELECTRICITY', - obis.ACTUAL_SWITCH_POSITION : 'ACTUAL_SWITCH_POSITION', - obis.VALVE_POSITION_GAS : 'VALVE_POSITION_GAS' + obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL', + obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1', + obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2', + obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1', + obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2', + obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF', + obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER', + obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE', + obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY', + obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT', + obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT', + obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG', + obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT', + obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT', + obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT', + obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT', + obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT', + obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT', + obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1', + obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2', + obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3', + obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1', + obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2', + obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3', + obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE', + obis.TEXT_MESSAGE: 'TEXT_MESSAGE', + obis.DEVICE_TYPE: 'DEVICE_TYPE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS', + obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING', + obis.GAS_METER_READING: 'GAS_METER_READING', + obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', + obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', + obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS' } -REVERSE_EN = dict([ (v,k) for k,v in EN.items()]) \ No newline at end of file +REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 07d576d..e313cd5 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,5 +1,6 @@ import dsmr_parser.obis_name_mapping + class Telegram(object): """ Container for raw and parsed telegram data. @@ -47,7 +48,7 @@ def __iter__(self): def __str__(self): output = "" for attr, value in self: - output += "{}: \t {} \t[{}]\n".format(attr,str(value.value),str(value.unit)) + output += "{}: \t {} \t[{}]\n".format(attr, str(value.value), str(value.unit)) return output diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 4609287..d9aeb5a 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,7 +3,7 @@ from ctypes import c_ushort -from dsmr_parser.objects import MBusObject, CosemObject, Telegram +from dsmr_parser.objects import MBusObject, CosemObject from dsmr_parser.exceptions import ParseError, InvalidChecksumError logger = logging.getLogger(__name__) diff --git a/test/experiment_telegram.py b/test/experiment_telegram.py index 2649f51..2892346 100644 --- a/test/experiment_telegram.py +++ b/test/experiment_telegram.py @@ -1,14 +1,8 @@ -from decimal import Decimal -import datetime -import unittest -import pytz -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, Telegram +from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser from example_telegrams import TELEGRAM_V4_2 parser = TelegramParser(telegram_specifications.V4) telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) -print(telegram) \ No newline at end of file +print(telegram) diff --git a/test/test_telegram.py b/test/test_telegram.py index d0e1042..ea85704 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -1,30 +1,21 @@ -from decimal import Decimal - -import datetime import unittest -import pytz - -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, Telegram +from dsmr_parser.objects import CosemObject +from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser from test.example_telegrams import TELEGRAM_V4_2 + class TelegramTest(unittest.TestCase): """ Test instantiation of Telegram object """ def test_instantiate(self): parser = TelegramParser(telegram_specifications.V4) - #result = parser.parse(TELEGRAM_V4_2) telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) - - - # P1_MESSAGE_HEADER (1-3:0.2.8) - #assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject) - #assert result[obis.P1_MESSAGE_HEADER].unit is None - #assert isinstance(result[obis.P1_MESSAGE_HEADER].value, str) - #assert result[obis.P1_MESSAGE_HEADER].value == '50' + testitem = telegram.P1_MESSAGE_HEADER + assert isinstance(testitem, CosemObject) + assert testitem.unit is None + assert testitem.value == '42' diff --git a/tox.ini b/tox.ini index 3efa213..6b0f152 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -envlist = py34,py35,p36 +envlist = py34,py35,py36,py37 +requires = coverage<=4.5.4 [testenv] deps= @@ -14,10 +15,13 @@ commands= pylama dsmr_parser test [pylama:dsmr_parser/clients/__init__.py] -ignore = W0611,W0605 +ignore = W0611 + +[pylama:dsmr_parser/parsers.py] +ignore = W605 [pylama:pylint] -max_line_length = 100 +max_line_length = 120 [pylama:pycodestyle] -max_line_length = 100 +max_line_length = 120 From 5d88284d8d4dbb6bda1b5d484c6cb5869ae9ad19 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 2 Feb 2020 22:34:17 +0100 Subject: [PATCH 051/226] remove requires property again --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6b0f152..a3e12f0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist = py34,py35,py36,py37 -requires = coverage<=4.5.4 [testenv] deps= From a7b0b03391f4ed82f670abda49276e3d45518bbf Mon Sep 17 00:00:00 2001 From: lowdef Date: Sun, 2 Feb 2020 22:52:06 +0100 Subject: [PATCH 052/226] remove conflicting entries --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index beb8e57..a33f5a4 100644 --- a/setup.py +++ b/setup.py @@ -6,13 +6,12 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.19', + version='0.18', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', 'pyserial-asyncio<1', - 'pytz', - 'Tailer==0.4.1' + 'pytz' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] From fe710e927286cde8c2628af4017409ece7ab0142 Mon Sep 17 00:00:00 2001 From: lowdef Date: Sun, 2 Feb 2020 22:54:21 +0100 Subject: [PATCH 053/226] redo essential change --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a33f5a4..9c47c51 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,8 @@ install_requires=[ 'pyserial>=3,<4', 'pyserial-asyncio<1', - 'pytz' + 'pytz', + 'Tailer==0.4.1' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] From 78419f6cc78e67404262de202a01db5f891fa879 Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Sun, 16 Feb 2020 09:14:31 +0100 Subject: [PATCH 054/226] Improve CRC speed --- dsmr_parser/parsers.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 4609287..0921dc7 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -11,6 +11,8 @@ class TelegramParser(object): + crc16_tab = [] + def __init__(self, telegram_specification, apply_checksum_validation=True): """ :param telegram_specification: determines how the telegram is parsed @@ -93,24 +95,28 @@ def validate_checksum(telegram): @staticmethod def crc16(telegram): - crc16_tab = [] - - for i in range(0, 256): - crc = c_ushort(i).value - for j in range(0, 8): - if (crc & 0x0001): - crc = c_ushort(crc >> 1).value ^ 0xA001 - else: - crc = c_ushort(crc >> 1).value - crc16_tab.append(hex(crc)) + """ + Calculate the CRC16 value for the given telegram + :param str telegram: + """ crcValue = 0x0000 + if len(TelegramParser.crc16_tab) == 0: + for i in range(0, 256): + crc = c_ushort(i).value + for j in range(0, 8): + if (crc & 0x0001): + crc = c_ushort(crc >> 1).value ^ 0xA001 + else: + crc = c_ushort(crc >> 1).value + TelegramParser.crc16_tab.append(hex(crc)) + for c in telegram: d = ord(c) tmp = crcValue ^ d rotated = c_ushort(crcValue >> 8).value - crcValue = rotated ^ int(crc16_tab[(tmp & 0x00ff)], 0) + crcValue = rotated ^ int(TelegramParser.crc16_tab[(tmp & 0x00ff)], 0) return crcValue From 88c9ccd83d3fc5807febfd31ffcbf9f240b88ed7 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Fri, 1 May 2020 20:36:35 +0200 Subject: [PATCH 055/226] Add following missing signatures to the V4 telegram specification SHORT_POWER_FAILURE_COUNT, INSTANTANEOUS_CURRENT_L1, INSTANTANEOUS_CURRENT_L2, INSTANTANEOUS_CURRENT_L3. --- dsmr_parser/telegram_specifications.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index a42806f..2e2ff45 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -58,6 +58,7 @@ obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), @@ -69,6 +70,9 @@ obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), obis.DEVICE_TYPE: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), From c4331f6cd6d2b182b2ba9f511d0badf14d4b4cc6 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sat, 2 May 2020 15:50:00 +0200 Subject: [PATCH 056/226] add tests for the missing elements and correct some test bugs --- test/test_parse_v4_2.py | 26 +++- test/test_parse_v5.py | 2 +- test/test_telegram.py | 283 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 305 insertions(+), 6 deletions(-) diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 681783b..cab34f7 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -80,6 +80,12 @@ def test_parse(self): assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal) assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0') + # SHORT_POWER_FAILURE_COUNT (1-0:96.7.21) + assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject) + assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None + assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int) + assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 15 + # LONG_POWER_FAILURE_COUNT (96.7.9) assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject) assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None @@ -132,8 +138,26 @@ def test_parse(self): assert result[obis.TEXT_MESSAGE].unit is None assert result[obis.TEXT_MESSAGE].value is None + # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0') + + # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('6') + + # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('2') + # DEVICE_TYPE (0-x:24.1.0) - assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) + assert isinstance(result[obis.DEVICE_TYPE], CosemObject) assert result[obis.DEVICE_TYPE].unit is None assert isinstance(result[obis.DEVICE_TYPE].value, int) assert result[obis.DEVICE_TYPE].value == 3 diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index e9cfbc1..67d7cd8 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -171,7 +171,7 @@ def test_parse(self): assert result[obis.TEXT_MESSAGE].value is None # DEVICE_TYPE (0-x:24.1.0) - assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) + assert isinstance(result[obis.DEVICE_TYPE], CosemObject) assert result[obis.DEVICE_TYPE].unit is None assert isinstance(result[obis.DEVICE_TYPE].value, int) assert result[obis.DEVICE_TYPE].value == 3 diff --git a/test/test_telegram.py b/test/test_telegram.py index ea85704..b553714 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -1,21 +1,296 @@ import unittest +import datetime +import pytz from dsmr_parser import telegram_specifications +from dsmr_parser import obis_name_mapping from dsmr_parser.objects import CosemObject +from dsmr_parser.objects import MBusObject from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser from test.example_telegrams import TELEGRAM_V4_2 +from decimal import Decimal class TelegramTest(unittest.TestCase): """ Test instantiation of Telegram object """ + def __init__(self, *args, **kwargs): + self.item_names_tested = [] + super(TelegramTest, self).__init__(*args, **kwargs) + + def verify_telegram_item(self, telegram, testitem_name, object_type, unit_val, value_type, value_val): + testitem = eval("telegram.{}".format(testitem_name)) + assert isinstance(testitem, object_type) + assert testitem.unit == unit_val + assert isinstance(testitem.value, value_type) + assert testitem.value == value_val + self.item_names_tested.append(testitem_name) + def test_instantiate(self): parser = TelegramParser(telegram_specifications.V4) telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) # P1_MESSAGE_HEADER (1-3:0.2.8) - testitem = telegram.P1_MESSAGE_HEADER - assert isinstance(testitem, CosemObject) - assert testitem.unit is None - assert testitem.value == '42' + self.verify_telegram_item(telegram, + 'P1_MESSAGE_HEADER', + object_type=CosemObject, + unit_val=None, + value_type=str, + value_val='42') + + # P1_MESSAGE_TIMESTAMP (0-0:1.0.0) + self.verify_telegram_item(telegram, + 'P1_MESSAGE_TIMESTAMP', + CosemObject, + unit_val=None, + value_type=datetime.datetime, + value_val=datetime.datetime(2016, 11, 13, 19, 57, 57, tzinfo=pytz.UTC)) + + # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) + self.verify_telegram_item(telegram, + 'ELECTRICITY_USED_TARIFF_1', + object_type=CosemObject, + unit_val='kWh', + value_type=Decimal, + value_val=Decimal('1581.123')) + + # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) + self.verify_telegram_item(telegram, + 'ELECTRICITY_USED_TARIFF_2', + object_type=CosemObject, + unit_val='kWh', + value_type=Decimal, + value_val=Decimal('1435.706')) + + # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) + self.verify_telegram_item(telegram, + 'ELECTRICITY_DELIVERED_TARIFF_1', + object_type=CosemObject, + unit_val='kWh', + value_type=Decimal, + value_val=Decimal('0')) + + # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) + self.verify_telegram_item(telegram, + 'ELECTRICITY_DELIVERED_TARIFF_2', + object_type=CosemObject, + unit_val='kWh', + value_type=Decimal, + value_val=Decimal('0')) + + # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) + self.verify_telegram_item(telegram, + 'ELECTRICITY_ACTIVE_TARIFF', + object_type=CosemObject, + unit_val=None, + value_type=str, + value_val='0002') + + # EQUIPMENT_IDENTIFIER (0-0:96.1.1) + self.verify_telegram_item(telegram, + 'EQUIPMENT_IDENTIFIER', + object_type=CosemObject, + unit_val=None, + value_type=str, + value_val='3960221976967177082151037881335713') + + # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) + self.verify_telegram_item(telegram, + 'CURRENT_ELECTRICITY_USAGE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('2.027')) + + # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) + self.verify_telegram_item(telegram, + 'CURRENT_ELECTRICITY_DELIVERY', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0')) + + # SHORT_POWER_FAILURE_COUNT (1-0:96.7.21) + self.verify_telegram_item(telegram, + 'SHORT_POWER_FAILURE_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=15) + + # LONG_POWER_FAILURE_COUNT (96.7.9) + self.verify_telegram_item(telegram, + 'LONG_POWER_FAILURE_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=7) + + # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SAG_L1_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SAG_L2_COUNT (1-0:52.32.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SAG_L2_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SAG_L3_COUNT (1-0:72.32.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SAG_L3_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SWELL_L1_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SWELL_L2_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SWELL_L3_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # TEXT_MESSAGE_CODE (0-0:96.13.1) + self.verify_telegram_item(telegram, + 'TEXT_MESSAGE_CODE', + object_type=CosemObject, + unit_val=None, + value_type=type(None), + value_val=None) + + # TEXT_MESSAGE (0-0:96.13.0) + self.verify_telegram_item(telegram, + 'TEXT_MESSAGE', + object_type=CosemObject, + unit_val=None, + value_type=type(None), + value_val=None) + + # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_CURRENT_L1', + object_type=CosemObject, + unit_val='A', + value_type=Decimal, + value_val=Decimal('0')) + + # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_CURRENT_L2', + object_type=CosemObject, + unit_val='A', + value_type=Decimal, + value_val=Decimal('6')) + + # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_CURRENT_L3', + object_type=CosemObject, + unit_val='A', + value_type=Decimal, + value_val=Decimal('2')) + + # DEVICE_TYPE (0-x:24.1.0) + self.verify_telegram_item(telegram, + 'DEVICE_TYPE', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=3) + + # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0.170')) + + # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('1.247')) + + # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0.209')) + + # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0')) + + # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0')) + + # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0')) + + # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) + self.verify_telegram_item(telegram, + 'EQUIPMENT_IDENTIFIER_GAS', + object_type=CosemObject, + unit_val=None, + value_type=str, + value_val='4819243993373755377509728609491464') + + # HOURLY_GAS_METER_READING (0-1:24.2.1) + self.verify_telegram_item(telegram, + 'HOURLY_GAS_METER_READING', + object_type=MBusObject, + unit_val='m3', + value_type=Decimal, + value_val=Decimal('981.443')) + + # check if all items in telegram V4 specification are covered + V4_name_list = [obis_name_mapping.EN[signature] for signature, parser in + telegram_specifications.V4['objects'].items()] + V4_name_set = set(V4_name_list) + item_names_tested_set = set(self.item_names_tested) + + assert item_names_tested_set == V4_name_set From fc4a96ebab4a9a6a536e542407541cabcc0e7d5d Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 3 May 2020 11:18:08 +0200 Subject: [PATCH 057/226] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9c47c51..beb8e57 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.18', + version='0.19', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From d98c93a57f2c9dc0e4009c999d6b1e7c56d7cac8 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 3 May 2020 11:37:53 +0200 Subject: [PATCH 058/226] modified changelog --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0fda0e3..5800449 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,14 @@ Change Log ---------- +**0.19** (2020-05-03) + +- Add following missing elements to telegram specification v4: + - SHORT_POWER_FAILURE_COUNT, + - INSTANTANEOUS_CURRENT_L1, + - INSTANTANEOUS_CURRENT_L2, + - INSTANTANEOUS_CURRENT_L3 +- Add missing tests + fix small test bugs +- Complete telegram object v4 parse test **0.18** (2020-01-28) From a44afb1a59536dc7ad28352165a231fe964e2126 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 10 May 2020 20:47:11 +0200 Subject: [PATCH 059/226] ignore .venv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4dfc343..33bb528 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.pyc .tox .cache +.venv *.egg-info /.project /.pydevproject From d1ad4fa5851946de3efa399dc49c02a4e5b39e91 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 11 May 2020 21:08:20 +0200 Subject: [PATCH 060/226] igonre venv/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 33bb528..6789bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,6 @@ /.coverage build/ dist/ +venv/ *.*~ *~ \ No newline at end of file From a0ce89054a02651c86fe8a4559a4218a5047df5c Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Tue, 12 May 2020 23:45:16 +0200 Subject: [PATCH 061/226] make all objects able to print their own values --- CHANGELOG.rst | 7 ++++++- dsmr_parser/objects.py | 14 +++++++++++++- dsmr_parser/parsers.py | 18 +++++++++++++++++- dsmr_parser/profile_generic_specifications.py | 14 ++++++++++++++ setup.py | 2 +- 5 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 dsmr_parser/profile_generic_specifications.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5800449..46a4645 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,10 @@ Change Log ---------- +**0.20** (2020-05-12) + +- All objects can now print their values +- Add parser + object for generic profile + **0.19** (2020-05-03) - Add following missing elements to telegram specification v4: @@ -45,7 +50,7 @@ Change Log **0.10** (2017-06-05) -- bugix: don't force full telegram signatures (`pull request #25 `_) +- bugfix: don't force full telegram signatures (`pull request #25 `_) - removed unused code for automatic telegram detection as this needs reworking after the fix mentioned above - InvalidChecksumError's are logged as warning instead of error diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index e313cd5..d07cd35 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,4 +1,5 @@ import dsmr_parser.obis_name_mapping +import datetime class Telegram(object): @@ -48,7 +49,7 @@ def __iter__(self): def __str__(self): output = "" for attr, value in self: - output += "{}: \t {} \t[{}]\n".format(attr, str(value.value), str(value.unit)) + output += "{}: \t {}\n".format(attr, str(value)) return output @@ -87,6 +88,10 @@ def unit(self): else: return self.values[1]['unit'] + def __str__(self): + output = "{}\t[{}] at {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat())) + return output + class CosemObject(DSMRObject): @@ -98,6 +103,13 @@ def value(self): def unit(self): return self.values[0]['unit'] + def __str__(self): + print_value = self.value + if isinstance(self.value, datetime.datetime): + print_value = self.value.astimezone().isoformat() + output = "{}\t[{}]".format(str(print_value), str(self.unit)) + return output + class ProfileGeneric(DSMRObject): pass # TODO implement diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index d9aeb5a..fd88798 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -183,7 +183,7 @@ def parse(self, line): return CosemObject(self._parse(line)) -class ProfileGenericParser(DSMRObjectParser): +class ProfileGenericParser(object): """ Power failure log parser. @@ -205,6 +205,22 @@ class ProfileGenericParser(DSMRObjectParser): 9) Unit of buffer values (Unit of capture objects attribute) """ + def _parse(self, line): + # Match value groups, but exclude the parentheses. Adapted to also match OBIS code in 3rd position. + pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') + values = re.findall(pattern, line) + + # Convert empty value groups to None for clarity. + values = [None if value == '' else value for value in values] + + buffer_length = int(values[0]) + + if (not values) or (len(values) != (buffer_length * 2 + 2)): + raise ParseError("Invalid '%s' line for '%s'", line, self) + + return [self.value_formats[i].parse(value) + for i, value in enumerate(values)] + def parse(self, line): raise NotImplementedError() diff --git a/dsmr_parser/profile_generic_specifications.py b/dsmr_parser/profile_generic_specifications.py new file mode 100644 index 0000000..470d03f --- /dev/null +++ b/dsmr_parser/profile_generic_specifications.py @@ -0,0 +1,14 @@ +from dsmr_parser.parsers import ValueParser, MBusParser +from dsmr_parser.value_types import timestamp + +FAILURE_EVENT = r'0-0\:96\.7\.19' + +V4 = { + 'objects': { + FAILURE_EVENT: MBusParser( + ValueParser(timestamp), + ValueParser(int) + ) + } + +} diff --git a/setup.py b/setup.py index beb8e57..c925b4d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.19', + version='0.20', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From b6278a8991c8729c22c35cc0960fa0ff1dc72518 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sat, 16 May 2020 16:31:26 +0200 Subject: [PATCH 062/226] ProfileGeneric parser working, TODO complete ProfileGenericObject + Test --- dsmr_parser/objects.py | 10 +-- dsmr_parser/parsers.py | 66 ++++++++++++------- dsmr_parser/profile_generic_specifications.py | 16 ++--- dsmr_parser/telegram_specifications.py | 14 ++-- test/test_telegram.py | 10 +++ 5 files changed, 73 insertions(+), 43 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index d07cd35..877934a 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -74,7 +74,7 @@ def value(self): # TODO object, but let the parse set them differently? So don't use # TODO hardcoded indexes here. if len(self.values) != 2: # v2 - return self.values[5]['value'] + return self.values[6]['value'] else: return self.values[1]['value'] @@ -84,7 +84,7 @@ def unit(self): # TODO object, but let the parse set them differently? So don't use # TODO hardcoded indexes here. if len(self.values) != 2: # v2 - return self.values[4]['value'] + return self.values[5]['value'] else: return self.values[1]['unit'] @@ -111,5 +111,7 @@ def __str__(self): return output -class ProfileGeneric(DSMRObject): - pass # TODO implement +class ProfileGenericObject(DSMRObject): + def __str__(self): + output = "{}".format(self.values) + return output diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index fd88798..2c7c017 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,7 +3,7 @@ from ctypes import c_ushort -from dsmr_parser.objects import MBusObject, CosemObject +from dsmr_parser.objects import MBusObject, CosemObject, ProfileGenericObject from dsmr_parser.exceptions import ParseError, InvalidChecksumError logger = logging.getLogger(__name__) @@ -123,19 +123,28 @@ class DSMRObjectParser(object): def __init__(self, *value_formats): self.value_formats = value_formats + def _is_line_wellformed(self, line, values): + # allows overriding by child class + return (values and (len(values) == len(self.value_formats))) + + def _parse_values(self, values): + # allows overriding by child class + return [self.value_formats[i].parse(value) + for i, value in enumerate(values)] + def _parse(self, line): # Match value groups, but exclude the parentheses - pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*]{0,}(?=\)))+') + pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') + values = re.findall(pattern, line) + if not self._is_line_wellformed(line, values): + raise ParseError("Invalid '%s' line for '%s'", line, self) + # Convert empty value groups to None for clarity. values = [None if value == '' else value for value in values] - if not values or len(values) != len(self.value_formats): - raise ParseError("Invalid '%s' line for '%s'", line, self) - - return [self.value_formats[i].parse(value) - for i, value in enumerate(values)] + return self._parse_values(values) class MBusParser(DSMRObjectParser): @@ -183,7 +192,7 @@ def parse(self, line): return CosemObject(self._parse(line)) -class ProfileGenericParser(object): +class ProfileGenericParser(DSMRObjectParser): """ Power failure log parser. @@ -204,25 +213,34 @@ class ProfileGenericParser(object): 8) Buffer value 2 (oldest entry of buffer attribute without unit) 9) Unit of buffer values (Unit of capture objects attribute) """ - - def _parse(self, line): - # Match value groups, but exclude the parentheses. Adapted to also match OBIS code in 3rd position. - pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') - values = re.findall(pattern, line) - - # Convert empty value groups to None for clarity. - values = [None if value == '' else value for value in values] - + def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): + self.value_formats = head_parsers + self.buffer_types = buffer_types + self.parsers_for_unidentified = parsers_for_unidentified + + def _is_line_wellformed(self, line, values): + if values and (len(values) >= 2) and (values[0].isdigit()): + buffer_length = int(values[0]) + return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2)) + else: + return False + + def _parse_values(self, values): buffer_length = int(values[0]) - - if (not values) or (len(values) != (buffer_length * 2 + 2)): - raise ParseError("Invalid '%s' line for '%s'", line, self) - - return [self.value_formats[i].parse(value) - for i, value in enumerate(values)] + buffer_value_obis_ID = values[1] + if (buffer_length > 0): + if buffer_value_obis_ID in self.buffer_types: + bufferValueParsers = self.buffer_types[buffer_value_obis_ID] + else: + bufferValueParsers = self.parsers_for_unidentified + # add the parsers for the encountered value type z times + for _ in range(buffer_length): + self.value_formats.extend(bufferValueParsers) + + return [self.value_formats[i].parse(value) for i, value in enumerate(values)] def parse(self, line): - raise NotImplementedError() + return ProfileGenericObject(self._parse(line)) class ValueParser(object): diff --git a/dsmr_parser/profile_generic_specifications.py b/dsmr_parser/profile_generic_specifications.py index 470d03f..a52416c 100644 --- a/dsmr_parser/profile_generic_specifications.py +++ b/dsmr_parser/profile_generic_specifications.py @@ -1,14 +1,10 @@ -from dsmr_parser.parsers import ValueParser, MBusParser +from dsmr_parser.parsers import ValueParser from dsmr_parser.value_types import timestamp -FAILURE_EVENT = r'0-0\:96\.7\.19' +PG_FAILURE_EVENT = r'0-0:96.7.19' -V4 = { - 'objects': { - FAILURE_EVENT: MBusParser( - ValueParser(timestamp), - ValueParser(int) - ) +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 2e2ff45..161ac91 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -2,9 +2,9 @@ from copy import deepcopy from dsmr_parser import obis_references as obis -from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser +from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser from dsmr_parser.value_types import timestamp - +from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS """ dsmr_parser.telegram_specifications @@ -37,8 +37,9 @@ ValueParser(int), ValueParser(int), ValueParser(int), - ValueParser(str), - ValueParser(Decimal), + ValueParser(str), # obis ref + ValueParser(str), # unit, position 5 + ValueParser(Decimal), # meter reading, position 6 ), } } @@ -60,7 +61,10 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO + obis.POWER_EVENT_FAILURE_LOG: + ProfileGenericParser(BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), diff --git a/test/test_telegram.py b/test/test_telegram.py index b553714..a330bc4 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -7,6 +7,7 @@ from dsmr_parser.objects import CosemObject from dsmr_parser.objects import MBusObject from dsmr_parser.objects import Telegram +from dsmr_parser.objects import ProfileGenericObject from dsmr_parser.parsers import TelegramParser from test.example_telegrams import TELEGRAM_V4_2 from decimal import Decimal @@ -286,6 +287,15 @@ def test_instantiate(self): unit_val='m3', value_type=Decimal, value_val=Decimal('981.443')) + # POWER_EVENT_FAILURE_LOG (1-0:99.97.0) + testitem_name = 'POWER_EVENT_FAILURE_LOG' + object_type = ProfileGenericObject + testitem = eval("telegram.{}".format(testitem_name)) + assert isinstance(testitem, object_type) +# assert testitem.unit == unit_val +# assert isinstance(testitem.value, value_type) +# assert testitem.value == value_val + self.item_names_tested.append(testitem_name) # check if all items in telegram V4 specification are covered V4_name_list = [obis_name_mapping.EN[signature] for signature, parser in From 789871899c4617ecb6a82ae82d571ec5abc698db Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 17 May 2020 01:25:02 +0200 Subject: [PATCH 063/226] ProfileGeneric parser working, ProfileGenericObject implemented and Test for V4 telegram completed. --- dsmr_parser/objects.py | 37 +++++++++++++++++++++++++- dsmr_parser/telegram_specifications.py | 5 +++- test/test_telegram.py | 22 ++++++++++++--- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 877934a..ce48a01 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -112,6 +112,41 @@ def __str__(self): class ProfileGenericObject(DSMRObject): + """ + Represents all data in a GenericProfile value. + All buffer values are returned as a list of MBusObjects, + containing the datetime (timestamp) and the value. + """ + + def __init__(self, values): + super().__init__(values) + self._buffer_list = None + + @property + def buffer_length(self): + return self.values[0]['value'] + + @property + def buffer_type(self): + return self.values[1]['value'] + + @property + def buffer(self): + if self._buffer_list is None: + self._buffer_list = [] + values_offset = 2 + for i in range(self.buffer_length): + offset = values_offset + i*2 + self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) + return self._buffer_list + def __str__(self): - output = "{}".format(self.values) + output = "\t buffer length: {}\n".format(self.buffer_length) + output += "\t buffer type: {}".format(self.buffer_type) + for buffer_value in self.buffer: + timestamp = buffer_value.datetime + if isinstance(timestamp, datetime.datetime): + timestamp = str(timestamp.astimezone().isoformat()) + output += "\n\t event occured at: {}".format(timestamp) + output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit) return output diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 161ac91..1341ded 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -107,7 +107,10 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO + obis.POWER_EVENT_FAILURE_LOG: + ProfileGenericParser(BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), diff --git a/test/test_telegram.py b/test/test_telegram.py index a330bc4..90b8eff 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -287,14 +287,30 @@ def test_instantiate(self): unit_val='m3', value_type=Decimal, value_val=Decimal('981.443')) + # POWER_EVENT_FAILURE_LOG (1-0:99.97.0) testitem_name = 'POWER_EVENT_FAILURE_LOG' object_type = ProfileGenericObject testitem = eval("telegram.{}".format(testitem_name)) assert isinstance(testitem, object_type) -# assert testitem.unit == unit_val -# assert isinstance(testitem.value, value_type) -# assert testitem.value == value_val + assert testitem.buffer_length == 3 + assert testitem.buffer_type == '0-0:96.7.19' + buffer = testitem.buffer + assert isinstance(testitem.buffer, list) + assert len(buffer) == 3 + assert all([isinstance(item, MBusObject) for item in buffer]) + date0 = datetime.datetime(2000, 1, 4, 17, 3, 20, tzinfo=datetime.timezone.utc) + date1 = datetime.datetime(1999, 12, 31, 23, 0, 1, tzinfo=datetime.timezone.utc) + date2 = datetime.datetime(2000, 1, 1, 23, 0, 3, tzinfo=datetime.timezone.utc) + assert buffer[0].datetime == date0 + assert buffer[1].datetime == date1 + assert buffer[2].datetime == date2 + assert buffer[0].value == 237126 + assert buffer[1].value == 2147583646 + assert buffer[2].value == 2317482647 + assert all([isinstance(item.value, int) for item in buffer]) + assert all([isinstance(item.unit, str) for item in buffer]) + assert all([(item.unit == 's') for item in buffer]) self.item_names_tested.append(testitem_name) # check if all items in telegram V4 specification are covered From c2dea29c83d5818581d2821db6bef073208dad42 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 17 May 2020 16:49:29 +0200 Subject: [PATCH 064/226] add a value property to GenericProfileObject, return a dict --- dsmr_parser/objects.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index ce48a01..3e99d53 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -140,6 +140,18 @@ def buffer(self): self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) return self._buffer_list + @property + def value(self): + list = [['buffer_length', self.buffer_length]] + list.append(['buffer_type', self.buffer_type]) + buffer_repr = [ + (['datetime', buffer_item.datetime], + ['value', buffer_item.value]) + for buffer_item in self.buffer + ] + list.append(['buffer', buffer_repr]) + return dict(list) + def __str__(self): output = "\t buffer length: {}\n".format(self.buffer_length) output += "\t buffer type: {}".format(self.buffer_type) From 94447c357128b38b11c203dcfeef42652179859d Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 17 May 2020 16:58:28 +0200 Subject: [PATCH 065/226] GenericProfileObject value: make embedded buffer items also appear as dicts --- dsmr_parser/objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 3e99d53..1ad1dbc 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -145,8 +145,8 @@ def value(self): list = [['buffer_length', self.buffer_length]] list.append(['buffer_type', self.buffer_type]) buffer_repr = [ - (['datetime', buffer_item.datetime], - ['value', buffer_item.value]) + dict([['datetime', buffer_item.datetime], + ['value', buffer_item.value]]) for buffer_item in self.buffer ] list.append(['buffer', buffer_repr]) From 837ba3b6f7645dd7bc6fc92980af12d549c8790a Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 25 May 2020 01:38:14 +0200 Subject: [PATCH 066/226] add json serialization --- CHANGELOG.rst | 4 +++ dsmr_parser/objects.py | 67 ++++++++++++++++++++++++++++++++++-------- setup.py | 2 +- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 46a4645..dae3e36 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,9 @@ Change Log ---------- +**0.21** (2020-05-25) + +- All objects can produce a json serialization of their state. + **0.20** (2020-05-12) - All objects can now print their values diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 1ad1dbc..4cd987d 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,5 +1,7 @@ import dsmr_parser.obis_name_mapping import datetime +import json +from decimal import Decimal class Telegram(object): @@ -52,6 +54,9 @@ def __str__(self): output += "{}: \t {}\n".format(attr, str(value)) return output + def to_json(self): + return json.dumps(dict([[attr, json.loads(value.to_json())] for attr, value in self])) + class DSMRObject(object): """ @@ -92,6 +97,22 @@ def __str__(self): output = "{}\t[{}] at {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat())) return output + def to_json(self): + timestamp = self.datetime + if isinstance(self.datetime, datetime.datetime): + timestamp = self.datetime.astimezone().isoformat() + value = self.value + if isinstance(self.value, datetime.datetime): + value = self.value.astimezone().isoformat() + if isinstance(self.value, Decimal): + value = float(self.value) + output = { + 'datetime': timestamp, + 'value': value, + 'unit': self.unit + } + return json.dumps(output) + class CosemObject(DSMRObject): @@ -110,6 +131,18 @@ def __str__(self): output = "{}\t[{}]".format(str(print_value), str(self.unit)) return output + def to_json(self): + json_value = self.value + if isinstance(self.value, datetime.datetime): + json_value = self.value.astimezone().isoformat() + if isinstance(self.value, Decimal): + json_value = float(self.value) + output = { + 'value': json_value, + 'unit': self.unit + } + return json.dumps(output) + class ProfileGenericObject(DSMRObject): """ @@ -140,18 +173,6 @@ def buffer(self): self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) return self._buffer_list - @property - def value(self): - list = [['buffer_length', self.buffer_length]] - list.append(['buffer_type', self.buffer_type]) - buffer_repr = [ - dict([['datetime', buffer_item.datetime], - ['value', buffer_item.value]]) - for buffer_item in self.buffer - ] - list.append(['buffer', buffer_repr]) - return dict(list) - def __str__(self): output = "\t buffer length: {}\n".format(self.buffer_length) output += "\t buffer type: {}".format(self.buffer_type) @@ -162,3 +183,25 @@ def __str__(self): output += "\n\t event occured at: {}".format(timestamp) output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit) return output + + def to_json(self): + """ + :return: A json of all values in the GenericProfileObject , with the following structure + {'buffer_length': n, + 'buffer_type': obis_ref, + 'buffer': [{'datetime': d1, + 'value': v1, + 'unit': u1}, + ... + {'datetime': dn, + 'value': vn, + 'unit': un} + ] + } + """ + list = [['buffer_length', self.buffer_length]] + list.append(['buffer_type', self.buffer_type]) + buffer_repr = [json.loads(buffer_item.to_json()) for buffer_item in self.buffer] + list.append(['buffer', buffer_repr]) + output = dict(list) + return json.dumps(output) diff --git a/setup.py b/setup.py index c925b4d..9072eef 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.20', + version='0.21', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 5630520771de46c9a777284f29d0cae5849b0c5e Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Tue, 21 Jul 2020 10:20:01 +0200 Subject: [PATCH 067/226] Pass exception to log.exception as it is not called from within an except: clause. Add Python 3.8 test support, drop Python 3.4 test support. --- .travis.yml | 2 +- dsmr_parser/clients/protocol.py | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 311a607..bc6b513 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,9 @@ language: python python: - 2.7 - - 3.4 - 3.5 - 3.6 + - 3.8 install: pip install tox-travis codecov diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index e43e230..3961853 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -95,7 +95,7 @@ def data_received(self, data): def connection_lost(self, exc): """Stop when connection is lost.""" if exc: - self.log.exception('disconnected due to exception') + self.log.exception('disconnected due to exception', exc_info=exc) else: self.log.info('disconnected because of close/abort.') self._closed.set() diff --git a/tox.ini b/tox.ini index a3e12f0..f2e6de4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34,py35,py36,py37 +envlist = py35,py36,py37,py38 [testenv] deps= From 0427ace079b2c691904cca2ae4425f7777623ae1 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 23 Aug 2020 09:36:31 +0200 Subject: [PATCH 068/226] Updated changelog --- CHANGELOG.rst | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dae3e36..5dcbdf0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,10 @@ Change Log ---------- +**0.22** (2020-08-23) + +- CRC check speed is improved +- Exception info improvement + **0.21** (2020-05-25) - All objects can produce a json serialization of their state. diff --git a/setup.py b/setup.py index 9072eef..689a6cd 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.21', + version='0.22', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From be4386bc595d2cb9007c9c79152a104c821e93b3 Mon Sep 17 00:00:00 2001 From: albert Date: Mon, 7 Sep 2020 18:15:49 +0200 Subject: [PATCH 069/226] tempfix for empty profileGenericParser --- dsmr_parser/parsers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 8528ec8..5dedd09 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -225,6 +225,8 @@ def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): self.parsers_for_unidentified = parsers_for_unidentified def _is_line_wellformed(self, line, values): + if values and (len(values) == 1) and (values[0] == ''): + return True if values and (len(values) >= 2) and (values[0].isdigit()): buffer_length = int(values[0]) return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2)) @@ -232,6 +234,9 @@ def _is_line_wellformed(self, line, values): return False def _parse_values(self, values): + if values and (len(values) == 1) and (values[0] == None): + return [self.value_formats[i].parse(value) + for i, value in enumerate(values)] buffer_length = int(values[0]) buffer_value_obis_ID = values[1] if (buffer_length > 0): From dc902a83e9fa683bac50e40251c175c79974d734 Mon Sep 17 00:00:00 2001 From: albert Date: Tue, 8 Sep 2020 12:30:51 +0200 Subject: [PATCH 070/226] referring to parent method from _parse_values in ProfileGenericParser --- dsmr_parser/parsers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 5dedd09..e8cc704 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -225,8 +225,11 @@ def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): self.parsers_for_unidentified = parsers_for_unidentified def _is_line_wellformed(self, line, values): + + # allow empty parentheses (indicated by empty string) if values and (len(values) == 1) and (values[0] == ''): return True + if values and (len(values) >= 2) and (values[0].isdigit()): buffer_length = int(values[0]) return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2)) @@ -234,9 +237,10 @@ def _is_line_wellformed(self, line, values): return False def _parse_values(self, values): + # in case of empty parentheses return if values and (len(values) == 1) and (values[0] == None): - return [self.value_formats[i].parse(value) - for i, value in enumerate(values)] + return super()._parse_values(values) #calling parent + buffer_length = int(values[0]) buffer_value_obis_ID = values[1] if (buffer_length > 0): From ec1d37ea08ee772bac75d640ac08b9fee8c8363a Mon Sep 17 00:00:00 2001 From: dunajski Date: Mon, 28 Sep 2020 10:22:16 +0200 Subject: [PATCH 071/226] Corrects example telegrams. In example telegrams there a few mistakes in header strings. Using '\' in strings have special feature to add to string some non-printable characters, for example to add new line we can add \n for tab \t etc. So whenever we want to just print backslash as normal character we have to add another '\' before. In short string "On\\Off" prints "On\Off" ascii string. In enquired telegrams there are few strings with \2 sign in header, '/ISk5\2MT382-1000\r\n' and when we calculate CRC for whole given telegram part of "..\2.." is interpreted as one character (value 0x02) instead of two characters ('\', '2'). I think in a header in the example enlosed in P1 DSMR Companion Standard /ISk5\2.. stand for printable '\' and '2' character instead of "converted" to special '\2' character. --- test/example_telegrams.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/example_telegrams.py b/test/example_telegrams.py index 2df8606..143b2a4 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -1,5 +1,5 @@ TELEGRAM_V2_2 = ( - '/ISk5\2MT382-1004\r\n' + '/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' @@ -22,7 +22,7 @@ ) TELEGRAM_V3 = ( - '/ISk5\2MT382-1000\r\n' + '/ISk5\\2MT382-1000\r\n' '\r\n' '0-0:96.1.1(4B384547303034303436333935353037)\r\n' '1-0:1.8.1(12345.678*kWh)\r\n' @@ -87,7 +87,7 @@ ) TELEGRAM_V5 = ( - '/ISk5\2MT382-1000\r\n' + '/ISk5\\2MT382-1000\r\n' '\r\n' '1-3:0.2.8(50)\r\n' '0-0:1.0.0(170102192002W)\r\n' @@ -126,5 +126,5 @@ '0-1:24.2.1(170102161005W)(00000.107*m3)\r\n' '0-2:24.1.0(003)\r\n' '0-2:96.1.0()\r\n' - '!87B3\r\n' -) + '!6EEE\r\n' +) \ No newline at end of file From 86d665df3955457a19354ab2ce26cb35ec998ed8 Mon Sep 17 00:00:00 2001 From: dunajski Date: Mon, 28 Sep 2020 11:35:47 +0200 Subject: [PATCH 072/226] Change README.md according to change in ec1d37e In ec1d37ea08ee772bac75d640ac08b9fee8c8363a changed example telegrams, so example telegram from README.md should be corrected too. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 47fc884..03de485 100644 --- a/README.rst +++ b/README.rst @@ -56,7 +56,7 @@ into a dictionary. # String is formatted in separate lines for readability. telegram_str = ( - '/ISk5\2MT382-1000\r\n' + '/ISk5\\2MT382-1000\r\n' '\r\n' '0-0:96.1.1(4B384547303034303436333935353037)\r\n' '1-0:1.8.1(12345.678*kWh)\r\n' From 7a687a99a6735d04aad95629f2fbc465421913c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 18 Oct 2020 10:49:09 +0300 Subject: [PATCH 073/226] Do not install tests --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 689a6cd..a30fde2 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', version='0.22', - packages=find_packages(), + packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', 'pyserial-asyncio<1', From 700cf6a2b33119fcc156fcf38956caaf7c4c0791 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 2 Nov 2020 00:29:03 +0100 Subject: [PATCH 074/226] issue#60 resolved issue with x-x:24.3.0 where it contains non-integer characters, parse 2nd value as string instead of integer --- dsmr_parser/telegram_specifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 1341ded..5029533 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -34,7 +34,7 @@ obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), obis.GAS_METER_READING: MBusParser( ValueParser(timestamp), - ValueParser(int), + ValueParser(str), # changed to str see issue60 ValueParser(int), ValueParser(int), ValueParser(str), # obis ref From 2a95d33c0212af5c1240dd64a688a6cc61661a0a Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Wed, 4 Nov 2020 20:42:38 +0100 Subject: [PATCH 075/226] Add Luxembourg equipment identifier --- dsmr_parser/clients/protocol.py | 3 +++ dsmr_parser/obis_references.py | 1 + dsmr_parser/telegram_specifications.py | 1 + 3 files changed, 5 insertions(+) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 3961853..66d0a39 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -29,6 +29,9 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): elif dsmr_version == '5B': specification = telegram_specifications.BELGIUM_FLUVIUS serial_settings = SERIAL_SETTINGS_V5 + elif dsmr_version == "5L": + specification = telegram_specifications.LUXEMBOURG_SMARTY + serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index cb7b158..ffc215e 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -63,5 +63,6 @@ # Alternate codes for foreign countries. 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-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 5029533..b06e4f4 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -152,6 +152,7 @@ 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)), }) From 1c7535448eceda9d1a288696c7ead181cee06fa7 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 7 Nov 2020 08:56:24 +0100 Subject: [PATCH 076/226] Updated changelog and version --- CHANGELOG.rst | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5dcbdf0..c094a5d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,11 @@ Change Log ---------- + +**0.23** (2020-11-07) +- Resolved issue with x-x:24.3.0 where it contains non-integer character (`pull request #61 `_). +- Tests are not installed anymore (`pull request #59 `_). +- Example telegram improvement (`pull request #58 `_). + **0.22** (2020-08-23) - CRC check speed is improved diff --git a/setup.py b/setup.py index a30fde2..8e5b325 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.22', + version='0.23', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From ba9f3f3c2563ee7a55f89780e25606e9d7ef13d1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 22 Nov 2020 23:10:04 +0100 Subject: [PATCH 077/226] First draft addin async documentation --- README.rst | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 03de485..a9514e1 100644 --- a/README.rst +++ b/README.rst @@ -41,7 +41,50 @@ process because the code is blocking (not asynchronous): **AsyncIO client** -To be documented. +For a test run using a tcp server (lasting 20 seconds) use the following example: + +.. code-block:: python + + import asyncio + import logging + from dsmr_parser import obis_references + from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader + + logging.basicConfig(level=logging.INFO, format='%(message)s') + + HOST = MY_HOST + PORT = MY_PORT + DSMR_VERSION = MY_DSMR_VERSION + + logger = logging.getLogger('tcpclient') + logger.debug("Logger created") + + def printTelegram(telegram): + logger.info(telegram) + + + async def main(): + try: + logger.debug("Getting loop") + loop = asyncio.get_event_loop() + logger.debug("Creating reader") + await create_tcp_dsmr_reader( + HOST, + PORT, + DSMR_VERSION, + printTelegram, + loop + ) + logger.debug("Reader created going to sleep now") + await asyncio.sleep(20) + logger.info('Finished run') + except Exception as e: + logger.error("Unexpected error: "+ e) + + asyncio.run(main()) + +Note the creation of a callback function to call when a telegram is received. In this case `printTelegram`. Normally the used loop is one running + Parsing module usage From 684023d0b64cd2519b9b34d7ec4410a04e54d413 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Fri, 27 Nov 2020 21:18:59 +0100 Subject: [PATCH 078/226] Preparing for release 0.24 --- CHANGELOG.rst | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c094a5d..bef44b1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Change Log ---------- +**0.24** (2020-11-27) +- Add Luxembourg equipment identifier (`pull request #62 `_). + **0.23** (2020-11-07) - Resolved issue with x-x:24.3.0 where it contains non-integer character (`pull request #61 `_). - Tests are not installed anymore (`pull request #59 `_). diff --git a/setup.py b/setup.py index 8e5b325..f8fba0c 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.23', + version='0.24', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From b7c8626d0c481711a33ebc674a0312244d7fd588 Mon Sep 17 00:00:00 2001 From: Guy Foetz Date: Sun, 13 Dec 2020 12:15:51 +0000 Subject: [PATCH 079/226] adding the # Alternate codes for foreign countries. --- dsmr_parser/obis_name_mapping.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 0401f5e..bf00f13 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -48,7 +48,11 @@ obis.GAS_METER_READING: 'GAS_METER_READING', obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', - obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS' + 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' } REVERSE_EN = dict([(v, k) for k, v in EN.items()]) From bc7961f8400f7c7b69dc22a2648c343fe986bff6 Mon Sep 17 00:00:00 2001 From: Maarten Kleijwegt <9350991+mjkl-gh@users.noreply.github.com> Date: Sun, 13 Dec 2020 16:36:30 +0100 Subject: [PATCH 080/226] add asyncio read as object --- README.rst | 140 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 38 deletions(-) diff --git a/README.rst b/README.rst index a9514e1..a09fb78 100644 --- a/README.rst +++ b/README.rst @@ -83,50 +83,114 @@ For a test run using a tcp server (lasting 20 seconds) use the following example asyncio.run(main()) -Note the creation of a callback function to call when a telegram is received. In this case `printTelegram`. Normally the used loop is one running +Note the creation of a callback function to call when a telegram is received. In this case `printTelegram`. Normally the used loop is the one running. +Currently the asyncio implementation does not support returning telegram objects directly as a `read_as_object()` for async tcp is currently not implemented. +Moreover, the telegram passed to `telegram_callback(telegram)` is already parsed. Therefore we can't feed it into the telegram constructor directly as that expects unparsed telegrams - -Parsing module usage --------------------- -The parsing module accepts complete unaltered telegram strings and parses these -into a dictionary. +However, if we construct a mock TelegramParser that just returns the already parsed object we can work around this. An example is below: .. code-block:: python - from dsmr_parser import telegram_specifications - from dsmr_parser.parsers import TelegramParser - - # String is formatted in separate lines for readability. - telegram_str = ( - '/ISk5\\2MT382-1000\r\n' - '\r\n' - '0-0:96.1.1(4B384547303034303436333935353037)\r\n' - '1-0:1.8.1(12345.678*kWh)\r\n' - '1-0:1.8.2(12345.678*kWh)\r\n' - '1-0:2.8.1(12345.678*kWh)\r\n' - '1-0:2.8.2(12345.678*kWh)\r\n' - '0-0:96.14.0(0002)\r\n' - '1-0:1.7.0(001.19*kW)\r\n' - '1-0:2.7.0(000.00*kW)\r\n' - '0-0:17.0.0(016*A)\r\n' - '0-0:96.3.10(1)\r\n' - '0-0:96.13.1(303132333435363738)\r\n' - '0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E' - '3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F30313233' - '3435363738393A3B3C3D3E3F)\r\n' - '0-1:96.1.0(3232323241424344313233343536373839)\r\n' - '0-1:24.1.0(03)\r\n' - '0-1:24.3.0(090212160000)(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' - ) + import asyncio + import logging + #from dsmr_parser import obis_references + #from dsmr_parser import telegram_specifications + #from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader + #from dsmr_parser.objects import Telegram - parser = TelegramParser(telegram_specifications.V3) - - telegram = parser.parse(telegram_str) - print(telegram) # see 'Telegram object' docs below + logging.basicConfig(level=logging.INFO, format='%(message)s') + + HOST = MY_HOST + PORT = MY_PORT + DSMR_VERSION = MY_DSMR_VERSION + + logger = logging.getLogger('tcpclient') + logger.debug("Logger created") + + class mockTelegramParser(object): + + def parse(self, telegram): + return telegram + + telegram_parser = mockTelegramParser() + + def printTelegram(telegram): + try: + logger.info(Telegram(telegram, telegram_parser, telegram_specifications.V4)) + except InvalidChecksumError as e: + logger.warning(str(e)) + except ParseError as e: + logger.error('Failed to parse telegram: %s', e) + + + async def main(): + try: + logger.debug("Getting loop") + loop = asyncio.get_event_loop() + logger.debug("Creating reader") + await create_tcp_dsmr_reader( + HOST, + PORT, + DSMR_VERSION, + printTelegram, + loop + ) + logger.debug("Reader created going to sleep now") + while True: + await asyncio.sleep(1) + except Exception as e: + logger.error("Unexpected error: "+ e) + raise + + if __name__ == '__main__': + try: + asyncio.run(main()) + except (KeyboardInterrupt, SystemExit): + logger.info('Closing down...') + except Exception as e: + logger.error("Unexpected error: "+ e) + + Parsing module usage + -------------------- + The parsing module accepts complete unaltered telegram strings and parses these + into a dictionary. + + .. code-block:: python + + from dsmr_parser import telegram_specifications + from dsmr_parser.parsers import TelegramParser + + # String is formatted in separate lines for readability. + telegram_str = ( + '/ISk5\\2MT382-1000\r\n' + '\r\n' + '0-0:96.1.1(4B384547303034303436333935353037)\r\n' + '1-0:1.8.1(12345.678*kWh)\r\n' + '1-0:1.8.2(12345.678*kWh)\r\n' + '1-0:2.8.1(12345.678*kWh)\r\n' + '1-0:2.8.2(12345.678*kWh)\r\n' + '0-0:96.14.0(0002)\r\n' + '1-0:1.7.0(001.19*kW)\r\n' + '1-0:2.7.0(000.00*kW)\r\n' + '0-0:17.0.0(016*A)\r\n' + '0-0:96.3.10(1)\r\n' + '0-0:96.13.1(303132333435363738)\r\n' + '0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E' + '3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F30313233' + '3435363738393A3B3C3D3E3F)\r\n' + '0-1:96.1.0(3232323241424344313233343536373839)\r\n' + '0-1:24.1.0(03)\r\n' + '0-1:24.3.0(090212160000)(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' + ) + + parser = TelegramParser(telegram_specifications.V3) + + telegram = parser.parse(telegram_str) + print(telegram) # see 'Telegram object' docs below Telegram dictionary ------------------- From a7b4929eabfa9b9fdd0e1aee7c3d3738f6e5c9f0 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 14 Dec 2020 17:21:16 +0100 Subject: [PATCH 081/226] preparation for v0.25 --- CHANGELOG.rst | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bef44b1..94071bf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Change Log ---------- +**0.25** (2020-12-14) +- fix for empty parentheses in ProfileGenericParser (`pull request #57 `_). + **0.24** (2020-11-27) - Add Luxembourg equipment identifier (`pull request #62 `_). diff --git a/setup.py b/setup.py index f8fba0c..ec32a09 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.24', + version='0.25', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From af9a99d995c70575e66f18ec96288aed47b30735 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 15 Dec 2020 14:59:35 +0100 Subject: [PATCH 082/226] Revert "fix for empty parentheses in ProfileGenericParser" --- dsmr_parser/parsers.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index e8cc704..8528ec8 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -225,11 +225,6 @@ def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): self.parsers_for_unidentified = parsers_for_unidentified def _is_line_wellformed(self, line, values): - - # allow empty parentheses (indicated by empty string) - if values and (len(values) == 1) and (values[0] == ''): - return True - if values and (len(values) >= 2) and (values[0].isdigit()): buffer_length = int(values[0]) return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2)) @@ -237,10 +232,6 @@ def _is_line_wellformed(self, line, values): return False def _parse_values(self, values): - # in case of empty parentheses return - if values and (len(values) == 1) and (values[0] == None): - return super()._parse_values(values) #calling parent - buffer_length = int(values[0]) buffer_value_obis_ID = values[1] if (buffer_length > 0): From feb0f88ddc0aa577d0d377e2b8e48a0be88feb4f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 15 Dec 2020 15:03:22 +0100 Subject: [PATCH 083/226] Preparing release 0.26 --- CHANGELOG.rst | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 94071bf..42a197a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Change Log ---------- +**0.26** (2020-12-15) +- reverted fix for empty parentheses in ProfileGenericParser (`pull request #68 `_). + **0.25** (2020-12-14) - fix for empty parentheses in ProfileGenericParser (`pull request #57 `_). diff --git a/setup.py b/setup.py index ec32a09..bc1a584 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.25', + version='0.26', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 1318204d0ccc0bbd6472f58603dfb3dbd82f571b Mon Sep 17 00:00:00 2001 From: albert Date: Mon, 7 Sep 2020 18:15:49 +0200 Subject: [PATCH 084/226] tempfix for empty profileGenericParser --- dsmr_parser/parsers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 8528ec8..5dedd09 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -225,6 +225,8 @@ def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): self.parsers_for_unidentified = parsers_for_unidentified def _is_line_wellformed(self, line, values): + if values and (len(values) == 1) and (values[0] == ''): + return True if values and (len(values) >= 2) and (values[0].isdigit()): buffer_length = int(values[0]) return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2)) @@ -232,6 +234,9 @@ def _is_line_wellformed(self, line, values): return False def _parse_values(self, values): + if values and (len(values) == 1) and (values[0] == None): + return [self.value_formats[i].parse(value) + for i, value in enumerate(values)] buffer_length = int(values[0]) buffer_value_obis_ID = values[1] if (buffer_length > 0): From 2d712b506d2d8c70f88552dced4eafa26ee21d0d Mon Sep 17 00:00:00 2001 From: albert Date: Tue, 8 Sep 2020 12:30:51 +0200 Subject: [PATCH 085/226] referring to parent method from _parse_values in ProfileGenericParser --- dsmr_parser/parsers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 5dedd09..e8cc704 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -225,8 +225,11 @@ def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): self.parsers_for_unidentified = parsers_for_unidentified def _is_line_wellformed(self, line, values): + + # allow empty parentheses (indicated by empty string) if values and (len(values) == 1) and (values[0] == ''): return True + if values and (len(values) >= 2) and (values[0].isdigit()): buffer_length = int(values[0]) return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2)) @@ -234,9 +237,10 @@ def _is_line_wellformed(self, line, values): return False def _parse_values(self, values): + # in case of empty parentheses return if values and (len(values) == 1) and (values[0] == None): - return [self.value_formats[i].parse(value) - for i, value in enumerate(values)] + return super()._parse_values(values) #calling parent + buffer_length = int(values[0]) buffer_value_obis_ID = values[1] if (buffer_length > 0): From 5b1e83001874b10c1e9705b7f760d57ea8a62918 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Thu, 24 Dec 2020 00:22:29 +0100 Subject: [PATCH 086/226] make sure that for the special case (actually invalid syntax) where a ProfileGeneric line only contains (); an empty ProfileGenericObject is created --- dsmr_parser/parsers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index e8cc704..272b59b 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -226,8 +226,9 @@ def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): def _is_line_wellformed(self, line, values): - # allow empty parentheses (indicated by empty string) + if values and (len(values) == 1) and (values[0] == ''): + # special case: single empty parentheses (indicated by empty string) return True if values and (len(values) >= 2) and (values[0].isdigit()): @@ -237,10 +238,9 @@ def _is_line_wellformed(self, line, values): return False def _parse_values(self, values): - # in case of empty parentheses return if values and (len(values) == 1) and (values[0] == None): - return super()._parse_values(values) #calling parent - + # special case: single empty parentheses; make sure empty ProfileGenericObject is created + values = [0, None] # buffer_length=0, buffer_value_obis_ID=None buffer_length = int(values[0]) buffer_value_obis_ID = values[1] if (buffer_length > 0): From 81cccbd2289e9a27ca7a2bf3e87930bdb60a8395 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Thu, 24 Dec 2020 01:52:17 +0100 Subject: [PATCH 087/226] fix tox tests --- dsmr_parser/obis_references.py | 2 +- dsmr_parser/parsers.py | 6 ++---- test/example_telegrams.py | 2 +- test/test_parse_v5.py | 3 +-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index ffc215e..fe8952e 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -63,6 +63,6 @@ # Alternate codes for foreign countries. 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_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-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 272b59b..5c44f8b 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -225,8 +225,6 @@ def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): self.parsers_for_unidentified = parsers_for_unidentified def _is_line_wellformed(self, line, values): - - if values and (len(values) == 1) and (values[0] == ''): # special case: single empty parentheses (indicated by empty string) return True @@ -238,9 +236,9 @@ def _is_line_wellformed(self, line, values): return False def _parse_values(self, values): - if values and (len(values) == 1) and (values[0] == None): + if values and (len(values) == 1) and (values[0] is None): # special case: single empty parentheses; make sure empty ProfileGenericObject is created - values = [0, None] # buffer_length=0, buffer_value_obis_ID=None + values = [0, None] # buffer_length=0, buffer_value_obis_ID=None buffer_length = int(values[0]) buffer_value_obis_ID = values[1] if (buffer_length > 0): diff --git a/test/example_telegrams.py b/test/example_telegrams.py index 143b2a4..f74ed16 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -127,4 +127,4 @@ '0-2:24.1.0(003)\r\n' '0-2:96.1.0()\r\n' '!6EEE\r\n' -) \ No newline at end of file +) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index 67d7cd8..fe3ed84 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -241,7 +241,6 @@ def test_checksum_invalid(self): def test_checksum_missing(self): # Remove the checksum value causing a ParseError. - corrupted_telegram = TELEGRAM_V5.replace('!87B3\r\n', '') - + corrupted_telegram = TELEGRAM_V5.replace('!6EEE\r\n', '') with self.assertRaises(ParseError): TelegramParser.validate_checksum(corrupted_telegram) From 629767590be0e7ed2fb13e414cb24c4c5d0774d5 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Thu, 24 Dec 2020 12:37:45 +0100 Subject: [PATCH 088/226] finished tox tests for issue 57 fix --- test/test_parser_corner_cases.py | 89 ++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 test/test_parser_corner_cases.py diff --git a/test/test_parser_corner_cases.py b/test/test_parser_corner_cases.py new file mode 100644 index 0000000..7ca927d --- /dev/null +++ b/test/test_parser_corner_cases.py @@ -0,0 +1,89 @@ +import unittest + +from dsmr_parser import telegram_specifications + +from dsmr_parser.objects import Telegram +from dsmr_parser.objects import ProfileGenericObject +from dsmr_parser.parsers import TelegramParser +from dsmr_parser.parsers import ProfileGenericParser +from dsmr_parser.profile_generic_specifications import BUFFER_TYPES +from dsmr_parser.profile_generic_specifications import PG_HEAD_PARSERS +from dsmr_parser.profile_generic_specifications import PG_UNIDENTIFIED_BUFFERTYPE_PARSERS +from test.example_telegrams import TELEGRAM_V5 + + +class TestParserCornerCases(unittest.TestCase): + """ Test instantiation of Telegram object """ + + def test_power_event_log_empty_1(self): + # POWER_EVENT_FAILURE_LOG (1-0:99.97.0) + parser = TelegramParser(telegram_specifications.V4) + telegram = Telegram(TELEGRAM_V5, parser, telegram_specifications.V5) + + object_type = ProfileGenericObject + testitem = telegram.POWER_EVENT_FAILURE_LOG + assert isinstance(testitem, object_type) + assert testitem.buffer_length == 0 + assert testitem.buffer_type == '0-0:96.7.19' + buffer = testitem.buffer + assert isinstance(testitem.buffer, list) + assert len(buffer) == 0 + + def test_power_event_log_empty_2(self): + pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS) + object_type = ProfileGenericObject + + # Power Event Log with 0 items and no object type + pefl_line = r'1-0:99.97.0(0)()\r\n' + testitem = pef_parser.parse(pefl_line) + + assert isinstance(testitem, object_type) + assert testitem.buffer_length == 0 + assert testitem.buffer_type is None + buffer = testitem.buffer + assert isinstance(testitem.buffer, list) + assert len(buffer) == 0 + assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}] + json = testitem.to_json() + assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}' + + def test_power_event_log_null_values(self): + pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS) + object_type = ProfileGenericObject + + # Power Event Log with 1 item and no object type and nno values for the item + pefl_line = r'1-0:99.97.0(1)()()()\r\n' + testitem = pef_parser.parse(pefl_line) + + assert isinstance(testitem, object_type) + assert testitem.buffer_length == 1 + assert testitem.buffer_type is None + buffer = testitem.buffer + assert isinstance(testitem.buffer, list) + assert len(buffer) == 1 + assert testitem.values == [{'value': 1, 'unit': None}, {'value': None, 'unit': None}, + {'value': None, 'unit': None}, {'value': None, 'unit': None}] + json = testitem.to_json() + assert json == \ + '{"buffer_length": 1, "buffer_type": null, "buffer": [{"datetime": null, "value": null, "unit": null}]}' + + def test_power_event_log_brackets_only(self): + # POWER_EVENT_FAILURE_LOG (1-0:99.97.0) + # Issue 57 + # Test of an ill formatted empty POWER_EVENT_FAILURE_LOG, observed on some smartmeters + # The idea is that instead of failing, the parser converts it to an empty POWER_EVENT_FAILURE_LOG + pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS) + object_type = ProfileGenericObject + + pefl_line = r'1-0:99.97.0()\r\n' + testitem = pef_parser.parse(pefl_line) + + assert isinstance(testitem, object_type) + assert testitem.buffer_length == 0 + assert testitem.buffer_type is None + buffer = testitem.buffer + assert isinstance(testitem.buffer, list) + assert len(buffer) == 0 + assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}] + json = testitem.to_json() + assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}' From 3ddf0366e60e925b78662608c9bfa1e42b6452b3 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Thu, 24 Dec 2020 12:44:49 +0100 Subject: [PATCH 089/226] small fix --- test/test_parser_corner_cases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_parser_corner_cases.py b/test/test_parser_corner_cases.py index 7ca927d..3f203e7 100644 --- a/test/test_parser_corner_cases.py +++ b/test/test_parser_corner_cases.py @@ -17,7 +17,7 @@ class TestParserCornerCases(unittest.TestCase): def test_power_event_log_empty_1(self): # POWER_EVENT_FAILURE_LOG (1-0:99.97.0) - parser = TelegramParser(telegram_specifications.V4) + parser = TelegramParser(telegram_specifications.V5) telegram = Telegram(TELEGRAM_V5, parser, telegram_specifications.V5) object_type = ProfileGenericObject From 3dc77a823139501f569f6925ee87ec3f5e6572ae Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 24 Dec 2020 21:46:41 +0100 Subject: [PATCH 090/226] Prepare for release 0.27 --- CHANGELOG.rst | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 42a197a..2549c80 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Change Log ---------- +**0.27** (2020-12-24) +- fix for empty parentheses in ProfileGenericParser (redone) (`pull request #69 `_). + **0.26** (2020-12-15) - reverted fix for empty parentheses in ProfileGenericParser (`pull request #68 `_). diff --git a/setup.py b/setup.py index bc1a584..427d205 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.26', + version='0.27', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From e12aff5c0d2dd7647313788aaa77ba3053b81a64 Mon Sep 17 00:00:00 2001 From: bremme Date: Sun, 27 Dec 2020 18:57:21 +0100 Subject: [PATCH 091/226] 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 092/226] 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 804747c3703d93f73e54b3b7a166d28bfd6efd49 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 3 Jan 2021 19:58:19 +0100 Subject: [PATCH 093/226] add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break --- dsmr_parser/objects.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 4cd987d..dcadfce 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -155,6 +155,16 @@ def __init__(self, values): super().__init__(values) self._buffer_list = None + @property + def value(self): + # value is added to make sure the telegram iterator does not break + return self.__str__() + + @property + def unit(self): + # value is added to make sure all items have a unit so code that relies on that does not break + return None + @property def buffer_length(self): return self.values[0]['value'] From bbd73897a0c4e8bdd75dd65f1ea4a4ad798e8c74 Mon Sep 17 00:00:00 2001 From: Rene Hogendoorn Date: Tue, 19 Jan 2021 08:26:28 +0100 Subject: [PATCH 094/226] Optional keep alive monitoring for TCP/IP connections * Since dsmr-parser is listen-only, it will not notice interrupted connections and DSMR device restarts. The connection will be reset after an (optional) keep-alive interval if no messages were received from the device. --- dsmr_parser/__main__.py | 4 ++-- dsmr_parser/clients/protocol.py | 28 +++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index 8d9da8b..a9fbaa0 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -16,8 +16,8 @@ def console(): help='alternatively connect using TCP host.') parser.add_argument('--port', default=None, help='TCP port to use for connection') - parser.add_argument('--version', default='2.2', choices=['2.2', '4'], - help='DSMR version (2.2, 4)') + parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L'], + help='DSMR version (2.2, 4, 5, 5B, 5L)') parser.add_argument('--verbose', '-v', action='count') args = parser.parse_args() diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 66d0a39..ef48ae7 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -14,7 +14,7 @@ SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 -def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): +def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **args): """Creates a DSMR asyncio protocol.""" if dsmr_version == '2.2': @@ -37,7 +37,7 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): dsmr_version) protocol = partial(DSMRProtocol, loop, TelegramParser(specification), - telegram_callback=telegram_callback) + telegram_callback=telegram_callback, **args) return protocol, serial_settings @@ -53,12 +53,14 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): def create_tcp_dsmr_reader(host, port, dsmr_version, - telegram_callback, loop=None): + telegram_callback, loop=None, + keep_alive_interval=None): """Creates a DSMR asyncio protocol coroutine using TCP connection.""" if not loop: loop = asyncio.get_event_loop() protocol, _ = create_dsmr_protocol( - dsmr_version, telegram_callback, loop=loop) + dsmr_version, telegram_callback, loop=loop, + keep_alive_interval=keep_alive_interval) conn = loop.create_connection(protocol, host, port) return conn @@ -69,7 +71,8 @@ class DSMRProtocol(asyncio.Protocol): transport = None telegram_callback = None - def __init__(self, loop, telegram_parser, telegram_callback=None): + def __init__(self, loop, telegram_parser, + telegram_callback=None, keep_alive_interval=None): """Initialize class.""" self.loop = loop self.log = logging.getLogger(__name__) @@ -80,21 +83,36 @@ def __init__(self, loop, telegram_parser, telegram_callback=None): self.telegram_buffer = TelegramBuffer() # keep a lock until the connection is closed self._closed = asyncio.Event() + self._keep_alive_interval = keep_alive_interval + self._active = True def connection_made(self, transport): """Just logging for now.""" self.transport = transport self.log.debug('connected') + self._active = False + if self._keep_alive_interval: + self.loop.call_later(self._keep_alive_interval, self.keep_alive) def data_received(self, data): """Add incoming data to buffer.""" data = data.decode('ascii') + self._active = True self.log.debug('received data: %s', data) self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): self.handle_telegram(telegram) + def keep_alive(self): + if self._active: + self.log.debug('keep-alive checked') + self._active = False + self.loop.call_later(self._keep_alive_interval, self.keep_alive) + else: + self.log.debug('keep-alive failed') + self.transport.close() + def connection_lost(self, exc): """Stop when connection is lost.""" if exc: From 74535349278e3b136894388888949584f28c1725 Mon Sep 17 00:00:00 2001 From: Rene Hogendoorn Date: Tue, 19 Jan 2021 10:55:20 +0100 Subject: [PATCH 095/226] Raised log level to warning for failed keep-alive check --- 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 ef48ae7..e5e6a66 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -14,7 +14,7 @@ SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 -def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **args): +def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): """Creates a DSMR asyncio protocol.""" if dsmr_version == '2.2': @@ -37,7 +37,7 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **args): dsmr_version) protocol = partial(DSMRProtocol, loop, TelegramParser(specification), - telegram_callback=telegram_callback, **args) + telegram_callback=telegram_callback, **kwargs) return protocol, serial_settings @@ -110,7 +110,7 @@ def keep_alive(self): self._active = False self.loop.call_later(self._keep_alive_interval, self.keep_alive) else: - self.log.debug('keep-alive failed') + self.log.warning('keep-alive check failed') self.transport.close() def connection_lost(self, exc): From 1cdda2eaba66a7b558586315da379c23f9620cec Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 7 Feb 2021 13:40:20 +0100 Subject: [PATCH 096/226] catch parse errors in TelegramParser, ignore lines that can not be parsed --- dsmr_parser/parsers.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 5c44f8b..fab9a50 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -10,7 +10,6 @@ class TelegramParser(object): - crc16_tab = [] def __init__(self, telegram_specification, apply_checksum_validation=True): @@ -56,7 +55,11 @@ def parse(self, telegram_data): # Some signatures are optional and may not be present, # so only parse lines that match if match: - telegram[signature] = parser.parse(match.group(0)) + try: + telegram[signature] = parser.parse(match.group(0)) + except Exception: + logger.error("ignore line with signature {}, because parsing failed.".format(signature), + exc_info=True) return telegram @@ -219,6 +222,7 @@ class ProfileGenericParser(DSMRObjectParser): 8) Buffer value 2 (oldest entry of buffer attribute without unit) 9) Unit of buffer values (Unit of capture objects attribute) """ + def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): self.value_formats = head_parsers self.buffer_types = buffer_types @@ -271,7 +275,6 @@ def __init__(self, coerce_type): self.coerce_type = coerce_type def parse(self, value): - unit_of_measurement = None if value and '*' in value: From b901b3f74eddd52883e7f7e75f78ec13d2e5d819 Mon Sep 17 00:00:00 2001 From: Rene Hogendoorn Date: Thu, 11 Feb 2021 11:08:19 +0100 Subject: [PATCH 097/226] Add unit test for keep-alive --- dsmr_parser/clients/protocol.py | 8 +++++--- test/test_protocol.py | 29 +++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index e5e6a66..9b4536e 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -91,7 +91,7 @@ def connection_made(self, transport): self.transport = transport self.log.debug('connected') self._active = False - if self._keep_alive_interval: + if self.loop and self._keep_alive_interval: self.loop.call_later(self._keep_alive_interval, self.keep_alive) def data_received(self, data): @@ -108,10 +108,12 @@ def keep_alive(self): if self._active: self.log.debug('keep-alive checked') self._active = False - self.loop.call_later(self._keep_alive_interval, self.keep_alive) + if self.loop: + self.loop.call_later(self._keep_alive_interval, self.keep_alive) else: self.log.warning('keep-alive check failed') - self.transport.close() + if self.transport: + self.transport.close() def connection_lost(self, exc): """Stop when connection is lost.""" diff --git a/test/test_protocol.py b/test/test_protocol.py index 2fb14e0..c298d5c 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -5,7 +5,7 @@ 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 DSMRProtocol +from dsmr_parser.clients.protocol import create_dsmr_protocol TELEGRAM_V2_2 = ( @@ -35,9 +35,10 @@ class ProtocolTest(unittest.TestCase): def setUp(self): - telegram_parser = TelegramParser(telegram_specifications.V2_2) - self.protocol = DSMRProtocol(None, telegram_parser, - telegram_callback=Mock()) + new_protocol, _ = create_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.""" @@ -52,3 +53,23 @@ def test_complete_packet(self): assert float(telegram[obis.GAS_METER_READING].value) == 1.001 assert telegram[obis.GAS_METER_READING].unit == 'm3' + + def test_receive_packet(self): + """Protocol packet reception.""" + + mock_transport = Mock() + self.protocol.connection_made(mock_transport) + assert not self.protocol._active + + self.protocol.data_received(TELEGRAM_V2_2.encode('ascii')) + assert self.protocol._active + + # 1st call of keep_alive resets 'active' flag + self.protocol.keep_alive() + assert not self.protocol._active + + # 2nd call of keep_alive should close the transport + self.protocol.keep_alive() + assert mock_transport.close.called_once() + + self.protocol.connection_lost(None) From f806cc01d37a9e2ffd9821d3317caf82dce401ad Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Fri, 12 Feb 2021 17:56:33 +0100 Subject: [PATCH 098/226] Preparing release 0.28 --- CHANGELOG.rst | 4 ++++ setup.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2549c80..8c24591 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.28** (2021-02-21) +- Optional keep alive monitoring for TCP/IP connections (`pull request #73 `_). +- Catch parse errors in TelegramParser, ignore lines that can not be parsed (`pull request #74 `_). + **0.27** (2020-12-24) - fix for empty parentheses in ProfileGenericParser (redone) (`pull request #69 `_). diff --git a/setup.py b/setup.py index 427d205..e08d8b3 100644 --- a/setup.py +++ b/setup.py @@ -3,10 +3,10 @@ setup( name='dsmr-parser', description='Library to parse Dutch Smart Meter Requirements (DSMR)', - author='Nigel Dokter', + author='Nigel Dokter and many others', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.27', + version='0.28', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 8c861ee308092368690a1d9e23a67b0197d2346b Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 14 Feb 2021 22:08:35 +0100 Subject: [PATCH 099/226] resolved comment --- dsmr_parser/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index dcadfce..c062d9d 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -158,7 +158,7 @@ def __init__(self, values): @property def value(self): # value is added to make sure the telegram iterator does not break - return self.__str__() + return self.values @property def unit(self): From adaa2dcad54fa3bfdcf679f6cc793675c9ed807d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Mar 2021 11:48:49 +0100 Subject: [PATCH 100/226] Remove deprecated asyncio coroutine decorator --- README.rst | 2 +- dsmr_parser/clients/protocol.py | 5 ++--- dsmr_parser/clients/serial_.py | 7 +++---- tox.ini | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 03de485..a8d405c 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ also includes client implementation to directly read and parse smart meter data. Features -------- -DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.4, 3.5 and 3.6. +DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.5, 3.6, 3.7. 3.8 and 3.9. Client module usage diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 9b4536e..34a405e 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -136,7 +136,6 @@ def handle_telegram(self, telegram): else: self.telegram_callback(parsed_telegram) - @asyncio.coroutine - def wait_closed(self): + async def wait_closed(self): """Wait until connection is closed.""" - yield from self._closed.wait() + await self._closed.wait() diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 94e3b6f..f63ff07 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -68,8 +68,7 @@ class AsyncSerialReader(SerialReader): PORT_KEY = 'url' - @asyncio.coroutine - def read(self, queue): + async def read(self, queue): """ Read complete DSMR telegram's from the serial interface and parse it into CosemObject's and MbusObject's. @@ -81,12 +80,12 @@ def read(self, queue): """ # create Serial StreamReader conn = serial_asyncio.open_serial_connection(**self.serial_settings) - reader, _ = yield from conn + reader, _ = await conn while True: # Read line if available or give control back to loop until new # data has arrived. - data = yield from reader.readline() + data = await reader.readline() self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): diff --git a/tox.ini b/tox.ini index f2e6de4..a9a403d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35,py36,py37,py38 +envlist = py35,py36,py37,py38,py39 [testenv] deps= From c590109a1d3e787bfd0ac64d008bddb5fd614cf8 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 4 Mar 2021 21:47:12 +0100 Subject: [PATCH 101/226] Update README.rst Fixed small typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a8d405c..403a723 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ also includes client implementation to directly read and parse smart meter data. Features -------- -DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.5, 3.6, 3.7. 3.8 and 3.9. +DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.5, 3.6, 3.7, 3.8 and 3.9. Client module usage From ada02bf993e33f0f0224d85e2592ee52dff696bf Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 21 Apr 2021 23:36:25 +0200 Subject: [PATCH 102/226] Add license tag --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e08d8b3..0551d71 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ description='Library to parse Dutch Smart Meter Requirements (DSMR)', author='Nigel Dokter and many others', author_email='nigel@nldr.net', + license='MIT', url='https://github.com/ndokter/dsmr_parser', version='0.28', packages=find_packages(exclude=('test', 'test.*')), From e30c951c7fc0c7b1477b8521f1e751d93dd2ea6b Mon Sep 17 00:00:00 2001 From: Lennart99 Date: Thu, 10 Jun 2021 12:23:20 +0200 Subject: [PATCH 103/226] add special option for Landis+Gyr E360 (DMSR 5 with serial version 4) --- dsmr_parser/clients/protocol.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 34a405e..06ff997 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -23,6 +23,9 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): elif dsmr_version == '4': specification = telegram_specifications.V4 serial_settings = SERIAL_SETTINGS_V4 + elif dsmr_version == '4+': + specification = telegram_specifications.V5 + serial_settings = SERIAL_SETTINGS_V4 elif dsmr_version == '5': specification = telegram_specifications.V5 serial_settings = SERIAL_SETTINGS_V5 From fa70ada0bfee02d33aa5f400672c930bd42ec1d9 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 14 Aug 2021 17:08:27 +0200 Subject: [PATCH 104/226] Add support for Swedish smart meters --- dsmr_parser/__main__.py | 4 ++-- dsmr_parser/clients/protocol.py | 3 +++ dsmr_parser/obis_name_mapping.py | 4 +++- dsmr_parser/obis_references.py | 2 ++ dsmr_parser/telegram_specifications.py | 25 +++++++++++++++++++++++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index a9fbaa0..a24a6a2 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -16,8 +16,8 @@ def console(): help='alternatively connect using TCP host.') parser.add_argument('--port', default=None, help='TCP port to use for connection') - parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L'], - help='DSMR version (2.2, 4, 5, 5B, 5L)') + parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L', '5S'], + help='DSMR version (2.2, 4, 5, 5B, 5L, 5S)') parser.add_argument('--verbose', '-v', action='count') args = parser.parse_args() diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 34a405e..fce549d 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -32,6 +32,9 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): elif dsmr_version == "5L": specification = telegram_specifications.LUXEMBOURG_SMARTY serial_settings = SERIAL_SETTINGS_V5 + elif dsmr_version == "5S": + specification = telegram_specifications.SWEDEN + serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index bf00f13..4028890 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -52,7 +52,9 @@ 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.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', } REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index fe8952e..5ac3b66 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -66,3 +66,5 @@ 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-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index b06e4f4..4e59f51 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -156,3 +156,28 @@ obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), 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 +SWEDEN = { + 'checksum_support': True, + '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.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), + } +} From 45de34906244cd7963f5b75f1ff0ed7f47a3ffad Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 18 Aug 2021 16:54:44 +0200 Subject: [PATCH 105/226] Prepare version 0.29 --- CHANGELOG.rst | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8c24591..724ae96 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Change Log ---------- +**0.29** (2021-08-18) +- Add support for Swedish smart meters (`pull request #86 `_). + **0.28** (2021-02-21) - Optional keep alive monitoring for TCP/IP connections (`pull request #73 `_). - Catch parse errors in TelegramParser, ignore lines that can not be parsed (`pull request #74 `_). diff --git a/setup.py b/setup.py index 0551d71..f15dcdd 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='0.28', + version='0.29', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From d7e1f4116239a3202ae0e8566144c69525e1f448 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 18 Aug 2021 16:59:19 +0200 Subject: [PATCH 106/226] Prepare version 0.30 --- CHANGELOG.rst | 6 +++++- setup.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 724ae96..d539d2b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,13 @@ Change Log ---------- -**0.29** (2021-08-18) +**0.30** (2021-08-18) - Add support for Swedish smart meters (`pull request #86 `_). +**0.29** (2021-04-18) +- Add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break (`pull request #71 `_). +Remove deprecated asyncio coroutine decorator (`pull request #76 `_). + **0.28** (2021-02-21) - Optional keep alive monitoring for TCP/IP connections (`pull request #73 `_). - Catch parse errors in TelegramParser, ignore lines that can not be parsed (`pull request #74 `_). diff --git a/setup.py b/setup.py index f15dcdd..7ad7c68 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='0.29', + version='0.30', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 4688cf905862a02f26cc63484d4e5b0edf8f9440 Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 22 Sep 2021 21:26:54 +0200 Subject: [PATCH 107/226] test/test_protocol.py:6:1: W0611 'dsmr_parser.telegram_specifications' imported but unused [pyflakes] test/test_protocol.py:7:1: W0611 'dsmr_parser.parsers.TelegramParser' imported but unused [pyflakes] --- test/test_protocol.py | 2 -- 1 file changed, 2 deletions(-) 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 @@ 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 From b3a705a74d8eac98210a1fbb3f18406d65c2299c Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 22 Sep 2021 21:27:22 +0200 Subject: [PATCH 108/226] "pytest-catchlog plugin has been merged into the core, please remove it from your requirements" --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index a9a403d..855f11c 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ deps= pytest-cov pylama pytest-asyncio - pytest-catchlog pytest-mock commands= py.test --cov=dsmr_parser test {posargs} From fcb0dc600baae87a7b5be0580f37cfb56a7e995b Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 22 Sep 2021 21:28:17 +0200 Subject: [PATCH 109/226] CI tests using Github Actions --- .github/workflows/tests.yml | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..95abb2c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,44 @@ +name: Tests + +on: + push: ~ + pull_request: ~ + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 # Don't run forever when stale + + strategy: + matrix: + python-version: + - 3.6 + - 3.7 + - 3.8 + - 3.9 + + name: Python ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v2 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Cached PIP dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cache/pip + ~/.tox/python/.pytest_cache + key: pip-${{ matrix.python-version }}-${{ hashFiles('setup.py', 'tox.ini') }} + restore-keys: pip-${{ matrix.python-version }}- + + - name: Run tests + run: | + pip install tox + tox + + - name: Code coverage upload + uses: codecov/codecov-action@v1 From da98cf1d057147f62d0329fc25eb45659e0219fb Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 22 Sep 2021 21:28:36 +0200 Subject: [PATCH 110/226] Drop Travis (travis.org is EOL) --- .travis.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bc6b513..0000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: python - -python: - - 2.7 - - 3.5 - - 3.6 - - 3.8 - -install: pip install tox-travis codecov - -script: tox - -after_success: - - codecov - -matrix: - allow_failures: - - python: 2.7 From 62aa0ac3abf22637d3c793e489227e0817493d1e Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 22 Sep 2021 21:31:11 +0200 Subject: [PATCH 111/226] W0611 'asyncio' imported but unused [pyflakes] --- dsmr_parser/clients/serial_.py | 1 - 1 file changed, 1 deletion(-) 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 From 947fd6437763cad68fc99a1182c65c857639c65e Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 22 Sep 2021 21:33:22 +0200 Subject: [PATCH 112/226] E501 line too long (170 > 120 characters) [pycodestyle] --- dsmr_parser/telegram_specifications.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 4e59f51..8a0abf5 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -157,7 +157,8 @@ 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 SWEDEN = { 'checksum_support': True, 'objects': { From 61de170a79d119606dee08f98309fdc938064722 Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 22 Sep 2021 21:36:46 +0200 Subject: [PATCH 113/226] Tox no longer relies on every Python version installed, due to multiple CI runners --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index 855f11c..27fc713 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,3 @@ -[tox] -envlist = py35,py36,py37,py38,py39 - [testenv] deps= pytest From 88ef4f4921ba9ac7ab6ba79384b69ec89b47042c Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 22 Sep 2021 21:38:24 +0200 Subject: [PATCH 114/226] Split dependency installation step in Actions --- .github/workflows/tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 95abb2c..5f6b2b0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,10 +35,11 @@ jobs: key: pip-${{ matrix.python-version }}-${{ hashFiles('setup.py', 'tox.ini') }} restore-keys: pip-${{ matrix.python-version }}- + - name: Install dependencies + run: pip install tox + - name: Run tests - run: | - pip install tox - tox + run: tox - name: Code coverage upload uses: codecov/codecov-action@v1 From dc608f9da3250e10121dcf112c9bd9941a70fd70 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 31 Oct 2021 13:08:42 +0100 Subject: [PATCH 115/226] Update README.rst --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 403a723..01b9af3 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,6 @@ +**Notice:** this repository is in need of a new maintainer. If you are interested or have ideas about this, please let me know. + + DSMR Parser =========== From 761aaccb3f7a7551733174d5dfeecb7e6ca2f834 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Fri, 12 Nov 2021 15:19:43 +0000 Subject: [PATCH 116/226] adding EasyMeter Q3D support --- dsmr_parser/__main__.py | 40 +++++---- dsmr_parser/clients/protocol.py | 73 ++++++++++------ dsmr_parser/obis_name_mapping.py | 97 +++++++++++---------- dsmr_parser/obis_references.py | 115 ++++++++++++++----------- dsmr_parser/telegram_specifications.py | 113 +++++++++++++++--------- 5 files changed, 256 insertions(+), 182 deletions(-) diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index a24a6a2..1a127cf 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -10,15 +10,20 @@ def console(): """Output DSMR data to console.""" parser = argparse.ArgumentParser(description=console.__doc__) - parser.add_argument('--device', default='/dev/ttyUSB0', - help='port to read DSMR data from') - parser.add_argument('--host', default=None, - help='alternatively connect using TCP host.') - parser.add_argument('--port', default=None, - help='TCP port to use for connection') - parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L', '5S'], - help='DSMR version (2.2, 4, 5, 5B, 5L, 5S)') - parser.add_argument('--verbose', '-v', action='count') + parser.add_argument( + "--device", default="/dev/ttyUSB0", help="port to read DSMR data from" + ) + parser.add_argument( + "--host", default=None, help="alternatively connect using TCP host." + ) + parser.add_argument("--port", default=None, help="TCP port to use for connection") + parser.add_argument( + "--version", + default="2.2", + choices=["2.2", "4", "5", "5B", "5L", "5S", "Q3D"], + help="DSMR version (2.2, 4, 5, 5B, 5L, 5S, Q3D)", + ) + parser.add_argument("--verbose", "-v", action="count") args = parser.parse_args() @@ -39,13 +44,18 @@ def print_callback(telegram): # create tcp or serial connection depending on args if args.host and args.port: - create_connection = partial(create_tcp_dsmr_reader, - args.host, args.port, args.version, - print_callback, loop=loop) + create_connection = partial( + create_tcp_dsmr_reader, + args.host, + args.port, + args.version, + print_callback, + loop=loop, + ) else: - create_connection = partial(create_dsmr_reader, - args.device, args.version, - print_callback, loop=loop) + create_connection = partial( + create_dsmr_reader, args.device, args.version, print_callback, loop=loop + ) try: # connect and keep connected until interrupted by ctrl-c diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index fce549d..560c0aa 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -10,23 +10,26 @@ from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.parsers import TelegramParser -from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ - SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 +from dsmr_parser.clients.settings import ( + SERIAL_SETTINGS_V2_2, + SERIAL_SETTINGS_V4, + SERIAL_SETTINGS_V5, +) def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): """Creates a DSMR asyncio protocol.""" - if dsmr_version == '2.2': + if dsmr_version == "2.2": specification = telegram_specifications.V2_2 serial_settings = SERIAL_SETTINGS_V2_2 - elif dsmr_version == '4': + elif dsmr_version == "4": specification = telegram_specifications.V4 serial_settings = SERIAL_SETTINGS_V4 - elif dsmr_version == '5': + elif dsmr_version == "5": specification = telegram_specifications.V5 serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == '5B': + elif dsmr_version == "5B": specification = telegram_specifications.BELGIUM_FLUVIUS serial_settings = SERIAL_SETTINGS_V5 elif dsmr_version == "5L": @@ -35,12 +38,21 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): elif dsmr_version == "5S": specification = telegram_specifications.SWEDEN serial_settings = SERIAL_SETTINGS_V5 + elif dsmr_version == "Q3D": + specification = telegram_specifications.Q3D + serial_settings = SERIAL_SETTINGS_V5 else: - raise NotImplementedError("No telegram parser found for version: %s", - dsmr_version) - - protocol = partial(DSMRProtocol, loop, TelegramParser(specification), - telegram_callback=telegram_callback, **kwargs) + raise NotImplementedError( + "No telegram parser found for version: %s", dsmr_version + ) + + protocol = partial( + DSMRProtocol, + loop, + TelegramParser(specification), + telegram_callback=telegram_callback, + **kwargs + ) return protocol, serial_settings @@ -48,22 +60,26 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): """Creates a DSMR asyncio protocol coroutine using serial port.""" protocol, serial_settings = create_dsmr_protocol( - dsmr_version, telegram_callback, loop=None) - serial_settings['url'] = port + dsmr_version, telegram_callback, loop=None + ) + serial_settings["url"] = port conn = create_serial_connection(loop, protocol, **serial_settings) return conn -def create_tcp_dsmr_reader(host, port, dsmr_version, - telegram_callback, loop=None, - keep_alive_interval=None): +def create_tcp_dsmr_reader( + host, port, dsmr_version, telegram_callback, loop=None, keep_alive_interval=None +): """Creates a DSMR asyncio protocol coroutine using TCP connection.""" if not loop: loop = asyncio.get_event_loop() protocol, _ = create_dsmr_protocol( - dsmr_version, telegram_callback, loop=loop, - keep_alive_interval=keep_alive_interval) + dsmr_version, + telegram_callback, + loop=loop, + keep_alive_interval=keep_alive_interval, + ) conn = loop.create_connection(protocol, host, port) return conn @@ -74,8 +90,9 @@ class DSMRProtocol(asyncio.Protocol): transport = None telegram_callback = None - def __init__(self, loop, telegram_parser, - telegram_callback=None, keep_alive_interval=None): + def __init__( + self, loop, telegram_parser, telegram_callback=None, keep_alive_interval=None + ): """Initialize class.""" self.loop = loop self.log = logging.getLogger(__name__) @@ -92,16 +109,16 @@ def __init__(self, loop, telegram_parser, def connection_made(self, transport): """Just logging for now.""" self.transport = transport - self.log.debug('connected') + self.log.debug("connected") self._active = False if self.loop and self._keep_alive_interval: self.loop.call_later(self._keep_alive_interval, self.keep_alive) def data_received(self, data): """Add incoming data to buffer.""" - data = data.decode('ascii') + data = data.decode("ascii") self._active = True - self.log.debug('received data: %s', data) + self.log.debug("received data: %s", data) self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): @@ -109,26 +126,26 @@ def data_received(self, data): def keep_alive(self): if self._active: - self.log.debug('keep-alive checked') + self.log.debug("keep-alive checked") self._active = False if self.loop: self.loop.call_later(self._keep_alive_interval, self.keep_alive) else: - self.log.warning('keep-alive check failed') + self.log.warning("keep-alive check failed") if self.transport: self.transport.close() def connection_lost(self, exc): """Stop when connection is lost.""" if exc: - self.log.exception('disconnected due to exception', exc_info=exc) + self.log.exception("disconnected due to exception", exc_info=exc) else: - self.log.info('disconnected because of close/abort.') + self.log.info("disconnected because of close/abort.") self._closed.set() def handle_telegram(self, telegram): """Send off parsed telegram to handling callback.""" - self.log.debug('got telegram: %s', telegram) + self.log.debug("got telegram: %s", telegram) try: parsed_telegram = self.telegram_parser.parse(telegram) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 4028890..f7aff77 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -8,53 +8,56 @@ """ EN = { - obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', - obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', - obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL', - obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1', - obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2', - obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1', - obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2', - obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF', - obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER', - obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE', - obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY', - obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT', - obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT', - obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG', - obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT', - obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT', - obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT', - obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT', - obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT', - obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT', - obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1', - obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2', - obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3', - obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1', - obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2', - obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3', - obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE', - obis.TEXT_MESSAGE: 'TEXT_MESSAGE', - obis.DEVICE_TYPE: 'DEVICE_TYPE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', - obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS', - obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING', - obis.GAS_METER_READING: 'GAS_METER_READING', - obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', - obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', - 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.P1_MESSAGE_HEADER: "P1_MESSAGE_HEADER", + obis.P1_MESSAGE_TIMESTAMP: "P1_MESSAGE_TIMESTAMP", + obis.ELECTRICITY_IMPORTED_TOTAL: "ELECTRICITY_IMPORTED_TOTAL", + obis.ELECTRICITY_USED_TARIFF_1: "ELECTRICITY_USED_TARIFF_1", + obis.ELECTRICITY_USED_TARIFF_2: "ELECTRICITY_USED_TARIFF_2", + obis.ELECTRICITY_DELIVERED_TARIFF_1: "ELECTRICITY_DELIVERED_TARIFF_1", + obis.ELECTRICITY_DELIVERED_TARIFF_2: "ELECTRICITY_DELIVERED_TARIFF_2", + obis.ELECTRICITY_ACTIVE_TARIFF: "ELECTRICITY_ACTIVE_TARIFF", + obis.EQUIPMENT_IDENTIFIER: "EQUIPMENT_IDENTIFIER", + obis.CURRENT_ELECTRICITY_USAGE: "CURRENT_ELECTRICITY_USAGE", + obis.CURRENT_ELECTRICITY_DELIVERY: "CURRENT_ELECTRICITY_DELIVERY", + obis.LONG_POWER_FAILURE_COUNT: "LONG_POWER_FAILURE_COUNT", + obis.SHORT_POWER_FAILURE_COUNT: "SHORT_POWER_FAILURE_COUNT", + obis.POWER_EVENT_FAILURE_LOG: "POWER_EVENT_FAILURE_LOG", + obis.VOLTAGE_SAG_L1_COUNT: "VOLTAGE_SAG_L1_COUNT", + obis.VOLTAGE_SAG_L2_COUNT: "VOLTAGE_SAG_L2_COUNT", + obis.VOLTAGE_SAG_L3_COUNT: "VOLTAGE_SAG_L3_COUNT", + obis.VOLTAGE_SWELL_L1_COUNT: "VOLTAGE_SWELL_L1_COUNT", + obis.VOLTAGE_SWELL_L2_COUNT: "VOLTAGE_SWELL_L2_COUNT", + obis.VOLTAGE_SWELL_L3_COUNT: "VOLTAGE_SWELL_L3_COUNT", + obis.INSTANTANEOUS_VOLTAGE_L1: "INSTANTANEOUS_VOLTAGE_L1", + obis.INSTANTANEOUS_VOLTAGE_L2: "INSTANTANEOUS_VOLTAGE_L2", + obis.INSTANTANEOUS_VOLTAGE_L3: "INSTANTANEOUS_VOLTAGE_L3", + obis.INSTANTANEOUS_CURRENT_L1: "INSTANTANEOUS_CURRENT_L1", + obis.INSTANTANEOUS_CURRENT_L2: "INSTANTANEOUS_CURRENT_L2", + obis.INSTANTANEOUS_CURRENT_L3: "INSTANTANEOUS_CURRENT_L3", + obis.TEXT_MESSAGE_CODE: "TEXT_MESSAGE_CODE", + obis.TEXT_MESSAGE: "TEXT_MESSAGE", + obis.DEVICE_TYPE: "DEVICE_TYPE", + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE", + obis.EQUIPMENT_IDENTIFIER_GAS: "EQUIPMENT_IDENTIFIER_GAS", + obis.HOURLY_GAS_METER_READING: "HOURLY_GAS_METER_READING", + obis.GAS_METER_READING: "GAS_METER_READING", + obis.ACTUAL_TRESHOLD_ELECTRICITY: "ACTUAL_TRESHOLD_ELECTRICITY", + obis.ACTUAL_SWITCH_POSITION: "ACTUAL_SWITCH_POSITION", + 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", } REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 5ac3b66..e355ead 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -6,65 +6,76 @@ Might be refactored in a backwards incompatible way as soon as proper telegram 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_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' -EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n' -CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' -CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n' -LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n' -SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n' -POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n' -VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n' -VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n' -VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n' -VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n' -VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n' -VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n' -INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n' -INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n' -INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n' -INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n' -INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n' -INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n' -TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n' -TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n' -DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'\d-\d:22\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'\d-\d:42\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'\d-\d:62\.7\.0.+?\r\n' -EQUIPMENT_IDENTIFIER_GAS = r'\d-\d:96\.1\.0.+?\r\n' +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_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" +EQUIPMENT_IDENTIFIER = r"\d-\d:96\.1\.1.+?\r\n" +CURRENT_ELECTRICITY_USAGE = r"\d-\d:1\.7\.0.+?\r\n" +CURRENT_ELECTRICITY_DELIVERY = r"\d-\d:2\.7\.0.+?\r\n" +LONG_POWER_FAILURE_COUNT = r"96\.7\.9.+?\r\n" +SHORT_POWER_FAILURE_COUNT = r"96\.7\.21.+?\r\n" +POWER_EVENT_FAILURE_LOG = r"99\.97\.0.+?\r\n" +VOLTAGE_SAG_L1_COUNT = r"\d-\d:32\.32\.0.+?\r\n" +VOLTAGE_SAG_L2_COUNT = r"\d-\d:52\.32\.0.+?\r\n" +VOLTAGE_SAG_L3_COUNT = r"\d-\d:72\.32\.0.+?\r\n" +VOLTAGE_SWELL_L1_COUNT = r"\d-\d:32\.36\.0.+?\r\n" +VOLTAGE_SWELL_L2_COUNT = r"\d-\d:52\.36\.0.+?\r\n" +VOLTAGE_SWELL_L3_COUNT = r"\d-\d:72\.36\.0.+?\r\n" +INSTANTANEOUS_VOLTAGE_L1 = r"\d-\d:32\.7\.0.+?\r\n" +INSTANTANEOUS_VOLTAGE_L2 = r"\d-\d:52\.7\.0.+?\r\n" +INSTANTANEOUS_VOLTAGE_L3 = r"\d-\d:72\.7\.0.+?\r\n" +INSTANTANEOUS_CURRENT_L1 = r"\d-\d:31\.7\.0.+?\r\n" +INSTANTANEOUS_CURRENT_L2 = r"\d-\d:51\.7\.0.+?\r\n" +INSTANTANEOUS_CURRENT_L3 = r"\d-\d:71\.7\.0.+?\r\n" +TEXT_MESSAGE_CODE = r"\d-\d:96\.13\.1.+?\r\n" +TEXT_MESSAGE = r"\d-\d:96\.13\.0.+?\r\n" +DEVICE_TYPE = r"\d-\d:24\.1\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r"\d-\d:21\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r"\d-\d:41\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r"\d-\d:61\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r"\d-\d:22\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r"\d-\d:42\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r"\d-\d:62\.7\.0.+?\r\n" +EQUIPMENT_IDENTIFIER_GAS = r"\d-\d:96\.1\.0.+?\r\n" # TODO differences between gas meter readings in v3 and lower and v4 and up -HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.1.+?\r\n' -GAS_METER_READING = r'\d-\d:24\.3\.0.+?\r\n.+?\r\n' -ACTUAL_TRESHOLD_ELECTRICITY = r'\d-\d:17\.0\.0.+?\r\n' -ACTUAL_SWITCH_POSITION = r'\d-\d:96\.3\.10.+?\r\n' -VALVE_POSITION_GAS = r'\d-\d:24\.4\.0.+?\r\n' +HOURLY_GAS_METER_READING = r"\d-\d:24\.2\.1.+?\r\n" +GAS_METER_READING = r"\d-\d:24\.3\.0.+?\r\n.+?\r\n" +ACTUAL_TRESHOLD_ELECTRICITY = r"\d-\d:17\.0\.0.+?\r\n" +ACTUAL_SWITCH_POSITION = r"\d-\d:96\.3\.10.+?\r\n" +VALVE_POSITION_GAS = r"\d-\d:24\.4\.0.+?\r\n" # TODO 17.0.0 # TODO 96.3.10 -ELECTRICITY_USED_TARIFF_ALL = ( - ELECTRICITY_USED_TARIFF_1, - ELECTRICITY_USED_TARIFF_2 -) +ELECTRICITY_USED_TARIFF_ALL = (ELECTRICITY_USED_TARIFF_1, ELECTRICITY_USED_TARIFF_2) ELECTRICITY_DELIVERED_TARIFF_ALL = ( ELECTRICITY_DELIVERED_TARIFF_1, - ELECTRICITY_DELIVERED_TARIFF_2 + ELECTRICITY_DELIVERED_TARIFF_2, ) # Alternate codes for foreign countries. -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-) +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 4e59f51..0e3ed61 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -2,9 +2,18 @@ from copy import deepcopy from dsmr_parser import obis_references as obis -from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser +from dsmr_parser.parsers import ( + CosemParser, + ValueParser, + MBusParser, + ProfileGenericParser, +) from dsmr_parser.value_types import timestamp -from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS +from dsmr_parser.profile_generic_specifications import ( + BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS, +) """ dsmr_parser.telegram_specifications @@ -15,8 +24,8 @@ """ V2_2 = { - 'checksum_support': False, - 'objects': { + "checksum_support": False, + "objects": { obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), @@ -41,14 +50,14 @@ ValueParser(str), # unit, position 5 ValueParser(Decimal), # meter reading, position 6 ), - } + }, } V3 = V2_2 V4 = { - 'checksum_support': True, - 'objects': { + "checksum_support": True, + "objects": { obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), @@ -61,10 +70,9 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: - ProfileGenericParser(BUFFER_TYPES, - PG_HEAD_PARSERS, - PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), + obis.POWER_EVENT_FAILURE_LOG: ProfileGenericParser( + BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS + ), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), @@ -85,15 +93,14 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ) - } + ValueParser(timestamp), ValueParser(Decimal) + ), + }, } V5 = { - 'checksum_support': True, - 'objects': { + "checksum_support": True, + "objects": { obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), @@ -107,10 +114,9 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: - ProfileGenericParser(BUFFER_TYPES, - PG_HEAD_PARSERS, - PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), + obis.POWER_EVENT_FAILURE_LOG: ProfileGenericParser( + BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS + ), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), @@ -133,38 +139,46 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ) - } + ValueParser(timestamp), ValueParser(Decimal) + ), + }, } ALL = (V2_2, V3, V4, V5) BELGIUM_FLUVIUS = deepcopy(V5) -BELGIUM_FLUVIUS['objects'].update({ - obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ) -}) +BELGIUM_FLUVIUS["objects"].update( + { + obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( + ValueParser(timestamp), ValueParser(Decimal) + ) + } +) 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)), -}) +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) + ), + } +) # Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf SWEDEN = { - 'checksum_support': True, - 'objects': { + "checksum_support": True, + "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.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 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)), @@ -179,5 +193,24 @@ obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), - } + }, +} + +Q3D = { + "checksum_support": False, + "objects": { + obis.Q3D_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser( + ValueParser(Decimal) + ), + obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser( + ValueParser(Decimal) + ), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + obis.Q3D_EQUIPMENT_STATE: CosemParser(ValueParser(str)), + obis.Q3D_EQUIPMENT_SERIALNUMBER: CosemParser(ValueParser(str)), + }, } From 007b3ea0895ecd91441fb918d48290cee94d1949 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Fri, 12 Nov 2021 18:13:48 +0100 Subject: [PATCH 117/226] ignoring trailing \xff in byte stream --- dsmr_parser/clients/protocol.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 560c0aa..56ed26d 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -116,7 +116,7 @@ def connection_made(self, transport): def data_received(self, data): """Add incoming data to buffer.""" - data = data.decode("ascii") + data = data.decode("ascii", errors="ignore") self._active = True self.log.debug("received data: %s", data) self.telegram_buffer.append(data) diff --git a/setup.py b/setup.py index 7ad7c68..3852064 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='0.30', + version='0.31', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 8b8b952ce15447cde6e2d0e18c53b37500de845a Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Fri, 12 Nov 2021 17:38:16 +0000 Subject: [PATCH 118/226] clean-up re-format --- dsmr_parser/__main__.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index 1a127cf..24ed65d 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -10,20 +10,15 @@ def console(): """Output DSMR data to console.""" parser = argparse.ArgumentParser(description=console.__doc__) - parser.add_argument( - "--device", default="/dev/ttyUSB0", help="port to read DSMR data from" - ) - parser.add_argument( - "--host", default=None, help="alternatively connect using TCP host." - ) - parser.add_argument("--port", default=None, help="TCP port to use for connection") - parser.add_argument( - "--version", - default="2.2", - choices=["2.2", "4", "5", "5B", "5L", "5S", "Q3D"], - help="DSMR version (2.2, 4, 5, 5B, 5L, 5S, Q3D)", - ) - parser.add_argument("--verbose", "-v", action="count") + parser.add_argument('--device', default='/dev/ttyUSB0', + help='port to read DSMR data from') + parser.add_argument('--host', default=None, + help='alternatively connect using TCP host.') + parser.add_argument('--port', default=None, + help='TCP port to use for connection') + parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L', '5S', 'Q3D'], + help='DSMR version (2.2, 4, 5, 5B, 5L, 5S, Q3D)') + parser.add_argument('--verbose', '-v', action='count') args = parser.parse_args() From e4f384c2b76615b68f32f6d6df24017b0a383fcf Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Fri, 12 Nov 2021 17:48:21 +0000 Subject: [PATCH 119/226] clean-up re-format --- dsmr_parser/__main__.py | 17 +++----- dsmr_parser/clients/protocol.py | 76 ++++++++++++++------------------- 2 files changed, 37 insertions(+), 56 deletions(-) diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index 24ed65d..9169318 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -39,18 +39,13 @@ def print_callback(telegram): # create tcp or serial connection depending on args if args.host and args.port: - create_connection = partial( - create_tcp_dsmr_reader, - args.host, - args.port, - args.version, - print_callback, - loop=loop, - ) + create_connection = partial(create_tcp_dsmr_reader, + args.host, args.port, args.version, + print_callback, loop=loop) else: - create_connection = partial( - create_dsmr_reader, args.device, args.version, print_callback, loop=loop - ) + create_connection = partial(create_dsmr_reader, + args.device, args.version, + print_callback, loop=loop) try: # connect and keep connected until interrupted by ctrl-c diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 56ed26d..6a7a17e 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -10,49 +10,40 @@ from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.parsers import TelegramParser -from dsmr_parser.clients.settings import ( - SERIAL_SETTINGS_V2_2, - SERIAL_SETTINGS_V4, - SERIAL_SETTINGS_V5, -) +from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ + SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): """Creates a DSMR asyncio protocol.""" - if dsmr_version == "2.2": + if dsmr_version == '2.2': specification = telegram_specifications.V2_2 serial_settings = SERIAL_SETTINGS_V2_2 - elif dsmr_version == "4": + elif dsmr_version == '4': specification = telegram_specifications.V4 serial_settings = SERIAL_SETTINGS_V4 - elif dsmr_version == "5": + elif dsmr_version == '5': specification = telegram_specifications.V5 serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == "5B": + elif dsmr_version == '5B': specification = telegram_specifications.BELGIUM_FLUVIUS serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == "5L": + elif dsmr_version == '5L': specification = telegram_specifications.LUXEMBOURG_SMARTY serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == "5S": + elif dsmr_version == '5S': specification = telegram_specifications.SWEDEN serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == "Q3D": + elif dsmr_version == 'Q3D': specification = telegram_specifications.Q3D serial_settings = SERIAL_SETTINGS_V5 else: - raise NotImplementedError( - "No telegram parser found for version: %s", dsmr_version - ) - - protocol = partial( - DSMRProtocol, - loop, - TelegramParser(specification), - telegram_callback=telegram_callback, - **kwargs - ) + raise NotImplementedError("No telegram parser found for version: %s", + dsmr_version) + + protocol = partial(DSMRProtocol, loop, TelegramParser(specification), + telegram_callback=telegram_callback, **kwargs) return protocol, serial_settings @@ -60,26 +51,22 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): """Creates a DSMR asyncio protocol coroutine using serial port.""" protocol, serial_settings = create_dsmr_protocol( - dsmr_version, telegram_callback, loop=None - ) - serial_settings["url"] = port + dsmr_version, telegram_callback, loop=None) + serial_settings['url'] = port conn = create_serial_connection(loop, protocol, **serial_settings) return conn -def create_tcp_dsmr_reader( - host, port, dsmr_version, telegram_callback, loop=None, keep_alive_interval=None -): +def create_tcp_dsmr_reader(host, port, dsmr_version, + telegram_callback, loop=None, + keep_alive_interval=None): """Creates a DSMR asyncio protocol coroutine using TCP connection.""" if not loop: loop = asyncio.get_event_loop() protocol, _ = create_dsmr_protocol( - dsmr_version, - telegram_callback, - loop=loop, - keep_alive_interval=keep_alive_interval, - ) + dsmr_version, telegram_callback, loop=loop, + keep_alive_interval=keep_alive_interval) conn = loop.create_connection(protocol, host, port) return conn @@ -90,9 +77,8 @@ class DSMRProtocol(asyncio.Protocol): transport = None telegram_callback = None - def __init__( - self, loop, telegram_parser, telegram_callback=None, keep_alive_interval=None - ): + def __init__(self, loop, telegram_parser, + telegram_callback=None, keep_alive_interval=None): """Initialize class.""" self.loop = loop self.log = logging.getLogger(__name__) @@ -109,16 +95,16 @@ def __init__( def connection_made(self, transport): """Just logging for now.""" self.transport = transport - self.log.debug("connected") + self.log.debug('connected') self._active = False if self.loop and self._keep_alive_interval: self.loop.call_later(self._keep_alive_interval, self.keep_alive) def data_received(self, data): """Add incoming data to buffer.""" - data = data.decode("ascii", errors="ignore") + data = data.decode('ascii', errors='ignore') self._active = True - self.log.debug("received data: %s", data) + self.log.debug('received data: %s', data) self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): @@ -126,26 +112,26 @@ def data_received(self, data): def keep_alive(self): if self._active: - self.log.debug("keep-alive checked") + self.log.debug('keep-alive checked') self._active = False if self.loop: self.loop.call_later(self._keep_alive_interval, self.keep_alive) else: - self.log.warning("keep-alive check failed") + self.log.warning('keep-alive check failed') if self.transport: self.transport.close() def connection_lost(self, exc): """Stop when connection is lost.""" if exc: - self.log.exception("disconnected due to exception", exc_info=exc) + self.log.exception('disconnected due to exception', exc_info=exc) else: - self.log.info("disconnected because of close/abort.") + self.log.info('disconnected because of close/abort.') self._closed.set() def handle_telegram(self, telegram): """Send off parsed telegram to handling callback.""" - self.log.debug("got telegram: %s", telegram) + self.log.debug('got telegram: %s', telegram) try: parsed_telegram = self.telegram_parser.parse(telegram) From 17b56ae07bff4080c27524872046cf377f8f9ccb Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Fri, 12 Nov 2021 17:51:24 +0000 Subject: [PATCH 120/226] clean-up re-format --- dsmr_parser/obis_name_mapping.py | 100 +++++++++++++------------- dsmr_parser/obis_references.py | 118 ++++++++++++++----------------- 2 files changed, 105 insertions(+), 113 deletions(-) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index f7aff77..fcba570 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -8,56 +8,56 @@ """ EN = { - obis.P1_MESSAGE_HEADER: "P1_MESSAGE_HEADER", - obis.P1_MESSAGE_TIMESTAMP: "P1_MESSAGE_TIMESTAMP", - obis.ELECTRICITY_IMPORTED_TOTAL: "ELECTRICITY_IMPORTED_TOTAL", - obis.ELECTRICITY_USED_TARIFF_1: "ELECTRICITY_USED_TARIFF_1", - obis.ELECTRICITY_USED_TARIFF_2: "ELECTRICITY_USED_TARIFF_2", - obis.ELECTRICITY_DELIVERED_TARIFF_1: "ELECTRICITY_DELIVERED_TARIFF_1", - obis.ELECTRICITY_DELIVERED_TARIFF_2: "ELECTRICITY_DELIVERED_TARIFF_2", - obis.ELECTRICITY_ACTIVE_TARIFF: "ELECTRICITY_ACTIVE_TARIFF", - obis.EQUIPMENT_IDENTIFIER: "EQUIPMENT_IDENTIFIER", - obis.CURRENT_ELECTRICITY_USAGE: "CURRENT_ELECTRICITY_USAGE", - obis.CURRENT_ELECTRICITY_DELIVERY: "CURRENT_ELECTRICITY_DELIVERY", - obis.LONG_POWER_FAILURE_COUNT: "LONG_POWER_FAILURE_COUNT", - obis.SHORT_POWER_FAILURE_COUNT: "SHORT_POWER_FAILURE_COUNT", - obis.POWER_EVENT_FAILURE_LOG: "POWER_EVENT_FAILURE_LOG", - obis.VOLTAGE_SAG_L1_COUNT: "VOLTAGE_SAG_L1_COUNT", - obis.VOLTAGE_SAG_L2_COUNT: "VOLTAGE_SAG_L2_COUNT", - obis.VOLTAGE_SAG_L3_COUNT: "VOLTAGE_SAG_L3_COUNT", - obis.VOLTAGE_SWELL_L1_COUNT: "VOLTAGE_SWELL_L1_COUNT", - obis.VOLTAGE_SWELL_L2_COUNT: "VOLTAGE_SWELL_L2_COUNT", - obis.VOLTAGE_SWELL_L3_COUNT: "VOLTAGE_SWELL_L3_COUNT", - obis.INSTANTANEOUS_VOLTAGE_L1: "INSTANTANEOUS_VOLTAGE_L1", - obis.INSTANTANEOUS_VOLTAGE_L2: "INSTANTANEOUS_VOLTAGE_L2", - obis.INSTANTANEOUS_VOLTAGE_L3: "INSTANTANEOUS_VOLTAGE_L3", - obis.INSTANTANEOUS_CURRENT_L1: "INSTANTANEOUS_CURRENT_L1", - obis.INSTANTANEOUS_CURRENT_L2: "INSTANTANEOUS_CURRENT_L2", - obis.INSTANTANEOUS_CURRENT_L3: "INSTANTANEOUS_CURRENT_L3", - obis.TEXT_MESSAGE_CODE: "TEXT_MESSAGE_CODE", - obis.TEXT_MESSAGE: "TEXT_MESSAGE", - obis.DEVICE_TYPE: "DEVICE_TYPE", - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE", - obis.EQUIPMENT_IDENTIFIER_GAS: "EQUIPMENT_IDENTIFIER_GAS", - obis.HOURLY_GAS_METER_READING: "HOURLY_GAS_METER_READING", - obis.GAS_METER_READING: "GAS_METER_READING", - obis.ACTUAL_TRESHOLD_ELECTRICITY: "ACTUAL_TRESHOLD_ELECTRICITY", - obis.ACTUAL_SWITCH_POSITION: "ACTUAL_SWITCH_POSITION", - 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", + obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', + obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', + obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL', + obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1', + obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2', + obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1', + obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2', + obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF', + obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER', + obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE', + obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY', + obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT', + obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT', + obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG', + obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT', + obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT', + obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT', + obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT', + obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT', + obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT', + obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1', + obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2', + obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3', + obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1', + obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2', + obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3', + obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE', + obis.TEXT_MESSAGE: 'TEXT_MESSAGE', + obis.DEVICE_TYPE: 'DEVICE_TYPE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS', + obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING', + obis.GAS_METER_READING: 'GAS_METER_READING', + obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', + obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', + 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', } REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index e355ead..3808848 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -6,76 +6,68 @@ Might be refactored in a backwards incompatible way as soon as proper telegram 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_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" -EQUIPMENT_IDENTIFIER = r"\d-\d:96\.1\.1.+?\r\n" -CURRENT_ELECTRICITY_USAGE = r"\d-\d:1\.7\.0.+?\r\n" -CURRENT_ELECTRICITY_DELIVERY = r"\d-\d:2\.7\.0.+?\r\n" -LONG_POWER_FAILURE_COUNT = r"96\.7\.9.+?\r\n" -SHORT_POWER_FAILURE_COUNT = r"96\.7\.21.+?\r\n" -POWER_EVENT_FAILURE_LOG = r"99\.97\.0.+?\r\n" -VOLTAGE_SAG_L1_COUNT = r"\d-\d:32\.32\.0.+?\r\n" -VOLTAGE_SAG_L2_COUNT = r"\d-\d:52\.32\.0.+?\r\n" -VOLTAGE_SAG_L3_COUNT = r"\d-\d:72\.32\.0.+?\r\n" -VOLTAGE_SWELL_L1_COUNT = r"\d-\d:32\.36\.0.+?\r\n" -VOLTAGE_SWELL_L2_COUNT = r"\d-\d:52\.36\.0.+?\r\n" -VOLTAGE_SWELL_L3_COUNT = r"\d-\d:72\.36\.0.+?\r\n" -INSTANTANEOUS_VOLTAGE_L1 = r"\d-\d:32\.7\.0.+?\r\n" -INSTANTANEOUS_VOLTAGE_L2 = r"\d-\d:52\.7\.0.+?\r\n" -INSTANTANEOUS_VOLTAGE_L3 = r"\d-\d:72\.7\.0.+?\r\n" -INSTANTANEOUS_CURRENT_L1 = r"\d-\d:31\.7\.0.+?\r\n" -INSTANTANEOUS_CURRENT_L2 = r"\d-\d:51\.7\.0.+?\r\n" -INSTANTANEOUS_CURRENT_L3 = r"\d-\d:71\.7\.0.+?\r\n" -TEXT_MESSAGE_CODE = r"\d-\d:96\.13\.1.+?\r\n" -TEXT_MESSAGE = r"\d-\d:96\.13\.0.+?\r\n" -DEVICE_TYPE = r"\d-\d:24\.1\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r"\d-\d:21\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r"\d-\d:41\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r"\d-\d:61\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r"\d-\d:22\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r"\d-\d:42\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r"\d-\d:62\.7\.0.+?\r\n" -EQUIPMENT_IDENTIFIER_GAS = r"\d-\d:96\.1\.0.+?\r\n" +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_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' +EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n' +CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' +CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n' +LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n' +SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n' +POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n' +VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n' +VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n' +VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n' +VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n' +VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n' +VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n' +TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n' +TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n' +DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'\d-\d:22\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'\d-\d:42\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'\d-\d:62\.7\.0.+?\r\n' +EQUIPMENT_IDENTIFIER_GAS = r'\d-\d:96\.1\.0.+?\r\n' # TODO differences between gas meter readings in v3 and lower and v4 and up -HOURLY_GAS_METER_READING = r"\d-\d:24\.2\.1.+?\r\n" -GAS_METER_READING = r"\d-\d:24\.3\.0.+?\r\n.+?\r\n" -ACTUAL_TRESHOLD_ELECTRICITY = r"\d-\d:17\.0\.0.+?\r\n" -ACTUAL_SWITCH_POSITION = r"\d-\d:96\.3\.10.+?\r\n" -VALVE_POSITION_GAS = r"\d-\d:24\.4\.0.+?\r\n" +HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.1.+?\r\n' +GAS_METER_READING = r'\d-\d:24\.3\.0.+?\r\n.+?\r\n' +ACTUAL_TRESHOLD_ELECTRICITY = r'\d-\d:17\.0\.0.+?\r\n' +ACTUAL_SWITCH_POSITION = r'\d-\d:96\.3\.10.+?\r\n' +VALVE_POSITION_GAS = r'\d-\d:24\.4\.0.+?\r\n' # TODO 17.0.0 # TODO 96.3.10 -ELECTRICITY_USED_TARIFF_ALL = (ELECTRICITY_USED_TARIFF_1, ELECTRICITY_USED_TARIFF_2) +ELECTRICITY_USED_TARIFF_ALL = ( + ELECTRICITY_USED_TARIFF_1, + ELECTRICITY_USED_TARIFF_2 +) ELECTRICITY_DELIVERED_TARIFF_ALL = ( ELECTRICITY_DELIVERED_TARIFF_1, - ELECTRICITY_DELIVERED_TARIFF_2, + ELECTRICITY_DELIVERED_TARIFF_2 ) # Alternate codes for foreign countries. -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 +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 From 2b724b87c3a18f9edda64b1ea5b79a1eef80bfd1 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Fri, 12 Nov 2021 19:21:32 +0000 Subject: [PATCH 121/226] clean-up re-format --- dsmr_parser/telegram_specifications.py | 104 ++++++++++--------------- 1 file changed, 43 insertions(+), 61 deletions(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 0e3ed61..bb82868 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -2,30 +2,20 @@ from copy import deepcopy from dsmr_parser import obis_references as obis -from dsmr_parser.parsers import ( - CosemParser, - ValueParser, - MBusParser, - ProfileGenericParser, -) +from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser from dsmr_parser.value_types import timestamp -from dsmr_parser.profile_generic_specifications import ( - BUFFER_TYPES, - PG_HEAD_PARSERS, - PG_UNIDENTIFIED_BUFFERTYPE_PARSERS, -) +from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS """ dsmr_parser.telegram_specifications ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - This module contains DSMR telegram specifications. Each specifications describes how the telegram lines are parsed. """ V2_2 = { - "checksum_support": False, - "objects": { + 'checksum_support': False, + 'objects': { obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), @@ -50,14 +40,14 @@ ValueParser(str), # unit, position 5 ValueParser(Decimal), # meter reading, position 6 ), - }, + } } V3 = V2_2 V4 = { - "checksum_support": True, - "objects": { + 'checksum_support': True, + 'objects': { obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), @@ -70,9 +60,10 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: ProfileGenericParser( - BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS - ), + obis.POWER_EVENT_FAILURE_LOG: + ProfileGenericParser(BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), @@ -93,14 +84,15 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), ValueParser(Decimal) - ), - }, + ValueParser(timestamp), + ValueParser(Decimal) + ) + } } V5 = { - "checksum_support": True, - "objects": { + 'checksum_support': True, + 'objects': { obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), @@ -114,9 +106,10 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: ProfileGenericParser( - BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS - ), + obis.POWER_EVENT_FAILURE_LOG: + ProfileGenericParser(BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), @@ -139,46 +132,38 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), ValueParser(Decimal) - ), - }, + ValueParser(timestamp), + ValueParser(Decimal) + ) + } } ALL = (V2_2, V3, V4, V5) BELGIUM_FLUVIUS = deepcopy(V5) -BELGIUM_FLUVIUS["objects"].update( - { - obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), ValueParser(Decimal) - ) - } -) +BELGIUM_FLUVIUS['objects'].update({ + obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ) +}) 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) - ), - } -) +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)), +}) # Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf SWEDEN = { - "checksum_support": True, - "objects": { + 'checksum_support': True, + '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.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 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)), @@ -193,19 +178,16 @@ obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), - }, + } } + Q3D = { "checksum_support": False, "objects": { obis.Q3D_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser( - ValueParser(Decimal) - ), - obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser( - ValueParser(Decimal) - ), + obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), + obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), From 8e2bdd32792949fc35898c94d402570aac509287 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Fri, 12 Nov 2021 19:23:38 +0000 Subject: [PATCH 122/226] clean-up re-format --- dsmr_parser/telegram_specifications.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index bb82868..83f6bd8 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -9,6 +9,7 @@ """ dsmr_parser.telegram_specifications ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + This module contains DSMR telegram specifications. Each specifications describes how the telegram lines are parsed. """ From 3092ba8b1f0a1895d12b7de2a3de7f4e4a538dd3 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Fri, 12 Nov 2021 19:24:44 +0000 Subject: [PATCH 123/226] clean-up re-format --- 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 6a7a17e..88b25fb 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -29,13 +29,13 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): elif dsmr_version == '5B': specification = telegram_specifications.BELGIUM_FLUVIUS serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == '5L': + elif dsmr_version == "5L": specification = telegram_specifications.LUXEMBOURG_SMARTY serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == '5S': + elif dsmr_version == "5S": specification = telegram_specifications.SWEDEN serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == 'Q3D': + elif dsmr_version == 'Q3D": specification = telegram_specifications.Q3D serial_settings = SERIAL_SETTINGS_V5 else: From c1202f33e9dc90283acaf8ad884aaabb83ca03d6 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Fri, 12 Nov 2021 20:12:47 +0000 Subject: [PATCH 124/226] clean-up re-format --- dsmr_parser/clients/protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 88b25fb..45f6d48 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -35,7 +35,7 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): elif dsmr_version == "5S": specification = telegram_specifications.SWEDEN serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == 'Q3D": + elif dsmr_version == "Q3D": specification = telegram_specifications.Q3D serial_settings = SERIAL_SETTINGS_V5 else: From 4073917d3e269d6fb7175fbbd18e61fb835010f8 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Sun, 14 Nov 2021 21:29:24 +0000 Subject: [PATCH 125/226] decode latin1 and added ELECTRICITY_EXPORTED_TOTAL --- dsmr_parser/clients/protocol.py | 70 +++++++++------ dsmr_parser/obis_name_mapping.py | 101 ++++++++++----------- dsmr_parser/obis_references.py | 118 +++++++++++++------------ dsmr_parser/telegram_specifications.py | 99 ++++++++++++--------- 4 files changed, 212 insertions(+), 176 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 45f6d48..3b4c062 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -10,23 +10,26 @@ from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.parsers import TelegramParser -from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ - SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 +from dsmr_parser.clients.settings import ( + SERIAL_SETTINGS_V2_2, + SERIAL_SETTINGS_V4, + SERIAL_SETTINGS_V5, +) def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): """Creates a DSMR asyncio protocol.""" - if dsmr_version == '2.2': + if dsmr_version == "2.2": specification = telegram_specifications.V2_2 serial_settings = SERIAL_SETTINGS_V2_2 - elif dsmr_version == '4': + elif dsmr_version == "4": specification = telegram_specifications.V4 serial_settings = SERIAL_SETTINGS_V4 - elif dsmr_version == '5': + elif dsmr_version == "5": specification = telegram_specifications.V5 serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == '5B': + elif dsmr_version == "5B": specification = telegram_specifications.BELGIUM_FLUVIUS serial_settings = SERIAL_SETTINGS_V5 elif dsmr_version == "5L": @@ -39,11 +42,17 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): specification = telegram_specifications.Q3D serial_settings = SERIAL_SETTINGS_V5 else: - raise NotImplementedError("No telegram parser found for version: %s", - dsmr_version) - - protocol = partial(DSMRProtocol, loop, TelegramParser(specification), - telegram_callback=telegram_callback, **kwargs) + raise NotImplementedError( + "No telegram parser found for version: %s", dsmr_version + ) + + protocol = partial( + DSMRProtocol, + loop, + TelegramParser(specification), + telegram_callback=telegram_callback, + **kwargs + ) return protocol, serial_settings @@ -51,22 +60,26 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): """Creates a DSMR asyncio protocol coroutine using serial port.""" protocol, serial_settings = create_dsmr_protocol( - dsmr_version, telegram_callback, loop=None) - serial_settings['url'] = port + dsmr_version, telegram_callback, loop=None + ) + serial_settings["url"] = port conn = create_serial_connection(loop, protocol, **serial_settings) return conn -def create_tcp_dsmr_reader(host, port, dsmr_version, - telegram_callback, loop=None, - keep_alive_interval=None): +def create_tcp_dsmr_reader( + host, port, dsmr_version, telegram_callback, loop=None, keep_alive_interval=None +): """Creates a DSMR asyncio protocol coroutine using TCP connection.""" if not loop: loop = asyncio.get_event_loop() protocol, _ = create_dsmr_protocol( - dsmr_version, telegram_callback, loop=loop, - keep_alive_interval=keep_alive_interval) + dsmr_version, + telegram_callback, + loop=loop, + keep_alive_interval=keep_alive_interval, + ) conn = loop.create_connection(protocol, host, port) return conn @@ -77,8 +90,9 @@ class DSMRProtocol(asyncio.Protocol): transport = None telegram_callback = None - def __init__(self, loop, telegram_parser, - telegram_callback=None, keep_alive_interval=None): + def __init__( + self, loop, telegram_parser, telegram_callback=None, keep_alive_interval=None + ): """Initialize class.""" self.loop = loop self.log = logging.getLogger(__name__) @@ -95,16 +109,16 @@ def __init__(self, loop, telegram_parser, def connection_made(self, transport): """Just logging for now.""" self.transport = transport - self.log.debug('connected') + self.log.debug("connected") self._active = False if self.loop and self._keep_alive_interval: self.loop.call_later(self._keep_alive_interval, self.keep_alive) def data_received(self, data): """Add incoming data to buffer.""" - data = data.decode('ascii', errors='ignore') + data = data.decode("latin1") self._active = True - self.log.debug('received data: %s', data) + self.log.debug("received data: %s", data) self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): @@ -112,26 +126,26 @@ def data_received(self, data): def keep_alive(self): if self._active: - self.log.debug('keep-alive checked') + self.log.debug("keep-alive checked") self._active = False if self.loop: self.loop.call_later(self._keep_alive_interval, self.keep_alive) else: - self.log.warning('keep-alive check failed') + self.log.warning("keep-alive check failed") if self.transport: self.transport.close() def connection_lost(self, exc): """Stop when connection is lost.""" if exc: - self.log.exception('disconnected due to exception', exc_info=exc) + self.log.exception("disconnected due to exception", exc_info=exc) else: - self.log.info('disconnected because of close/abort.') + self.log.info("disconnected because of close/abort.") self._closed.set() def handle_telegram(self, telegram): """Send off parsed telegram to handling callback.""" - self.log.debug('got telegram: %s', telegram) + self.log.debug("got telegram: %s", telegram) try: parsed_telegram = self.telegram_parser.parse(telegram) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index fcba570..5a05f00 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -8,56 +8,57 @@ """ EN = { - obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', - obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', - obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL', - obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1', - obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2', - obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1', - obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2', - obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF', - obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER', - obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE', - obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY', - obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT', - obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT', - obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG', - obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT', - obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT', - obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT', - obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT', - obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT', - obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT', - obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1', - obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2', - obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3', - obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1', - obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2', - obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3', - obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE', - obis.TEXT_MESSAGE: 'TEXT_MESSAGE', - obis.DEVICE_TYPE: 'DEVICE_TYPE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', - obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS', - obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING', - obis.GAS_METER_READING: 'GAS_METER_READING', - obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', - obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', - 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', + obis.P1_MESSAGE_HEADER: "P1_MESSAGE_HEADER", + obis.P1_MESSAGE_TIMESTAMP: "P1_MESSAGE_TIMESTAMP", + obis.ELECTRICITY_IMPORTED_TOTAL: "ELECTRICITY_IMPORTED_TOTAL", + obis.ELECTRICITY_USED_TARIFF_1: "ELECTRICITY_USED_TARIFF_1", + obis.ELECTRICITY_USED_TARIFF_2: "ELECTRICITY_USED_TARIFF_2", + obis.ELECTRICITY_EXPORTED_TOTAL: "ELECTRICITY_EXPORTED_TOTAL", + obis.ELECTRICITY_DELIVERED_TARIFF_1: "ELECTRICITY_DELIVERED_TARIFF_1", + obis.ELECTRICITY_DELIVERED_TARIFF_2: "ELECTRICITY_DELIVERED_TARIFF_2", + obis.ELECTRICITY_ACTIVE_TARIFF: "ELECTRICITY_ACTIVE_TARIFF", + obis.EQUIPMENT_IDENTIFIER: "EQUIPMENT_IDENTIFIER", + obis.CURRENT_ELECTRICITY_USAGE: "CURRENT_ELECTRICITY_USAGE", + obis.CURRENT_ELECTRICITY_DELIVERY: "CURRENT_ELECTRICITY_DELIVERY", + obis.LONG_POWER_FAILURE_COUNT: "LONG_POWER_FAILURE_COUNT", + obis.SHORT_POWER_FAILURE_COUNT: "SHORT_POWER_FAILURE_COUNT", + obis.POWER_EVENT_FAILURE_LOG: "POWER_EVENT_FAILURE_LOG", + obis.VOLTAGE_SAG_L1_COUNT: "VOLTAGE_SAG_L1_COUNT", + obis.VOLTAGE_SAG_L2_COUNT: "VOLTAGE_SAG_L2_COUNT", + obis.VOLTAGE_SAG_L3_COUNT: "VOLTAGE_SAG_L3_COUNT", + obis.VOLTAGE_SWELL_L1_COUNT: "VOLTAGE_SWELL_L1_COUNT", + obis.VOLTAGE_SWELL_L2_COUNT: "VOLTAGE_SWELL_L2_COUNT", + obis.VOLTAGE_SWELL_L3_COUNT: "VOLTAGE_SWELL_L3_COUNT", + obis.INSTANTANEOUS_VOLTAGE_L1: "INSTANTANEOUS_VOLTAGE_L1", + obis.INSTANTANEOUS_VOLTAGE_L2: "INSTANTANEOUS_VOLTAGE_L2", + obis.INSTANTANEOUS_VOLTAGE_L3: "INSTANTANEOUS_VOLTAGE_L3", + obis.INSTANTANEOUS_CURRENT_L1: "INSTANTANEOUS_CURRENT_L1", + obis.INSTANTANEOUS_CURRENT_L2: "INSTANTANEOUS_CURRENT_L2", + obis.INSTANTANEOUS_CURRENT_L3: "INSTANTANEOUS_CURRENT_L3", + obis.TEXT_MESSAGE_CODE: "TEXT_MESSAGE_CODE", + obis.TEXT_MESSAGE: "TEXT_MESSAGE", + obis.DEVICE_TYPE: "DEVICE_TYPE", + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE", + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE", + obis.EQUIPMENT_IDENTIFIER_GAS: "EQUIPMENT_IDENTIFIER_GAS", + obis.HOURLY_GAS_METER_READING: "HOURLY_GAS_METER_READING", + obis.GAS_METER_READING: "GAS_METER_READING", + obis.ACTUAL_TRESHOLD_ELECTRICITY: "ACTUAL_TRESHOLD_ELECTRICITY", + obis.ACTUAL_SWITCH_POSITION: "ACTUAL_SWITCH_POSITION", + 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", } REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 3808848..02e1e33 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -6,68 +6,76 @@ Might be refactored in a backwards incompatible way as soon as proper telegram 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_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' -EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n' -CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' -CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n' -LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n' -SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n' -POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n' -VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n' -VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n' -VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n' -VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n' -VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n' -VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n' -INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n' -INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n' -INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n' -INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n' -INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n' -INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n' -TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n' -TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n' -DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'\d-\d:22\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'\d-\d:42\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'\d-\d:62\.7\.0.+?\r\n' -EQUIPMENT_IDENTIFIER_GAS = r'\d-\d:96\.1\.0.+?\r\n' +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" +EQUIPMENT_IDENTIFIER = r"\d-\d:96\.1\.1.+?\r\n" +CURRENT_ELECTRICITY_USAGE = r"\d-\d:1\.7\.0.+?\r\n" +CURRENT_ELECTRICITY_DELIVERY = r"\d-\d:2\.7\.0.+?\r\n" +LONG_POWER_FAILURE_COUNT = r"96\.7\.9.+?\r\n" +SHORT_POWER_FAILURE_COUNT = r"96\.7\.21.+?\r\n" +POWER_EVENT_FAILURE_LOG = r"99\.97\.0.+?\r\n" +VOLTAGE_SAG_L1_COUNT = r"\d-\d:32\.32\.0.+?\r\n" +VOLTAGE_SAG_L2_COUNT = r"\d-\d:52\.32\.0.+?\r\n" +VOLTAGE_SAG_L3_COUNT = r"\d-\d:72\.32\.0.+?\r\n" +VOLTAGE_SWELL_L1_COUNT = r"\d-\d:32\.36\.0.+?\r\n" +VOLTAGE_SWELL_L2_COUNT = r"\d-\d:52\.36\.0.+?\r\n" +VOLTAGE_SWELL_L3_COUNT = r"\d-\d:72\.36\.0.+?\r\n" +INSTANTANEOUS_VOLTAGE_L1 = r"\d-\d:32\.7\.0.+?\r\n" +INSTANTANEOUS_VOLTAGE_L2 = r"\d-\d:52\.7\.0.+?\r\n" +INSTANTANEOUS_VOLTAGE_L3 = r"\d-\d:72\.7\.0.+?\r\n" +INSTANTANEOUS_CURRENT_L1 = r"\d-\d:31\.7\.0.+?\r\n" +INSTANTANEOUS_CURRENT_L2 = r"\d-\d:51\.7\.0.+?\r\n" +INSTANTANEOUS_CURRENT_L3 = r"\d-\d:71\.7\.0.+?\r\n" +TEXT_MESSAGE_CODE = r"\d-\d:96\.13\.1.+?\r\n" +TEXT_MESSAGE = r"\d-\d:96\.13\.0.+?\r\n" +DEVICE_TYPE = r"\d-\d:24\.1\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r"\d-\d:21\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r"\d-\d:41\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r"\d-\d:61\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r"\d-\d:22\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r"\d-\d:42\.7\.0.+?\r\n" +INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r"\d-\d:62\.7\.0.+?\r\n" +EQUIPMENT_IDENTIFIER_GAS = r"\d-\d:96\.1\.0.+?\r\n" # TODO differences between gas meter readings in v3 and lower and v4 and up -HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.1.+?\r\n' -GAS_METER_READING = r'\d-\d:24\.3\.0.+?\r\n.+?\r\n' -ACTUAL_TRESHOLD_ELECTRICITY = r'\d-\d:17\.0\.0.+?\r\n' -ACTUAL_SWITCH_POSITION = r'\d-\d:96\.3\.10.+?\r\n' -VALVE_POSITION_GAS = r'\d-\d:24\.4\.0.+?\r\n' +HOURLY_GAS_METER_READING = r"\d-\d:24\.2\.1.+?\r\n" +GAS_METER_READING = r"\d-\d:24\.3\.0.+?\r\n.+?\r\n" +ACTUAL_TRESHOLD_ELECTRICITY = r"\d-\d:17\.0\.0.+?\r\n" +ACTUAL_SWITCH_POSITION = r"\d-\d:96\.3\.10.+?\r\n" +VALVE_POSITION_GAS = r"\d-\d:24\.4\.0.+?\r\n" # TODO 17.0.0 # TODO 96.3.10 -ELECTRICITY_USED_TARIFF_ALL = ( - ELECTRICITY_USED_TARIFF_1, - ELECTRICITY_USED_TARIFF_2 -) +ELECTRICITY_USED_TARIFF_ALL = (ELECTRICITY_USED_TARIFF_1, ELECTRICITY_USED_TARIFF_2) ELECTRICITY_DELIVERED_TARIFF_ALL = ( ELECTRICITY_DELIVERED_TARIFF_1, - ELECTRICITY_DELIVERED_TARIFF_2 + ELECTRICITY_DELIVERED_TARIFF_2, ) # Alternate codes for foreign countries. -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 +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 83f6bd8..77ea49e 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -2,9 +2,18 @@ from copy import deepcopy from dsmr_parser import obis_references as obis -from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser +from dsmr_parser.parsers import ( + CosemParser, + ValueParser, + MBusParser, + ProfileGenericParser, +) from dsmr_parser.value_types import timestamp -from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS +from dsmr_parser.profile_generic_specifications import ( + BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS, +) """ dsmr_parser.telegram_specifications @@ -15,8 +24,8 @@ """ V2_2 = { - 'checksum_support': False, - 'objects': { + "checksum_support": False, + "objects": { obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), @@ -41,14 +50,14 @@ ValueParser(str), # unit, position 5 ValueParser(Decimal), # meter reading, position 6 ), - } + }, } V3 = V2_2 V4 = { - 'checksum_support': True, - 'objects': { + "checksum_support": True, + "objects": { obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), @@ -61,10 +70,9 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: - ProfileGenericParser(BUFFER_TYPES, - PG_HEAD_PARSERS, - PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), + obis.POWER_EVENT_FAILURE_LOG: ProfileGenericParser( + BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS + ), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), @@ -85,15 +93,14 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ) - } + ValueParser(timestamp), ValueParser(Decimal) + ), + }, } V5 = { - 'checksum_support': True, - 'objects': { + "checksum_support": True, + "objects": { obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), @@ -107,10 +114,9 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: - ProfileGenericParser(BUFFER_TYPES, - PG_HEAD_PARSERS, - PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), + obis.POWER_EVENT_FAILURE_LOG: ProfileGenericParser( + BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS + ), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), @@ -133,38 +139,46 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ) - } + ValueParser(timestamp), ValueParser(Decimal) + ), + }, } ALL = (V2_2, V3, V4, V5) BELGIUM_FLUVIUS = deepcopy(V5) -BELGIUM_FLUVIUS['objects'].update({ - obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ) -}) +BELGIUM_FLUVIUS["objects"].update( + { + obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( + ValueParser(timestamp), ValueParser(Decimal) + ) + } +) 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)), -}) +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) + ), + } +) # Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf SWEDEN = { - 'checksum_support': True, - 'objects': { + "checksum_support": True, + "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.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 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)), @@ -179,16 +193,15 @@ obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), - } + }, } - Q3D = { "checksum_support": False, "objects": { obis.Q3D_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)), obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), From df59f3a4989bacb70e8748b4ad56f51b14c8f011 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Sun, 14 Nov 2021 21:49:43 +0000 Subject: [PATCH 126/226] decode latin1 and added ELECTRICITY_EXPORTED_TOTAL --- dsmr_parser/clients/protocol.py | 68 ++++++-------- dsmr_parser/obis_name_mapping.py | 102 ++++++++++----------- dsmr_parser/obis_references.py | 119 ++++++++++++------------- dsmr_parser/telegram_specifications.py | 94 +++++++++---------- 4 files changed, 174 insertions(+), 209 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 3b4c062..208ed1b 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -10,26 +10,23 @@ from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.parsers import TelegramParser -from dsmr_parser.clients.settings import ( - SERIAL_SETTINGS_V2_2, - SERIAL_SETTINGS_V4, - SERIAL_SETTINGS_V5, -) +from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ + SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): """Creates a DSMR asyncio protocol.""" - if dsmr_version == "2.2": + if dsmr_version == '2.2': specification = telegram_specifications.V2_2 serial_settings = SERIAL_SETTINGS_V2_2 - elif dsmr_version == "4": + elif dsmr_version == '4': specification = telegram_specifications.V4 serial_settings = SERIAL_SETTINGS_V4 - elif dsmr_version == "5": + elif dsmr_version == '5': specification = telegram_specifications.V5 serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == "5B": + elif dsmr_version == '5B': specification = telegram_specifications.BELGIUM_FLUVIUS serial_settings = SERIAL_SETTINGS_V5 elif dsmr_version == "5L": @@ -42,17 +39,11 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): specification = telegram_specifications.Q3D serial_settings = SERIAL_SETTINGS_V5 else: - raise NotImplementedError( - "No telegram parser found for version: %s", dsmr_version - ) - - protocol = partial( - DSMRProtocol, - loop, - TelegramParser(specification), - telegram_callback=telegram_callback, - **kwargs - ) + raise NotImplementedError("No telegram parser found for version: %s", + dsmr_version) + + protocol = partial(DSMRProtocol, loop, TelegramParser(specification), + telegram_callback=telegram_callback, **kwargs) return protocol, serial_settings @@ -60,26 +51,22 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): """Creates a DSMR asyncio protocol coroutine using serial port.""" protocol, serial_settings = create_dsmr_protocol( - dsmr_version, telegram_callback, loop=None - ) - serial_settings["url"] = port + dsmr_version, telegram_callback, loop=None) + serial_settings['url'] = port conn = create_serial_connection(loop, protocol, **serial_settings) return conn -def create_tcp_dsmr_reader( - host, port, dsmr_version, telegram_callback, loop=None, keep_alive_interval=None -): +def create_tcp_dsmr_reader(host, port, dsmr_version, + telegram_callback, loop=None, + keep_alive_interval=None): """Creates a DSMR asyncio protocol coroutine using TCP connection.""" if not loop: loop = asyncio.get_event_loop() protocol, _ = create_dsmr_protocol( - dsmr_version, - telegram_callback, - loop=loop, - keep_alive_interval=keep_alive_interval, - ) + dsmr_version, telegram_callback, loop=loop, + keep_alive_interval=keep_alive_interval) conn = loop.create_connection(protocol, host, port) return conn @@ -90,9 +77,8 @@ class DSMRProtocol(asyncio.Protocol): transport = None telegram_callback = None - def __init__( - self, loop, telegram_parser, telegram_callback=None, keep_alive_interval=None - ): + def __init__(self, loop, telegram_parser, + telegram_callback=None, keep_alive_interval=None): """Initialize class.""" self.loop = loop self.log = logging.getLogger(__name__) @@ -109,7 +95,7 @@ def __init__( def connection_made(self, transport): """Just logging for now.""" self.transport = transport - self.log.debug("connected") + self.log.debug('connected') self._active = False if self.loop and self._keep_alive_interval: self.loop.call_later(self._keep_alive_interval, self.keep_alive) @@ -118,7 +104,7 @@ def data_received(self, data): """Add incoming data to buffer.""" data = data.decode("latin1") self._active = True - self.log.debug("received data: %s", data) + self.log.debug('received data: %s', data) self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): @@ -126,26 +112,26 @@ def data_received(self, data): def keep_alive(self): if self._active: - self.log.debug("keep-alive checked") + self.log.debug('keep-alive checked') self._active = False if self.loop: self.loop.call_later(self._keep_alive_interval, self.keep_alive) else: - self.log.warning("keep-alive check failed") + self.log.warning('keep-alive check failed') if self.transport: self.transport.close() def connection_lost(self, exc): """Stop when connection is lost.""" if exc: - self.log.exception("disconnected due to exception", exc_info=exc) + self.log.exception('disconnected due to exception', exc_info=exc) else: - self.log.info("disconnected because of close/abort.") + self.log.info('disconnected because of close/abort.') self._closed.set() def handle_telegram(self, telegram): """Send off parsed telegram to handling callback.""" - self.log.debug("got telegram: %s", telegram) + self.log.debug('got telegram: %s', telegram) try: parsed_telegram = self.telegram_parser.parse(telegram) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 5a05f00..87c720d 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -8,57 +8,57 @@ """ EN = { - obis.P1_MESSAGE_HEADER: "P1_MESSAGE_HEADER", - obis.P1_MESSAGE_TIMESTAMP: "P1_MESSAGE_TIMESTAMP", - obis.ELECTRICITY_IMPORTED_TOTAL: "ELECTRICITY_IMPORTED_TOTAL", - obis.ELECTRICITY_USED_TARIFF_1: "ELECTRICITY_USED_TARIFF_1", - obis.ELECTRICITY_USED_TARIFF_2: "ELECTRICITY_USED_TARIFF_2", - obis.ELECTRICITY_EXPORTED_TOTAL: "ELECTRICITY_EXPORTED_TOTAL", - obis.ELECTRICITY_DELIVERED_TARIFF_1: "ELECTRICITY_DELIVERED_TARIFF_1", - obis.ELECTRICITY_DELIVERED_TARIFF_2: "ELECTRICITY_DELIVERED_TARIFF_2", - obis.ELECTRICITY_ACTIVE_TARIFF: "ELECTRICITY_ACTIVE_TARIFF", - obis.EQUIPMENT_IDENTIFIER: "EQUIPMENT_IDENTIFIER", - obis.CURRENT_ELECTRICITY_USAGE: "CURRENT_ELECTRICITY_USAGE", - obis.CURRENT_ELECTRICITY_DELIVERY: "CURRENT_ELECTRICITY_DELIVERY", - obis.LONG_POWER_FAILURE_COUNT: "LONG_POWER_FAILURE_COUNT", - obis.SHORT_POWER_FAILURE_COUNT: "SHORT_POWER_FAILURE_COUNT", - obis.POWER_EVENT_FAILURE_LOG: "POWER_EVENT_FAILURE_LOG", - obis.VOLTAGE_SAG_L1_COUNT: "VOLTAGE_SAG_L1_COUNT", - obis.VOLTAGE_SAG_L2_COUNT: "VOLTAGE_SAG_L2_COUNT", - obis.VOLTAGE_SAG_L3_COUNT: "VOLTAGE_SAG_L3_COUNT", - obis.VOLTAGE_SWELL_L1_COUNT: "VOLTAGE_SWELL_L1_COUNT", - obis.VOLTAGE_SWELL_L2_COUNT: "VOLTAGE_SWELL_L2_COUNT", - obis.VOLTAGE_SWELL_L3_COUNT: "VOLTAGE_SWELL_L3_COUNT", - obis.INSTANTANEOUS_VOLTAGE_L1: "INSTANTANEOUS_VOLTAGE_L1", - obis.INSTANTANEOUS_VOLTAGE_L2: "INSTANTANEOUS_VOLTAGE_L2", - obis.INSTANTANEOUS_VOLTAGE_L3: "INSTANTANEOUS_VOLTAGE_L3", - obis.INSTANTANEOUS_CURRENT_L1: "INSTANTANEOUS_CURRENT_L1", - obis.INSTANTANEOUS_CURRENT_L2: "INSTANTANEOUS_CURRENT_L2", - obis.INSTANTANEOUS_CURRENT_L3: "INSTANTANEOUS_CURRENT_L3", - obis.TEXT_MESSAGE_CODE: "TEXT_MESSAGE_CODE", - obis.TEXT_MESSAGE: "TEXT_MESSAGE", - obis.DEVICE_TYPE: "DEVICE_TYPE", - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: "INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE", - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: "INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE", - obis.EQUIPMENT_IDENTIFIER_GAS: "EQUIPMENT_IDENTIFIER_GAS", - obis.HOURLY_GAS_METER_READING: "HOURLY_GAS_METER_READING", - obis.GAS_METER_READING: "GAS_METER_READING", - obis.ACTUAL_TRESHOLD_ELECTRICITY: "ACTUAL_TRESHOLD_ELECTRICITY", - obis.ACTUAL_SWITCH_POSITION: "ACTUAL_SWITCH_POSITION", - 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", + obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', + obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', + obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL', + obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1', + obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2', + obis.ELECTRICITY_EXPORTED_TOTAL: 'ELECTRICITY_EXPORTED_TOTAL', + obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1', + obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2', + obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF', + obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER', + obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE', + obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY', + obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT', + obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT', + obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG', + obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT', + obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT', + obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT', + obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT', + obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT', + obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT', + obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1', + obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2', + obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3', + obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1', + obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2', + obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3', + obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE', + obis.TEXT_MESSAGE: 'TEXT_MESSAGE', + obis.DEVICE_TYPE: 'DEVICE_TYPE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS', + obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING', + obis.GAS_METER_READING: 'GAS_METER_READING', + obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', + obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', + 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', } REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 02e1e33..ad3bd98 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -6,76 +6,69 @@ Might be refactored in a backwards incompatible way as soon as proper telegram 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" -EQUIPMENT_IDENTIFIER = r"\d-\d:96\.1\.1.+?\r\n" -CURRENT_ELECTRICITY_USAGE = r"\d-\d:1\.7\.0.+?\r\n" -CURRENT_ELECTRICITY_DELIVERY = r"\d-\d:2\.7\.0.+?\r\n" -LONG_POWER_FAILURE_COUNT = r"96\.7\.9.+?\r\n" -SHORT_POWER_FAILURE_COUNT = r"96\.7\.21.+?\r\n" -POWER_EVENT_FAILURE_LOG = r"99\.97\.0.+?\r\n" -VOLTAGE_SAG_L1_COUNT = r"\d-\d:32\.32\.0.+?\r\n" -VOLTAGE_SAG_L2_COUNT = r"\d-\d:52\.32\.0.+?\r\n" -VOLTAGE_SAG_L3_COUNT = r"\d-\d:72\.32\.0.+?\r\n" -VOLTAGE_SWELL_L1_COUNT = r"\d-\d:32\.36\.0.+?\r\n" -VOLTAGE_SWELL_L2_COUNT = r"\d-\d:52\.36\.0.+?\r\n" -VOLTAGE_SWELL_L3_COUNT = r"\d-\d:72\.36\.0.+?\r\n" -INSTANTANEOUS_VOLTAGE_L1 = r"\d-\d:32\.7\.0.+?\r\n" -INSTANTANEOUS_VOLTAGE_L2 = r"\d-\d:52\.7\.0.+?\r\n" -INSTANTANEOUS_VOLTAGE_L3 = r"\d-\d:72\.7\.0.+?\r\n" -INSTANTANEOUS_CURRENT_L1 = r"\d-\d:31\.7\.0.+?\r\n" -INSTANTANEOUS_CURRENT_L2 = r"\d-\d:51\.7\.0.+?\r\n" -INSTANTANEOUS_CURRENT_L3 = r"\d-\d:71\.7\.0.+?\r\n" -TEXT_MESSAGE_CODE = r"\d-\d:96\.13\.1.+?\r\n" -TEXT_MESSAGE = r"\d-\d:96\.13\.0.+?\r\n" -DEVICE_TYPE = r"\d-\d:24\.1\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r"\d-\d:21\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r"\d-\d:41\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r"\d-\d:61\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r"\d-\d:22\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r"\d-\d:42\.7\.0.+?\r\n" -INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r"\d-\d:62\.7\.0.+?\r\n" -EQUIPMENT_IDENTIFIER_GAS = r"\d-\d:96\.1\.0.+?\r\n" +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' +EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n' +CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' +CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n' +LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n' +SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n' +POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n' +VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n' +VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n' +VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n' +VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n' +VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n' +VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n' +TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n' +TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n' +DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'\d-\d:22\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'\d-\d:42\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'\d-\d:62\.7\.0.+?\r\n' +EQUIPMENT_IDENTIFIER_GAS = r'\d-\d:96\.1\.0.+?\r\n' # TODO differences between gas meter readings in v3 and lower and v4 and up -HOURLY_GAS_METER_READING = r"\d-\d:24\.2\.1.+?\r\n" -GAS_METER_READING = r"\d-\d:24\.3\.0.+?\r\n.+?\r\n" -ACTUAL_TRESHOLD_ELECTRICITY = r"\d-\d:17\.0\.0.+?\r\n" -ACTUAL_SWITCH_POSITION = r"\d-\d:96\.3\.10.+?\r\n" -VALVE_POSITION_GAS = r"\d-\d:24\.4\.0.+?\r\n" +HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.1.+?\r\n' +GAS_METER_READING = r'\d-\d:24\.3\.0.+?\r\n.+?\r\n' +ACTUAL_TRESHOLD_ELECTRICITY = r'\d-\d:17\.0\.0.+?\r\n' +ACTUAL_SWITCH_POSITION = r'\d-\d:96\.3\.10.+?\r\n' +VALVE_POSITION_GAS = r'\d-\d:24\.4\.0.+?\r\n' # TODO 17.0.0 # TODO 96.3.10 -ELECTRICITY_USED_TARIFF_ALL = (ELECTRICITY_USED_TARIFF_1, ELECTRICITY_USED_TARIFF_2) +ELECTRICITY_USED_TARIFF_ALL = ( + ELECTRICITY_USED_TARIFF_1, + ELECTRICITY_USED_TARIFF_2 +) ELECTRICITY_DELIVERED_TARIFF_ALL = ( ELECTRICITY_DELIVERED_TARIFF_1, - ELECTRICITY_DELIVERED_TARIFF_2, + ELECTRICITY_DELIVERED_TARIFF_2 ) # Alternate codes for foreign countries. -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 +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 77ea49e..00d0c31 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -2,18 +2,9 @@ from copy import deepcopy from dsmr_parser import obis_references as obis -from dsmr_parser.parsers import ( - CosemParser, - ValueParser, - MBusParser, - ProfileGenericParser, -) +from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser from dsmr_parser.value_types import timestamp -from dsmr_parser.profile_generic_specifications import ( - BUFFER_TYPES, - PG_HEAD_PARSERS, - PG_UNIDENTIFIED_BUFFERTYPE_PARSERS, -) +from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS """ dsmr_parser.telegram_specifications @@ -24,8 +15,8 @@ """ V2_2 = { - "checksum_support": False, - "objects": { + 'checksum_support': False, + 'objects': { obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), @@ -50,14 +41,14 @@ ValueParser(str), # unit, position 5 ValueParser(Decimal), # meter reading, position 6 ), - }, + } } V3 = V2_2 V4 = { - "checksum_support": True, - "objects": { + 'checksum_support': True, + 'objects': { obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), @@ -70,9 +61,10 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: ProfileGenericParser( - BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS - ), + obis.POWER_EVENT_FAILURE_LOG: + ProfileGenericParser(BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), @@ -93,14 +85,15 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), ValueParser(Decimal) - ), - }, + ValueParser(timestamp), + ValueParser(Decimal) + ) + } } V5 = { - "checksum_support": True, - "objects": { + 'checksum_support': True, + 'objects': { obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), @@ -114,9 +107,10 @@ obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: ProfileGenericParser( - BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS - ), + obis.POWER_EVENT_FAILURE_LOG: + ProfileGenericParser(BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), @@ -139,46 +133,38 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), ValueParser(Decimal) - ), - }, + ValueParser(timestamp), + ValueParser(Decimal) + ) + } } ALL = (V2_2, V3, V4, V5) BELGIUM_FLUVIUS = deepcopy(V5) -BELGIUM_FLUVIUS["objects"].update( - { - obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), ValueParser(Decimal) - ) - } -) +BELGIUM_FLUVIUS['objects'].update({ + obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ) +}) 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) - ), - } -) +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)), +}) # Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf SWEDEN = { - "checksum_support": True, - "objects": { + 'checksum_support': True, + '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.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 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)), @@ -193,7 +179,7 @@ obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), - }, + } } Q3D = { From 6866e7d585bca78839e881737c5d4827710cc825 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Sun, 14 Nov 2021 21:52:38 +0000 Subject: [PATCH 127/226] leaving version as it was --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3852064..7ad7c68 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='0.31', + version='0.30', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From de4dc2ec9823f8ee4c83af5cc0cc76a514638878 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Sun, 14 Nov 2021 22:05:27 +0000 Subject: [PATCH 128/226] Q3D COM-1 test telegrams --- test/example_telegrams.py | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/example_telegrams.py b/test/example_telegrams.py index f74ed16..c59280c 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -128,3 +128,43 @@ '0-2:96.1.0()\r\n' '!6EEE\r\n' ) + +# EasyMeter via COM-1 Ethernet Gateway +# Q3D Manual (german) https://www.easymeter.com/downloads/products/zaehler/Q3D/Easymeter_Q3D_DE_2016-06-15.pdf +# - type code on page 8 +# - D0-Specs on page 20 +# +# last two lines are added by the COM-1 Ethernet Gateway + +TELEGRAM_ESY5Q3DB1024_V304 = ( # Easymeter an Hauptstromzähler + '/ESY5Q3DB1024 V3.04\r\n' + '\r\n' + '1-0:0.0.0*255(0272031312565)\r\n' + '1-0:1.8.0*255(00052185.7825309*kWh)\r\n' + '1-0:2.8.0*255(00019949.3221493*kWh)\r\n' + '1-0:21.7.0*255(000747.85*W)\r\n' + '1-0:41.7.0*255(000737.28*W)\r\n' + '1-0:61.7.0*255(000639.73*W)\r\n' + '1-0:1.7.0*255(002124.86*W)\r\n' + '1-0:96.5.5*255(80)\r\n' + '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' +) + +TELEGRAM_ESY5Q3DA1004_V304 = ( # Easymeter an Wärmepumpe + '/ESY5Q3DA1004 V3.04\r\n' + '\r\n' + '1-0:0.0.0*255(1336001560)\r\n' + '1-0:1.8.0*255(00032549.5061662*kWh)\r\n' + '1-0:21.7.0*255(000557.29*W)\r\n' + '1-0:41.7.0*255(000521.62*W)\r\n' + '1-0:61.7.0*255(000609.30*W)\r\n' + '1-0:1.7.0*255(001688.21*W)\r\n' + '1-0:96.5.5*255(80)\r\n' + '0-0:96.1.255*255(1ESY1336001560)\r\n' + '!\r\n' + ' 25818685\r\n' + 'DE0000000000000000000000000000003\r\n' +) From 7a89d6e97be0a2798d33dfdae16398efa6aaa87d Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Sun, 14 Nov 2021 22:20:00 +0000 Subject: [PATCH 129/226] re-adding ascii decoding at telegram level --- dsmr_parser/clients/protocol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 208ed1b..a9a9205 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -134,6 +134,7 @@ def handle_telegram(self, telegram): self.log.debug('got telegram: %s', telegram) try: + telegram = telegram.decode("ascii") parsed_telegram = self.telegram_parser.parse(telegram) except InvalidChecksumError as e: self.log.warning(str(e)) From 26ac27c347c39d473c7f034eab2bdb295927d2df Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Sun, 14 Nov 2021 22:29:37 +0000 Subject: [PATCH 130/226] re-adding ascii decoding at telegram level --- dsmr_parser/clients/protocol.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index a9a9205..c2d032f 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -134,7 +134,10 @@ def handle_telegram(self, telegram): self.log.debug('got telegram: %s', telegram) try: - telegram = telegram.decode("ascii") + # we accepted 8-bit at transport level (e.g. tcp) + telegram_data = telegram.encode("latin1") + # we need to ensure 7-bit at telegram level (IEC 646 required in section 5.4 of IEC 62056-21) + telegram = telegram_data.decode("ascii") parsed_telegram = self.telegram_parser.parse(telegram) except InvalidChecksumError as e: self.log.warning(str(e)) From 45cc88942a6394a1bca9dd984b86903cfda97690 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Sun, 21 Nov 2021 11:25:05 +0000 Subject: [PATCH 131/226] latin-1 changes folded into data_received --- dsmr_parser/clients/protocol.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index c2d032f..bbdcff6 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -102,12 +102,16 @@ def connection_made(self, transport): def data_received(self, data): """Add incoming data to buffer.""" + + # accept latin-1 (8-bit) on the line, to allow for non-ascii transport or padding data = data.decode("latin1") self._active = True self.log.debug('received data: %s', data) 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) + telegram = telegram.encode("latin1").decode("ascii") self.handle_telegram(telegram) def keep_alive(self): @@ -134,10 +138,6 @@ def handle_telegram(self, telegram): self.log.debug('got telegram: %s', telegram) try: - # we accepted 8-bit at transport level (e.g. tcp) - telegram_data = telegram.encode("latin1") - # we need to ensure 7-bit at telegram level (IEC 646 required in section 5.4 of IEC 62056-21) - telegram = telegram_data.decode("ascii") parsed_telegram = self.telegram_parser.parse(telegram) except InvalidChecksumError as e: self.log.warning(str(e)) From 60af45be257b6b1547050f03eca9dd5cb94bdc7f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 21 Nov 2021 14:06:11 +0100 Subject: [PATCH 132/226] Prepare version 0.31 --- CHANGELOG.rst | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d539d2b..d320b73 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Change Log ---------- +**0.31** (2021-11-21) +- Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway (`pull request #92 `_). + **0.30** (2021-08-18) - Add support for Swedish smart meters (`pull request #86 `_). diff --git a/setup.py b/setup.py index 7ad7c68..3852064 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='0.30', + version='0.31', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', 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 133/226] 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 @@ def data_received(self, data): 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 @@ # # 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 @@ '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 134/226] 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 @@ def __init__(self, host, port, 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 @@ -88,4 +87,4 @@ def read_as_object(self): 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 @@ 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 @@ """ 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_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 @@ 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 @@ '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 135/226] 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 @@ def buffer(self): 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_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 136/226] 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 @@ def handle_telegram(self, telegram): 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 137/226] 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 @@ async def wait_closed(self): 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 @@ def data_received(self, data): 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 138/226] 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 @@ 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 @@ def handle_telegram(self, telegram): 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 139/226] 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 140/226] 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 141/226] 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 @@ def data_received(self, data): 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 142/226] 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 @@ def data_received(self, data): 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 143/226] 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 @@ def buffer(self): 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_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 @@ 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 @@ 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 @@ 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 From 9f66c075d69f695c6376c0b18d69b4585a61494a Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 4 Jan 2022 21:14:53 +0100 Subject: [PATCH 144/226] Prepare version 0.32 --- CHANGELOG.rst | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d320b73..3a8876c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Change Log ---------- +**0.32** (2022-01-04) +- Support DSMR data read via RFXtrx with integrated P1 reader (`pull request #98 `_). + **0.31** (2021-11-21) - Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway (`pull request #92 `_). diff --git a/setup.py b/setup.py index 3852064..197759a 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='0.31', + version='0.32', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 5c838efbe1826e2c3e717b065af34a562413d587 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 4 Jan 2022 21:16:03 +0100 Subject: [PATCH 145/226] Fix CHANGELOG formatting --- CHANGELOG.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3a8876c..497c0a7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,35 +2,45 @@ Change Log ---------- **0.32** (2022-01-04) + - Support DSMR data read via RFXtrx with integrated P1 reader (`pull request #98 `_). **0.31** (2021-11-21) + - Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway (`pull request #92 `_). **0.30** (2021-08-18) + - Add support for Swedish smart meters (`pull request #86 `_). **0.29** (2021-04-18) + - Add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break (`pull request #71 `_). Remove deprecated asyncio coroutine decorator (`pull request #76 `_). **0.28** (2021-02-21) + - Optional keep alive monitoring for TCP/IP connections (`pull request #73 `_). - Catch parse errors in TelegramParser, ignore lines that can not be parsed (`pull request #74 `_). **0.27** (2020-12-24) + - fix for empty parentheses in ProfileGenericParser (redone) (`pull request #69 `_). **0.26** (2020-12-15) + - reverted fix for empty parentheses in ProfileGenericParser (`pull request #68 `_). **0.25** (2020-12-14) + - fix for empty parentheses in ProfileGenericParser (`pull request #57 `_). **0.24** (2020-11-27) + - Add Luxembourg equipment identifier (`pull request #62 `_). **0.23** (2020-11-07) + - Resolved issue with x-x:24.3.0 where it contains non-integer character (`pull request #61 `_). - Tests are not installed anymore (`pull request #59 `_). - Example telegram improvement (`pull request #58 `_). From 0340b25733bd9c51b2cabc0b12a7625cecbfd68a Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 21 Feb 2022 13:38:06 +0100 Subject: [PATCH 146/226] Update README.rst --- README.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.rst b/README.rst index b97d440..a166aec 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,3 @@ -**Notice:** this repository is in need of a new maintainer. If you are interested or have ideas about this, please let me know. - - DSMR Parser =========== From a6bc66667bdd316c89ac8027d00272f9f047be9e Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 21 Feb 2022 20:05:23 +0100 Subject: [PATCH 147/226] fix FileReader from not completing --- dsmr_parser/clients/filereader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index 061eda7..9b9cf6e 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -64,8 +64,11 @@ def read_as_object(self): with open(self._file, "rb") as file_handle: while True: data = file_handle.readline() - str = data.decode() - self.telegram_buffer.append(str) + + if not data: + break + + self.telegram_buffer.append(data.decode()) for telegram in self.telegram_buffer.get_all(): try: From 7610646e10b4f725cb7e6a4466bbc24a2bffac1a Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 22 Feb 2022 09:50:11 +0100 Subject: [PATCH 148/226] Add FileReader test --- test/test_filereader.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 test/test_filereader.py diff --git a/test/test_filereader.py b/test/test_filereader.py new file mode 100644 index 0000000..857a111 --- /dev/null +++ b/test/test_filereader.py @@ -0,0 +1,21 @@ +import unittest +import tempfile + +from dsmr_parser.clients.filereader import FileReader +from dsmr_parser.telegram_specifications import V5 +from test.example_telegrams import TELEGRAM_V5 + + +class FileReaderTest(unittest.TestCase): + def test_read_as_object(self): + with tempfile.NamedTemporaryFile() as file: + with open(file.name, "w") as f: + f.write(TELEGRAM_V5) + + telegrams = [] + reader = FileReader(file=file.name, telegram_specification=V5) + # Call + for telegram in reader.read_as_object(): + telegrams.append(telegram) + + self.assertEqual(len(telegrams), 1) From 3f41a73b9d98bb1d97e003ab66737f0c9b2b13e8 Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:28:57 +0200 Subject: [PATCH 149/226] Update obis_references.py --- dsmr_parser/obis_references.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index d4a4cbf..b52d1e6 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -65,7 +65,9 @@ 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. +BELGIUM_5MIN_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n' # Different code, same format. +BELGIUM_MAX_POWER_PER_PHASE = r'\d-\d:17\.0\.0.+?\r\n' # Applicable when power limitation is active +BELGIUM_MAX_CURRENT_PER_PHASE = r'\d-\d:31\.4\.0.+?\r\n' # Applicable when current limitation is active LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'\d-\d:42\.0\.0.+?\r\n' # Logical device name 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) From 527730781c5b6cfac42b5b644fba3f6d3254c5ed Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:30:12 +0200 Subject: [PATCH 150/226] Update obis_name_mapping.py --- dsmr_parser/obis_name_mapping.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index b224b7a..7490b06 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -50,7 +50,9 @@ obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS', - obis.BELGIUM_HOURLY_GAS_METER_READING: 'BELGIUM_HOURLY_GAS_METER_READING', + obis.BELGIUM_5MIN_GAS_METER_READING: 'BELGIUM_5MIN_GAS_METER_READING', + obis.BELGIUM_MAX_POWER_PER_PHASE: 'BELGIUM_MAX_POWER_PER_PHASE', + obis.BELGIUM_MAX_CURRENT_PER_PHASE : 'BELGIUM_MAX_CURRENT_PER_PHASE', obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE', From 15b3653a0270cdb7307161e5000f9b4d88420e7f Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:30:58 +0200 Subject: [PATCH 151/226] Update telegram_specifications.py --- dsmr_parser/telegram_specifications.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index c8f05a5..f14cef2 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -144,10 +144,18 @@ BELGIUM_FLUVIUS = deepcopy(V5) BELGIUM_FLUVIUS['objects'].update({ - obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( + obis.BELGIUM_5MIN_GAS_METER_READING: MBusParser( ValueParser(timestamp), ValueParser(Decimal) - ) + ), + obis.BELGIUM_MAX_POWER_PER_PHASE: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + obis.BELGIUM_MAX_CURRENT_PER_PHASE: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), }) LUXEMBOURG_SMARTY = deepcopy(V5) From 99ab86fffb2e7e2d785b29bf92c7065b0ddf995f Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:56:36 +0200 Subject: [PATCH 152/226] Update telegram_specifications.py --- dsmr_parser/telegram_specifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index f14cef2..07f60cb 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -155,7 +155,7 @@ obis.BELGIUM_MAX_CURRENT_PER_PHASE: MBusParser( ValueParser(timestamp), ValueParser(Decimal) - ), + ), }) LUXEMBOURG_SMARTY = deepcopy(V5) From 3d5599289ebe9d3f7f0fcc693c12e721c1b510c0 Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:57:02 +0200 Subject: [PATCH 153/226] Update obis_name_mapping.py --- dsmr_parser/obis_name_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 7490b06..d5a6d92 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -52,7 +52,7 @@ obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS', obis.BELGIUM_5MIN_GAS_METER_READING: 'BELGIUM_5MIN_GAS_METER_READING', obis.BELGIUM_MAX_POWER_PER_PHASE: 'BELGIUM_MAX_POWER_PER_PHASE', - obis.BELGIUM_MAX_CURRENT_PER_PHASE : 'BELGIUM_MAX_CURRENT_PER_PHASE', + obis.BELGIUM_MAX_CURRENT_PER_PHASE: 'BELGIUM_MAX_CURRENT_PER_PHASE', obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE', From 0ed5fc02203dfaccff3056fe992b98778ebd77ad Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Fri, 8 Apr 2022 14:09:56 +0200 Subject: [PATCH 154/226] Update telegram_specifications.py --- dsmr_parser/telegram_specifications.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 07f60cb..4abe328 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -148,14 +148,10 @@ ValueParser(timestamp), ValueParser(Decimal) ), - obis.BELGIUM_MAX_POWER_PER_PHASE: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MAX_CURRENT_PER_PHASE: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), + obis.BELGIUM_MAX_POWER_PER_PHASE: CosemParser(ValueParser(Decimal)), + obis.BELGIUM_MAX_CURRENT_PER_PHASE: CosemParser(ValueParser(Decimal)), + obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), + obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), }) LUXEMBOURG_SMARTY = deepcopy(V5) From e5eddd006e63fb7ffa3e088069fdcee1938f3e45 Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Fri, 8 Apr 2022 14:20:11 +0200 Subject: [PATCH 155/226] Update telegram_specifications.py --- dsmr_parser/telegram_specifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 4abe328..ca2f23f 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -151,7 +151,7 @@ obis.BELGIUM_MAX_POWER_PER_PHASE: CosemParser(ValueParser(Decimal)), obis.BELGIUM_MAX_CURRENT_PER_PHASE: CosemParser(ValueParser(Decimal)), obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), - obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), + obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), }) LUXEMBOURG_SMARTY = deepcopy(V5) From bc6eab73debbe7dc66a775967435c0359c65e08a Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 20 Apr 2022 21:25:24 +0200 Subject: [PATCH 156/226] Replace legacy Travis status badge with GitHub Actions --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a166aec..d53d649 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ DSMR Parser .. image:: https://img.shields.io/pypi/v/dsmr-parser.svg :target: https://pypi.python.org/pypi/dsmr-parser -.. image:: https://travis-ci.org/ndokter/dsmr_parser.svg?branch=master - :target: https://travis-ci.org/ndokter/dsmr_parser +.. image:: https://img.shields.io/github/workflow/status/ndokter/dsmr_parser/Tests/master + :target: https://github.com/ndokter/dsmr_parser/actions/workflows/tests.yml A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It also includes client implementation to directly read and parse smart meter data. From f5bedb1e6eafb9ca70bd2dcd913b995398bf727f Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 20 Apr 2022 21:26:07 +0200 Subject: [PATCH 157/226] CI now tests Python 3.10 as well --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5f6b2b0..0d9d338 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,6 +16,7 @@ jobs: - 3.7 - 3.8 - 3.9 + - 3.10 name: Python ${{ matrix.python-version }} steps: From 602ed4928a2df3ffa8fdbaa694829278384dd5de Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 20 Apr 2022 21:28:31 +0200 Subject: [PATCH 158/226] YAML fix for CI config --- .github/workflows/tests.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0d9d338..e3b6f4d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,11 +12,11 @@ jobs: strategy: matrix: python-version: - - 3.6 - - 3.7 - - 3.8 - - 3.9 - - 3.10 + - '3.6' + - '3.7' + - '3.8' + - '3.9' + - '3.10' name: Python ${{ matrix.python-version }} steps: From 32c20b61ac0746af714f52a55fc77586ce39eb27 Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 20 Apr 2022 21:29:08 +0200 Subject: [PATCH 159/226] Refer to CI config for the currently supported/tested Python versions --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d53d649..83f1e2f 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ also includes client implementation to directly read and parse smart meter data. Features -------- -DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.5, 3.6, 3.7, 3.8 and 3.9. +DSMR Parser supports DSMR versions 2, 3, 4 and 5. See for the [currently supported/tested Python versions here](https://github.com/ndokter/dsmr_parser/blob/master/.github/workflows/tests.yml#L14). Client module usage From 179a75e58c31953135d22d35021fe4a67a8b762f Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 20 Apr 2022 21:30:50 +0200 Subject: [PATCH 160/226] Typo fix --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 83f1e2f..1b7fe63 100644 --- a/README.rst +++ b/README.rst @@ -155,7 +155,7 @@ Example to get some of the values: gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING] # See dsmr_reader.obis_references for all readable telegram values. - # Note that the avilable values differ per DSMR version. + # Note that the available values differ per DSMR version. Telegram as an Object --------------------- From f238eb14a12143290bdffe64c6989c6a8e55467d Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 20 Apr 2022 21:32:41 +0200 Subject: [PATCH 161/226] Converted markdown link to restructured link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1b7fe63..071a58a 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ also includes client implementation to directly read and parse smart meter data. Features -------- -DSMR Parser supports DSMR versions 2, 3, 4 and 5. See for the [currently supported/tested Python versions here](https://github.com/ndokter/dsmr_parser/blob/master/.github/workflows/tests.yml#L14). +DSMR Parser supports DSMR versions 2, 3, 4 and 5. See for the `currently supported/tested Python versions here `_. Client module usage From b825faa719e4790b9a932c83b39e9e795804b60f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 20 Apr 2022 22:00:58 +0200 Subject: [PATCH 162/226] Update README.rst --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 7b366ed..9e09193 100644 --- a/README.rst +++ b/README.rst @@ -63,6 +63,7 @@ It should be run in a separate process because the code is blocking (not asynchr For a test run using a tcp server (lasting 20 seconds) use the following example: .. code-block:: python + import asyncio import logging from dsmr_parser import obis_references From 5c378f3419adbd3bc121834f44db45a85a4ada36 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 20 Apr 2022 22:02:35 +0200 Subject: [PATCH 163/226] Update README.rst --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 9e09193..0b716e7 100644 --- a/README.rst +++ b/README.rst @@ -110,6 +110,7 @@ Moreover, the telegram passed to `telegram_callback(telegram)` is already parsed However, if we construct a mock TelegramParser that just returns the already parsed object we can work around this. An example is below: .. code-block:: python + import asyncio import logging #from dsmr_parser import obis_references From 247a7446f5d658cc2b3f8b76d595f92d8434d050 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 20 Apr 2022 22:10:06 +0200 Subject: [PATCH 164/226] Prepare release 0.33 --- CHANGELOG.rst | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 497c0a7..ed37db8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Change Log ---------- +**0.33** (2022-04-20) + +- Test Python 3.10 in CI + legacy badge fix (`pull request #105 `_). +- Update telegram_specifications.py (`pull request #106 `_). +- Improve compatiblity with Belgian standard (`pull request #107 `_). +- Improve documentation asyncio (`pull request #63 `_). + **0.32** (2022-01-04) - Support DSMR data read via RFXtrx with integrated P1 reader (`pull request #98 `_). diff --git a/setup.py b/setup.py index 197759a..cb4f092 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='0.32', + version='0.33', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 09f4afcadadeb05f4294789ee3163f750a179197 Mon Sep 17 00:00:00 2001 From: Richard Schleich Date: Fri, 14 Oct 2022 17:50:02 +0200 Subject: [PATCH 165/226] Adds support for the Sagemcom T210-D-r smart meter installed by Austrian grid operators. The smart meter uses the DSMR/P1 standard and embeds the data in an encrypted and authenticated "DLMS General Global Cipher" frame. The encryption/decryption is handled by the "dlms_cosem" lib. Test cases are included. Adds OBIS codes for reactive energy. --- dsmr_parser/obis_references.py | 8 ++ dsmr_parser/parsers.py | 41 +++++++++- dsmr_parser/telegram_specifications.py | 30 +++++++ setup.py | 3 +- test/example_telegrams.py | 24 ++++++ test/test_parse_sagemcom_t210_d_r.py | 107 +++++++++++++++++++++++++ tox.ini | 1 + 7 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 test/test_parse_sagemcom_t210_d_r.py diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index b52d1e6..a5ffd61 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -12,6 +12,14 @@ ELECTRICITY_USED_TARIFF_2 = r'\d-\d:1\.8\.2.+?\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' +CURRENT_REACTIVE_EXPORTED = r'\d-\d:3\.7\.0.+?\r\n' +ELECTRICITY_REACTIVE_IMPORTED_TOTAL = r'\d-\d:3\.8\.0.+?\r\n' +ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1 = r'\d-\d:3\.8\.1.+?\r\n' +ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2 = r'\d-\d:3\.8\.2.+?\r\n' +CURRENT_REACTIVE_IMPORTED = r'\d-\d:4\.7\.0.+?\r\n' +ELECTRICITY_REACTIVE_EXPORTED_TOTAL = r'\d-\d:4\.8\.0.+?\r\n' +ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1 = r'\d-\d:4\.8\.1.+?\r\n' +ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2 = r'\d-\d:4\.8\.2.+?\r\n' ELECTRICITY_ACTIVE_TARIFF = r'\d-\d:96\.14\.0.+?\r\n' EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n' CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index fab9a50..984253e 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -1,8 +1,12 @@ import logging import re +from binascii import unhexlify from ctypes import c_ushort +from dlms_cosem.connection import XDlmsApduFactory +from dlms_cosem.protocol.xdlms import GeneralGlobalCipher + from dsmr_parser.objects import MBusObject, CosemObject, ProfileGenericObject from dsmr_parser.exceptions import ParseError, InvalidChecksumError @@ -22,14 +26,15 @@ def __init__(self, telegram_specification, apply_checksum_validation=True): self.telegram_specification = telegram_specification self.apply_checksum_validation = apply_checksum_validation - def parse(self, telegram_data): + def parse(self, telegram_data, encryption_key="", authentication_key=""): # noqa: C901 """ Parse telegram from string to dict. - The telegram str type makes python 2.x integration easier. :param str telegram_data: full telegram from start ('/') to checksum ('!ABCD') including line endings in between the telegram's lines + :param str encryption_key: encryption key + :param str authentication_key: authentication key :rtype: dict :returns: Shortened example: { @@ -43,6 +48,38 @@ def parse(self, telegram_data): :raises InvalidChecksumError: """ + if "general_global_cipher" in self.telegram_specification: + if self.telegram_specification["general_global_cipher"]: + enc_key = unhexlify(encryption_key) + auth_key = unhexlify(authentication_key) + telegram_data = unhexlify(telegram_data) + apdu = XDlmsApduFactory.apdu_from_bytes(apdu_bytes=telegram_data) + if apdu.security_control.security_suite != 0: + logger.warning("Untested security suite") + if apdu.security_control.authenticated and not apdu.security_control.encrypted: + logger.warning("Untested authentication only") + if not apdu.security_control.authenticated and not apdu.security_control.encrypted: + logger.warning("Untested not encrypted or authenticated") + if apdu.security_control.compressed: + logger.warning("Untested compression") + if apdu.security_control.broadcast_key: + logger.warning("Untested broadcast key") + telegram_data = apdu.to_plain_apdu(enc_key, auth_key).decode("ascii") + else: + try: + if unhexlify(telegram_data[0:2])[0] == GeneralGlobalCipher.TAG: + raise RuntimeError("Looks like a general_global_cipher frame " + "but telegram specification is not matching!") + except Exception: + pass + else: + try: + if unhexlify(telegram_data[0:2])[0] == GeneralGlobalCipher.TAG: + raise RuntimeError( + "Looks like a general_global_cipher frame but telegram specification is not matching!") + except Exception: + pass + if self.apply_checksum_validation \ and self.telegram_specification['checksum_support']: self.validate_checksum(telegram_data) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index ca2f23f..000d61c 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -201,3 +201,33 @@ obis.Q3D_EQUIPMENT_SERIALNUMBER: CosemParser(ValueParser(str)), }, } + + +SAGEMCOM_T210_D_R = { + "general_global_cipher": True, + "checksum_support": True, + 'objects': { + obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), + obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), + obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + + obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.CURRENT_REACTIVE_IMPORTED: CosemParser(ValueParser(Decimal)), + + obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + + obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.CURRENT_REACTIVE_EXPORTED: CosemParser(ValueParser(Decimal)), + } +} +AUSTRIA_ENERGIENETZE_STEIERMARK = SAGEMCOM_T210_D_R diff --git a/setup.py b/setup.py index cb4f092..062872b 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,8 @@ 'pyserial>=3,<4', 'pyserial-asyncio<1', 'pytz', - 'Tailer==0.4.1' + 'Tailer==0.4.1', + 'dlms_cosem==21.3.2' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] diff --git a/test/example_telegrams.py b/test/example_telegrams.py index 1ccb8ce..04b9005 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -169,3 +169,27 @@ ' 25818685\r\n' 'DE0000000000000000000000000000003\r\n' ) + +TELEGRAM_SAGEMCOM_T210_D_R = ( + '/EST5\\253710000_A\r\n' + '\r\n' + '1-3:0.2.8(50)\r\n' + '0-0:1.0.0(221006155014S)\r\n' + '1-0:1.8.0(006545766*Wh)\r\n' + '1-0:1.8.1(005017120*Wh)\r\n' + '1-0:1.8.2(001528646*Wh)\r\n' + '1-0:1.7.0(000000286*W)\r\n' + '1-0:2.8.0(000000058*Wh)\r\n' + '1-0:2.8.1(000000000*Wh)\r\n' + '1-0:2.8.2(000000058*Wh)\r\n' + '1-0:2.7.0(000000000*W)\r\n' + '1-0:3.8.0(000000747*varh)\r\n' + '1-0:3.8.1(000000000*varh)\r\n' + '1-0:3.8.2(000000747*varh)\r\n' + '1-0:3.7.0(000000000*var)\r\n' + '1-0:4.8.0(003897726*varh)\r\n' + '1-0:4.8.1(002692848*varh)\r\n' + '1-0:4.8.2(001204878*varh)\r\n' + '1-0:4.7.0(000000166*var)\r\n' + '!7EF9\r\n' +) diff --git a/test/test_parse_sagemcom_t210_d_r.py b/test/test_parse_sagemcom_t210_d_r.py new file mode 100644 index 0000000..525172c --- /dev/null +++ b/test/test_parse_sagemcom_t210_d_r.py @@ -0,0 +1,107 @@ +from binascii import unhexlify +from copy import deepcopy + +import unittest + +from dlms_cosem.exceptions import DecryptionError +from dlms_cosem.protocol.xdlms import GeneralGlobalCipher +from dlms_cosem.security import SecurityControlField, encrypt + +from dsmr_parser import telegram_specifications +from dsmr_parser.exceptions import ParseError +from dsmr_parser.parsers import TelegramParser +from test.example_telegrams import TELEGRAM_SAGEMCOM_T210_D_R + + +class TelegramParserEncryptedTest(unittest.TestCase): + """ Test parsing of a DSML encypted DSMR v5.x telegram. """ + DUMMY_ENCRYPTION_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + DUMMY_AUTHENTICATION_KEY = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + + def __generate_encrypted(self, security_suite=0, authenticated=True, encrypted=True): + security_control = SecurityControlField( + security_suite=security_suite, authenticated=authenticated, encrypted=encrypted + ) + encryption_key = unhexlify(self.DUMMY_ENCRYPTION_KEY) + authentication_key = unhexlify(self.DUMMY_AUTHENTICATION_KEY) + system_title = "SYSTEMID".encode("ascii") + invocation_counter = int.from_bytes(bytes.fromhex("10000001"), "big") + plain_data = TELEGRAM_SAGEMCOM_T210_D_R.encode("ascii") + + encrypted = encrypt( + security_control=security_control, + key=encryption_key, + auth_key=authentication_key, + system_title=system_title, + invocation_counter=invocation_counter, + plain_text=plain_data, + ) + + full_frame = bytearray(GeneralGlobalCipher.TAG.to_bytes(1, "big", signed=False)) + full_frame.extend(len(system_title).to_bytes(1, "big", signed=False)) + full_frame.extend(system_title) + full_frame.extend([0x82]) # Length of the following length bytes + # https://github.com/pwitab/dlms-cosem/blob/739f81a58e5f07663a512d4a128851333a0ed5e6/dlms_cosem/a_xdr.py#L33 + + security_control = security_control.to_bytes() + invocation_counter = invocation_counter.to_bytes(4, "big", signed=False) + full_frame.extend((len(encrypted) + + len(invocation_counter) + + len(security_control)).to_bytes(2, "big", signed=False)) + full_frame.extend(security_control) + full_frame.extend(invocation_counter) + full_frame.extend(encrypted) + + return full_frame + + def test_parse(self): + parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R) + result = parser.parse(self.__generate_encrypted().hex(), + self.DUMMY_ENCRYPTION_KEY, + self.DUMMY_AUTHENTICATION_KEY) + self.assertEqual(len(result), 18) + + def test_damaged_frame(self): + # If the frame is damaged decrypting fails (crc is technically not needed) + parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R) + + generated = self.__generate_encrypted() + generated[150] = 0x00 + generated = generated.hex() + + with self.assertRaises(DecryptionError): + parser.parse(generated, self.DUMMY_ENCRYPTION_KEY, self.DUMMY_AUTHENTICATION_KEY) + + def test_plain(self): + # If a plain request is parsed with "general_global_cipher": True it fails + parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R) + + with self.assertRaises(Exception): + parser.parse(TELEGRAM_SAGEMCOM_T210_D_R, self.DUMMY_ENCRYPTION_KEY, self.DUMMY_AUTHENTICATION_KEY) + + def test_general_global_cipher_not_specified(self): + # If a GGC frame is detected but general_global_cipher is not set it fails + parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R) + parser = deepcopy(parser) # We do not want to change the module value + parser.telegram_specification['general_global_cipher'] = False + + with self.assertRaises(ParseError): + parser.parse(self.__generate_encrypted().hex(), self.DUMMY_ENCRYPTION_KEY, self.DUMMY_AUTHENTICATION_KEY) + + def test_only_encrypted(self): + # Not implemented by dlms_cosem + parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R) + + only_auth = self.__generate_encrypted(0, authenticated=False, encrypted=True).hex() + + with self.assertRaises(ValueError): + parser.parse(only_auth, self.DUMMY_ENCRYPTION_KEY) + + def test_only_auth(self): + # Not implemented by dlms_cosem + parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R) + + only_auth = self.__generate_encrypted(0, authenticated=True, encrypted=False).hex() + + with self.assertRaises(ValueError): + parser.parse(only_auth, authentication_key=self.DUMMY_AUTHENTICATION_KEY) diff --git a/tox.ini b/tox.ini index 27fc713..011d394 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ deps= pylama pytest-asyncio pytest-mock + dlms_cosem commands= py.test --cov=dsmr_parser test {posargs} pylama dsmr_parser test From 00ac7c71bea0eb1e9207fcd2e9f2a121abf866aa Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 19 Oct 2022 13:52:08 +0200 Subject: [PATCH 166/226] Prepare version 0.34 --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ed37db8..3da868d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.34** (2022-10-19) + +- Adds support for the Sagemcom T210-D-r smart meter (`pull request #110 `_). + **0.33** (2022-04-20) - Test Python 3.10 in CI + legacy badge fix (`pull request #105 `_). diff --git a/setup.py b/setup.py index 062872b..621e4e1 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='0.33', + version='0.34', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 98a10998e98c46dacf656bbe86f2dd4321cbc32a Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 19 Oct 2022 14:20:29 +0200 Subject: [PATCH 167/226] Remove pylama because of incompatibility with python3.6, maybe find replacement --- tox.ini | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tox.ini b/tox.ini index 011d394..da14c9a 100644 --- a/tox.ini +++ b/tox.ini @@ -2,22 +2,8 @@ deps= pytest pytest-cov - pylama pytest-asyncio pytest-mock dlms_cosem commands= py.test --cov=dsmr_parser test {posargs} - pylama dsmr_parser test - -[pylama:dsmr_parser/clients/__init__.py] -ignore = W0611 - -[pylama:dsmr_parser/parsers.py] -ignore = W605 - -[pylama:pylint] -max_line_length = 120 - -[pylama:pycodestyle] -max_line_length = 120 From 357bae1bce702f4d9d369ece457c2bd2fc6aaefe Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 7 Dec 2022 20:30:58 +0100 Subject: [PATCH 168/226] Run tests for Python 3.11 as well --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e3b6f4d..76155db 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,6 +17,7 @@ jobs: - '3.8' - '3.9' - '3.10' + - '3.11' name: Python ${{ matrix.python-version }} steps: From cffcac2e8759eb0d15d5da0cb76d603f995aa536 Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 7 Dec 2022 20:32:01 +0100 Subject: [PATCH 169/226] Bump versions of Actions used in CI --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 76155db..bc2289d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: name: Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -29,7 +29,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cached PIP dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ~/.cache/pip @@ -44,4 +44,4 @@ jobs: run: tox - name: Code coverage upload - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 From e80ba9862b4b3b59b8da31327cab8eaf815a3fab Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Wed, 7 Dec 2022 20:33:58 +0100 Subject: [PATCH 170/226] No longer run Python 3.6 in CI as it is end-of-life --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bc2289d..65f86d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,6 @@ jobs: strategy: matrix: python-version: - - '3.6' - '3.7' - '3.8' - '3.9' From d05fe2692bce9da5bca4103008a6327b4901106a Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Fri, 9 Dec 2022 16:04:23 +0100 Subject: [PATCH 171/226] Add support for Fluvius V1.7.1 DSMR messages These include (since 1.6) the water mater messages. And since 1.7.X also peak usage values. https://maakjemeterslim.be/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBZ0lEIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--cdd9b48fd0838e89b177f03b745b23450fd8f53e/e-MUCS_P1_Ed_1_7_1.pdf?disposition=attachment --- dsmr_parser/obis_name_mapping.py | 22 +- dsmr_parser/obis_references.py | 38 +++- dsmr_parser/objects.py | 42 ++++ dsmr_parser/parsers.py | 41 +++- dsmr_parser/telegram_specifications.py | 96 +++++++-- test/example_telegrams.py | 42 ++++ test/test_parse_fluvius.py | 269 +++++++++++++++++++++++++ 7 files changed, 534 insertions(+), 16 deletions(-) create mode 100644 test/test_parse_fluvius.py diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index d5a6d92..d9a21a3 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -50,9 +50,29 @@ obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS', - obis.BELGIUM_5MIN_GAS_METER_READING: 'BELGIUM_5MIN_GAS_METER_READING', + obis.BELGIUM_EQUIPMENT_IDENTIFIER: 'BELGIUM_EQUIPMENT_IDENTIFIER', + obis.BELGIUM_CURRENT_AVERAGE_DEMAND: 'BELGIUM_CURRENT_AVERAGE_DEMAND', + obis.BELGIUM_MAXIMUM_DEMAND_MONTH: 'BELGIUM_MAXIMUM_DEMAND_MONTH', + obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS: 'BELGIUM_MAXIMUM_DEMAND_13_MONTHS', obis.BELGIUM_MAX_POWER_PER_PHASE: 'BELGIUM_MAX_POWER_PER_PHASE', obis.BELGIUM_MAX_CURRENT_PER_PHASE: 'BELGIUM_MAX_CURRENT_PER_PHASE', + obis.BELGIUM_MBUS1_DEVICE_TYPE: 'BELGIUM_MBUS1_DEVICE_TYPE', + obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER', + obis.BELGIUM_MBUS1_VALVE_POSITION: 'BELGIUM_MBUS1_VALVE_POSITION', + obis.BELGIUM_MBUS1_METER_READING1: 'BELGIUM_MBUS1_METER_READING1', + obis.BELGIUM_MBUS1_METER_READING2: 'BELGIUM_MBUS1_METER_READING2', + obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER', + obis.BELGIUM_MBUS2_VALVE_POSITION: 'BELGIUM_MBUS2_VALVE_POSITION', + obis.BELGIUM_MBUS2_METER_READING1: 'BELGIUM_MBUS2_METER_READING1', + obis.BELGIUM_MBUS2_METER_READING2: 'BELGIUM_MBUS2_METER_READING2', + obis.BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: 'BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER', + obis.BELGIUM_MBUS3_VALVE_POSITION: 'BELGIUM_MBUS3_VALVE_POSITION', + obis.BELGIUM_MBUS3_METER_READING1: 'BELGIUM_MBUS3_METER_READING1', + obis.BELGIUM_MBUS3_METER_READING2: 'BELGIUM_MBUS3_METER_READING2', + obis.BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER: 'BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER', + obis.BELGIUM_MBUS4_VALVE_POSITION: 'BELGIUM_MBUS4_VALVE_POSITION', + obis.BELGIUM_MBUS4_METER_READING1: 'BELGIUM_MBUS4_METER_READING1', + obis.BELGIUM_MBUS4_METER_READING2: 'BELGIUM_MBUS4_METER_READING2', obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE', diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index a5ffd61..b09252b 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -73,10 +73,46 @@ 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_5MIN_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n' # Different code, same format. +BELGIUM_VERSION_INFORMATION = r'\d-\d:96\.1\.4.+?\r\n' +BELGIUM_EQUIPMENT_IDENTIFIER = r'\d-0:96\.1\.1.+?\r\n' +BELGIUM_CURRENT_AVERAGE_DEMAND = r'\d-\d:1\.4\.0.+?\r\n' +BELGIUM_MAXIMUM_DEMAND_MONTH = r'\d-\d:1\.6\.0.+?\r\n' +BELGIUM_MAXIMUM_DEMAND_13_MONTHS = r'\d-\d:98\.1\.0.+?\r\n' BELGIUM_MAX_POWER_PER_PHASE = r'\d-\d:17\.0\.0.+?\r\n' # Applicable when power limitation is active BELGIUM_MAX_CURRENT_PER_PHASE = r'\d-\d:31\.4\.0.+?\r\n' # Applicable when current limitation is active + +# Multiple 'slaves' can be linked to the main device. +# Mostly MBUS1 = GAS METER with values on 24.2.3 +# While WATER METER reports it's values on 24.2.1 +# The GAS METER also reports its valve state on 24.4.0 +# Dev type for gas = 7 and water = 8 +BELGIUM_MBUS1_DEVICE_TYPE = r'\d-1:24\.1\.0.+?\r\n' +BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER = r'\d-1:96\.1\.1.+?\r\n' +BELGIUM_MBUS1_VALVE_POSITION = r'\d-1:24\.4\.0.+?\r\n' +BELGIUM_MBUS1_METER_READING1 = r'\d-1:24\.2\.1.+?\r\n' +BELGIUM_MBUS1_METER_READING2 = r'\d-1:24\.2\.3.+?\r\n' + +BELGIUM_MBUS2_DEVICE_TYPE = r'\d-2:24\.1\.0.+?\r\n' +BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER = r'\d-2:96\.1\.1.+?\r\n' +BELGIUM_MBUS2_VALVE_POSITION = r'\d-2:24\.4\.0.+?\r\n' +BELGIUM_MBUS2_METER_READING1 = r'\d-2:24\.2\.1.+?\r\n' +BELGIUM_MBUS2_METER_READING2 = r'\d-2:24\.2\.3.+?\r\n' + +BELGIUM_MBUS3_DEVICE_TYPE = r'\d-3:24\.1\.0.+?\r\n' +BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER = r'\d-3:96\.1\.1.+?\r\n' +BELGIUM_MBUS3_VALVE_POSITION = r'\d-3:24\.4\.0.+?\r\n' +BELGIUM_MBUS3_METER_READING1 = r'\d-3:24\.2\.1.+?\r\n' +BELGIUM_MBUS3_METER_READING2 = r'\d-3:24\.2\.3.+?\r\n' + +BELGIUM_MBUS4_DEVICE_TYPE = r'\d-4:24\.1\.0.+?\r\n' +BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER = r'\d-4:96\.1\.1.+?\r\n' +BELGIUM_MBUS4_VALVE_POSITION = r'\d-4:24\.4\.0.+?\r\n' +BELGIUM_MBUS4_METER_READING1 = r'\d-4:24\.2\.1.+?\r\n' +BELGIUM_MBUS4_METER_READING2 = r'\d-4:24\.2\.3.+?\r\n' + + LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'\d-\d:42\.0\.0.+?\r\n' # Logical device name + 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/objects.py b/dsmr_parser/objects.py index af7d068..510a77b 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -113,6 +113,48 @@ def to_json(self): } return json.dumps(output) +class MBusObjectPeak(DSMRObject): + + @property + def datetime(self): + return self.values[0]['value'] + + @property + def occurred(self): + return self.values[1]['value'] + + @property + def value(self): + return self.values[2]['value'] + + @property + def unit(self): + return self.values[2]['unit'] + + def __str__(self): + output = "{}\t[{}] at {} occurred {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat()), str(self.occurred.astimezone().isoformat())) + return output + + def to_json(self): + timestamp = self.datetime + if isinstance(self.datetime, datetime.datetime): + timestamp = self.datetime.astimezone().isoformat() + timestamp_occurred = self.occurred + if isinstance(self.occurred, datetime.datetime): + timestamp_occurred = self.occurred.astimezone().isoformat() + value = self.value + if isinstance(self.value, datetime.datetime): + value = self.value.astimezone().isoformat() + if isinstance(self.value, Decimal): + value = float(self.value) + output = { + 'datetime': timestamp, + 'occurred': timestamp_occurred, + 'value': value, + 'unit': self.unit + } + return json.dumps(output) + class CosemObject(DSMRObject): diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 984253e..101d08f 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,12 +3,14 @@ from binascii import unhexlify from ctypes import c_ushort +from decimal import Decimal from dlms_cosem.connection import XDlmsApduFactory from dlms_cosem.protocol.xdlms import GeneralGlobalCipher -from dsmr_parser.objects import MBusObject, CosemObject, ProfileGenericObject +from dsmr_parser.objects import MBusObject, MBusObjectPeak, CosemObject, ProfileGenericObject from dsmr_parser.exceptions import ParseError, InvalidChecksumError +from dsmr_parser.value_types import timestamp logger = logging.getLogger(__name__) @@ -214,6 +216,43 @@ def parse(self, line): return MBusObject(self._parse(line)) +class MaxDemandParser(DSMRObjectParser): + """ + Max demand history parser. + + These are lines with multiple values. Each containing 2 timestamps and a value + + Line format: + 'ID (Count) (ID) (ID) (TST) (TST) (Mv1*U1)' + + 1 2 3 4 5 6 7 + + 1) OBIS Reduced ID-code + 2) Amount of values in the response + 3) ID of the source + 4) ^^ + 5) Time Stamp (TST) of the month + 6) Time Stamp (TST) when the max demand occured + 6) Measurement value 1 (most recent entry of buffer attribute without unit) + 7) Unit of measurement values (Unit of capture objects attribute) + """ + + def parse(self, line): + pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') + values = re.findall(pattern, line) + + objects = [] + + count = int(values[0]) + for i in range(1, count+1): + timestamp_month = ValueParser(timestamp).parse(values[i*3+1]) + timestamp_occurred = ValueParser(timestamp).parse(values[i*3+1]) + value = ValueParser(Decimal).parse(values[i*3+2]) + objects.append(MBusObjectPeak([timestamp_month, timestamp_occurred, value])) + + return objects + + class CosemParser(DSMRObjectParser): """ Cosem object parser. diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 000d61c..2729073 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -2,7 +2,7 @@ from copy import deepcopy from dsmr_parser import obis_references as obis -from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser +from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser, MaxDemandParser from dsmr_parser.value_types import timestamp from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS @@ -141,18 +141,88 @@ ALL = (V2_2, V3, V4, V5) - -BELGIUM_FLUVIUS = deepcopy(V5) -BELGIUM_FLUVIUS['objects'].update({ - obis.BELGIUM_5MIN_GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MAX_POWER_PER_PHASE: CosemParser(ValueParser(Decimal)), - obis.BELGIUM_MAX_CURRENT_PER_PHASE: CosemParser(ValueParser(Decimal)), - obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), - obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), -}) +BELGIUM_FLUVIUS = { + 'checksum_support': True, + 'objects': { + obis.BELGIUM_VERSION_INFORMATION: CosemParser(ValueParser(str)), + obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), + obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), + obis.BELGIUM_CURRENT_AVERAGE_DEMAND: CosemParser(ValueParser(Decimal)), + obis.BELGIUM_MAXIMUM_DEMAND_MONTH: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS: MaxDemandParser(), + obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), + obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(int)), + obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), + obis.BELGIUM_MAX_POWER_PER_PHASE: CosemParser(ValueParser(Decimal)), + obis.BELGIUM_MAX_CURRENT_PER_PHASE: CosemParser(ValueParser(Decimal)), + obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), + obis.BELGIUM_MBUS1_DEVICE_TYPE: CosemParser(ValueParser(int)), + obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.BELGIUM_MBUS1_VALVE_POSITION: CosemParser(ValueParser(int)), + obis.BELGIUM_MBUS1_METER_READING1: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + obis.BELGIUM_MBUS1_METER_READING2: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + obis.BELGIUM_MBUS2_DEVICE_TYPE: CosemParser(ValueParser(int)), + obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.BELGIUM_MBUS2_VALVE_POSITION: CosemParser(ValueParser(int)), + obis.BELGIUM_MBUS2_METER_READING1: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + obis.BELGIUM_MBUS2_METER_READING2: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + obis.BELGIUM_MBUS3_DEVICE_TYPE: CosemParser(ValueParser(int)), + obis.BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.BELGIUM_MBUS3_VALVE_POSITION: CosemParser(ValueParser(int)), + obis.BELGIUM_MBUS3_METER_READING1: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + obis.BELGIUM_MBUS3_METER_READING2: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + obis.BELGIUM_MBUS4_DEVICE_TYPE: CosemParser(ValueParser(int)), + obis.BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.BELGIUM_MBUS4_VALVE_POSITION: CosemParser(ValueParser(int)), + obis.BELGIUM_MBUS4_METER_READING1: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + obis.BELGIUM_MBUS4_METER_READING2: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + } +} LUXEMBOURG_SMARTY = deepcopy(V5) LUXEMBOURG_SMARTY['objects'].update({ diff --git a/test/example_telegrams.py b/test/example_telegrams.py index 04b9005..5a350ed 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -129,6 +129,48 @@ '!6EEE\r\n' ) +TELEGRAM_FLUVIUS_V171 = ( + '/FLU5\253769484_A\r\n' + '\r\n' + '0-0:96.1.4(50217)\r\n' + '0-0:96.1.1(3153414733313031303231363035)\r\n' + '0-0:1.0.0(200512135409S)\r\n' + '1-0:1.8.1(000000.034*kWh)\r\n' + '1-0:1.8.2(000015.758*kWh)\r\n' + '1-0:2.8.1(000000.000*kWh)\r\n' + '1-0:2.8.2(000000.011*kWh)\r\n' + '1-0:1.4.0(02.351*kW)\r\n' + '1-0:1.6.0(200509134558S)(02.589*kW)\r\n' + '0-0:98.1.0(3)(1-0:1.6.0)(1-0:1.6.0)(200501000000S)(200423192538S)(03.695*kW)(200401000000S)(200305122139S)(05.980*kW)(200301000000S)(200210035421W)(04.318*kW)\r\n' + '0-0:96.14.0(0001)\r\n' + '1-0:1.7.0(00.000*kW)\r\n' + '1-0:2.7.0(00.000*kW)\r\n' + '1-0:21.7.0(00.000*kW)\r\n' + '1-0:41.7.0(00.000*kW)\r\n' + '1-0:61.7.0(00.000*kW)\r\n' + '1-0:22.7.0(00.000*kW)\r\n' + '1-0:42.7.0(00.000*kW)\r\n' + '1-0:62.7.0(00.000*kW)\r\n' + '1-0:32.7.0(234.7*V)\r\n' + '1-0:52.7.0(234.7*V)\r\n' + '1-0:72.7.0(234.7*V)\r\n' + '1-0:31.7.0(000.00*A)\r\n' + '1-0:51.7.0(000.00*A)\r\n' + '1-0:71.7.0(000.00*A)\r\n' + '0-0:96.3.10(1)\r\n' + '0-0:17.0.0(999.9*kW)\r\n' + '1-0:31.4.0(999*A)\r\n' + '0-0:96.13.0()\r\n' + '0-1:24.1.0(003)\r\n' + '0-1:96.1.1(37464C4F32313139303333373333)\r\n' + '0-1:24.4.0(1)\r\n' + '0-1:24.2.3(200512134558S)(00112.384*m3)\r\n' + '0-2:24.1.0(007)\r\n' + '0-2:96.1.1(3853414731323334353637383930)\r\n' + '0-2:24.2.1(200512134558S)(00872.234*m3)\r\n' + '!911C\r\n' +) + # EasyMeter via COM-1 Ethernet Gateway # Q3D Manual (german) https://www.easymeter.com/downloads/products/zaehler/Q3D/Easymeter_Q3D_DE_2016-06-15.pdf # - type code on page 8 diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py new file mode 100644 index 0000000..c530248 --- /dev/null +++ b/test/test_parse_fluvius.py @@ -0,0 +1,269 @@ +from decimal import Decimal + +import datetime +import unittest + +import pytz + +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, MBusObjectPeak +from dsmr_parser.parsers import TelegramParser +from test.example_telegrams import TELEGRAM_FLUVIUS_V171 + + +class TelegramParserFluviusTest(unittest.TestCase): + """ Test parsing of a DSMR Fluvius telegram. """ + + def test_parse(self): + parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS) + result = parser.parse(TELEGRAM_FLUVIUS_V171) + + # BELGIUM_VERSION_INFORMATION (0-0:96.1.4) + assert isinstance(result[obis.BELGIUM_VERSION_INFORMATION], CosemObject) + assert result[obis.BELGIUM_VERSION_INFORMATION].unit is None + assert isinstance(result[obis.BELGIUM_VERSION_INFORMATION].value, str) + assert result[obis.BELGIUM_VERSION_INFORMATION].value == '50217' + + # EQUIPMENT_IDENTIFIER (0-0:96.1.1) + assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject) + assert result[obis.EQUIPMENT_IDENTIFIER].unit is None + assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str) + assert result[obis.EQUIPMENT_IDENTIFIER].value == '3153414733313031303231363035' + + # P1_MESSAGE_TIMESTAMP (0-0:1.0.0) + assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP], CosemObject) + assert result[obis.P1_MESSAGE_TIMESTAMP].unit is None + assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP].value, datetime.datetime) + assert result[obis.P1_MESSAGE_TIMESTAMP].value == \ + datetime.datetime(2020, 5, 12, 11, 54, 9, tzinfo=pytz.UTC) + + # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) + assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal) + assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('0.034') + + # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject) + assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal) + assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('15.758') + + # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('0.000') + + # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('0.011') + + # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) + assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject) + assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None + assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str) + assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0001' + + # BELGIUM_CURRENT_AVERAGE_DEMAND (1-0:1.4.0) + assert isinstance(result[obis.BELGIUM_CURRENT_AVERAGE_DEMAND], CosemObject) + assert result[obis.BELGIUM_CURRENT_AVERAGE_DEMAND].unit == 'kW' + assert isinstance(result[obis.BELGIUM_CURRENT_AVERAGE_DEMAND].value, Decimal) + assert result[obis.BELGIUM_CURRENT_AVERAGE_DEMAND].value == Decimal('2.351') + + # BELGIUM_MAXIMUM_DEMAND_MONTH (1-0:1.6.0) + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH], MBusObject) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].unit == 'kW' + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].value, Decimal) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].value == Decimal('2.589') + + # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 0 + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0], MBusObjectPeak) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].unit == 'kW' + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].value, Decimal) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].value == Decimal('3.695') + # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 1 + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1], MBusObjectPeak) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].unit == 'kW' + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].value, Decimal) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].value == Decimal('5.980') + # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 2 + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2], MBusObjectPeak) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].unit == 'kW' + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].value, Decimal) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].value == Decimal('4.318') + + # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) + assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject) + assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' + assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal) + assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.000') + + # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) + assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject) + assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW' + assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal) + assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0.000') + + # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.000') + + # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.000') + + # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.000') + + # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0.000') + + # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0.000') + + # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0.000') + + # INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0) + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject) + assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V' + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal) + assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('234.7') + + # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0) + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject) + assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V' + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal) + assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('234.7') + + # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0) + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject) + assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V' + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal) + assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('234.7') + + # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.000') + + # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.000') + + # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.000') + + # ACTUAL_SWITCH_POSITION (0-0:96.3.10) + assert isinstance(result[obis.ACTUAL_SWITCH_POSITION], CosemObject) + assert result[obis.ACTUAL_SWITCH_POSITION].unit is None + assert isinstance(result[obis.ACTUAL_SWITCH_POSITION].value, int) + assert result[obis.ACTUAL_SWITCH_POSITION].value == 1 + + # BELGIUM_MAX_POWER_PER_PHASE (0-0:17.0.0) + assert isinstance(result[obis.BELGIUM_MAX_POWER_PER_PHASE], CosemObject) + assert result[obis.BELGIUM_MAX_POWER_PER_PHASE].unit == 'kW' + assert isinstance(result[obis.BELGIUM_MAX_POWER_PER_PHASE].value, Decimal) + assert result[obis.BELGIUM_MAX_POWER_PER_PHASE].value == Decimal('999.9') + + # BELGIUM_MAX_POWER_PER_PHASE (1-0:31.4.0) + assert isinstance(result[obis.BELGIUM_MAX_CURRENT_PER_PHASE], CosemObject) + assert result[obis.BELGIUM_MAX_CURRENT_PER_PHASE].unit == 'A' + assert isinstance(result[obis.BELGIUM_MAX_CURRENT_PER_PHASE].value, Decimal) + assert result[obis.BELGIUM_MAX_CURRENT_PER_PHASE].value == Decimal('999') + + # TEXT_MESSAGE (0-0:96.13.0) + assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) + assert result[obis.TEXT_MESSAGE].unit is None + assert result[obis.TEXT_MESSAGE].value is None + + # BELGIUM_MBUS1_DEVICE_TYPE (0-1:24.1.0) + assert isinstance(result[obis.BELGIUM_MBUS1_DEVICE_TYPE], CosemObject) + assert result[obis.BELGIUM_MBUS1_DEVICE_TYPE].unit is None + assert isinstance(result[obis.BELGIUM_MBUS1_DEVICE_TYPE].value, int) + assert result[obis.BELGIUM_MBUS1_DEVICE_TYPE].value == 3 + + # BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER (0-1:96.1.1) + assert isinstance(result[obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER], CosemObject) + assert result[obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER].unit is None + assert isinstance(result[obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER].value, str) + assert result[obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER].value == '37464C4F32313139303333373333' + + # BELGIUM_MBUS1_VALVE_POSITION (0-1:24.4.0) + assert isinstance(result[obis.BELGIUM_MBUS1_VALVE_POSITION], CosemObject) + assert result[obis.BELGIUM_MBUS1_VALVE_POSITION].unit is None + assert isinstance(result[obis.BELGIUM_MBUS1_VALVE_POSITION].value, int) + assert result[obis.BELGIUM_MBUS1_VALVE_POSITION].value == 1 + + # BELGIUM_MBUS1_METER_READING2 (0-1:24.2.3) + assert isinstance(result[obis.BELGIUM_MBUS1_METER_READING2], MBusObject) + assert result[obis.BELGIUM_MBUS1_METER_READING2].unit == 'm3' + assert isinstance(result[obis.BELGIUM_MBUS1_METER_READING2].value, Decimal) + assert result[obis.BELGIUM_MBUS1_METER_READING2].value == Decimal('112.384') + + # BELGIUM_MBUS2_DEVICE_TYPE (0-2:24.1.0) + assert isinstance(result[obis.BELGIUM_MBUS2_DEVICE_TYPE], CosemObject) + assert result[obis.BELGIUM_MBUS2_DEVICE_TYPE].unit is None + assert isinstance(result[obis.BELGIUM_MBUS2_DEVICE_TYPE].value, int) + assert result[obis.BELGIUM_MBUS2_DEVICE_TYPE].value == 7 + + # BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER (0-2:96.1.1) + assert isinstance(result[obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER], CosemObject) + assert result[obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER].unit is None + assert isinstance(result[obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER].value, str) + assert result[obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER].value == '3853414731323334353637383930' + + # BELGIUM_MBUS2_METER_READING1 (0-1:24.2.1) + assert isinstance(result[obis.BELGIUM_MBUS2_METER_READING1], MBusObject) + assert result[obis.BELGIUM_MBUS2_METER_READING1].unit == 'm3' + assert isinstance(result[obis.BELGIUM_MBUS2_METER_READING1].value, Decimal) + assert result[obis.BELGIUM_MBUS2_METER_READING1].value == Decimal('872.234') + + + def test_checksum_valid(self): + # No exception is raised. + TelegramParser.validate_checksum(TELEGRAM_FLUVIUS_V171) + + def test_checksum_invalid(self): + # Remove the electricty used data value. This causes the checksum to + # not match anymore. + corrupted_telegram = TELEGRAM_FLUVIUS_V171.replace( + '1-0:1.8.1(000000.034*kWh)\r\n', + '' + ) + + with self.assertRaises(InvalidChecksumError): + TelegramParser.validate_checksum(corrupted_telegram) + + def test_checksum_missing(self): + # Remove the checksum value causing a ParseError. + corrupted_telegram = TELEGRAM_FLUVIUS_V171.replace('!911C\r\n', '') + with self.assertRaises(ParseError): + TelegramParser.validate_checksum(corrupted_telegram) From 301a00e226305545f53e54b3055f7b9f6ed0da7c Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 22 Dec 2022 10:13:26 +0100 Subject: [PATCH 172/226] Prepare release 1.0.0 --- CHANGELOG.rst | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3da868d..2153ad5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Change Log ---------- +**1.0.0** (2022-12-22) + +- switched to new numbering scheme https://semver.org/ +- Added support for Python 3.11 and dropped support for Python 3.6 (`pull request #110 `_) +- Add support for Fluvius V1.7.1 DSMR messages (`pull request #110 `_) + **0.34** (2022-10-19) - Adds support for the Sagemcom T210-D-r smart meter (`pull request #110 `_). diff --git a/setup.py b/setup.py index 621e4e1..f7b1896 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='nigel@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='0.34', + version='1.0.0', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 650d16862b94d513cf935802ae82abc1feabff69 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 22 Dec 2022 10:18:47 +0100 Subject: [PATCH 173/226] Fix changelog --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2153ad5..ada6184 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Change Log **1.0.0** (2022-12-22) - switched to new numbering scheme https://semver.org/ -- Added support for Python 3.11 and dropped support for Python 3.6 (`pull request #110 `_) +- Added support for Python 3.11 and dropped support for Python 3.6 (`pull request #112 `_) - Add support for Fluvius V1.7.1 DSMR messages (`pull request #110 `_) **0.34** (2022-10-19) From 9e01dc32c00baf758078f0d4b2207935c22a6c51 Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Mon, 26 Dec 2022 12:50:59 +0100 Subject: [PATCH 174/226] Fix missing value from name mapping --- dsmr_parser/obis_name_mapping.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index d9a21a3..316e43a 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -50,6 +50,7 @@ obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS', + obis.BELGIUM_VERSION_INFORMATION: 'BELGIUM_VERSION_INFORMATION', obis.BELGIUM_EQUIPMENT_IDENTIFIER: 'BELGIUM_EQUIPMENT_IDENTIFIER', obis.BELGIUM_CURRENT_AVERAGE_DEMAND: 'BELGIUM_CURRENT_AVERAGE_DEMAND', obis.BELGIUM_MAXIMUM_DEMAND_MONTH: 'BELGIUM_MAXIMUM_DEMAND_MONTH', From 431ddadb5f669293efd178b0f87c4ba3a35bb044 Mon Sep 17 00:00:00 2001 From: Rob Calon <31725587+robcalon@users.noreply.github.com> Date: Tue, 27 Dec 2022 17:20:24 +0100 Subject: [PATCH 175/226] asynchronous read_as_object method fix inheritance of synchronous read_as_object method from parent --- dsmr_parser/clients/serial_.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 12d2245..7adbc46 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -95,3 +95,32 @@ async def read(self, queue): ) except ParseError as e: logger.warning('Failed to parse telegram: %s', e) + + async def read_as_object(self, queue): + """ + Read complete DSMR telegram's from the serial interface and return a Telegram object. + + :rtype: generator + """ + + # create Serial StreamReader + conn = serial_asyncio.open_serial_connection(**self.serial_settings) + reader, _ = await conn + + while True: + + # Read line if available or give control back to loop until new + # data has arrived. + data = await reader.readline() + self.telegram_buffer.append(data.decode('ascii')) + + for telegram in self.telegram_buffer.get_all(): + + try: + queue.put_nowait( + 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) From 0eae28e183699f6db2ecd8224407e6a8721766f5 Mon Sep 17 00:00:00 2001 From: Rob Calon <31725587+robcalon@users.noreply.github.com> Date: Tue, 27 Dec 2022 17:27:23 +0100 Subject: [PATCH 176/226] correct rtype --- dsmr_parser/clients/serial_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 7adbc46..d8bc529 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -100,7 +100,7 @@ async def read_as_object(self, queue): """ Read complete DSMR telegram's from the serial interface and return a Telegram object. - :rtype: generator + :rtype: None """ # create Serial StreamReader From 371e1f3c5e079aa51b9cfe3f1e9017234e2f42fc Mon Sep 17 00:00:00 2001 From: Rob Calon <31725587+robcalon@users.noreply.github.com> Date: Tue, 27 Dec 2022 17:36:10 +0100 Subject: [PATCH 177/226] update docstring --- dsmr_parser/clients/serial_.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index d8bc529..f6e892f 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -98,7 +98,11 @@ async def read(self, queue): async def read_as_object(self, queue): """ - Read complete DSMR telegram's from the serial interface and return a Telegram object. + Read complete DSMR telegram's from the serial interface + and return a Telegram object. + + Instead of being a generator, Telegram objects are pushed + to provided queue for asynchronous processing. :rtype: None """ From 5f58bc8f72d3134665198e4223d354ba74ef978b Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Tue, 27 Dec 2022 18:58:48 +0100 Subject: [PATCH 178/226] add pylama back in + fix uniform style issues reported by pylama --- dsmr_parser/objects.py | 5 ++++- dsmr_parser/parsers.py | 8 ++++---- test/test_parse_fluvius.py | 1 - tox.ini | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 510a77b..351b5c6 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -113,6 +113,7 @@ def to_json(self): } return json.dumps(output) + class MBusObjectPeak(DSMRObject): @property @@ -132,7 +133,9 @@ def unit(self): return self.values[2]['unit'] def __str__(self): - output = "{}\t[{}] at {} occurred {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat()), str(self.occurred.astimezone().isoformat())) + output = "{}\t[{}] at {} occurred {}"\ + .format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat()), + str(self.occurred.astimezone().isoformat())) return output def to_json(self): diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 101d08f..9b10b4d 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -244,10 +244,10 @@ def parse(self, line): objects = [] count = int(values[0]) - for i in range(1, count+1): - timestamp_month = ValueParser(timestamp).parse(values[i*3+1]) - timestamp_occurred = ValueParser(timestamp).parse(values[i*3+1]) - value = ValueParser(Decimal).parse(values[i*3+2]) + for i in range(1, count + 1): + timestamp_month = ValueParser(timestamp).parse(values[i * 3 + 1]) + timestamp_occurred = ValueParser(timestamp).parse(values[i * 3 + 1]) + value = ValueParser(Decimal).parse(values[i * 3 + 2]) objects.append(MBusObjectPeak([timestamp_month, timestamp_occurred, value])) return objects diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py index c530248..3213fd9 100644 --- a/test/test_parse_fluvius.py +++ b/test/test_parse_fluvius.py @@ -246,7 +246,6 @@ def test_parse(self): assert isinstance(result[obis.BELGIUM_MBUS2_METER_READING1].value, Decimal) assert result[obis.BELGIUM_MBUS2_METER_READING1].value == Decimal('872.234') - def test_checksum_valid(self): # No exception is raised. TelegramParser.validate_checksum(TELEGRAM_FLUVIUS_V171) diff --git a/tox.ini b/tox.ini index da14c9a..533aa72 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,25 @@ deps= pytest pytest-cov + pylama pytest-asyncio pytest-mock dlms_cosem commands= py.test --cov=dsmr_parser test {posargs} + pylama dsmr_parser test + +[pylama:dsmr_parser/clients/__init__.py] +ignore = W0611 + +[pylama:dsmr_parser/parsers.py] +ignore = W605 + +[pylama:test/example_telegrams.py] +ignore = E501 + +[pylama:pylint] +max_line_length = 120 + +[pylama:pycodestyle] +max_line_length = 120 \ No newline at end of file From d50d775e38545a77f3f6a7aeb159af06ed130a6c Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Fri, 30 Dec 2022 21:05:17 +0100 Subject: [PATCH 179/226] Fixed Shields badge in favor of badges/shields#8671 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0b716e7..75e572c 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ DSMR Parser .. image:: https://img.shields.io/pypi/v/dsmr-parser.svg :target: https://pypi.python.org/pypi/dsmr-parser -.. image:: https://img.shields.io/github/workflow/status/ndokter/dsmr_parser/Tests/master +.. image:: https://img.shields.io/github/actions/workflow/status/ndokter/dsmr_parser/tests.yml?branch=master :target: https://github.com/ndokter/dsmr_parser/actions/workflows/tests.yml A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It From 0a259a3c9d1c315824d7f120e7eeedc91551ce12 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 1 Jan 2023 20:55:58 +0100 Subject: [PATCH 180/226] fix code style --- dsmr_parser/clients/serial_.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index f6e892f..a76780f 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -95,15 +95,15 @@ async def read(self, queue): ) except ParseError as e: logger.warning('Failed to parse telegram: %s', e) - + async def read_as_object(self, queue): """ - Read complete DSMR telegram's from the serial interface + Read complete DSMR telegram's from the serial interface and return a Telegram object. - - Instead of being a generator, Telegram objects are pushed + + Instead of being a generator, Telegram objects are pushed to provided queue for asynchronous processing. - + :rtype: None """ @@ -119,12 +119,11 @@ async def read_as_object(self, queue): self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): - - try: - queue.put_nowait( - 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) + try: + queue.put_nowait( + 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) From a2b9fe5e3bda547bac91df264c6cd1289f786768 Mon Sep 17 00:00:00 2001 From: yada75 Date: Wed, 18 Jan 2023 19:31:39 +0100 Subject: [PATCH 181/226] Update obis_references.py --- dsmr_parser/obis_references.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index b09252b..da3ae9b 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -12,11 +12,11 @@ ELECTRICITY_USED_TARIFF_2 = r'\d-\d:1\.8\.2.+?\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' -CURRENT_REACTIVE_EXPORTED = r'\d-\d:3\.7\.0.+?\r\n' +CURRENT_REACTIVE_IMPORTED = r'\d-\d:3\.7\.0.+?\r\n' ELECTRICITY_REACTIVE_IMPORTED_TOTAL = r'\d-\d:3\.8\.0.+?\r\n' ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1 = r'\d-\d:3\.8\.1.+?\r\n' ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2 = r'\d-\d:3\.8\.2.+?\r\n' -CURRENT_REACTIVE_IMPORTED = r'\d-\d:4\.7\.0.+?\r\n' +CURRENT_REACTIVE_EXPORTED = r'\d-\d:4\.7\.0.+?\r\n' ELECTRICITY_REACTIVE_EXPORTED_TOTAL = r'\d-\d:4\.8\.0.+?\r\n' ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1 = r'\d-\d:4\.8\.1.+?\r\n' ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2 = r'\d-\d:4\.8\.2.+?\r\n' @@ -48,6 +48,12 @@ INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'\d-\d:22\.7\.0.+?\r\n' INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'\d-\d:42\.7\.0.+?\r\n' INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'\d-\d:62\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE = r'\d-\d:23\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE = r'\d-\d:24\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE = r'\d-\d:43\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE = r'\d-\d:44\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE = r'\d-\d:63\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE = r'\d-\d:64\.7\.0.+?\r\n' EQUIPMENT_IDENTIFIER_GAS = r'\d-\d:96\.1\.0.+?\r\n' # TODO differences between gas meter readings in v3 and lower and v4 and up HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.1.+?\r\n' From 21b8547655e4ed5f5142a4c804b63117989964d6 Mon Sep 17 00:00:00 2001 From: yada75 Date: Wed, 18 Jan 2023 19:34:35 +0100 Subject: [PATCH 182/226] Update obis_name_mapping.py --- dsmr_parser/obis_name_mapping.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 316e43a..7213262 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -44,6 +44,12 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + obis.INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE', + obis.INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE', + obis.INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE', + obis.INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE', + obis.INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE', + obis.INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE', obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS', obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING', obis.GAS_METER_READING: 'GAS_METER_READING', From 142b7359a1cbb23758c6bccf5c38a8fc8bbee630 Mon Sep 17 00:00:00 2001 From: yada75 Date: Wed, 18 Jan 2023 19:39:29 +0100 Subject: [PATCH 183/226] Update telegram_specifications.py --- dsmr_parser/telegram_specifications.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 2729073..214fa52 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -240,6 +240,8 @@ obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_REACTIVE_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)), @@ -248,6 +250,12 @@ obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), From 02eb3a34dd1f651d94caca4510fceadf4ebd8d2f Mon Sep 17 00:00:00 2001 From: yada75 Date: Wed, 18 Jan 2023 20:02:54 +0100 Subject: [PATCH 184/226] Update obis_name_mapping.py --- dsmr_parser/obis_name_mapping.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 7213262..cc85a02 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -11,9 +11,11 @@ obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL', + obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL: 'ELECTRICITY_REACTIVE_IMPORTED_TOTAL', obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1', obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2', obis.ELECTRICITY_EXPORTED_TOTAL: 'ELECTRICITY_EXPORTED_TOTAL', + obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL: 'ELECTRICITY_REACTIVE_EXPORTED_TOTAL', obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1', obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2', obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF', From edf535f987fdb6df7e18a2790ecc0d37f3562946 Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Mon, 30 Jan 2023 14:09:17 +0100 Subject: [PATCH 185/226] Fix parsing with invalid timestamps Sometimes the timestamp in the DSMR message is invalid (when no data read read from the mbus meter?), and then parsing fails. Fixing this by handling the exception and returning None for invalid timestamps. Fixes: #120 --- dsmr_parser/value_types.py | 13 +++++++++++-- test/test_parse_v5.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/value_types.py b/dsmr_parser/value_types.py index 4bc9ef3..487e98c 100644 --- a/dsmr_parser/value_types.py +++ b/dsmr_parser/value_types.py @@ -4,14 +4,23 @@ def timestamp(value): - naive_datetime = datetime.datetime.strptime(value[:-1], '%y%m%d%H%M%S') + try: + naive_datetime = datetime.datetime.strptime(value[:-1], '%y%m%d%H%M%S') + except ValueError: + return None - # TODO comment on this exception + # Timestamp has the following format: + # YYMMDDhhmmssX + # ASCII presentation of Time stamp with + # Year, Month, Day, Hour, Minute, Second, + # and an indication whether DST is active + # (X=S) or DST is not active (X=W) if len(value) == 13: is_dst = value[12] == 'S' # assume format 160322150000W else: is_dst = False + # TODO : Use system timezone local_tz = pytz.timezone('Europe/Amsterdam') localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index fe3ed84..fcc27db 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -244,3 +244,21 @@ def test_checksum_missing(self): corrupted_telegram = TELEGRAM_V5.replace('!6EEE\r\n', '') with self.assertRaises(ParseError): TelegramParser.validate_checksum(corrupted_telegram) + + def test_gas_timestamp_invalid(self): + # Issue 120 + # Sometimes a MBUS device (For ex a Gas Meter) returns an invalid timestamp + # Instead of failing, we should just ignore the timestamp + invalid_date_telegram = TELEGRAM_V5.replace( + '0-1:24.2.1(170102161005W)(00000.107*m3)\r\n', + '0-1:24.2.1(632525252525S)(00000.000)\r\n' + ) + invalid_date_telegram = invalid_date_telegram.replace('!6EEE\r\n', '!90C2\r\n') + parser = TelegramParser(telegram_specifications.V5) + result = parser.parse(invalid_date_telegram) + + # HOURLY_GAS_METER_READING (0-1:24.2.1) + assert isinstance(result[obis.HOURLY_GAS_METER_READING], MBusObject) + assert result[obis.HOURLY_GAS_METER_READING].unit is None + assert isinstance(result[obis.HOURLY_GAS_METER_READING].value, Decimal) + assert result[obis.HOURLY_GAS_METER_READING].value == Decimal('0.000') From ca8392504dc11a9293029e78c2b28e0b1ef7257f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 8 Feb 2023 12:02:14 +0100 Subject: [PATCH 186/226] Release 1.1.0 --- CHANGELOG.rst | 5 +++++ setup.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ada6184..588657e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Change Log ---------- +**1.1.0** (2023-02-08) + +- Add instantaneous reactive power + fixed swapped reactive total import export (`pull request #124 `_ by `yada75 `_) + + **1.0.0** (2022-12-22) - switched to new numbering scheme https://semver.org/ diff --git a/setup.py b/setup.py index f7b1896..e381170 100644 --- a/setup.py +++ b/setup.py @@ -4,10 +4,10 @@ name='dsmr-parser', description='Library to parse Dutch Smart Meter Requirements (DSMR)', author='Nigel Dokter and many others', - author_email='nigel@nldr.net', + author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.0.0', + version='1.1.0', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 78880a1881a6748865efa45e6d89de7cf917be0e Mon Sep 17 00:00:00 2001 From: AA61SL Date: Thu, 9 Feb 2023 11:01:00 +0100 Subject: [PATCH 187/226] feat: add Iskra IE.x meters --- dsmr_parser/clients/protocol.py | 3 + dsmr_parser/telegram_specifications.py | 30 +++++ test/example_telegrams.py | 31 +++++ test/test_parse_iskra_ie.py | 174 +++++++++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 test/test_parse_iskra_ie.py diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 40cdfc3..a7fb74f 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -48,6 +48,9 @@ def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, elif dsmr_version == "Q3D": specification = telegram_specifications.Q3D serial_settings = SERIAL_SETTINGS_V5 + elif dsmr_version == 'ISKRA_IE': + specification = telegram_specifications.ISKRA_IE + serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 214fa52..83eb29d 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -309,3 +309,33 @@ } } AUSTRIA_ENERGIENETZE_STEIERMARK = SAGEMCOM_T210_D_R + +ISKRA_IE = { + "checksum_support": False, + 'objects': { + obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), + obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), + obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), + obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), + obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), + obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), + obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + } +} diff --git a/test/example_telegrams.py b/test/example_telegrams.py index 5a350ed..050d3a8 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -235,3 +235,34 @@ '1-0:4.7.0(000000166*var)\r\n' '!7EF9\r\n' ) + +TELEGRAM_ISKRA_IE = ( + '/ISk5\2MIE5T-200\r\n' + '\r\n' + '1-0:0.0.0(00000000)\r\n' + '0-0:96.1.0(09610)\r\n' + '0-0:1.0.0(230202132747S)\r\n' + '1-0:1.8.1(000010.181*kWh)\r\n' + '1-0:1.8.2(000010.182*kWh)\r\n' + '1-0:2.8.1(000010.281*kWh)\r\n' + '1-0:2.8.2(000010.282*kWh)\r\n' + '0-0:96.14.0(0001)\r\n' + '1-0:1.7.0(00.170*kW)\r\n' + '1-0:2.7.0(00.270*kW)\r\n' + '1-0:21.7.0(00.217*kW)\r\n' + '1-0:41.7.0(00.417*kW)\r\n' + '1-0:61.7.0(00.617*kW)\r\n' + '1-0:22.7.0(00.227*kW)\r\n' + '1-0:42.7.0(00.427*kW)\r\n' + '1-0:62.7.0(00.627*kW)\r\n' + '1-0:32.7.0(242.5*V)\r\n' + '1-0:52.7.0(241.7*V)\r\n' + '1-0:72.7.0(243.3*V)\r\n' + '1-0:31.7.0(000*A)\r\n' + '1-0:51.7.0(000*A)\r\n' + '1-0:71.7.0(000*A)\r\n' + '0-0:96.3.10(1)\r\n' + '0-0:96.13.0()\r\n' + '0-1:96.1.1()\r\n' + '!AD3B\r\n' +) diff --git a/test/test_parse_iskra_ie.py b/test/test_parse_iskra_ie.py new file mode 100644 index 0000000..642fc0d --- /dev/null +++ b/test/test_parse_iskra_ie.py @@ -0,0 +1,174 @@ +import unittest + +from decimal import Decimal + +from dsmr_parser.exceptions import InvalidChecksumError, ParseError +from dsmr_parser.objects import CosemObject +from dsmr_parser.parsers import TelegramParser +from dsmr_parser import telegram_specifications +from dsmr_parser import obis_references as obis +from test.example_telegrams import TELEGRAM_ISKRA_IE + + +class TelegramParserIskraIETest(unittest.TestCase): + """ Test parsing of a Iskra IE5 telegram. """ + + def test_parse(self): + parser = TelegramParser(telegram_specifications.ISKRA_IE) + result = parser.parse(TELEGRAM_ISKRA_IE) + + # EQUIPMENT_IDENTIFIER_GAS (0-0:96.1.0) + assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject) + assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None + assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str) + assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '09610' + + # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) + assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal) + assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('10.181') + + # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject) + assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal) + assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('10.182') + + # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('10.281') + + # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('10.282') + + # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) + assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject) + assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None + assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str) + assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0001' + + # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) + assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject) + assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' + assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal) + assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.170') + + # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) + assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject) + assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW' + assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal) + assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0.270') + + # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.217') + + # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.417') + + # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.617') + + # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0.227') + + # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0.427') + + # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0.627') + + # INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0) + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject) + assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V' + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal) + assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('242.5') + + # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0) + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject) + assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V' + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal) + assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('241.7') + + # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0) + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject) + assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V' + assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal) + assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('243.3') + + # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.000') + + # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.000') + + # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject) + assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A' + assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal) + assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.000') + + # ACTUAL_SWITCH_POSITION (0-0:96.3.10) + assert isinstance(result[obis.ACTUAL_SWITCH_POSITION], CosemObject) + assert result[obis.ACTUAL_SWITCH_POSITION].unit is None + assert isinstance(result[obis.ACTUAL_SWITCH_POSITION].value, str) + assert result[obis.ACTUAL_SWITCH_POSITION].value == '1' + + # TEXT_MESSAGE (0-0:96.13.0) + assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) + assert result[obis.TEXT_MESSAGE].unit is None + assert result[obis.TEXT_MESSAGE].value is None + + # EQUIPMENT_IDENTIFIER (0-0:96.1.1) + assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject) + assert result[obis.EQUIPMENT_IDENTIFIER].unit is None + assert result[obis.EQUIPMENT_IDENTIFIER].value is None + + def test_checksum_valid(self): + # No exception is raised. + TelegramParser.validate_checksum(TELEGRAM_ISKRA_IE) + + def test_checksum_invalid(self): + # Remove the electricty used data value. This causes the checksum to not match anymore. + corrupted_telegram = TELEGRAM_ISKRA_IE.replace( + '1-0:1.8.1(000010.181*kWh)\r\n', + '' + ) + + with self.assertRaises(InvalidChecksumError): + TelegramParser.validate_checksum(corrupted_telegram) + + def test_checksum_missing(self): + # Remove the checksum value causing a ParseError. + corrupted_telegram = TELEGRAM_ISKRA_IE.replace('!AD3B\r\n', '') + with self.assertRaises(ParseError): + TelegramParser.validate_checksum(corrupted_telegram) From fe175a9a6943842caf46cf6296867f97544fb8ef Mon Sep 17 00:00:00 2001 From: AA61SL Date: Thu, 9 Feb 2023 11:07:21 +0100 Subject: [PATCH 188/226] feat: Fixing test file to run setup.py test --- test/experiment_telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/experiment_telegram.py b/test/experiment_telegram.py index 2892346..2c3fff2 100644 --- a/test/experiment_telegram.py +++ b/test/experiment_telegram.py @@ -1,7 +1,7 @@ from dsmr_parser import telegram_specifications from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser -from example_telegrams import TELEGRAM_V4_2 +from test.example_telegrams import TELEGRAM_V4_2 parser = TelegramParser(telegram_specifications.V4) telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) From de167c89b6b4c01f166cbf745ae431665b6443e7 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 19 Feb 2023 12:24:44 +0100 Subject: [PATCH 189/226] =?UTF-8?q?issue-51-telegram=20refactored=20Telegr?= =?UTF-8?q?amParser.parse=20to=20return=20Telegram=20=E2=80=A6=20(#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * issue-51-telegram improved mbus device parsing; refactored TelegramParser.parse to return Telegram object which is backwards compatible with the dict result --- CHANGELOG.rst | 83 ++--- README.rst | 172 +++------- dsmr_parser/clients/filereader.py | 7 +- dsmr_parser/clients/serial_.py | 5 +- dsmr_parser/clients/socket_.py | 3 +- dsmr_parser/obis_name_mapping.py | 7 + dsmr_parser/obis_references.py | 6 +- dsmr_parser/objects.py | 193 +++++++++--- dsmr_parser/parsers.py | 64 ++-- setup.py | 2 +- test/example_telegrams.py | 45 +++ test/experiment_telegram.py | 3 +- test/objects/__init__.py | 0 test/objects/test_mbusdevice.py | 61 ++++ .../{ => objects}/test_parser_corner_cases.py | 3 +- test/{ => objects}/test_telegram.py | 175 ++++++++++- test/test_parse_v5.py | 294 +++++++++--------- test/test_protocol.py | 4 +- test/test_rfxtrx_protocol.py | 4 +- 19 files changed, 724 insertions(+), 407 deletions(-) create mode 100644 test/objects/__init__.py create mode 100644 test/objects/test_mbusdevice.py rename test/{ => objects}/test_parser_corner_cases.py (97%) rename test/{ => objects}/test_telegram.py (60%) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 588657e..a57f8c3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,71 +1,74 @@ Change Log ---------- -**1.1.0** (2023-02-08) +**1.2.0** (2023-02-18) + +- Improved gas meter (mbus devices) support and replaced Telegram dictionary with backwards compatible object (`PR #121 `_ by `ndokter `_) -- Add instantaneous reactive power + fixed swapped reactive total import export (`pull request #124 `_ by `yada75 `_) +**1.1.0** (2023-02-08) +- Add instantaneous reactive power + fixed swapped reactive total import export (`PR #124 `_ by `yada75 `_) **1.0.0** (2022-12-22) - switched to new numbering scheme https://semver.org/ -- Added support for Python 3.11 and dropped support for Python 3.6 (`pull request #112 `_) -- Add support for Fluvius V1.7.1 DSMR messages (`pull request #110 `_) +- Added support for Python 3.11 and dropped support for Python 3.6 (`PR #112 `_ by `dennissiemensma `_) +- Add support for Fluvius V1.7.1 DSMR messages (`PR #110 `_ by `dupondje `_) **0.34** (2022-10-19) -- Adds support for the Sagemcom T210-D-r smart meter (`pull request #110 `_). +- Adds support for the Sagemcom T210-D-r smart meter (`PR #110 `_). **0.33** (2022-04-20) -- Test Python 3.10 in CI + legacy badge fix (`pull request #105 `_). -- Update telegram_specifications.py (`pull request #106 `_). -- Improve compatiblity with Belgian standard (`pull request #107 `_). -- Improve documentation asyncio (`pull request #63 `_). +- Test Python 3.10 in CI + legacy badge fix (`PR #105 `_). +- Update telegram_specifications.py (`PR #106 `_). +- Improve compatiblity with Belgian standard (`PR #107 `_). +- Improve documentation asyncio (`PR #63 `_). **0.32** (2022-01-04) -- Support DSMR data read via RFXtrx with integrated P1 reader (`pull request #98 `_). +- Support DSMR data read via RFXtrx with integrated P1 reader (`PR #98 `_). **0.31** (2021-11-21) -- Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway (`pull request #92 `_). +- Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway (`PR #92 `_). **0.30** (2021-08-18) -- Add support for Swedish smart meters (`pull request #86 `_). +- Add support for Swedish smart meters (`PR #86 `_). **0.29** (2021-04-18) -- Add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break (`pull request #71 `_). -Remove deprecated asyncio coroutine decorator (`pull request #76 `_). +- Add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break (`PR #71 `_). +Remove deprecated asyncio coroutine decorator (`PR #76 `_). **0.28** (2021-02-21) -- Optional keep alive monitoring for TCP/IP connections (`pull request #73 `_). -- Catch parse errors in TelegramParser, ignore lines that can not be parsed (`pull request #74 `_). +- Optional keep alive monitoring for TCP/IP connections (`PR #73 `_). +- Catch parse errors in TelegramParser, ignore lines that can not be parsed (`PR #74 `_). **0.27** (2020-12-24) -- fix for empty parentheses in ProfileGenericParser (redone) (`pull request #69 `_). +- fix for empty parentheses in ProfileGenericParser (redone) (`PR #69 `_). **0.26** (2020-12-15) -- reverted fix for empty parentheses in ProfileGenericParser (`pull request #68 `_). +- reverted fix for empty parentheses in ProfileGenericParser (`PR #68 `_). **0.25** (2020-12-14) -- fix for empty parentheses in ProfileGenericParser (`pull request #57 `_). +- fix for empty parentheses in ProfileGenericParser (`PR #57 `_). **0.24** (2020-11-27) -- Add Luxembourg equipment identifier (`pull request #62 `_). +- Add Luxembourg equipment identifier (`PR #62 `_). **0.23** (2020-11-07) -- Resolved issue with x-x:24.3.0 where it contains non-integer character (`pull request #61 `_). -- Tests are not installed anymore (`pull request #59 `_). -- Example telegram improvement (`pull request #58 `_). +- Resolved issue with x-x:24.3.0 where it contains non-integer character (`PR #61 `_). +- Tests are not installed anymore (`PR #59 `_). +- Example telegram improvement (`PR #58 `_). **0.22** (2020-08-23) @@ -93,40 +96,40 @@ Remove deprecated asyncio coroutine decorator (`pull request #76 `_). +- PyCRC replacement (`PR #48 `_). **0.17** (2019-12-21) -- Add a true telegram object (`pull request #40 `_). +- Add a true telegram object (`PR #40 `_). **0.16** (2019-12-21) -- Add support for Belgian and Smarty meters (`pull request #44 `_). +- Add support for Belgian and Smarty meters (`PR #44 `_). **0.15** (2019-12-12) -- Fixed asyncio loop issue (`pull request #43 `_). +- Fixed asyncio loop issue (`PR #43 `_). **0.14** (2019-10-08) -- Changed serial reading to reduce CPU usage (`pull request #37 `_). +- Changed serial reading to reduce CPU usage (`PR #37 `_). **0.13** (2019-03-04) -- Fix DSMR v5.0 serial settings which were not used (`pull request #33 `_). +- Fix DSMR v5.0 serial settings which were not used (`PR #33 `_). **0.12** (2018-09-23) -- Add serial settings for DSMR v5.0 (`pull request #31 `_). -- Lux-creos-obis-1.8.0 (`pull request #32 `_). +- Add serial settings for DSMR v5.0 (`PR #31 `_). +- Lux-creos-obis-1.8.0 (`PR #32 `_). **0.11** (2017-09-18) -- NULL value fix in checksum (`pull request #26 `_) +- NULL value fix in checksum (`PR #26 `_) **0.10** (2017-06-05) -- bugfix: don't force full telegram signatures (`pull request #25 `_) +- bugfix: don't force full telegram signatures (`PR #25 `_) - removed unused code for automatic telegram detection as this needs reworking after the fix mentioned above - InvalidChecksumError's are logged as warning instead of error @@ -146,7 +149,7 @@ Remove deprecated asyncio coroutine decorator (`pull request #76 `_) +- Internal refactoring related to the way clients feed their data into the parse module. Clients can now supply the telegram data in single characters, lines (which was common) or complete telegram strings. (`PR #17 `_) **IMPORTANT: this release has the following backwards incompatible changes:** @@ -156,8 +159,8 @@ Remove deprecated asyncio coroutine decorator (`pull request #76 `_) -- Support added for TCP connections using the asyncio client (`pull request #12 `_) +- Fixed bug in CRC checksum verification for the asyncio client (`PR #15 `_) +- Support added for TCP connections using the asyncio client (`PR #12 `_) **0.5** (2016-12-29) @@ -165,16 +168,16 @@ Remove deprecated asyncio coroutine decorator (`pull request #76 `_) -- improved asyncio reader and improve it's error handling (`pull request #8 `_) +- DSMR v2.2 serial settings now uses parity serial.EVEN by default (`PR #5 `_) +- improved asyncio reader and improve it's error handling (`PR #8 `_) **0.3** (2016-11-12) -- asyncio reader for non-blocking reads (`pull request #3 `_) +- asyncio reader for non-blocking reads (`PR #3 `_) **0.2** (2016-11-08) -- support for DMSR version 2.2 (`pull request #2 `_) +- support for DMSR version 2.2 (`PR #2 `_) **0.1** (2016-08-22) diff --git a/README.rst b/README.rst index 75e572c..9d80b96 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,6 @@ Features DSMR Parser supports DSMR versions 2, 3, 4 and 5. See for the `currently supported/tested Python versions here `_. - Client module usage ------------------- @@ -113,10 +112,8 @@ However, if we construct a mock TelegramParser that just returns the already par import asyncio import logging - #from dsmr_parser import obis_references - #from dsmr_parser import telegram_specifications - #from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader - #from dsmr_parser.objects import Telegram + from dsmr_parser import telegram_specifications + from dsmr_parser.clients.protocol import create_tcp_dsmr_reader logging.basicConfig(level=logging.INFO, format='%(message)s') @@ -142,19 +139,18 @@ However, if we construct a mock TelegramParser that just returns the already par except ParseError as e: logger.error('Failed to parse telegram: %s', e) - async def main(): try: logger.debug("Getting loop") loop = asyncio.get_event_loop() logger.debug("Creating reader") - await create_tcp_dsmr_reader( - HOST, - PORT, - DSMR_VERSION, - printTelegram, - loop - ) + await create_tcp_dsmr_reader( + HOST, + PORT, + DSMR_VERSION, + printTelegram, + loop + ) logger.debug("Reader created going to sleep now") while True: await asyncio.sleep(1) @@ -173,7 +169,9 @@ However, if we construct a mock TelegramParser that just returns the already par Parsing module usage -------------------- The parsing module accepts complete unaltered telegram strings and parses these -into a dictionary. +into a Telegram object. + +Tip: getting full telegrams from a bytestream can be made easier by using the TelegramBuffer helper class. .. code-block:: python @@ -208,135 +206,48 @@ into a dictionary. parser = TelegramParser(telegram_specifications.V3) + # see 'Telegram object' docs below telegram = parser.parse(telegram_str) - print(telegram) # see 'Telegram object' docs below - -Telegram dictionary -------------------- - -A dictionary of which the key indicates the field type. These regex values -correspond to one of dsmr_parser.obis_reference constants. - -The value is either a CosemObject or MBusObject. These have a 'value' and 'unit' -property. MBusObject's additionally have a 'datetime' property. The 'value' can -contain any python type (int, str, Decimal) depending on the field. The 'unit' -contains 'kW', 'A', 'kWh' or 'm3'. - -.. code-block:: python - - # Contents of a parsed DSMR v3 telegram - {'\\d-\\d:17\\.0\\.0.+?\\r\\n': , - '\\d-\\d:1\\.7\\.0.+?\\r\\n': , - '\\d-\\d:1\\.8\\.1.+?\\r\\n': , - '\\d-\\d:1\\.8\\.2.+?\\r\\n': , - '\\d-\\d:24\\.1\\.0.+?\\r\\n': , - '\\d-\\d:24\\.3\\.0.+?\\r\\n.+?\\r\\n': , - '\\d-\\d:24\\.4\\.0.+?\\r\\n': , - '\\d-\\d:2\\.7\\.0.+?\\r\\n': , - '\\d-\\d:2\\.8\\.1.+?\\r\\n': , - '\\d-\\d:2\\.8\\.2.+?\\r\\n': , - '\\d-\\d:96\\.13\\.0.+?\\r\\n': , - '\\d-\\d:96\\.13\\.1.+?\\r\\n': , - '\\d-\\d:96\\.14\\.0.+?\\r\\n': , - '\\d-\\d:96\\.1\\.0.+?\\r\\n': , - '\\d-\\d:96\\.1\\.1.+?\\r\\n': , - '\\d-\\d:96\\.3\\.10.+?\\r\\n': } - -Example to get some of the values: - -.. code-block:: python - - from dsmr_parser import obis_references - - # The telegram message timestamp. - message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP] - - # Using the active tariff to determine the electricity being used and - # delivered for the right tariff. - active_tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF] - active_tariff = int(tariff.value) - electricity_used_total = telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[active_tariff - 1]] - electricity_delivered_total = telegram[obis_references.ELECTRICITY_DELIVERED_TARIFF_ALL[active_tariff - 1]] - - gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING] +Telegram object +--------------------- - # See dsmr_reader.obis_references for all readable telegram values. - # Note that the available values differ per DSMR version. +A Telegram has attributes for all the parsed values according to the given telegram specification. Each value is a DsmrObject which have a 'value' and 'unit' property. MBusObject's, which are DsmrObject's as well additionally have a 'datetime' property. The 'value' can contain any python type (int, str, Decimal) depending on the field. The 'unit' contains 'kW', 'A', 'kWh' or 'm3'. -Telegram as an Object ---------------------- -An object version of the telegram is available as well. +Note: Telegram extends dictionary, which done for backwards compatibility. The use of keys (e.g. `telegram[obis_references.CURRENT_ELECTRICITY_USAGE]`) is deprecated. +Below are some examples on how to get the meter data. Alternatively check out the following unit test for a complete example: TelegramParserV5Test.test_parse .. code-block:: python - # DSMR v4.2 p1 using dsmr_parser and telegram objects + # Print contents of all available values + # See dsmr_parser.obis_name_mapping for all readable telegram values. + # The available values differ per DSMR version and meter. + print(telegram) + # P1_MESSAGE_HEADER: 42 [None] + # P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None] + # EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None] + # ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh] + # etc. - from dsmr_parser import telegram_specifications - from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5 - from dsmr_parser.objects import CosemObject, MBusObject, Telegram - from dsmr_parser.parsers import TelegramParser - import os + # Example to get current electricity usage + print(telegram.CURRENT_ELECTRICITY_USAGE) # + print(telegram.CURRENT_ELECTRICITY_USAGE.value) # Decimal('2.027') + print(telegram.CURRENT_ELECTRICITY_USAGE.unit) # 'kW' - serial_reader = SerialReader( - device='/dev/ttyUSB0', - serial_settings=SERIAL_SETTINGS_V5, - telegram_specification=telegram_specifications.V4 - ) + # All Mbus device readings like gas meters and water meters can be retrieved as follows. This + # returns a list of MbusDevice objects: + mbus_devices = telegram.MBUS_DEVICES - # telegram = next(serial_reader.read_as_object()) - # print(telegram) - - for telegram in serial_reader.read_as_object(): - os.system('clear') - print(telegram) - -Example of output of print of the telegram object: - -.. code-block:: console - - P1_MESSAGE_HEADER: 42 [None] - P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None] - EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None] - ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh] - ELECTRICITY_USED_TARIFF_2: 1435.706 [kWh] - ELECTRICITY_DELIVERED_TARIFF_1: 0.000 [kWh] - ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh] - ELECTRICITY_ACTIVE_TARIFF: 0002 [None] - CURRENT_ELECTRICITY_USAGE: 2.027 [kW] - CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW] - LONG_POWER_FAILURE_COUNT: 7 [None] - VOLTAGE_SAG_L1_COUNT: 0 [None] - VOLTAGE_SAG_L2_COUNT: 0 [None] - VOLTAGE_SAG_L3_COUNT: 0 [None] - VOLTAGE_SWELL_L1_COUNT: 0 [None] - VOLTAGE_SWELL_L2_COUNT: 0 [None] - VOLTAGE_SWELL_L3_COUNT: 0 [None] - TEXT_MESSAGE_CODE: None [None] - TEXT_MESSAGE: None [None] - DEVICE_TYPE: 3 [None] - INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.170 [kW] - INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 1.247 [kW] - INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.209 [kW] - INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW] - INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW] - INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW] - EQUIPMENT_IDENTIFIER_GAS: 4819243993373755377509728609491464 [None] - HOURLY_GAS_METER_READING: 981.443 [m3] - -Accessing the telegrams information as attributes directly: + # A specific MbusDevice based on the channel it's connected to, can be retrieved as follows: + mbus_device = telegram.get_mbus_device_by_channel(1) + print(mbus_device.DEVICE_TYPE.value) # 3 + print(mbus_device.EQUIPMENT_IDENTIFIER_GAS.value) # '4730303339303031393336393930363139' + print(mbus_device.HOURLY_GAS_METER_READING.value) # Decimal('246.138') -.. code-block:: python + # DEPRECATED: the dictionary approach of getting the values by key or `.items()' or '.get() is deprecated + telegram[obis_references.CURRENT_ELECTRICITY_USAGE] - telegram - Out[3]: - telegram.CURRENT_ELECTRICITY_USAGE - Out[4]: - telegram.CURRENT_ELECTRICITY_USAGE.value - Out[5]: Decimal('2.027') - telegram.CURRENT_ELECTRICITY_USAGE.unit - Out[6]: 'kW' The telegram object has an iterator, can be used to find all the information elements in the current telegram: @@ -373,7 +284,6 @@ The telegram object has an iterator, can be used to find all the information ele 'EQUIPMENT_IDENTIFIER_GAS', 'HOURLY_GAS_METER_READING'] - Installation ------------ diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index 9b9cf6e..a2ab525 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -4,7 +4,6 @@ from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError -from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser logger = logging.getLogger(__name__) @@ -72,7 +71,7 @@ def read_as_object(self): for telegram in self.telegram_buffer.get_all(): try: - yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + yield self.telegram_parser.parse(telegram) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: @@ -121,7 +120,7 @@ def read_as_object(self): for telegram in self.telegram_buffer.get_all(): try: - yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + yield self.telegram_parser.parse(telegram) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: @@ -167,7 +166,7 @@ def read_as_object(self): for telegram in self.telegram_buffer.get_all(): try: - yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + yield self.telegram_parser.parse(telegram) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index a76780f..945c4e7 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -5,7 +5,6 @@ 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__) @@ -55,7 +54,7 @@ def read_as_object(self): for telegram in self.telegram_buffer.get_all(): try: - yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + yield self.telegram_parser.parse(telegram) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: @@ -121,7 +120,7 @@ async def read_as_object(self, queue): for telegram in self.telegram_buffer.get_all(): try: queue.put_nowait( - Telegram(telegram, self.telegram_parser, self.telegram_specification) + self.telegram_parser.parse(telegram) ) except InvalidChecksumError as e: logger.warning(str(e)) diff --git a/dsmr_parser/clients/socket_.py b/dsmr_parser/clients/socket_.py index 6727979..b7490ec 100644 --- a/dsmr_parser/clients/socket_.py +++ b/dsmr_parser/clients/socket_.py @@ -4,7 +4,6 @@ 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__) @@ -81,7 +80,7 @@ def read_as_object(self): for telegram in self.telegram_buffer.get_all(): try: - yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + yield self.telegram_parser.parse(telegram) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index cc85a02..44ebb82 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -19,6 +19,12 @@ obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1', obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2', obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF', + obis.CURRENT_REACTIVE_EXPORTED: 'CURRENT_REACTIVE_EXPORTED', + obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1: 'ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1', + obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2: 'ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2', + obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1: 'ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1', + obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2: 'ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2', + obis.CURRENT_REACTIVE_IMPORTED: 'CURRENT_REACTIVE_IMPORTED', obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER', obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE', obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY', @@ -86,6 +92,7 @@ obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE', obis.Q3D_EQUIPMENT_SERIALNUMBER: 'Q3D_EQUIPMENT_SERIALNUMBER', + obis.BELGIUM_MBUS2_DEVICE_TYPE: 'BELGIUM_MBUS2_DEVICE_TYPE' } REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index da3ae9b..8aa7461 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -24,9 +24,9 @@ EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n' CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n' -LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n' -SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n' -POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n' +LONG_POWER_FAILURE_COUNT = r'\d-\d:96\.7\.9.+?\r\n' +SHORT_POWER_FAILURE_COUNT = r'\d-\d:96\.7\.21.+?\r\n' +POWER_EVENT_FAILURE_LOG = r'\d-\d:99\.97\.0.+?\r\n' VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n' VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n' VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n' diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 351b5c6..583c7f7 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,20 +1,16 @@ -import dsmr_parser.obis_name_mapping +from decimal import Decimal + import datetime import json -from decimal import Decimal +import pytz -class Telegram(object): +from dsmr_parser import obis_name_mapping + + +class Telegram(dict): """ - Container for raw and parsed telegram data. - Initializing: - from dsmr_parser import telegram_specifications - from dsmr_parser.exceptions import InvalidChecksumError, ParseError - from dsmr_parser.objects import CosemObject, MBusObject, Telegram - from dsmr_parser.parsers import TelegramParser - from test.example_telegrams import TELEGRAM_V4_2 - parser = TelegramParser(telegram_specifications.V4) - telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) + Container for parsed telegram data. Attributes can be accessed on a telegram object by addressing by their english name, for example: telegram.ELECTRICITY_USED_TARIFF_1 @@ -23,25 +19,55 @@ class Telegram(object): [k for k,v in telegram] yields: ['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...] + + Note: Dict like usage is deprecated. The inheritance from dict is because of backwards compatibility. """ - def __init__(self, telegram_data, telegram_parser, telegram_specification): - self._telegram_data = telegram_data - self._telegram_specification = telegram_specification - self._telegram_parser = telegram_parser - self._obis_name_mapping = dsmr_parser.obis_name_mapping.EN - self._reverse_obis_name_mapping = dsmr_parser.obis_name_mapping.REVERSE_EN - self._dictionary = self._telegram_parser.parse(telegram_data) - self._item_names = self._get_item_names() - - def __getattr__(self, name): - ''' will only get called for undefined attributes ''' - obis_reference = self._reverse_obis_name_mapping[name] - value = self._dictionary[obis_reference] - setattr(self, name, value) - return value - - def _get_item_names(self): - return [self._obis_name_mapping[k] for k, v in self._dictionary.items()] + def __init__(self, *args, **kwargs): + self._item_names = [] + self._mbus_devices = [] + super().__init__(*args, **kwargs) + + def add(self, obis_reference, dsmr_object): + # Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER + obis_name = obis_name_mapping.EN[obis_reference] + setattr(self, obis_name, dsmr_object) + if obis_name not in self._item_names: # TODO repeating obis references + self._item_names.append(obis_name) + + # TODO isinstance check: MaxDemandParser (BELGIUM_MAXIMUM_DEMAND_13_MONTHS) returns a list + if isinstance(dsmr_object, DSMRObject) and dsmr_object.is_mbus_reading: + self._add_mbus(obis_reference, dsmr_object) + + # Fill dict which is only used for backwards compatibility + if obis_reference not in self: + self[obis_reference] = dsmr_object + + def _add_mbus(self, obis_reference, dsmr_object): + """ + The given DsmrObject is assumed to be Mbus related and will be grouped into a MbusDevice. + Grouping is done by the DsmrObject channel ID. + """ + channel_id = dsmr_object.obis_id_code[1] + + # Create new MbusDevice or update existing one as it's records are being added one by one. + mbus_device = self.get_mbus_device_by_channel(channel_id) + if not mbus_device: + mbus_device = MbusDevice(channel_id=channel_id) + self._mbus_devices.append(mbus_device) + + mbus_device.add(obis_reference, dsmr_object) + + if not hasattr(self, 'MBUS_DEVICES'): + setattr(self, 'MBUS_DEVICES', self._mbus_devices) + self._item_names.append('MBUS_DEVICES') + + def get_mbus_device_by_channel(self, channel_id): + """ + :rtype: MbusDevice|None + """ + for mbus_device in self._mbus_devices: + if mbus_device.channel_id == channel_id: + return mbus_device def __iter__(self): for attr in self._item_names: @@ -51,21 +77,44 @@ def __iter__(self): def __str__(self): output = "" for attr, value in self: - output += "{}: \t {}\n".format(attr, str(value)) + if isinstance(value, list): + output += ''.join(map(str, value)) + else: + output += "{}: \t {}\n".format(attr, str(value)) + return output def to_json(self): - return json.dumps(dict([[attr, json.loads(value.to_json())] for attr, value in self])) + json_data = {} + + for attr, value in self: + if isinstance(value, list): + json_data[attr] = [json.loads(item.to_json() if hasattr(item, 'to_json') else item) + for item in value] + elif hasattr(value, 'to_json'): + json_data[attr] = json.loads(value.to_json()) + + return json.dumps(json_data) class DSMRObject(object): """ Represents all data from a single telegram line. """ - - def __init__(self, values): + def __init__(self, obis_id_code, values): + self.obis_id_code = obis_id_code self.values = values + @property + def is_mbus_reading(self): + """ Detect Mbus related readings using obis id + channel. """ + obis_id, channel_id = self.obis_id_code + + return obis_id == 0 and channel_id != 0 + + def to_json(self): + raise NotImplementedError + class MBusObject(DSMRObject): @@ -94,16 +143,20 @@ def unit(self): return self.values[1]['unit'] def __str__(self): - output = "{}\t[{}] at {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat())) + output = "{}\t[{}] at {}".format( + str(self.value), + str(self.unit), + str(self.datetime.astimezone().astimezone(pytz.utc).isoformat()) + ) return output def to_json(self): timestamp = self.datetime if isinstance(self.datetime, datetime.datetime): - timestamp = self.datetime.astimezone().isoformat() + timestamp = self.datetime.astimezone().astimezone(pytz.utc).isoformat() value = self.value if isinstance(self.value, datetime.datetime): - value = self.value.astimezone().isoformat() + value = self.value.astimezone().astimezone(pytz.utc).isoformat() if isinstance(self.value, Decimal): value = float(self.value) output = { @@ -134,20 +187,20 @@ def unit(self): def __str__(self): output = "{}\t[{}] at {} occurred {}"\ - .format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat()), - str(self.occurred.astimezone().isoformat())) + .format(str(self.value), str(self.unit), str(self.datetime.astimezone().astimezone(pytz.utc).isoformat()), + str(self.occurred.astimezone().astimezone(pytz.utc).isoformat())) return output def to_json(self): timestamp = self.datetime if isinstance(self.datetime, datetime.datetime): - timestamp = self.datetime.astimezone().isoformat() + timestamp = self.datetime.astimezone().astimezone(pytz.utc).isoformat() timestamp_occurred = self.occurred if isinstance(self.occurred, datetime.datetime): - timestamp_occurred = self.occurred.astimezone().isoformat() + timestamp_occurred = self.occurred.astimezone().astimezone(pytz.utc).isoformat() value = self.value if isinstance(self.value, datetime.datetime): - value = self.value.astimezone().isoformat() + value = self.value.astimezone().astimezone(pytz.utc).isoformat() if isinstance(self.value, Decimal): value = float(self.value) output = { @@ -172,14 +225,14 @@ def unit(self): def __str__(self): print_value = self.value if isinstance(self.value, datetime.datetime): - print_value = self.value.astimezone().isoformat() + print_value = self.value.astimezone().astimezone(pytz.utc).isoformat() output = "{}\t[{}]".format(str(print_value), str(self.unit)) return output def to_json(self): json_value = self.value if isinstance(self.value, datetime.datetime): - json_value = self.value.astimezone().isoformat() + json_value = self.value.astimezone().astimezone(pytz.utc).isoformat() if isinstance(self.value, Decimal): json_value = float(self.value) output = { @@ -196,8 +249,8 @@ class ProfileGenericObject(DSMRObject): containing the datetime (timestamp) and the value. """ - def __init__(self, values): - super().__init__(values) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self._buffer_list = None @property @@ -223,9 +276,16 @@ def buffer(self): if self._buffer_list is None: self._buffer_list = [] values_offset = 2 + for i in range(self.buffer_length): offset = values_offset + i * 2 - self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) + self._buffer_list.append( + MBusObject( + obis_id_code=self.obis_id_code, + values=[self.values[offset], self.values[offset + 1]] + ) + ) + return self._buffer_list def __str__(self): @@ -234,7 +294,7 @@ def __str__(self): for buffer_value in self.buffer: timestamp = buffer_value.datetime if isinstance(timestamp, datetime.datetime): - timestamp = str(timestamp.astimezone().isoformat()) + timestamp = str(timestamp.astimezone().astimezone(pytz.utc).isoformat()) output += "\n\t event occured at: {}".format(timestamp) output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit) return output @@ -260,3 +320,40 @@ def to_json(self): list.append(['buffer', buffer_repr]) output = dict(list) return json.dumps(output) + + +class MbusDevice: + """ + This object is similar to the Telegram except that it only contains readings related to the same mbus device. + """ + + def __init__(self, channel_id): + self.channel_id = channel_id + self._item_names = [] + + def add(self, obis_reference, dsmr_object): + # Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER + # Also keep track of the added names internally + obis_name = obis_name_mapping.EN[obis_reference] + setattr(self, obis_name, dsmr_object) + self._item_names.append(obis_name) + + def __len__(self): + return len(self._item_names) + + def __iter__(self): + for attr in self._item_names: + value = getattr(self, attr) + yield attr, value + + def __str__(self): + output = "MBUS DEVICE (channel {})\n".format(self.channel_id) + for attr, value in self: + output += "\t{}: \t {}\n".format(attr, str(value)) + return output + + def to_json(self): + data = {obis_name: json.loads(value.to_json()) for obis_name, value in self} + data['CHANNEL_ID'] = self.channel_id + + return json.dumps(data) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 9b10b4d..c80de39 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -8,7 +8,7 @@ from dlms_cosem.connection import XDlmsApduFactory from dlms_cosem.protocol.xdlms import GeneralGlobalCipher -from dsmr_parser.objects import MBusObject, MBusObjectPeak, CosemObject, ProfileGenericObject +from dsmr_parser.objects import MBusObject, MBusObjectPeak, CosemObject, ProfileGenericObject, Telegram from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.value_types import timestamp @@ -37,15 +37,7 @@ def parse(self, telegram_data, encryption_key="", authentication_key=""): # noq ('!ABCD') including line endings in between the telegram's lines :param str encryption_key: encryption key :param str authentication_key: authentication key - :rtype: dict - :returns: Shortened example: - { - .. - r'\d-\d:96\.1\.1.+?\r\n': , # EQUIPMENT_IDENTIFIER - r'\d-\d:1\.8\.1.+?\r\n': , # ELECTRICITY_USED_TARIFF_1 - r'\d-\d:24\.3\.0.+?\r\n.+?\r\n': , # GAS_METER_READING - .. - } + :rtype: Telegram :raises ParseError: :raises InvalidChecksumError: """ @@ -82,23 +74,25 @@ def parse(self, telegram_data, encryption_key="", authentication_key=""): # noq except Exception: pass - if self.apply_checksum_validation \ - and self.telegram_specification['checksum_support']: + if self.apply_checksum_validation and self.telegram_specification['checksum_support']: self.validate_checksum(telegram_data) - telegram = {} + telegram = Telegram() for signature, parser in self.telegram_specification['objects'].items(): - match = re.search(signature, telegram_data, re.DOTALL) + pattern = re.compile(signature, re.DOTALL) + matches = pattern.findall(telegram_data) # Some signatures are optional and may not be present, # so only parse lines that match - if match: + for match in matches: try: - telegram[signature] = parser.parse(match.group(0)) + dsmr_object = parser.parse(match) except Exception: logger.error("ignore line with signature {}, because parsing failed.".format(signature), exc_info=True) + else: + telegram.add(obis_reference=signature, dsmr_object=dsmr_object) return telegram @@ -180,6 +174,20 @@ def _parse_values(self, values): return [self.value_formats[i].parse(value) for i, value in enumerate(values)] + def _parse_obis_id_code(self, line): + """ + Get the OBIS ID code + + Example line: + '0-2:24.2.1(200426223001S)(00246.138*m3)' + + OBIS ID code = 0-2 returned as tuple + """ + try: + return int(line[0]), int(line[2]) + except ValueError: + raise ParseError("Invalid OBIS ID code for line '%s' in '%s'", line, self) + def _parse(self, line): # Match value groups, but exclude the parentheses pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') @@ -213,7 +221,10 @@ class MBusParser(DSMRObjectParser): """ def parse(self, line): - return MBusObject(self._parse(line)) + return MBusObject( + obis_id_code=self._parse_obis_id_code(line), + values=self._parse(line) + ) class MaxDemandParser(DSMRObjectParser): @@ -241,6 +252,8 @@ def parse(self, line): pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') values = re.findall(pattern, line) + obis_id_code = self._parse_obis_id_code(line) + objects = [] count = int(values[0]) @@ -248,7 +261,10 @@ def parse(self, line): timestamp_month = ValueParser(timestamp).parse(values[i * 3 + 1]) timestamp_occurred = ValueParser(timestamp).parse(values[i * 3 + 1]) value = ValueParser(Decimal).parse(values[i * 3 + 2]) - objects.append(MBusObjectPeak([timestamp_month, timestamp_occurred, value])) + objects.append(MBusObjectPeak( + obis_id_code=obis_id_code, + values=[timestamp_month, timestamp_occurred, value] + )) return objects @@ -274,7 +290,10 @@ class CosemParser(DSMRObjectParser): """ def parse(self, line): - return CosemObject(self._parse(line)) + return CosemObject( + obis_id_code=self._parse_obis_id_code(line), + values=self._parse(line) + ) class ProfileGenericParser(DSMRObjectParser): @@ -333,7 +352,10 @@ def _parse_values(self, values): return [self.value_formats[i].parse(value) for i, value in enumerate(values)] def parse(self, line): - return ProfileGenericObject(self._parse(line)) + return ProfileGenericObject( + obis_id_code=self._parse_obis_id_code(line), + values=self._parse(line) + ) class ValueParser(object): @@ -341,7 +363,7 @@ class ValueParser(object): Parses a single value from DSMRObject's. Example with coerce_type being int: - (002*A) becomes {'value': 1, 'unit': 'A'} + (002*A) becomes {'value': 2, 'unit': 'A'} Example with coerce_type being str: (42) becomes {'value': '42', 'unit': None} diff --git a/setup.py b/setup.py index e381170..164667c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.1.0', + version='1.2.0', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', diff --git a/test/example_telegrams.py b/test/example_telegrams.py index 050d3a8..d59ce83 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -129,6 +129,51 @@ '!6EEE\r\n' ) +# V5 telegram with 2 MBUS devices +TELEGRAM_V5_TWO_MBUS = ( + '/ISK5\\2M550T-1012\r\n' + '\r\n' + '1-3:0.2.8(50)\r\n' + '0-0:1.0.0(200426223325S)\r\n' + '0-0:96.1.1(4530303434303037333832323436303139)\r\n' + '1-0:1.8.1(002130.115*kWh)\r\n' + '1-0:1.8.2(000245.467*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(0001)\r\n' + '1-0:1.7.0(00.111*kW)\r\n' + '1-0:2.7.0(00.000*kW)\r\n' + '0-0:96.7.21(00005)\r\n' + '0-0:96.7.9(00003)\r\n' + '1-0:99.97.0(1)(0-0:96.7.19)(190326095015W)(0000002014*s)\r\n' + '1-0:32.32.0(00001)\r\n' + '1-0:52.32.0(00001)\r\n' + '1-0:72.32.0(00192)\r\n' + '1-0:32.36.0(00001)\r\n' + '1-0:52.36.0(00001)\r\n' + '1-0:72.36.0(00001)\r\n' + '0-0:96.13.0()\r\n' + '1-0:32.7.0(229.9*V)\r\n' + '1-0:52.7.0(229.2*V)\r\n' + '1-0:72.7.0(222.9*V)\r\n' + '1-0:31.7.0(000*A)\r\n' + '1-0:51.7.0(000*A)\r\n' + '1-0:71.7.0(001*A)\r\n' + '1-0:21.7.0(00.056*kW)\r\n' + '1-0:41.7.0(00.000*kW)\r\n' + '1-0:61.7.0(00.055*kW)\r\n' + '1-0:22.7.0(00.000*kW)\r\n' + '1-0:42.7.0(00.000*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()\r\n' + '0-1:24.2.1(700101010000W)(00000000)\r\n' + '0-2:24.1.0(003)\r\n' + '0-2:96.1.0(4730303339303031393336393930363139)\r\n' + '0-2:24.2.1(200426223001S)(00246.138*m3)\r\n' + '!56DD\r\n' +) + TELEGRAM_FLUVIUS_V171 = ( '/FLU5\253769484_A\r\n' '\r\n' diff --git a/test/experiment_telegram.py b/test/experiment_telegram.py index 2c3fff2..c815072 100644 --- a/test/experiment_telegram.py +++ b/test/experiment_telegram.py @@ -1,8 +1,7 @@ from dsmr_parser import telegram_specifications -from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser from test.example_telegrams import TELEGRAM_V4_2 parser = TelegramParser(telegram_specifications.V4) -telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) +telegram = parser.parse(TELEGRAM_V4_2) print(telegram) diff --git a/test/objects/__init__.py b/test/objects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/objects/test_mbusdevice.py b/test/objects/test_mbusdevice.py new file mode 100644 index 0000000..b92d4af --- /dev/null +++ b/test/objects/test_mbusdevice.py @@ -0,0 +1,61 @@ +from decimal import Decimal + +import json +import unittest + +from dsmr_parser import telegram_specifications, obis_references +from dsmr_parser.objects import MbusDevice + + +class MbusDeviceTest(unittest.TestCase): + + def setUp(self): + v5_objects = telegram_specifications.V5['objects'] + + device_type_parser = v5_objects[obis_references.DEVICE_TYPE] + device_type = device_type_parser.parse('0-2:24.1.0(003)\r\n') + + equipment_parser = v5_objects[obis_references.EQUIPMENT_IDENTIFIER_GAS] + equipment = equipment_parser.parse('0-2:96.1.0(4730303339303031393336393930363139)\r\n') + + gas_reading_parser = v5_objects[obis_references.HOURLY_GAS_METER_READING] + gas_reading = gas_reading_parser.parse('0-2:24.2.1(200426223001S)(00246.138*m3)\r\n') + + mbus_device = MbusDevice(channel_id=1) + mbus_device.add(obis_references.DEVICE_TYPE, device_type) + mbus_device.add(obis_references.EQUIPMENT_IDENTIFIER_GAS, equipment) + mbus_device.add(obis_references.HOURLY_GAS_METER_READING, gas_reading) + + self.mbus_device = mbus_device + + def test_attributes(self): + self.assertEqual(self.mbus_device.DEVICE_TYPE.value, 3) + self.assertEqual(self.mbus_device.DEVICE_TYPE.unit, None) + + self.assertEqual(self.mbus_device.EQUIPMENT_IDENTIFIER_GAS.value, + '4730303339303031393336393930363139') + self.assertEqual(self.mbus_device.EQUIPMENT_IDENTIFIER_GAS.unit, None) + + self.assertEqual(self.mbus_device.HOURLY_GAS_METER_READING.value, Decimal('246.138')) + self.assertEqual(self.mbus_device.HOURLY_GAS_METER_READING.unit, 'm3') + + def test_to_json(self): + self.assertEqual( + json.loads(self.mbus_device.to_json()), + { + 'CHANNEL_ID': 1, + 'DEVICE_TYPE': {'value': 3, 'unit': None}, + 'EQUIPMENT_IDENTIFIER_GAS': {'value': '4730303339303031393336393930363139', 'unit': None}, + 'HOURLY_GAS_METER_READING': {'datetime': '2020-04-26T20:30:01+00:00', 'value': 246.138, 'unit': 'm3'}} + ) + + def test_str(self): + self.assertEqual( + str(self.mbus_device), + ( + 'MBUS DEVICE (channel 1)\n' + '\tDEVICE_TYPE: 3 [None]\n' + '\tEQUIPMENT_IDENTIFIER_GAS: 4730303339303031393336393930363139 [None]\n' + '\tHOURLY_GAS_METER_READING: 246.138 [m3] at 2020-04-26T20:30:01+00:00\n' + ) + ) diff --git a/test/test_parser_corner_cases.py b/test/objects/test_parser_corner_cases.py similarity index 97% rename from test/test_parser_corner_cases.py rename to test/objects/test_parser_corner_cases.py index 3f203e7..9b26956 100644 --- a/test/test_parser_corner_cases.py +++ b/test/objects/test_parser_corner_cases.py @@ -2,7 +2,6 @@ from dsmr_parser import telegram_specifications -from dsmr_parser.objects import Telegram from dsmr_parser.objects import ProfileGenericObject from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import ProfileGenericParser @@ -18,7 +17,7 @@ class TestParserCornerCases(unittest.TestCase): def test_power_event_log_empty_1(self): # POWER_EVENT_FAILURE_LOG (1-0:99.97.0) parser = TelegramParser(telegram_specifications.V5) - telegram = Telegram(TELEGRAM_V5, parser, telegram_specifications.V5) + telegram = parser.parse(TELEGRAM_V5) object_type = ProfileGenericObject testitem = telegram.POWER_EVENT_FAILURE_LOG diff --git a/test/test_telegram.py b/test/objects/test_telegram.py similarity index 60% rename from test/test_telegram.py rename to test/objects/test_telegram.py index 90b8eff..17a6891 100644 --- a/test/test_telegram.py +++ b/test/objects/test_telegram.py @@ -1,15 +1,15 @@ +import json import unittest import datetime import pytz -from dsmr_parser import telegram_specifications +from dsmr_parser import telegram_specifications, obis_references from dsmr_parser import obis_name_mapping from dsmr_parser.objects import CosemObject from dsmr_parser.objects import MBusObject -from dsmr_parser.objects import Telegram from dsmr_parser.objects import ProfileGenericObject from dsmr_parser.parsers import TelegramParser -from test.example_telegrams import TELEGRAM_V4_2 +from test.example_telegrams import TELEGRAM_V4_2, TELEGRAM_V5_TWO_MBUS, TELEGRAM_V5 from decimal import Decimal @@ -30,7 +30,7 @@ def verify_telegram_item(self, telegram, testitem_name, object_type, unit_val, v def test_instantiate(self): parser = TelegramParser(telegram_specifications.V4) - telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) + telegram = parser.parse(TELEGRAM_V4_2) # P1_MESSAGE_HEADER (1-3:0.2.8) self.verify_telegram_item(telegram, @@ -320,3 +320,170 @@ def test_instantiate(self): item_names_tested_set = set(self.item_names_tested) assert item_names_tested_set == V4_name_set + + def test_iter(self): + parser = TelegramParser(telegram_specifications.V5) + telegram = parser.parse(TELEGRAM_V5) + + for obis_name, dsmr_object in telegram: + break + + # Verify that the iterator works for at least one value + self.assertEqual(obis_name, obis_name_mapping.EN[obis_references.P1_MESSAGE_HEADER]) + self.assertEqual(dsmr_object.value, '50') + + def test_mbus_devices(self): + parser = TelegramParser(telegram_specifications.V5) + telegram = parser.parse(TELEGRAM_V5_TWO_MBUS) + mbus_devices = telegram.MBUS_DEVICES + + self.assertEqual(len(mbus_devices), 2) + + mbus_device_1 = mbus_devices[0] + self.assertEqual(mbus_device_1.DEVICE_TYPE.value, 3) + self.assertEqual(mbus_device_1.EQUIPMENT_IDENTIFIER_GAS.value, None) + self.assertEqual(mbus_device_1.HOURLY_GAS_METER_READING.value, Decimal('0')) + + mbus_device_2 = mbus_devices[1] + self.assertEqual(mbus_device_2.DEVICE_TYPE.value, 3) + self.assertEqual(mbus_device_2.EQUIPMENT_IDENTIFIER_GAS.value, '4730303339303031393336393930363139') + self.assertEqual(mbus_device_2.HOURLY_GAS_METER_READING.value, Decimal('246.138')) + + def test_get_mbus_device_by_channel(self): + parser = TelegramParser(telegram_specifications.V5) + telegram = parser.parse(TELEGRAM_V5_TWO_MBUS) + + mbus_device_1 = telegram.get_mbus_device_by_channel(1) + self.assertEqual(mbus_device_1.DEVICE_TYPE.value, 3) + self.assertEqual(mbus_device_1.EQUIPMENT_IDENTIFIER_GAS.value, None) + self.assertEqual(mbus_device_1.HOURLY_GAS_METER_READING.value, Decimal('0')) + + mbus_device_2 = telegram.get_mbus_device_by_channel(2) + self.assertEqual(mbus_device_2.DEVICE_TYPE.value, 3) + self.assertEqual(mbus_device_2.EQUIPMENT_IDENTIFIER_GAS.value, '4730303339303031393336393930363139') + self.assertEqual(mbus_device_2.HOURLY_GAS_METER_READING.value, Decimal('246.138')) + + def test_without_mbus_devices(self): + parser = TelegramParser(telegram_specifications.V5, apply_checksum_validation=False) + telegram = parser.parse('') + + self.assertFalse(hasattr(telegram, 'MBUS_DEVICES')) + self.assertIsNone(telegram.get_mbus_device_by_channel(1)) + + def test_to_json(self): + parser = TelegramParser(telegram_specifications.V5) + telegram = parser.parse(TELEGRAM_V5) + json_data = json.loads(telegram.to_json()) + + self.assertEqual( + json_data, + {'CURRENT_ELECTRICITY_DELIVERY': {'unit': 'kW', 'value': 0.0}, + 'CURRENT_ELECTRICITY_USAGE': {'unit': 'kW', 'value': 0.244}, + 'DEVICE_TYPE': {'unit': None, 'value': 3}, + 'ELECTRICITY_ACTIVE_TARIFF': {'unit': None, 'value': '0002'}, + 'ELECTRICITY_DELIVERED_TARIFF_1': {'unit': 'kWh', 'value': 2.444}, + 'ELECTRICITY_DELIVERED_TARIFF_2': {'unit': 'kWh', 'value': 0.0}, + 'ELECTRICITY_USED_TARIFF_1': {'unit': 'kWh', 'value': 4.426}, + 'ELECTRICITY_USED_TARIFF_2': {'unit': 'kWh', 'value': 2.399}, + 'EQUIPMENT_IDENTIFIER': {'unit': None, + 'value': '4B384547303034303436333935353037'}, + 'EQUIPMENT_IDENTIFIER_GAS': {'unit': None, 'value': None}, + 'HOURLY_GAS_METER_READING': {'datetime': '2017-01-02T15:10:05+00:00', + 'unit': 'm3', + 'value': 0.107}, + 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE': {'unit': 'kW', 'value': 0.0}, + 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE': {'unit': 'kW', 'value': 0.07}, + 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE': {'unit': 'kW', 'value': 0.0}, + 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE': {'unit': 'kW', 'value': 0.032}, + 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE': {'unit': 'kW', 'value': 0.0}, + 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE': {'unit': 'kW', 'value': 0.142}, + 'INSTANTANEOUS_CURRENT_L1': {'unit': 'A', 'value': 0.48}, + 'INSTANTANEOUS_CURRENT_L2': {'unit': 'A', 'value': 0.44}, + 'INSTANTANEOUS_CURRENT_L3': {'unit': 'A', 'value': 0.86}, + 'INSTANTANEOUS_VOLTAGE_L1': {'unit': 'V', 'value': 230.0}, + 'INSTANTANEOUS_VOLTAGE_L2': {'unit': 'V', 'value': 230.0}, + 'INSTANTANEOUS_VOLTAGE_L3': {'unit': 'V', 'value': 229.0}, + 'LONG_POWER_FAILURE_COUNT': {'unit': None, 'value': 0}, + 'MBUS_DEVICES': [{'CHANNEL_ID': 1, + 'DEVICE_TYPE': {'unit': None, 'value': 3}, + 'EQUIPMENT_IDENTIFIER_GAS': {'unit': None, + 'value': '3232323241424344313233343536373839'}, + 'HOURLY_GAS_METER_READING': {'datetime': '2017-01-02T15:10:05+00:00', + 'unit': 'm3', + 'value': 0.107}}, + {'CHANNEL_ID': 2, + 'DEVICE_TYPE': {'unit': None, 'value': 3}, + 'EQUIPMENT_IDENTIFIER_GAS': {'unit': None, 'value': None}}], + 'P1_MESSAGE_HEADER': {'unit': None, 'value': '50'}, + 'P1_MESSAGE_TIMESTAMP': {'unit': None, 'value': '2017-01-02T18:20:02+00:00'}, + 'POWER_EVENT_FAILURE_LOG': {'buffer': [], + 'buffer_length': 0, + 'buffer_type': '0-0:96.7.19'}, + 'SHORT_POWER_FAILURE_COUNT': {'unit': None, 'value': 13}, + 'TEXT_MESSAGE': {'unit': None, 'value': None}, + 'VOLTAGE_SAG_L1_COUNT': {'unit': None, 'value': 0}, + 'VOLTAGE_SAG_L2_COUNT': {'unit': None, 'value': 0}, + 'VOLTAGE_SAG_L3_COUNT': {'unit': None, 'value': 0}, + 'VOLTAGE_SWELL_L1_COUNT': {'unit': None, 'value': 0}, + 'VOLTAGE_SWELL_L2_COUNT': {'unit': None, 'value': 0}, + 'VOLTAGE_SWELL_L3_COUNT': {'unit': None, 'value': 0}} + ) + + def test_to_str(self): + parser = TelegramParser(telegram_specifications.V5) + telegram = parser.parse(TELEGRAM_V5) + + self.assertEqual( + str(telegram), + ( + 'P1_MESSAGE_HEADER: 50 [None]\n' + 'P1_MESSAGE_TIMESTAMP: 2017-01-02T18:20:02+00:00 [None]\n' + 'EQUIPMENT_IDENTIFIER: 4B384547303034303436333935353037 [None]\n' + 'ELECTRICITY_USED_TARIFF_1: 4.426 [kWh]\n' + 'ELECTRICITY_USED_TARIFF_2: 2.399 [kWh]\n' + 'ELECTRICITY_DELIVERED_TARIFF_1: 2.444 [kWh]\n' + 'ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh]\n' + 'ELECTRICITY_ACTIVE_TARIFF: 0002 [None]\n' + 'CURRENT_ELECTRICITY_USAGE: 0.244 [kW]\n' + 'CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW]\n' + 'LONG_POWER_FAILURE_COUNT: 0 [None]\n' + 'SHORT_POWER_FAILURE_COUNT: 13 [None]\n' + 'POWER_EVENT_FAILURE_LOG: buffer length: 0\n' + ' buffer type: 0-0:96.7.19\n' + 'VOLTAGE_SAG_L1_COUNT: 0 [None]\n' + 'VOLTAGE_SAG_L2_COUNT: 0 [None]\n' + 'VOLTAGE_SAG_L3_COUNT: 0 [None]\n' + 'VOLTAGE_SWELL_L1_COUNT: 0 [None]\n' + 'VOLTAGE_SWELL_L2_COUNT: 0 [None]\n' + 'VOLTAGE_SWELL_L3_COUNT: 0 [None]\n' + 'INSTANTANEOUS_VOLTAGE_L1: 230.0 [V]\n' + 'INSTANTANEOUS_VOLTAGE_L2: 230.0 [V]\n' + 'INSTANTANEOUS_VOLTAGE_L3: 229.0 [V]\n' + 'INSTANTANEOUS_CURRENT_L1: 0.48 [A]\n' + 'INSTANTANEOUS_CURRENT_L2: 0.44 [A]\n' + 'INSTANTANEOUS_CURRENT_L3: 0.86 [A]\n' + 'TEXT_MESSAGE: None [None]\n' + 'DEVICE_TYPE: 3 [None]\n' + 'MBUS DEVICE (channel 1)\n' + ' DEVICE_TYPE: 3 [None]\n' + ' EQUIPMENT_IDENTIFIER_GAS: 3232323241424344313233343536373839 [None]\n' + ' HOURLY_GAS_METER_READING: 0.107 [m3] at 2017-01-02T15:10:05+00:00\n' + 'MBUS DEVICE (channel 2)\n' + ' DEVICE_TYPE: 3 [None]\n' + ' EQUIPMENT_IDENTIFIER_GAS: None [None]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.070 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 0.032 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.142 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW]\n' + 'EQUIPMENT_IDENTIFIER_GAS: None [None]\n' + 'HOURLY_GAS_METER_READING: 0.107 [m3] at 2017-01-02T15:10:05+00:00\n' + ) + ) + + def test_getitem(self): + parser = TelegramParser(telegram_specifications.V5) + telegram = parser.parse(TELEGRAM_V5) + + self.assertEqual(telegram[obis_references.P1_MESSAGE_HEADER].value, '50') diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index fcc27db..5b47bf2 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -5,7 +5,6 @@ import pytz -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 @@ -18,211 +17,222 @@ class TelegramParserV5Test(unittest.TestCase): def test_parse(self): parser = TelegramParser(telegram_specifications.V5) - result = parser.parse(TELEGRAM_V5) + telegram = parser.parse(TELEGRAM_V5) # P1_MESSAGE_HEADER (1-3:0.2.8) - assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject) - assert result[obis.P1_MESSAGE_HEADER].unit is None - assert isinstance(result[obis.P1_MESSAGE_HEADER].value, str) - assert result[obis.P1_MESSAGE_HEADER].value == '50' + assert isinstance(telegram.P1_MESSAGE_HEADER, CosemObject) + assert telegram.P1_MESSAGE_HEADER.unit is None + assert isinstance(telegram.P1_MESSAGE_HEADER.value, str) + assert telegram.P1_MESSAGE_HEADER.value == '50' # P1_MESSAGE_TIMESTAMP (0-0:1.0.0) - assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP], CosemObject) - assert result[obis.P1_MESSAGE_TIMESTAMP].unit is None - assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP].value, datetime.datetime) - assert result[obis.P1_MESSAGE_TIMESTAMP].value == \ + assert isinstance(telegram.P1_MESSAGE_TIMESTAMP, CosemObject) + assert telegram.P1_MESSAGE_TIMESTAMP.unit is None + assert isinstance(telegram.P1_MESSAGE_TIMESTAMP.value, datetime.datetime) + assert telegram.P1_MESSAGE_TIMESTAMP.value == \ datetime.datetime(2017, 1, 2, 18, 20, 2, tzinfo=pytz.UTC) # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) - assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) - assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh' - assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal) - assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('4.426') + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1, CosemObject) + assert telegram.ELECTRICITY_USED_TARIFF_1.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1.value, Decimal) + assert telegram.ELECTRICITY_USED_TARIFF_1.value == Decimal('4.426') # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) - assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject) - assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh' - assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal) - assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('2.399') + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2, CosemObject) + assert telegram.ELECTRICITY_USED_TARIFF_2.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2.value, Decimal) + assert telegram.ELECTRICITY_USED_TARIFF_2.value == Decimal('2.399') # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) - assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject) - assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh' - assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal) - assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('2.444') + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('2.444') # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) - assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject) - assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh' - assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal) - assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('0') + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('0') # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) - assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject) - assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None - assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str) - assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0002' + assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF, CosemObject) + assert telegram.ELECTRICITY_ACTIVE_TARIFF.unit is None + assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF.value, str) + assert telegram.ELECTRICITY_ACTIVE_TARIFF.value == '0002' # EQUIPMENT_IDENTIFIER (0-0:96.1.1) - assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject) - assert result[obis.EQUIPMENT_IDENTIFIER].unit is None - assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str) - assert result[obis.EQUIPMENT_IDENTIFIER].value == '4B384547303034303436333935353037' + assert isinstance(telegram.EQUIPMENT_IDENTIFIER, CosemObject) + assert telegram.EQUIPMENT_IDENTIFIER.unit is None + assert isinstance(telegram.EQUIPMENT_IDENTIFIER.value, str) + assert telegram.EQUIPMENT_IDENTIFIER.value == '4B384547303034303436333935353037' # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) - assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject) - assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' - assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal) - assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.244') + assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE, CosemObject) + assert telegram.CURRENT_ELECTRICITY_USAGE.unit == 'kW' + assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE.value, Decimal) + assert telegram.CURRENT_ELECTRICITY_USAGE.value == Decimal('0.244') # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) - assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject) - assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW' - assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal) - assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0') + assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY, CosemObject) + assert telegram.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW' + assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY.value, Decimal) + assert telegram.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('0') # LONG_POWER_FAILURE_COUNT (96.7.9) - assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject) - assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None - assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int) - assert result[obis.LONG_POWER_FAILURE_COUNT].value == 0 + assert isinstance(telegram.LONG_POWER_FAILURE_COUNT, CosemObject) + assert telegram.LONG_POWER_FAILURE_COUNT.unit is None + assert isinstance(telegram.LONG_POWER_FAILURE_COUNT.value, int) + assert telegram.LONG_POWER_FAILURE_COUNT.value == 0 # SHORT_POWER_FAILURE_COUNT (1-0:96.7.21) - assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject) - assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None - assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int) - assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 13 + assert isinstance(telegram.SHORT_POWER_FAILURE_COUNT, CosemObject) + assert telegram.SHORT_POWER_FAILURE_COUNT.unit is None + assert isinstance(telegram.SHORT_POWER_FAILURE_COUNT.value, int) + assert telegram.SHORT_POWER_FAILURE_COUNT.value == 13 # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0) - assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject) - assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None - assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT].value, int) - assert result[obis.VOLTAGE_SAG_L1_COUNT].value == 0 + assert isinstance(telegram.VOLTAGE_SAG_L1_COUNT, CosemObject) + assert telegram.VOLTAGE_SAG_L1_COUNT.unit is None + assert isinstance(telegram.VOLTAGE_SAG_L1_COUNT.value, int) + assert telegram.VOLTAGE_SAG_L1_COUNT.value == 0 # VOLTAGE_SAG_L2_COUNT (1-0:52.32.0) - assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT], CosemObject) - assert result[obis.VOLTAGE_SAG_L2_COUNT].unit is None - assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT].value, int) - assert result[obis.VOLTAGE_SAG_L2_COUNT].value == 0 + assert isinstance(telegram.VOLTAGE_SAG_L2_COUNT, CosemObject) + assert telegram.VOLTAGE_SAG_L2_COUNT.unit is None + assert isinstance(telegram.VOLTAGE_SAG_L2_COUNT.value, int) + assert telegram.VOLTAGE_SAG_L2_COUNT.value == 0 # VOLTAGE_SAG_L3_COUNT (1-0:72.32.0) - assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT], CosemObject) - assert result[obis.VOLTAGE_SAG_L3_COUNT].unit is None - assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT].value, int) - assert result[obis.VOLTAGE_SAG_L3_COUNT].value == 0 + assert isinstance(telegram.VOLTAGE_SAG_L3_COUNT, CosemObject) + assert telegram.VOLTAGE_SAG_L3_COUNT.unit is None + assert isinstance(telegram.VOLTAGE_SAG_L3_COUNT.value, int) + assert telegram.VOLTAGE_SAG_L3_COUNT.value == 0 # VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0) - assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT], CosemObject) - assert result[obis.VOLTAGE_SWELL_L1_COUNT].unit is None - assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT].value, int) - assert result[obis.VOLTAGE_SWELL_L1_COUNT].value == 0 + assert isinstance(telegram.VOLTAGE_SWELL_L1_COUNT, CosemObject) + assert telegram.VOLTAGE_SWELL_L1_COUNT.unit is None + assert isinstance(telegram.VOLTAGE_SWELL_L1_COUNT.value, int) + assert telegram.VOLTAGE_SWELL_L1_COUNT.value == 0 # VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0) - assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT], CosemObject) - assert result[obis.VOLTAGE_SWELL_L2_COUNT].unit is None - assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT].value, int) - assert result[obis.VOLTAGE_SWELL_L2_COUNT].value == 0 + assert isinstance(telegram.VOLTAGE_SWELL_L2_COUNT, CosemObject) + assert telegram.VOLTAGE_SWELL_L2_COUNT.unit is None + assert isinstance(telegram.VOLTAGE_SWELL_L2_COUNT.value, int) + assert telegram.VOLTAGE_SWELL_L2_COUNT.value == 0 # VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0) - assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT], CosemObject) - assert result[obis.VOLTAGE_SWELL_L3_COUNT].unit is None - assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int) - assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0 + assert isinstance(telegram.VOLTAGE_SWELL_L3_COUNT, CosemObject) + assert telegram.VOLTAGE_SWELL_L3_COUNT.unit is None + assert isinstance(telegram.VOLTAGE_SWELL_L3_COUNT.value, int) + assert telegram.VOLTAGE_SWELL_L3_COUNT.value == 0 # INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0) - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject) - assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V' - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal) - assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('230.0') + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1, CosemObject) + assert telegram.INSTANTANEOUS_VOLTAGE_L1.unit == 'V' + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1.value, Decimal) + assert telegram.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('230.0') # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0) - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject) - assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V' - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal) - assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('230.0') + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2, CosemObject) + assert telegram.INSTANTANEOUS_VOLTAGE_L2.unit == 'V' + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2.value, Decimal) + assert telegram.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('230.0') # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0) - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject) - assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V' - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal) - assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('229.0') + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3, CosemObject) + assert telegram.INSTANTANEOUS_VOLTAGE_L3.unit == 'V' + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3.value, Decimal) + assert telegram.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('229.0') # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject) - assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A' - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal) - assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.48') + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1, CosemObject) + assert telegram.INSTANTANEOUS_CURRENT_L1.unit == 'A' + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1.value, Decimal) + assert telegram.INSTANTANEOUS_CURRENT_L1.value == Decimal('0.48') # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject) - assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A' - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal) - assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.44') + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2, CosemObject) + assert telegram.INSTANTANEOUS_CURRENT_L2.unit == 'A' + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2.value, Decimal) + assert telegram.INSTANTANEOUS_CURRENT_L2.value == Decimal('0.44') # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject) - assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A' - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal) - assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.86') + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3, CosemObject) + assert telegram.INSTANTANEOUS_CURRENT_L3.unit == 'A' + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3.value, Decimal) + assert telegram.INSTANTANEOUS_CURRENT_L3.value == Decimal('0.86') # TEXT_MESSAGE (0-0:96.13.0) - assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) - assert result[obis.TEXT_MESSAGE].unit is None - assert result[obis.TEXT_MESSAGE].value is None + assert isinstance(telegram.TEXT_MESSAGE, CosemObject) + assert telegram.TEXT_MESSAGE.unit is None + assert telegram.TEXT_MESSAGE.value is None # DEVICE_TYPE (0-x:24.1.0) - assert isinstance(result[obis.DEVICE_TYPE], CosemObject) - assert result[obis.DEVICE_TYPE].unit is None - assert isinstance(result[obis.DEVICE_TYPE].value, int) - assert result[obis.DEVICE_TYPE].value == 3 + assert isinstance(telegram.DEVICE_TYPE, CosemObject) + assert telegram.DEVICE_TYPE.unit is None + assert isinstance(telegram.DEVICE_TYPE.value, int) + assert telegram.DEVICE_TYPE.value == 3 # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.070') + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, CosemObject) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.unit == 'kW' + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value, Decimal) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value == Decimal('0.070') # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.032') + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, CosemObject) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.unit == 'kW' + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value, Decimal) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value == Decimal('0.032') # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.142') + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, CosemObject) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.unit == 'kW' + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value, Decimal) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value == Decimal('0.142') # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0') + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, CosemObject) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.unit == 'kW' + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value, Decimal) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value == Decimal('0') # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0') + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, CosemObject) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.unit == 'kW' + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value, Decimal) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value == Decimal('0') # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0') + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, CosemObject) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.unit == 'kW' + assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value, Decimal) + assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value == Decimal('0') + + # There's only one Mbus device (gas meter) in this case. Alternatively + # use get_mbget_mbus_device_by_channel + gas_meter_devices = telegram.MBUS_DEVICES + gas_meter_device = gas_meter_devices[0] + + # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) + assert isinstance(gas_meter_device.DEVICE_TYPE, CosemObject) + assert gas_meter_device.DEVICE_TYPE.unit is None + assert isinstance(gas_meter_device.DEVICE_TYPE.value, int) + assert gas_meter_device.DEVICE_TYPE.value == 3 # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) - assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject) - assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None - assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str) - assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '3232323241424344313233343536373839' + assert isinstance(gas_meter_device.EQUIPMENT_IDENTIFIER_GAS, CosemObject) + assert gas_meter_device.EQUIPMENT_IDENTIFIER_GAS.unit is None + assert isinstance(gas_meter_device.EQUIPMENT_IDENTIFIER_GAS.value, str) + assert gas_meter_device.EQUIPMENT_IDENTIFIER_GAS.value == '3232323241424344313233343536373839' # HOURLY_GAS_METER_READING (0-1:24.2.1) - assert isinstance(result[obis.HOURLY_GAS_METER_READING], MBusObject) - assert result[obis.HOURLY_GAS_METER_READING].unit == 'm3' - assert isinstance(result[obis.HOURLY_GAS_METER_READING].value, Decimal) - assert result[obis.HOURLY_GAS_METER_READING].value == Decimal('0.107') + assert isinstance(gas_meter_device.HOURLY_GAS_METER_READING, MBusObject) + assert gas_meter_device.HOURLY_GAS_METER_READING.unit == 'm3' + assert isinstance(telegram.HOURLY_GAS_METER_READING.value, Decimal) + assert gas_meter_device.HOURLY_GAS_METER_READING.value == Decimal('0.107') def test_checksum_valid(self): # No exception is raised. @@ -255,10 +265,10 @@ def test_gas_timestamp_invalid(self): ) invalid_date_telegram = invalid_date_telegram.replace('!6EEE\r\n', '!90C2\r\n') parser = TelegramParser(telegram_specifications.V5) - result = parser.parse(invalid_date_telegram) + telegram = parser.parse(invalid_date_telegram) # HOURLY_GAS_METER_READING (0-1:24.2.1) - assert isinstance(result[obis.HOURLY_GAS_METER_READING], MBusObject) - assert result[obis.HOURLY_GAS_METER_READING].unit is None - assert isinstance(result[obis.HOURLY_GAS_METER_READING].value, Decimal) - assert result[obis.HOURLY_GAS_METER_READING].value == Decimal('0.000') + assert isinstance(telegram.HOURLY_GAS_METER_READING, MBusObject) + assert telegram.HOURLY_GAS_METER_READING.unit is None + assert isinstance(telegram.HOURLY_GAS_METER_READING.value, Decimal) + assert telegram.HOURLY_GAS_METER_READING.value == Decimal('0.000') diff --git a/test/test_protocol.py b/test/test_protocol.py index d1393f3..1e7440b 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -4,7 +4,7 @@ from dsmr_parser import obis_references as obis from dsmr_parser.clients.protocol import create_dsmr_protocol - +from dsmr_parser.objects import Telegram TELEGRAM_V2_2 = ( '/ISk5\2MT382-1004\r\n' @@ -44,7 +44,7 @@ def test_complete_packet(self): self.protocol.data_received(TELEGRAM_V2_2.encode('ascii')) telegram = self.protocol.telegram_callback.call_args_list[0][0][0] - assert isinstance(telegram, dict) + assert isinstance(telegram, Telegram) assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01 assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' diff --git a/test/test_rfxtrx_protocol.py b/test/test_rfxtrx_protocol.py index 7c79d22..6770bd5 100644 --- a/test/test_rfxtrx_protocol.py +++ b/test/test_rfxtrx_protocol.py @@ -4,7 +4,7 @@ from dsmr_parser import obis_references as obis from dsmr_parser.clients.rfxtrx_protocol import create_rfxtrx_dsmr_protocol, PACKETTYPE_DSMR, SUBTYPE_P1 - +from dsmr_parser.objects import Telegram TELEGRAM_V2_2 = ( '/ISk5\2MT382-1004\r\n' @@ -68,7 +68,7 @@ def test_complete_packet(self): self.protocol.data_received(data[200:]) telegram = self.protocol.telegram_callback.call_args_list[0][0][0] - assert isinstance(telegram, dict) + assert isinstance(telegram, Telegram) assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01 assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' From c03d2483d73cb018fa197f681897aa83722b0273 Mon Sep 17 00:00:00 2001 From: Emilio Jose Palacios-Garcia Date: Wed, 5 Apr 2023 11:06:21 +0200 Subject: [PATCH 190/226] Bug/duplicate index BELGIUM_MAXIMUM_DEMAND_13_MONTHS (#129) * fix index offset for month timestamp in BELGIUM_MAXIMUM_DEMAND_13_MONTHS * add timestamps to test case --------- Co-authored-by: Emilio Jose Palacios Garcia --- dsmr_parser/parsers.py | 2 +- test/test_parse_fluvius.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index c80de39..52b53f8 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -258,7 +258,7 @@ def parse(self, line): count = int(values[0]) for i in range(1, count + 1): - timestamp_month = ValueParser(timestamp).parse(values[i * 3 + 1]) + timestamp_month = ValueParser(timestamp).parse(values[i * 3 + 0]) timestamp_occurred = ValueParser(timestamp).parse(values[i * 3 + 1]) value = ValueParser(Decimal).parse(values[i * 3 + 2]) objects.append(MBusObjectPeak( diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py index 3213fd9..a43e5b1 100644 --- a/test/test_parse_fluvius.py +++ b/test/test_parse_fluvius.py @@ -37,7 +37,7 @@ def test_parse(self): assert result[obis.P1_MESSAGE_TIMESTAMP].unit is None assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP].value, datetime.datetime) assert result[obis.P1_MESSAGE_TIMESTAMP].value == \ - datetime.datetime(2020, 5, 12, 11, 54, 9, tzinfo=pytz.UTC) + pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 5, 12, 13, 54, 9)) # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) @@ -80,22 +80,43 @@ def test_parse(self): assert result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].unit == 'kW' assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].value, Decimal) assert result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].value == Decimal('2.589') + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].datetime, datetime.datetime) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].datetime == \ + pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 5, 9, 13, 45, 58)) # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 0 assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0], MBusObjectPeak) assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].unit == 'kW' assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].value, Decimal) assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].value == Decimal('3.695') + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].datetime, datetime.datetime) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].datetime == \ + pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 5, 1, 0, 0, 0)) + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].occurred, datetime.datetime) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].occurred == \ + pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 4, 23, 19, 25, 38)) # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 1 assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1], MBusObjectPeak) assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].unit == 'kW' assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].value, Decimal) assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].value == Decimal('5.980') + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].datetime, datetime.datetime) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].datetime == \ + pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 4, 1, 0, 0, 0)) + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].occurred, datetime.datetime) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].occurred == \ + pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 3, 5, 12, 21, 39)) # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 2 assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2], MBusObjectPeak) assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].unit == 'kW' assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].value, Decimal) assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].value == Decimal('4.318') + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].datetime, datetime.datetime) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].datetime == \ + pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 3, 1, 0, 0, 0)) + assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].occurred, datetime.datetime) + assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].occurred == \ + pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 2, 10, 3, 54, 21)) # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject) From 76da334234e11ebffa8a5fa84dc9d6d0524856b3 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 5 Apr 2023 11:14:43 +0200 Subject: [PATCH 191/226] Update release notes for 1.2.0 where not all additions where added --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a57f8c3..04ce6fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,8 @@ Change Log **1.2.0** (2023-02-18) - Improved gas meter (mbus devices) support and replaced Telegram dictionary with backwards compatible object (`PR #121 `_ by `ndokter `_) +- Fix parsing with invalid timestamps (`PR #125 `_ by `dupondje `_) +- Add Iskra IE.x meters specification (`PR #126 `_ by `jchevalier7 `_) **1.1.0** (2023-02-08) From 9847bdf66b71819df57d04e0439813e595b2efa2 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 5 Apr 2023 11:19:42 +0200 Subject: [PATCH 192/226] Prepare releasse 1.2.1 --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 04ce6fd..29ec45a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**1.2.1** (2023-04-05) + +- Bug/duplicate index BELGIUM_MAXIMUM_DEMAND_13_MONTHS (`PR #129 `_ by `ejpalacios `_) + **1.2.0** (2023-02-18) - Improved gas meter (mbus devices) support and replaced Telegram dictionary with backwards compatible object (`PR #121 `_ by `ndokter `_) diff --git a/setup.py b/setup.py index 164667c..b7f5f88 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.2.0', + version='1.2.1', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 5a59c36646a57123ae5b47b582fbb4a88c63479e Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 12 Apr 2023 10:52:11 +0200 Subject: [PATCH 193/226] improve performance parsing speed by about 6% by caching compiled regexes (#131) * improve performance parsing speed by about 6% by caching compiled regexes --- dsmr_parser/clients/telegram_buffer.py | 30 +++++++++----------------- dsmr_parser/parsers.py | 9 ++++++-- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/dsmr_parser/clients/telegram_buffer.py b/dsmr_parser/clients/telegram_buffer.py index 5933296..29278b1 100644 --- a/dsmr_parser/clients/telegram_buffer.py +++ b/dsmr_parser/clients/telegram_buffer.py @@ -1,5 +1,13 @@ import re +# - 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. +_FIND_TELEGRAMS_REGEX = re.compile(r"\/[^\/]+?\![A-F0-9]{0,4}\0?\r\n", re.DOTALL) + class TelegramBuffer(object): """ @@ -8,14 +16,14 @@ class TelegramBuffer(object): """ def __init__(self): - self._buffer = '' + self._buffer = "" def get_all(self): """ Remove complete telegrams from buffer and yield them. :rtype generator: """ - for telegram in self._find_telegrams(): + for telegram in _FIND_TELEGRAMS_REGEX.findall(self._buffer): self._remove(telegram) yield telegram @@ -37,21 +45,3 @@ def _remove(self, telegram): index = self._buffer.index(telegram) + len(telegram) self._buffer = self._buffer[index:] - - def _find_telegrams(self): - """ - Find complete telegrams in 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}\0?\r\n', - self._buffer, - re.DOTALL - ) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 52b53f8..7eb6d9c 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -25,8 +25,13 @@ def __init__(self, telegram_specification, apply_checksum_validation=True): telegram DSMR version (v4 and up). :type telegram_specification: dict """ - self.telegram_specification = telegram_specification self.apply_checksum_validation = apply_checksum_validation + self.telegram_specification = telegram_specification + # Regexes are compiled once to improve performance + self.telegram_specification_regexes = { + signature: re.compile(signature, re.DOTALL) + for signature in self.telegram_specification['objects'].keys() + } def parse(self, telegram_data, encryption_key="", authentication_key=""): # noqa: C901 """ @@ -80,7 +85,7 @@ def parse(self, telegram_data, encryption_key="", authentication_key=""): # noq telegram = Telegram() for signature, parser in self.telegram_specification['objects'].items(): - pattern = re.compile(signature, re.DOTALL) + pattern = self.telegram_specification_regexes[signature] matches = pattern.findall(telegram_data) # Some signatures are optional and may not be present, From d3fab4f105740a52f84cd55d243cd17b2f6f094f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 12 Apr 2023 10:57:38 +0200 Subject: [PATCH 194/226] Release 1.2.2 --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 29ec45a..4bf0f8a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**1.2.2** (2023-04-12) + +- Improve performance. Thanks to `ejpalacios `_ (`PR #130 `_ by `ndokter `_) + **1.2.1** (2023-04-05) - Bug/duplicate index BELGIUM_MAXIMUM_DEMAND_13_MONTHS (`PR #129 `_ by `ejpalacios `_) diff --git a/setup.py b/setup.py index b7f5f88..68defd3 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.2.1', + version='1.2.2', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 84973872af5fadcf3d0862cb617310f6ce4eca64 Mon Sep 17 00:00:00 2001 From: dupondje Date: Fri, 14 Apr 2023 16:51:01 +0200 Subject: [PATCH 195/226] Fix parsing tests and line start matching (#132) * Raise parsing exception and check it in tests * use multiline regex and match everything at the line start --- dsmr_parser/obis_references.py | 174 ++++++++++++++++----------------- dsmr_parser/parsers.py | 11 ++- test/test_parse_fluvius.py | 5 +- test/test_parse_iskra_ie.py | 5 +- test/test_parse_v2_2.py | 5 +- test/test_parse_v3.py | 5 +- test/test_parse_v4_2.py | 5 +- test/test_parse_v5.py | 5 +- 8 files changed, 119 insertions(+), 96 deletions(-) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 8aa7461..c710504 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -6,61 +6,61 @@ Might be refactored in a backwards incompatible way as soon as proper telegram 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_USED_TARIFF_1 = r'\d-\d:1\.8\.1.+?\r\n' -ELECTRICITY_USED_TARIFF_2 = r'\d-\d:1\.8\.2.+?\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' -CURRENT_REACTIVE_IMPORTED = r'\d-\d:3\.7\.0.+?\r\n' -ELECTRICITY_REACTIVE_IMPORTED_TOTAL = r'\d-\d:3\.8\.0.+?\r\n' -ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1 = r'\d-\d:3\.8\.1.+?\r\n' -ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2 = r'\d-\d:3\.8\.2.+?\r\n' -CURRENT_REACTIVE_EXPORTED = r'\d-\d:4\.7\.0.+?\r\n' -ELECTRICITY_REACTIVE_EXPORTED_TOTAL = r'\d-\d:4\.8\.0.+?\r\n' -ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1 = r'\d-\d:4\.8\.1.+?\r\n' -ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2 = r'\d-\d:4\.8\.2.+?\r\n' -ELECTRICITY_ACTIVE_TARIFF = r'\d-\d:96\.14\.0.+?\r\n' -EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n' -CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' -CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n' -LONG_POWER_FAILURE_COUNT = r'\d-\d:96\.7\.9.+?\r\n' -SHORT_POWER_FAILURE_COUNT = r'\d-\d:96\.7\.21.+?\r\n' -POWER_EVENT_FAILURE_LOG = r'\d-\d:99\.97\.0.+?\r\n' -VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n' -VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n' -VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n' -VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n' -VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n' -VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n' -INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n' -INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n' -INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n' -INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n' -INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n' -INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n' -TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n' -TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n' -DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'\d-\d:22\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'\d-\d:42\.7\.0.+?\r\n' -INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'\d-\d:62\.7\.0.+?\r\n' -INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE = r'\d-\d:23\.7\.0.+?\r\n' -INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE = r'\d-\d:24\.7\.0.+?\r\n' -INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE = r'\d-\d:43\.7\.0.+?\r\n' -INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE = r'\d-\d:44\.7\.0.+?\r\n' -INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE = r'\d-\d:63\.7\.0.+?\r\n' -INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE = r'\d-\d:64\.7\.0.+?\r\n' -EQUIPMENT_IDENTIFIER_GAS = r'\d-\d:96\.1\.0.+?\r\n' +P1_MESSAGE_HEADER = r'^\d-\d:0\.2\.8.+?\r\n' +P1_MESSAGE_TIMESTAMP = r'^\d-\d:1\.0\.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_DELIVERED_TARIFF_1 = r'^\d-\d:2\.8\.1.+?\r\n' +ELECTRICITY_DELIVERED_TARIFF_2 = r'^\d-\d:2\.8\.2.+?\r\n' +CURRENT_REACTIVE_IMPORTED = r'^\d-\d:3\.7\.0.+?\r\n' +ELECTRICITY_REACTIVE_IMPORTED_TOTAL = r'^\d-\d:3\.8\.0.+?\r\n' +ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1 = r'^\d-\d:3\.8\.1.+?\r\n' +ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2 = r'^\d-\d:3\.8\.2.+?\r\n' +CURRENT_REACTIVE_EXPORTED = r'^\d-\d:4\.7\.0.+?\r\n' +ELECTRICITY_REACTIVE_EXPORTED_TOTAL = r'^\d-\d:4\.8\.0.+?\r\n' +ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1 = r'^\d-\d:4\.8\.1.+?\r\n' +ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2 = r'^\d-\d:4\.8\.2.+?\r\n' +ELECTRICITY_ACTIVE_TARIFF = r'^\d-\d:96\.14\.0.+?\r\n' +EQUIPMENT_IDENTIFIER = r'^\d-\d:96\.1\.1.+?\r\n' +CURRENT_ELECTRICITY_USAGE = r'^\d-\d:1\.7\.0.+?\r\n' +CURRENT_ELECTRICITY_DELIVERY = r'^\d-\d:2\.7\.0.+?\r\n' +LONG_POWER_FAILURE_COUNT = r'^\d-\d:96\.7\.9.+?\r\n' +SHORT_POWER_FAILURE_COUNT = r'^\d-\d:96\.7\.21.+?\r\n' +POWER_EVENT_FAILURE_LOG = r'^\d-\d:99\.97\.0.+?\r\n' +VOLTAGE_SAG_L1_COUNT = r'^\d-\d:32\.32\.0.+?\r\n' +VOLTAGE_SAG_L2_COUNT = r'^\d-\d:52\.32\.0.+?\r\n' +VOLTAGE_SAG_L3_COUNT = r'^\d-\d:72\.32\.0.+?\r\n' +VOLTAGE_SWELL_L1_COUNT = r'^\d-\d:32\.36\.0.+?\r\n' +VOLTAGE_SWELL_L2_COUNT = r'^\d-\d:52\.36\.0.+?\r\n' +VOLTAGE_SWELL_L3_COUNT = r'^\d-\d:72\.36\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L1 = r'^\d-\d:32\.7\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L2 = r'^\d-\d:52\.7\.0.+?\r\n' +INSTANTANEOUS_VOLTAGE_L3 = r'^\d-\d:72\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L1 = r'^\d-\d:31\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L2 = r'^\d-\d:51\.7\.0.+?\r\n' +INSTANTANEOUS_CURRENT_L3 = r'^\d-\d:71\.7\.0.+?\r\n' +TEXT_MESSAGE_CODE = r'^\d-\d:96\.13\.1.+?\r\n' +TEXT_MESSAGE = r'^\d-\d:96\.13\.0.+?\r\n' +DEVICE_TYPE = r'^\d-\d:24\.1\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'^\d-\d:21\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'^\d-\d:41\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'^\d-\d:61\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'^\d-\d:22\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'^\d-\d:42\.7\.0.+?\r\n' +INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'^\d-\d:62\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE = r'^\d-\d:23\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE = r'^\d-\d:24\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE = r'^\d-\d:43\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE = r'^\d-\d:44\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE = r'^\d-\d:63\.7\.0.+?\r\n' +INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE = r'^\d-\d:64\.7\.0.+?\r\n' +EQUIPMENT_IDENTIFIER_GAS = r'^\d-\d:96\.1\.0.+?\r\n' # TODO differences between gas meter readings in v3 and lower and v4 and up -HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.1.+?\r\n' -GAS_METER_READING = r'\d-\d:24\.3\.0.+?\r\n.+?\r\n' -ACTUAL_TRESHOLD_ELECTRICITY = r'\d-\d:17\.0\.0.+?\r\n' -ACTUAL_SWITCH_POSITION = r'\d-\d:96\.3\.10.+?\r\n' -VALVE_POSITION_GAS = r'\d-\d:24\.4\.0.+?\r\n' +HOURLY_GAS_METER_READING = r'^\d-\d:24\.2\.1.+?\r\n' +GAS_METER_READING = r'^\d-\d:24\.3\.0.+?\r\n.+?\r\n' +ACTUAL_TRESHOLD_ELECTRICITY = r'^\d-\d:17\.0\.0.+?\r\n' +ACTUAL_SWITCH_POSITION = r'^\d-\d:96\.3\.10.+?\r\n' +VALVE_POSITION_GAS = r'^\d-\d:24\.4\.0.+?\r\n' # TODO 17.0.0 # TODO 96.3.10 @@ -75,50 +75,50 @@ ) # 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-) +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_VERSION_INFORMATION = r'\d-\d:96\.1\.4.+?\r\n' -BELGIUM_EQUIPMENT_IDENTIFIER = r'\d-0:96\.1\.1.+?\r\n' -BELGIUM_CURRENT_AVERAGE_DEMAND = r'\d-\d:1\.4\.0.+?\r\n' -BELGIUM_MAXIMUM_DEMAND_MONTH = r'\d-\d:1\.6\.0.+?\r\n' -BELGIUM_MAXIMUM_DEMAND_13_MONTHS = r'\d-\d:98\.1\.0.+?\r\n' -BELGIUM_MAX_POWER_PER_PHASE = r'\d-\d:17\.0\.0.+?\r\n' # Applicable when power limitation is active -BELGIUM_MAX_CURRENT_PER_PHASE = r'\d-\d:31\.4\.0.+?\r\n' # Applicable when current limitation is active +BELGIUM_VERSION_INFORMATION = r'^\d-\d:96\.1\.4.+?\r\n' +BELGIUM_EQUIPMENT_IDENTIFIER = r'^\d-0:96\.1\.1.+?\r\n' +BELGIUM_CURRENT_AVERAGE_DEMAND = r'^\d-\d:1\.4\.0.+?\r\n' +BELGIUM_MAXIMUM_DEMAND_MONTH = r'^\d-\d:1\.6\.0.+?\r\n' +BELGIUM_MAXIMUM_DEMAND_13_MONTHS = r'^\d-\d:98\.1\.0.+?\r\n' +BELGIUM_MAX_POWER_PER_PHASE = r'^\d-\d:17\.0\.0.+?\r\n' # Applicable when power limitation is active +BELGIUM_MAX_CURRENT_PER_PHASE = r'^\d-\d:31\.4\.0.+?\r\n' # Applicable when current limitation is active # Multiple 'slaves' can be linked to the main device. # Mostly MBUS1 = GAS METER with values on 24.2.3 # While WATER METER reports it's values on 24.2.1 # The GAS METER also reports its valve state on 24.4.0 # Dev type for gas = 7 and water = 8 -BELGIUM_MBUS1_DEVICE_TYPE = r'\d-1:24\.1\.0.+?\r\n' -BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER = r'\d-1:96\.1\.1.+?\r\n' -BELGIUM_MBUS1_VALVE_POSITION = r'\d-1:24\.4\.0.+?\r\n' -BELGIUM_MBUS1_METER_READING1 = r'\d-1:24\.2\.1.+?\r\n' -BELGIUM_MBUS1_METER_READING2 = r'\d-1:24\.2\.3.+?\r\n' +BELGIUM_MBUS1_DEVICE_TYPE = r'^\d-1:24\.1\.0.+?\r\n' +BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER = r'^\d-1:96\.1\.1.+?\r\n' +BELGIUM_MBUS1_VALVE_POSITION = r'^\d-1:24\.4\.0.+?\r\n' +BELGIUM_MBUS1_METER_READING1 = r'^\d-1:24\.2\.1.+?\r\n' +BELGIUM_MBUS1_METER_READING2 = r'^\d-1:24\.2\.3.+?\r\n' -BELGIUM_MBUS2_DEVICE_TYPE = r'\d-2:24\.1\.0.+?\r\n' -BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER = r'\d-2:96\.1\.1.+?\r\n' -BELGIUM_MBUS2_VALVE_POSITION = r'\d-2:24\.4\.0.+?\r\n' -BELGIUM_MBUS2_METER_READING1 = r'\d-2:24\.2\.1.+?\r\n' -BELGIUM_MBUS2_METER_READING2 = r'\d-2:24\.2\.3.+?\r\n' +BELGIUM_MBUS2_DEVICE_TYPE = r'^\d-2:24\.1\.0.+?\r\n' +BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER = r'^\d-2:96\.1\.1.+?\r\n' +BELGIUM_MBUS2_VALVE_POSITION = r'^\d-2:24\.4\.0.+?\r\n' +BELGIUM_MBUS2_METER_READING1 = r'^\d-2:24\.2\.1.+?\r\n' +BELGIUM_MBUS2_METER_READING2 = r'^\d-2:24\.2\.3.+?\r\n' -BELGIUM_MBUS3_DEVICE_TYPE = r'\d-3:24\.1\.0.+?\r\n' -BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER = r'\d-3:96\.1\.1.+?\r\n' -BELGIUM_MBUS3_VALVE_POSITION = r'\d-3:24\.4\.0.+?\r\n' -BELGIUM_MBUS3_METER_READING1 = r'\d-3:24\.2\.1.+?\r\n' -BELGIUM_MBUS3_METER_READING2 = r'\d-3:24\.2\.3.+?\r\n' +BELGIUM_MBUS3_DEVICE_TYPE = r'^\d-3:24\.1\.0.+?\r\n' +BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER = r'^\d-3:96\.1\.1.+?\r\n' +BELGIUM_MBUS3_VALVE_POSITION = r'^\d-3:24\.4\.0.+?\r\n' +BELGIUM_MBUS3_METER_READING1 = r'^\d-3:24\.2\.1.+?\r\n' +BELGIUM_MBUS3_METER_READING2 = r'^\d-3:24\.2\.3.+?\r\n' -BELGIUM_MBUS4_DEVICE_TYPE = r'\d-4:24\.1\.0.+?\r\n' -BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER = r'\d-4:96\.1\.1.+?\r\n' -BELGIUM_MBUS4_VALVE_POSITION = r'\d-4:24\.4\.0.+?\r\n' -BELGIUM_MBUS4_METER_READING1 = r'\d-4:24\.2\.1.+?\r\n' -BELGIUM_MBUS4_METER_READING2 = r'\d-4:24\.2\.3.+?\r\n' +BELGIUM_MBUS4_DEVICE_TYPE = r'^\d-4:24\.1\.0.+?\r\n' +BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER = r'^\d-4:96\.1\.1.+?\r\n' +BELGIUM_MBUS4_VALVE_POSITION = r'^\d-4:24\.4\.0.+?\r\n' +BELGIUM_MBUS4_METER_READING1 = r'^\d-4:24\.2\.1.+?\r\n' +BELGIUM_MBUS4_METER_READING2 = r'^\d-4:24\.2\.3.+?\r\n' -LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'\d-\d:42\.0\.0.+?\r\n' # Logical device name +LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'^\d-\d:42\.0\.0.+?\r\n' # Logical device name -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 +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/parsers.py b/dsmr_parser/parsers.py index 7eb6d9c..3b48cfa 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -29,11 +29,11 @@ def __init__(self, telegram_specification, apply_checksum_validation=True): self.telegram_specification = telegram_specification # Regexes are compiled once to improve performance self.telegram_specification_regexes = { - signature: re.compile(signature, re.DOTALL) + signature: re.compile(signature, re.DOTALL | re.MULTILINE) for signature in self.telegram_specification['objects'].keys() } - def parse(self, telegram_data, encryption_key="", authentication_key=""): # noqa: C901 + def parse(self, telegram_data, encryption_key="", authentication_key="", throw_ex=False): # noqa: C901 """ Parse telegram from string to dict. The telegram str type makes python 2.x integration easier. @@ -93,9 +93,14 @@ def parse(self, telegram_data, encryption_key="", authentication_key=""): # noq for match in matches: try: dsmr_object = parser.parse(match) - except Exception: + except ParseError: logger.error("ignore line with signature {}, because parsing failed.".format(signature), exc_info=True) + if throw_ex: + raise + except Exception as err: + logger.error("Unexpected {}: {}".format(type(err), err)) + raise else: telegram.add(obis_reference=signature, dsmr_object=dsmr_object) diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py index a43e5b1..968b9ca 100644 --- a/test/test_parse_fluvius.py +++ b/test/test_parse_fluvius.py @@ -18,7 +18,10 @@ class TelegramParserFluviusTest(unittest.TestCase): def test_parse(self): parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS) - result = parser.parse(TELEGRAM_FLUVIUS_V171) + try: + result = parser.parse(TELEGRAM_FLUVIUS_V171, throw_ex=True) + except Exception as ex: + assert False, f"parse trigged an exception {ex}" # BELGIUM_VERSION_INFORMATION (0-0:96.1.4) assert isinstance(result[obis.BELGIUM_VERSION_INFORMATION], CosemObject) diff --git a/test/test_parse_iskra_ie.py b/test/test_parse_iskra_ie.py index 642fc0d..adba995 100644 --- a/test/test_parse_iskra_ie.py +++ b/test/test_parse_iskra_ie.py @@ -15,7 +15,10 @@ class TelegramParserIskraIETest(unittest.TestCase): def test_parse(self): parser = TelegramParser(telegram_specifications.ISKRA_IE) - result = parser.parse(TELEGRAM_ISKRA_IE) + try: + result = parser.parse(TELEGRAM_ISKRA_IE, throw_ex=True) + except Exception as ex: + assert False, f"parse trigged an exception {ex}" # EQUIPMENT_IDENTIFIER_GAS (0-0:96.1.0) assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject) diff --git a/test/test_parse_v2_2.py b/test/test_parse_v2_2.py index e7203ab..358f84e 100644 --- a/test/test_parse_v2_2.py +++ b/test/test_parse_v2_2.py @@ -14,7 +14,10 @@ class TelegramParserV2_2Test(unittest.TestCase): def test_parse(self): parser = TelegramParser(telegram_specifications.V2_2) - result = parser.parse(TELEGRAM_V2_2) + try: + result = parser.parse(TELEGRAM_V2_2, throw_ex=True) + except Exception as ex: + assert False, f"parse trigged an exception {ex}" # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) diff --git a/test/test_parse_v3.py b/test/test_parse_v3.py index c50a86e..ef41d04 100644 --- a/test/test_parse_v3.py +++ b/test/test_parse_v3.py @@ -14,7 +14,10 @@ class TelegramParserV3Test(unittest.TestCase): def test_parse(self): parser = TelegramParser(telegram_specifications.V3) - result = parser.parse(TELEGRAM_V3) + try: + result = parser.parse(TELEGRAM_V3, throw_ex=True) + except Exception as ex: + assert False, f"parse trigged an exception {ex}" # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index cab34f7..35d8064 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -17,7 +17,10 @@ class TelegramParserV4_2Test(unittest.TestCase): def test_parse(self): parser = TelegramParser(telegram_specifications.V4) - result = parser.parse(TELEGRAM_V4_2) + try: + result = parser.parse(TELEGRAM_V4_2, throw_ex=True) + except Exception as ex: + assert False, f"parse trigged an exception {ex}" # P1_MESSAGE_HEADER (1-3:0.2.8) assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index 5b47bf2..f977e00 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -17,7 +17,10 @@ class TelegramParserV5Test(unittest.TestCase): def test_parse(self): parser = TelegramParser(telegram_specifications.V5) - telegram = parser.parse(TELEGRAM_V5) + try: + telegram = parser.parse(TELEGRAM_V5, throw_ex=True) + except Exception as ex: + assert False, f"parse trigged an exception {ex}" # P1_MESSAGE_HEADER (1-3:0.2.8) assert isinstance(telegram.P1_MESSAGE_HEADER, CosemObject) From 99c7b0d9f07728903c4ab432e604768d51ef3ab7 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 18 Apr 2023 20:48:30 +0200 Subject: [PATCH 196/226] Prepare release 1.2.3 --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4bf0f8a..5cc73a4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**1.2.3** (2023-04-18) + +- Fix parsing tests and line start matching (`PR #132 `_ by `dupondje `_) + **1.2.2** (2023-04-12) - Improve performance. Thanks to `ejpalacios `_ (`PR #130 `_ by `ndokter `_) diff --git a/setup.py b/setup.py index 68defd3..44ad9ee 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.2.2', + version='1.2.3', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From f3444a515dcd23636cc18e7f173afdab698c3fdd Mon Sep 17 00:00:00 2001 From: Emilio Jose Palacios-Garcia Date: Tue, 11 Jul 2023 21:43:25 +0200 Subject: [PATCH 197/226] EQUIPMENT IDENTIFIER is wrong for Fluvius meters when other mbus devices are present (#133) * change obis of fluvius meter ID to BELGIUM_EQUIPMENT_IDENTIFIER --- dsmr_parser/telegram_specifications.py | 2 +- test/test_parse_fluvius.py | 331 ++++++++++++------------- 2 files changed, 166 insertions(+), 167 deletions(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 83eb29d..f9f8a79 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -145,7 +145,7 @@ 'checksum_support': True, 'objects': { obis.BELGIUM_VERSION_INFORMATION: CosemParser(ValueParser(str)), - obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.BELGIUM_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py index 968b9ca..6969087 100644 --- a/test/test_parse_fluvius.py +++ b/test/test_parse_fluvius.py @@ -5,7 +5,6 @@ import pytz -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, MBusObjectPeak @@ -24,251 +23,251 @@ def test_parse(self): assert False, f"parse trigged an exception {ex}" # BELGIUM_VERSION_INFORMATION (0-0:96.1.4) - assert isinstance(result[obis.BELGIUM_VERSION_INFORMATION], CosemObject) - assert result[obis.BELGIUM_VERSION_INFORMATION].unit is None - assert isinstance(result[obis.BELGIUM_VERSION_INFORMATION].value, str) - assert result[obis.BELGIUM_VERSION_INFORMATION].value == '50217' + assert isinstance(result.BELGIUM_VERSION_INFORMATION, CosemObject) + assert result.BELGIUM_VERSION_INFORMATION.unit is None + assert isinstance(result.BELGIUM_VERSION_INFORMATION.value, str) + assert result.BELGIUM_VERSION_INFORMATION.value == '50217' # EQUIPMENT_IDENTIFIER (0-0:96.1.1) - assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject) - assert result[obis.EQUIPMENT_IDENTIFIER].unit is None - assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str) - assert result[obis.EQUIPMENT_IDENTIFIER].value == '3153414733313031303231363035' + assert isinstance(result.BELGIUM_EQUIPMENT_IDENTIFIER, CosemObject) + assert result.BELGIUM_EQUIPMENT_IDENTIFIER.unit is None + assert isinstance(result.BELGIUM_EQUIPMENT_IDENTIFIER.value, str) + assert result.BELGIUM_EQUIPMENT_IDENTIFIER.value == '3153414733313031303231363035' # P1_MESSAGE_TIMESTAMP (0-0:1.0.0) - assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP], CosemObject) - assert result[obis.P1_MESSAGE_TIMESTAMP].unit is None - assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP].value, datetime.datetime) - assert result[obis.P1_MESSAGE_TIMESTAMP].value == \ + assert isinstance(result.P1_MESSAGE_TIMESTAMP, CosemObject) + assert result.P1_MESSAGE_TIMESTAMP.unit is None + assert isinstance(result.P1_MESSAGE_TIMESTAMP.value, datetime.datetime) + assert result.P1_MESSAGE_TIMESTAMP.value == \ pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 5, 12, 13, 54, 9)) # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) - assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) - assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh' - assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal) - assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('0.034') + assert isinstance(result.ELECTRICITY_USED_TARIFF_1, CosemObject) + assert result.ELECTRICITY_USED_TARIFF_1.unit == 'kWh' + assert isinstance(result.ELECTRICITY_USED_TARIFF_1.value, Decimal) + assert result.ELECTRICITY_USED_TARIFF_1.value == Decimal('0.034') # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) - assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject) - assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh' - assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal) - assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('15.758') + assert isinstance(result.ELECTRICITY_USED_TARIFF_2, CosemObject) + assert result.ELECTRICITY_USED_TARIFF_2.unit == 'kWh' + assert isinstance(result.ELECTRICITY_USED_TARIFF_2.value, Decimal) + assert result.ELECTRICITY_USED_TARIFF_2.value == Decimal('15.758') # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) - assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject) - assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh' - assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal) - assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('0.000') + assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject) + assert result.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh' + assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal) + assert result.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('0.000') # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) - assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject) - assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh' - assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal) - assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('0.011') + assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject) + assert result.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh' + assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal) + assert result.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('0.011') # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) - assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject) - assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None - assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str) - assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0001' + assert isinstance(result.ELECTRICITY_ACTIVE_TARIFF, CosemObject) + assert result.ELECTRICITY_ACTIVE_TARIFF.unit is None + assert isinstance(result.ELECTRICITY_ACTIVE_TARIFF.value, str) + assert result.ELECTRICITY_ACTIVE_TARIFF.value == '0001' # BELGIUM_CURRENT_AVERAGE_DEMAND (1-0:1.4.0) - assert isinstance(result[obis.BELGIUM_CURRENT_AVERAGE_DEMAND], CosemObject) - assert result[obis.BELGIUM_CURRENT_AVERAGE_DEMAND].unit == 'kW' - assert isinstance(result[obis.BELGIUM_CURRENT_AVERAGE_DEMAND].value, Decimal) - assert result[obis.BELGIUM_CURRENT_AVERAGE_DEMAND].value == Decimal('2.351') + assert isinstance(result.BELGIUM_CURRENT_AVERAGE_DEMAND, CosemObject) + assert result.BELGIUM_CURRENT_AVERAGE_DEMAND.unit == 'kW' + assert isinstance(result.BELGIUM_CURRENT_AVERAGE_DEMAND.value, Decimal) + assert result.BELGIUM_CURRENT_AVERAGE_DEMAND.value == Decimal('2.351') # BELGIUM_MAXIMUM_DEMAND_MONTH (1-0:1.6.0) - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH], MBusObject) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].unit == 'kW' - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].value, Decimal) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].value == Decimal('2.589') - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].datetime, datetime.datetime) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_MONTH].datetime == \ + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_MONTH, MBusObject) + assert result.BELGIUM_MAXIMUM_DEMAND_MONTH.unit == 'kW' + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_MONTH.value, Decimal) + assert result.BELGIUM_MAXIMUM_DEMAND_MONTH.value == Decimal('2.589') + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_MONTH.datetime, datetime.datetime) + assert result.BELGIUM_MAXIMUM_DEMAND_MONTH.datetime == \ pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 5, 9, 13, 45, 58)) # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 0 - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0], MBusObjectPeak) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].unit == 'kW' - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].value, Decimal) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].value == Decimal('3.695') - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].datetime, datetime.datetime) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].datetime == \ + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0], MBusObjectPeak) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].unit == 'kW' + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].value, Decimal) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].value == Decimal('3.695') + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].datetime, datetime.datetime) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].datetime == \ pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 5, 1, 0, 0, 0)) - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].occurred, datetime.datetime) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][0].occurred == \ + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].occurred, datetime.datetime) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].occurred == \ pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 4, 23, 19, 25, 38)) # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 1 - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1], MBusObjectPeak) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].unit == 'kW' - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].value, Decimal) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].value == Decimal('5.980') - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].datetime, datetime.datetime) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].datetime == \ + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1], MBusObjectPeak) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].unit == 'kW' + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].value, Decimal) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].value == Decimal('5.980') + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].datetime, datetime.datetime) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].datetime == \ pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 4, 1, 0, 0, 0)) - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].occurred, datetime.datetime) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][1].occurred == \ + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].occurred, datetime.datetime) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].occurred == \ pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 3, 5, 12, 21, 39)) # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 2 - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2], MBusObjectPeak) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].unit == 'kW' - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].value, Decimal) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].value == Decimal('4.318') - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].datetime, datetime.datetime) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].datetime == \ + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2], MBusObjectPeak) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].unit == 'kW' + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].value, Decimal) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].value == Decimal('4.318') + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].datetime, datetime.datetime) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].datetime == \ pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 3, 1, 0, 0, 0)) - assert isinstance(result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].occurred, datetime.datetime) - assert result[obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS][2].occurred == \ + assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].occurred, datetime.datetime) + assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].occurred == \ pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 2, 10, 3, 54, 21)) # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) - assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject) - assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' - assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal) - assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.000') + assert isinstance(result.CURRENT_ELECTRICITY_USAGE, CosemObject) + assert result.CURRENT_ELECTRICITY_USAGE.unit == 'kW' + assert isinstance(result.CURRENT_ELECTRICITY_USAGE.value, Decimal) + assert result.CURRENT_ELECTRICITY_USAGE.value == Decimal('0.000') # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) - assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject) - assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW' - assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal) - assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0.000') + assert isinstance(result.CURRENT_ELECTRICITY_DELIVERY, CosemObject) + assert result.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW' + assert isinstance(result.CURRENT_ELECTRICITY_DELIVERY.value, Decimal) + assert result.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('0.000') # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.000') + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, CosemObject) + assert result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.unit == 'kW' + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value, Decimal) + assert result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value == Decimal('0.000') # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.000') + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, CosemObject) + assert result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.unit == 'kW' + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value, Decimal) + assert result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value == Decimal('0.000') # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.000') + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, CosemObject) + assert result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.unit == 'kW' + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value, Decimal) + assert result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value == Decimal('0.000') # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0.000') + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, CosemObject) + assert result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.unit == 'kW' + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value, Decimal) + assert result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value == Decimal('0.000') # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0.000') + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, CosemObject) + assert result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.unit == 'kW' + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value, Decimal) + assert result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value == Decimal('0.000') # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0) - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW' - assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal) - assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0.000') + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, CosemObject) + assert result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.unit == 'kW' + assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value, Decimal) + assert result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value == Decimal('0.000') # INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0) - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject) - assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V' - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal) - assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('234.7') + assert isinstance(result.INSTANTANEOUS_VOLTAGE_L1, CosemObject) + assert result.INSTANTANEOUS_VOLTAGE_L1.unit == 'V' + assert isinstance(result.INSTANTANEOUS_VOLTAGE_L1.value, Decimal) + assert result.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('234.7') # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0) - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject) - assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V' - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal) - assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('234.7') + assert isinstance(result.INSTANTANEOUS_VOLTAGE_L2, CosemObject) + assert result.INSTANTANEOUS_VOLTAGE_L2.unit == 'V' + assert isinstance(result.INSTANTANEOUS_VOLTAGE_L2.value, Decimal) + assert result.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('234.7') # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0) - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject) - assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V' - assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal) - assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('234.7') + assert isinstance(result.INSTANTANEOUS_VOLTAGE_L3, CosemObject) + assert result.INSTANTANEOUS_VOLTAGE_L3.unit == 'V' + assert isinstance(result.INSTANTANEOUS_VOLTAGE_L3.value, Decimal) + assert result.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('234.7') # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject) - assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A' - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal) - assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.000') + assert isinstance(result.INSTANTANEOUS_CURRENT_L1, CosemObject) + assert result.INSTANTANEOUS_CURRENT_L1.unit == 'A' + assert isinstance(result.INSTANTANEOUS_CURRENT_L1.value, Decimal) + assert result.INSTANTANEOUS_CURRENT_L1.value == Decimal('0.000') # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject) - assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A' - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal) - assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.000') + assert isinstance(result.INSTANTANEOUS_CURRENT_L2, CosemObject) + assert result.INSTANTANEOUS_CURRENT_L2.unit == 'A' + assert isinstance(result.INSTANTANEOUS_CURRENT_L2.value, Decimal) + assert result.INSTANTANEOUS_CURRENT_L2.value == Decimal('0.000') # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject) - assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A' - assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal) - assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.000') + assert isinstance(result.INSTANTANEOUS_CURRENT_L3, CosemObject) + assert result.INSTANTANEOUS_CURRENT_L3.unit == 'A' + assert isinstance(result.INSTANTANEOUS_CURRENT_L3.value, Decimal) + assert result.INSTANTANEOUS_CURRENT_L3.value == Decimal('0.000') # ACTUAL_SWITCH_POSITION (0-0:96.3.10) - assert isinstance(result[obis.ACTUAL_SWITCH_POSITION], CosemObject) - assert result[obis.ACTUAL_SWITCH_POSITION].unit is None - assert isinstance(result[obis.ACTUAL_SWITCH_POSITION].value, int) - assert result[obis.ACTUAL_SWITCH_POSITION].value == 1 + assert isinstance(result.ACTUAL_SWITCH_POSITION, CosemObject) + assert result.ACTUAL_SWITCH_POSITION.unit is None + assert isinstance(result.ACTUAL_SWITCH_POSITION.value, int) + assert result.ACTUAL_SWITCH_POSITION.value == 1 # BELGIUM_MAX_POWER_PER_PHASE (0-0:17.0.0) - assert isinstance(result[obis.BELGIUM_MAX_POWER_PER_PHASE], CosemObject) - assert result[obis.BELGIUM_MAX_POWER_PER_PHASE].unit == 'kW' - assert isinstance(result[obis.BELGIUM_MAX_POWER_PER_PHASE].value, Decimal) - assert result[obis.BELGIUM_MAX_POWER_PER_PHASE].value == Decimal('999.9') + assert isinstance(result.BELGIUM_MAX_POWER_PER_PHASE, CosemObject) + assert result.BELGIUM_MAX_POWER_PER_PHASE.unit == 'kW' + assert isinstance(result.BELGIUM_MAX_POWER_PER_PHASE.value, Decimal) + assert result.BELGIUM_MAX_POWER_PER_PHASE.value == Decimal('999.9') # BELGIUM_MAX_POWER_PER_PHASE (1-0:31.4.0) - assert isinstance(result[obis.BELGIUM_MAX_CURRENT_PER_PHASE], CosemObject) - assert result[obis.BELGIUM_MAX_CURRENT_PER_PHASE].unit == 'A' - assert isinstance(result[obis.BELGIUM_MAX_CURRENT_PER_PHASE].value, Decimal) - assert result[obis.BELGIUM_MAX_CURRENT_PER_PHASE].value == Decimal('999') + assert isinstance(result.BELGIUM_MAX_CURRENT_PER_PHASE, CosemObject) + assert result.BELGIUM_MAX_CURRENT_PER_PHASE.unit == 'A' + assert isinstance(result.BELGIUM_MAX_CURRENT_PER_PHASE.value, Decimal) + assert result.BELGIUM_MAX_CURRENT_PER_PHASE.value == Decimal('999') # TEXT_MESSAGE (0-0:96.13.0) - assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) - assert result[obis.TEXT_MESSAGE].unit is None - assert result[obis.TEXT_MESSAGE].value is None + assert isinstance(result.TEXT_MESSAGE, CosemObject) + assert result.TEXT_MESSAGE.unit is None + assert result.TEXT_MESSAGE.value is None # BELGIUM_MBUS1_DEVICE_TYPE (0-1:24.1.0) - assert isinstance(result[obis.BELGIUM_MBUS1_DEVICE_TYPE], CosemObject) - assert result[obis.BELGIUM_MBUS1_DEVICE_TYPE].unit is None - assert isinstance(result[obis.BELGIUM_MBUS1_DEVICE_TYPE].value, int) - assert result[obis.BELGIUM_MBUS1_DEVICE_TYPE].value == 3 + assert isinstance(result.BELGIUM_MBUS1_DEVICE_TYPE, CosemObject) + assert result.BELGIUM_MBUS1_DEVICE_TYPE.unit is None + assert isinstance(result.BELGIUM_MBUS1_DEVICE_TYPE.value, int) + assert result.BELGIUM_MBUS1_DEVICE_TYPE.value == 3 # BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER (0-1:96.1.1) - assert isinstance(result[obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER], CosemObject) - assert result[obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER].unit is None - assert isinstance(result[obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER].value, str) - assert result[obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER].value == '37464C4F32313139303333373333' + assert isinstance(result.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, CosemObject) + assert result.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER.unit is None + assert isinstance(result.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER.value, str) + assert result.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER.value == '37464C4F32313139303333373333' # BELGIUM_MBUS1_VALVE_POSITION (0-1:24.4.0) - assert isinstance(result[obis.BELGIUM_MBUS1_VALVE_POSITION], CosemObject) - assert result[obis.BELGIUM_MBUS1_VALVE_POSITION].unit is None - assert isinstance(result[obis.BELGIUM_MBUS1_VALVE_POSITION].value, int) - assert result[obis.BELGIUM_MBUS1_VALVE_POSITION].value == 1 + assert isinstance(result.BELGIUM_MBUS1_VALVE_POSITION, CosemObject) + assert result.BELGIUM_MBUS1_VALVE_POSITION.unit is None + assert isinstance(result.BELGIUM_MBUS1_VALVE_POSITION.value, int) + assert result.BELGIUM_MBUS1_VALVE_POSITION.value == 1 # BELGIUM_MBUS1_METER_READING2 (0-1:24.2.3) - assert isinstance(result[obis.BELGIUM_MBUS1_METER_READING2], MBusObject) - assert result[obis.BELGIUM_MBUS1_METER_READING2].unit == 'm3' - assert isinstance(result[obis.BELGIUM_MBUS1_METER_READING2].value, Decimal) - assert result[obis.BELGIUM_MBUS1_METER_READING2].value == Decimal('112.384') + assert isinstance(result.BELGIUM_MBUS1_METER_READING2, MBusObject) + assert result.BELGIUM_MBUS1_METER_READING2.unit == 'm3' + assert isinstance(result.BELGIUM_MBUS1_METER_READING2.value, Decimal) + assert result.BELGIUM_MBUS1_METER_READING2.value == Decimal('112.384') # BELGIUM_MBUS2_DEVICE_TYPE (0-2:24.1.0) - assert isinstance(result[obis.BELGIUM_MBUS2_DEVICE_TYPE], CosemObject) - assert result[obis.BELGIUM_MBUS2_DEVICE_TYPE].unit is None - assert isinstance(result[obis.BELGIUM_MBUS2_DEVICE_TYPE].value, int) - assert result[obis.BELGIUM_MBUS2_DEVICE_TYPE].value == 7 + assert isinstance(result.BELGIUM_MBUS2_DEVICE_TYPE, CosemObject) + assert result.BELGIUM_MBUS2_DEVICE_TYPE.unit is None + assert isinstance(result.BELGIUM_MBUS2_DEVICE_TYPE.value, int) + assert result.BELGIUM_MBUS2_DEVICE_TYPE.value == 7 # BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER (0-2:96.1.1) - assert isinstance(result[obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER], CosemObject) - assert result[obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER].unit is None - assert isinstance(result[obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER].value, str) - assert result[obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER].value == '3853414731323334353637383930' + assert isinstance(result.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, CosemObject) + assert result.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER.unit is None + assert isinstance(result.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER.value, str) + assert result.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER.value == '3853414731323334353637383930' # BELGIUM_MBUS2_METER_READING1 (0-1:24.2.1) - assert isinstance(result[obis.BELGIUM_MBUS2_METER_READING1], MBusObject) - assert result[obis.BELGIUM_MBUS2_METER_READING1].unit == 'm3' - assert isinstance(result[obis.BELGIUM_MBUS2_METER_READING1].value, Decimal) - assert result[obis.BELGIUM_MBUS2_METER_READING1].value == Decimal('872.234') + assert isinstance(result.BELGIUM_MBUS2_METER_READING1, MBusObject) + assert result.BELGIUM_MBUS2_METER_READING1.unit == 'm3' + assert isinstance(result.BELGIUM_MBUS2_METER_READING1.value, Decimal) + assert result.BELGIUM_MBUS2_METER_READING1.value == Decimal('872.234') def test_checksum_valid(self): # No exception is raised. From 4602e8c99fe611f15dc543af5cf40afb46c225ff Mon Sep 17 00:00:00 2001 From: Emilio Jose Palacios-Garcia Date: Tue, 11 Jul 2023 21:43:41 +0200 Subject: [PATCH 198/226] EQUIPMENT IDENTIFIER is wrong for Fluvius meters when other mbus devices are present (#133) * change obis of fluvius meter ID to BELGIUM_EQUIPMENT_IDENTIFIER * update test case to use direct access to attributes * fix test case for pipeline From 3fd032f18f5199a33062fd3585a63b75a58cb2a5 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 11 Jul 2023 22:01:00 +0200 Subject: [PATCH 199/226] Release 1.2.4 --- CHANGELOG.rst | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5cc73a4..7d97743 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Change Log ---------- +**1.2.4** (2023-07-11) + +- EQUIPMENT IDENTIFIER is wrong for Fluvius meters when other mbus devices are present + (`PR #133 `_ by `ejpalacios `_) + + **1.2.3** (2023-04-18) - Fix parsing tests and line start matching (`PR #132 `_ by `dupondje `_) diff --git a/setup.py b/setup.py index 44ad9ee..86d160b 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.2.3', + version='1.2.4', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 0752deb58aeb287626832a6c1191a3d775980f0d Mon Sep 17 00:00:00 2001 From: balazs92117 Date: Wed, 26 Jul 2023 13:31:59 +0200 Subject: [PATCH 200/226] Added E.ON HUNGARY specification (#134) * Added EON HUNGARY specification --- dsmr_parser/clients/protocol.py | 3 + dsmr_parser/clients/socket_.py | 7 +- dsmr_parser/obis_name_mapping.py | 24 +- dsmr_parser/obis_references.py | 24 ++ dsmr_parser/telegram_specifications.py | 57 +++++ test/example_telegrams.py | 52 +++++ test/test_parse_v5_eon_hungary.py | 308 +++++++++++++++++++++++++ 7 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 test/test_parse_v5_eon_hungary.py diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index a7fb74f..ba38fe5 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -51,6 +51,9 @@ def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, elif dsmr_version == 'ISKRA_IE': specification = telegram_specifications.ISKRA_IE serial_settings = SERIAL_SETTINGS_V5 + elif dsmr_version == '5EONHU': + specification = telegram_specifications.EON_HUNGARY + serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) diff --git a/dsmr_parser/clients/socket_.py b/dsmr_parser/clients/socket_.py index b7490ec..8ee16fa 100644 --- a/dsmr_parser/clients/socket_.py +++ b/dsmr_parser/clients/socket_.py @@ -43,7 +43,12 @@ def read(self): continue for data in lines: - self.telegram_buffer.append(data.decode('ascii')) + try: + self.telegram_buffer.append(data.decode('ascii')) + except UnicodeDecodeError: + # Some garbage came through the channel + # E.g.: Happens at EON_HUNGARY, but only once at the start of the socket. + logger.error('Failed to parse telegram due to unicode decode error') for telegram in self.telegram_buffer.get_all(): try: diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 44ebb82..95f1016 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -92,7 +92,29 @@ obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE', obis.Q3D_EQUIPMENT_SERIALNUMBER: 'Q3D_EQUIPMENT_SERIALNUMBER', - obis.BELGIUM_MBUS2_DEVICE_TYPE: 'BELGIUM_MBUS2_DEVICE_TYPE' + obis.BELGIUM_MBUS2_DEVICE_TYPE: 'BELGIUM_MBUS2_DEVICE_TYPE', + obis.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3: 'EON_HU_ELECTRICITY_DELIVERED_TARIFF_3', + obis.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4: 'EON_HU_ELECTRICITY_DELIVERED_TARIFF_4', + obis.EON_HU_ELECTRICITY_USED_TARIFF_3: 'EON_HU_ELECTRICITY_USED_TARIFF_3', + obis.EON_HU_ELECTRICITY_USED_TARIFF_4: 'EON_HU_ELECTRICITY_USED_TARIFF_4', + obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1: 'EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1', + obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2: 'EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2', + obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3: 'EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3', + obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4: 'EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4', + obis.EON_HU_ELECTRICITY_COMBINED: 'EON_HU_ELECTRICITY_COMBINED', + obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL: 'EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL', + obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1: 'EON_HU_INSTANTANEOUS_POWER_FACTOR_L1', + obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2: 'EON_HU_INSTANTANEOUS_POWER_FACTOR_L2', + obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3: 'EON_HU_INSTANTANEOUS_POWER_FACTOR_L3', + obis.EON_HU_FREQUENCY: 'EON_HU_FREQUENCY', + obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1: 'EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1', + obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2: 'EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2', + obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3: 'EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3', + obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4: 'EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4', + obis.EON_HU_MAX_POWER_ON_L1: 'EON_HU_MAX_POWER_ON_L1', + obis.EON_HU_MAX_POWER_ON_L2: 'EON_HU_MAX_POWER_ON_L2', + obis.EON_HU_MAX_POWER_ON_L3: 'EON_HU_MAX_POWER_ON_L3', + obis.EON_HU_LAST_MONTH_DATA: 'EON_HU_LAST_MONTH_DATA' } REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index c710504..920e39e 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -122,3 +122,27 @@ 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 + +# EON Hungary +EON_HU_ELECTRICITY_USED_TARIFF_3 = r'^\d-\d:1\.8\.3.+?\r\n' +EON_HU_ELECTRICITY_USED_TARIFF_4 = r'^\d-\d:1\.8\.4.+?\r\n' +EON_HU_ELECTRICITY_DELIVERED_TARIFF_3 = r'^\d-\d:2\.8\.3.+?\r\n' +EON_HU_ELECTRICITY_DELIVERED_TARIFF_4 = r'^\d-\d:2\.8\.4.+?\r\n' +EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 = r'^\d-\d:5\.8\.0.+?\r\n' +EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 = r'^\d-\d:6\.8\.0.+?\r\n' +EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 = r'^\d-\d:7\.8\.0.+?\r\n' +EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 = r'^\d-\d:8\.8\.0.+?\r\n' +EON_HU_ELECTRICITY_COMBINED = r'^\d-\d:15\.8\.0.+?\r\n' +EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL = r'^\d-\d:13\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_POWER_FACTOR_L1 = r'^\d-\d:33\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_POWER_FACTOR_L2 = r'^\d-\d:53\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_POWER_FACTOR_L3 = r'^\d-\d:73\.7\.0.+?\r\n' +EON_HU_FREQUENCY = r'^\d-\d:14\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 = r'^\d-\d:5\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 = r'^\d-\d:6\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 = r'^\d-\d:7\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 = r'^\d-\d:8\.7\.0.+?\r\n' +EON_HU_MAX_POWER_ON_L1 = r'^\d-\d:31\.4\.0.+?\r\n' +EON_HU_MAX_POWER_ON_L2 = r'^\d-\d:51\.4\.0.+?\r\n' +EON_HU_MAX_POWER_ON_L3 = r'^\d-\d:71\.4\.0.+?\r\n' +EON_HU_LAST_MONTH_DATA = r'^\d-\d:98\.1\.0.+?\r\n' diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index f9f8a79..a5030a4 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -339,3 +339,60 @@ obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), } } + +EON_HUNGARY = { + # Revision: 2023.02.10 + # Based on V5 + # Reference: https://www.eon.hu/content/dam/eon/eon-hungary/documents/Lakossagi/aram/muszaki-ugyek/p1_port%20felhaszn_interfesz_taj_%2020230210.pdf + 'checksum_support': True, + 'objects': { + obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), + obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), # "COSEM logical equipment name" + obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), # This obis is already defined, so it is not possible to "rename" it to "EQUIPMENT_SERIAL_NUMBER" + obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), + obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), # This seems to be wrong in documentation, it's not 0-0:96.50.68, but 0-0:96.3.10 + obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), # This obis is already duplicated, so it will show up as "BELGIUM_MAX_POWER_PER_PHASE" + obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.EON_HU_ELECTRICITY_USED_TARIFF_3: CosemParser(ValueParser(Decimal)), + obis.EON_HU_ELECTRICITY_USED_TARIFF_4: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3: CosemParser(ValueParser(Decimal)), + obis.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), + obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1: CosemParser(ValueParser(Decimal)), + obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2: CosemParser(ValueParser(Decimal)), + obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3: CosemParser(ValueParser(Decimal)), + obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4: CosemParser(ValueParser(Decimal)), + obis.EON_HU_ELECTRICITY_COMBINED: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters + obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters + obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters + obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters + obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL: CosemParser(ValueParser(Decimal)), + obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1: CosemParser(ValueParser(Decimal)), + obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters + obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters + obis.EON_HU_FREQUENCY: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1: CosemParser(ValueParser(Decimal)), + obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2: CosemParser(ValueParser(Decimal)), + obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3: CosemParser(ValueParser(Decimal)), + obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4: CosemParser(ValueParser(Decimal)), + obis.EON_HU_MAX_POWER_ON_L1: CosemParser(ValueParser(Decimal)), + obis.EON_HU_MAX_POWER_ON_L2: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters + obis.EON_HU_MAX_POWER_ON_L3: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters + # This is a list of last month data (on last day of last month @ 23:59:59), + # But it is not clear that what are the elements of the list. + # This is not well documented enough, so it is ignored for now. + # obis.EON_HU_LAST_MONTH_DATA: + obis.TEXT_MESSAGE: CosemParser(ValueParser(str)) + } +} diff --git a/test/example_telegrams.py b/test/example_telegrams.py index d59ce83..55a3dae 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -311,3 +311,55 @@ '0-1:96.1.1()\r\n' '!AD3B\r\n' ) + +# V5 telegram of EON in Hungary +TELEGRAM_V5_EON_HU = ( + '/SAG5SAG-METER\r\n' + '\r\n' + '0-0:1.0.0(230724150730S)\r\n' + '0-0:42.0.0(53414733303832323030303032313630)\r\n' + '0-0:96.1.0(383930303832323030303032313630)\r\n' + '0-0:96.14.0(0001)\r\n' + '0-0:96.3.10(1)\r\n' + '0-0:17.0.0(90.000*kW)\r\n' + '1-0:1.8.0(000173.640*kWh)\r\n' + '1-0:1.8.1(000047.719*kWh)\r\n' + '1-0:1.8.2(000125.921*kWh)\r\n' + '1-0:1.8.3(000000.000*kWh)\r\n' + '1-0:1.8.4(000000.000*kWh)\r\n' + '1-0:2.8.0(000627.177*kWh)\r\n' + '1-0:2.8.1(000401.829*kWh)\r\n' + '1-0:2.8.2(000225.348*kWh)\r\n' + '1-0:2.8.3(000000.000*kWh)\r\n' + '1-0:2.8.4(000000.000*kWh)\r\n' + '1-0:3.8.0(000000.123*kvarh)\r\n' + '1-0:4.8.0(000303.131*kvarh)\r\n' + '1-0:5.8.0(000000.668*kvarh)\r\n' + '1-0:6.8.0(000000.071*kvarh)\r\n' + '1-0:7.8.0(000160.487*kvarh)\r\n' + '1-0:8.8.0(000143.346*kvarh)\r\n' + '1-0:15.8.0(000800.817*kWh)\r\n' + '1-0:32.7.0(240.4*V)\r\n' + '1-0:52.7.0(239.1*V)\r\n' + '1-0:72.7.0(241.2*V)\r\n' + '1-0:31.7.0(003*A)\r\n' + '1-0:51.7.0(004*A)\r\n' + '1-0:71.7.0(003*A)\r\n' + '1-0:13.7.0(4.556)\r\n' + '1-0:33.7.0(4.591)\r\n' + '1-0:53.7.0(4.542)\r\n' + '1-0:73.7.0(4.552)\r\n' + '1-0:14.7.0(50.00*Hz)\r\n' + '1-0:1.7.0(00.000*kW)\r\n' + '1-0:2.7.0(02.601*kW)\r\n' + '1-0:5.7.0(00.000*kvar)\r\n' + '1-0:6.7.0(00.000*kvar)\r\n' + '1-0:7.7.0(00.504*kvar)\r\n' + '1-0:8.7.0(00.000*kvar)\r\n' + '1-0:31.4.0(200.00*A)\r\n' + '1-0:51.4.0(200.00*A)\r\n' + '1-0:71.4.0(200.00*A)\r\n' + '0-0:98.1.0(230701000000S)(000040.777*kWh)(000008.950*kWh)(000031.827*kWh)(000142.250*kWh)(000111.164*kWh)(000031.086*kWh)(000000.030*kvarh)(000073.988*kvarh)(000000.205*kvarh)(000000.048*kvarh)(000039.199*kvarh)(000035.020*kvarh)(000183.027*kWh)(03.564*kW)(02.156*kW)(03.564*kW)(04.104*kW)(04.104*kW)(03.400*kW)\r\n' + '0-0:96.13.0()\r\n' + '!99DA\r\n' +) \ No newline at end of file diff --git a/test/test_parse_v5_eon_hungary.py b/test/test_parse_v5_eon_hungary.py new file mode 100644 index 0000000..401f3dd --- /dev/null +++ b/test/test_parse_v5_eon_hungary.py @@ -0,0 +1,308 @@ +from decimal import Decimal + +import datetime +import unittest + +import pytz + +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 +from test.example_telegrams import TELEGRAM_V5_EON_HU + + +class TelegramParserV5EONHUTest(unittest.TestCase): + """ Test parsing of a DSMR v5 EON Hungary telegram. """ + + def test_parse(self): + parser = TelegramParser(telegram_specifications.EON_HUNGARY) + try: + telegram = parser.parse(TELEGRAM_V5_EON_HU, throw_ex=True) + except Exception as ex: + assert False, f"parse trigged an exception {ex}" + + # P1_MESSAGE_TIMESTAMP (0-0:1.0.0) + assert isinstance(telegram.P1_MESSAGE_TIMESTAMP, CosemObject) + assert telegram.P1_MESSAGE_TIMESTAMP.unit is None + assert isinstance(telegram.P1_MESSAGE_TIMESTAMP.value, datetime.datetime) + assert telegram.P1_MESSAGE_TIMESTAMP.value == \ + pytz.timezone("Europe/Budapest").localize(datetime.datetime(2023, 7, 24, 15, 7, 30)) + + # LUXEMBOURG_EQUIPMENT_IDENTIFIER (0-0:42.0.0) + assert isinstance(telegram.LUXEMBOURG_EQUIPMENT_IDENTIFIER, CosemObject) + assert telegram.LUXEMBOURG_EQUIPMENT_IDENTIFIER.unit is None + assert isinstance(telegram.LUXEMBOURG_EQUIPMENT_IDENTIFIER.value, str) + assert telegram.LUXEMBOURG_EQUIPMENT_IDENTIFIER.value == '53414733303832323030303032313630' + + # EQUIPMENT_IDENTIFIER_GAS (0-0:96.1.0) + assert isinstance(telegram.EQUIPMENT_IDENTIFIER_GAS, CosemObject) + assert telegram.EQUIPMENT_IDENTIFIER_GAS.unit is None + assert isinstance(telegram.EQUIPMENT_IDENTIFIER_GAS.value, str) + assert telegram.EQUIPMENT_IDENTIFIER_GAS.value == '383930303832323030303032313630' + + # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) + assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF, CosemObject) + assert telegram.ELECTRICITY_ACTIVE_TARIFF.unit is None + assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF.value, str) + assert telegram.ELECTRICITY_ACTIVE_TARIFF.value == '0001' + + # ACTUAL_SWITCH_POSITION (0-0:96.3.10) + assert isinstance(telegram.ACTUAL_SWITCH_POSITION, CosemObject) + assert telegram.ACTUAL_SWITCH_POSITION.unit is None + assert isinstance(telegram.ACTUAL_SWITCH_POSITION.value, str) + assert telegram.ACTUAL_SWITCH_POSITION.value == '1' + + # BELGIUM_MAX_POWER_PER_PHASE (ACTUAL_TRESHOLD_ELECTRICITY) (0-0:17.0.0) + assert isinstance(telegram.BELGIUM_MAX_POWER_PER_PHASE, CosemObject) + assert telegram.BELGIUM_MAX_POWER_PER_PHASE.unit == 'kW' + assert isinstance(telegram.BELGIUM_MAX_POWER_PER_PHASE.value, Decimal) + assert telegram.BELGIUM_MAX_POWER_PER_PHASE.value == Decimal('90.000') + + # ELECTRICITY_IMPORTED_TOTAL (1-0:1.8.0) + assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL, CosemObject) + assert telegram.ELECTRICITY_IMPORTED_TOTAL.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL.value, Decimal) + assert telegram.ELECTRICITY_IMPORTED_TOTAL.value == Decimal('000173.640') + + # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1, CosemObject) + assert telegram.ELECTRICITY_USED_TARIFF_1.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1.value, Decimal) + assert telegram.ELECTRICITY_USED_TARIFF_1.value == Decimal('000047.719') + + # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2, CosemObject) + assert telegram.ELECTRICITY_USED_TARIFF_2.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2.value, Decimal) + assert telegram.ELECTRICITY_USED_TARIFF_2.value == Decimal('000125.921') + + # EON_HU_ELECTRICITY_USED_TARIFF_3 (1-0:1.8.3) + assert isinstance(telegram.EON_HU_ELECTRICITY_USED_TARIFF_3, CosemObject) + assert telegram.EON_HU_ELECTRICITY_USED_TARIFF_3.unit == 'kWh' + assert isinstance(telegram.EON_HU_ELECTRICITY_USED_TARIFF_3.value, Decimal) + assert telegram.EON_HU_ELECTRICITY_USED_TARIFF_3.value == Decimal('000000.000') + + # EON_HU_ELECTRICITY_USED_TARIFF_4 (1-0:1.8.4) + assert isinstance(telegram.EON_HU_ELECTRICITY_USED_TARIFF_4, CosemObject) + assert telegram.EON_HU_ELECTRICITY_USED_TARIFF_4.unit == 'kWh' + assert isinstance(telegram.EON_HU_ELECTRICITY_USED_TARIFF_4.value, Decimal) + assert telegram.EON_HU_ELECTRICITY_USED_TARIFF_4.value == Decimal('000000.000') + + # ELECTRICITY_EXPORTED_TOTAL (1-0:2.8.0) + assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL, CosemObject) + assert telegram.ELECTRICITY_EXPORTED_TOTAL.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL.value, Decimal) + assert telegram.ELECTRICITY_EXPORTED_TOTAL.value == Decimal('000627.177') + + # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('000401.829') + + # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('000225.348') + + # EON_HU_ELECTRICITY_DELIVERED_TARIFF_3 (1-0:2.8.3) + assert isinstance(telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3, CosemObject) + assert telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3.unit == 'kWh' + assert isinstance(telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3.value, Decimal) + assert telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3.value == Decimal('000000.000') + + # EON_HU_ELECTRICITY_DELIVERED_TARIFF_4 (1-0:2.8.4) + assert isinstance(telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4, CosemObject) + assert telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4.unit == 'kWh' + assert isinstance(telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4.value, Decimal) + assert telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4.value == Decimal('000000.000') + + # ELECTRICITY_REACTIVE_IMPORTED_TOTAL (1-0:3.8.0) + assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL, CosemObject) + assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.unit == 'kvarh' + assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value, Decimal) + assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value == Decimal('000000.123') + + # ELECTRICITY_REACTIVE_EXPORTED_TOTAL (1-0:4.8.0) + assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL, CosemObject) + assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.unit == 'kvarh' + assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value, Decimal) + assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value == Decimal('000303.131') + + # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 (1-0:5.8.0) + assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1, CosemObject) + assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1.unit == 'kvarh' + assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1.value, Decimal) + assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1.value == Decimal('000000.668') + + # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 (1-0:6.8.0) + assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2, CosemObject) + assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2.unit == 'kvarh' + assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2.value, Decimal) + assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2.value == Decimal('000000.071') + + # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 (1-0:7.8.0) + assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3, CosemObject) + assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3.unit == 'kvarh' + assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3.value, Decimal) + assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3.value == Decimal('000160.487') + + # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 (1-0:8.8.0) + assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4, CosemObject) + assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4.unit == 'kvarh' + assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4.value, Decimal) + assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4.value == Decimal('000143.346') + + # EON_HU_ELECTRICITY_COMBINED (1-0:15.8.0) + assert isinstance(telegram.EON_HU_ELECTRICITY_COMBINED, CosemObject) + assert telegram.EON_HU_ELECTRICITY_COMBINED.unit == 'kWh' + assert isinstance(telegram.EON_HU_ELECTRICITY_COMBINED.value, Decimal) + assert telegram.EON_HU_ELECTRICITY_COMBINED.value == Decimal('000800.817') + + # INSTANTANEOUS_VOLTAGE_L2 (1-0:32.7.0) + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1, CosemObject) + assert telegram.INSTANTANEOUS_VOLTAGE_L1.unit == 'V' + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1.value, Decimal) + assert telegram.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('240.4') + + # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0) + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2, CosemObject) + assert telegram.INSTANTANEOUS_VOLTAGE_L2.unit == 'V' + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2.value, Decimal) + assert telegram.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('239.1') + + # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0) + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3, CosemObject) + assert telegram.INSTANTANEOUS_VOLTAGE_L3.unit == 'V' + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3.value, Decimal) + assert telegram.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('241.2') + + # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1, CosemObject) + assert telegram.INSTANTANEOUS_CURRENT_L1.unit == 'A' + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1.value, Decimal) + assert telegram.INSTANTANEOUS_CURRENT_L1.value == Decimal('003') + + # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2, CosemObject) + assert telegram.INSTANTANEOUS_CURRENT_L2.unit == 'A' + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2.value, Decimal) + assert telegram.INSTANTANEOUS_CURRENT_L2.value == Decimal('004') + + # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3, CosemObject) + assert telegram.INSTANTANEOUS_CURRENT_L3.unit == 'A' + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3.value, Decimal) + assert telegram.INSTANTANEOUS_CURRENT_L3.value == Decimal('003') + + # EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL (1-0:13.7.0) + assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL, CosemObject) + assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL.unit is None + assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL.value, Decimal) + assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL.value == Decimal('4.556') + + # EON_HU_INSTANTANEOUS_POWER_FACTOR_L1 (1-0:33.7.0) + assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1, CosemObject) + assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1.unit is None + assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1.value, Decimal) + assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1.value == Decimal('4.591') + + # EON_HU_INSTANTANEOUS_POWER_FACTOR_L2 (1-0:53.7.0) + assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2, CosemObject) + assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2.unit is None + assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2.value, Decimal) + assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2.value == Decimal('4.542') + + # EON_HU_INSTANTANEOUS_POWER_FACTOR_L3 (1-0:73.7.0) + assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3, CosemObject) + assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3.unit is None + assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3.value, Decimal) + assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3.value == Decimal('4.552') + + # EON_HU_FREQUENCY (1-0:14.7.0) + assert isinstance(telegram.EON_HU_FREQUENCY, CosemObject) + assert telegram.EON_HU_FREQUENCY.unit == "Hz" + assert isinstance(telegram.EON_HU_FREQUENCY.value, Decimal) + assert telegram.EON_HU_FREQUENCY.value == Decimal('50.00') + + # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) + assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE, CosemObject) + assert telegram.CURRENT_ELECTRICITY_USAGE.unit == 'kW' + assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE.value, Decimal) + assert telegram.CURRENT_ELECTRICITY_USAGE.value == Decimal('00.000') + + # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) + assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY, CosemObject) + assert telegram.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW' + assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY.value, Decimal) + assert telegram.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('02.601') + + # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 (1-0:5.7.0) + assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1, CosemObject) + assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1.unit == 'kvar' + assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1.value, Decimal) + assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1.value == Decimal('00.000') + + # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 (1-0:6.7.0) + assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2, CosemObject) + assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2.unit == 'kvar' + assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2.value, Decimal) + assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2.value == Decimal('00.000') + + # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 (1-0:7.7.0) + assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3, CosemObject) + assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3.unit == 'kvar' + assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3.value, Decimal) + assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3.value == Decimal('00.504') + + # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 (1-0:8.7.0) + assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4, CosemObject) + assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4.unit == 'kvar' + assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4.value, Decimal) + assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4.value == Decimal('00.000') + + # EON_HU_MAX_POWER_ON_L1 (1-0:31.4.0) + assert isinstance(telegram.EON_HU_MAX_POWER_ON_L1, CosemObject) + assert telegram.EON_HU_MAX_POWER_ON_L1.unit == 'A' + assert isinstance(telegram.EON_HU_MAX_POWER_ON_L1.value, Decimal) + assert telegram.EON_HU_MAX_POWER_ON_L1.value == Decimal('200.00') + + # EON_HU_MAX_POWER_ON_L2 (1-0:31.4.0) + assert isinstance(telegram.EON_HU_MAX_POWER_ON_L2, CosemObject) + assert telegram.EON_HU_MAX_POWER_ON_L2.unit == 'A' + assert isinstance(telegram.EON_HU_MAX_POWER_ON_L2.value, Decimal) + assert telegram.EON_HU_MAX_POWER_ON_L2.value == Decimal('200.00') + + # EON_HU_MAX_POWER_ON_L3 (1-0:31.4.0) + assert isinstance(telegram.EON_HU_MAX_POWER_ON_L3, CosemObject) + assert telegram.EON_HU_MAX_POWER_ON_L3.unit == 'A' + assert isinstance(telegram.EON_HU_MAX_POWER_ON_L3.value, Decimal) + assert telegram.EON_HU_MAX_POWER_ON_L3.value == Decimal('200.00') + + # TEXT_MESSAGE (0-0:96.13.0) + assert isinstance(telegram.TEXT_MESSAGE, CosemObject) + assert telegram.TEXT_MESSAGE.unit is None + assert telegram.TEXT_MESSAGE.value is None + + def test_checksum_valid(self): + # No exception is raised. + TelegramParser.validate_checksum(TELEGRAM_V5_EON_HU) + + def test_checksum_invalid(self): + # Remove the electricty used data value. This causes the checksum to + # not match anymore. + corrupted_telegram = TELEGRAM_V5_EON_HU.replace( + '1-0:1.8.1(000047.719*kWh)\r\n', + '' + ) + + with self.assertRaises(InvalidChecksumError): + TelegramParser.validate_checksum(corrupted_telegram) + + def test_checksum_missing(self): + # Remove the checksum value causing a ParseError. + corrupted_telegram = TELEGRAM_V5_EON_HU.replace('!99DA\r\n', '') + with self.assertRaises(ParseError): + TelegramParser.validate_checksum(corrupted_telegram) From af2e6558deb512dbce6e7a35f745450ff679f564 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 26 Jul 2023 15:03:29 +0200 Subject: [PATCH 201/226] Revert "Added E.ON HUNGARY specification (#134)" (#135) This reverts commit 0752deb58aeb287626832a6c1191a3d775980f0d. --- dsmr_parser/clients/protocol.py | 3 - dsmr_parser/clients/socket_.py | 7 +- dsmr_parser/obis_name_mapping.py | 24 +- dsmr_parser/obis_references.py | 24 -- dsmr_parser/telegram_specifications.py | 57 ----- test/example_telegrams.py | 52 ----- test/test_parse_v5_eon_hungary.py | 308 ------------------------- 7 files changed, 2 insertions(+), 473 deletions(-) delete mode 100644 test/test_parse_v5_eon_hungary.py diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index ba38fe5..a7fb74f 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -51,9 +51,6 @@ def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, elif dsmr_version == 'ISKRA_IE': specification = telegram_specifications.ISKRA_IE serial_settings = SERIAL_SETTINGS_V5 - elif dsmr_version == '5EONHU': - specification = telegram_specifications.EON_HUNGARY - serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) diff --git a/dsmr_parser/clients/socket_.py b/dsmr_parser/clients/socket_.py index 8ee16fa..b7490ec 100644 --- a/dsmr_parser/clients/socket_.py +++ b/dsmr_parser/clients/socket_.py @@ -43,12 +43,7 @@ def read(self): continue for data in lines: - try: - self.telegram_buffer.append(data.decode('ascii')) - except UnicodeDecodeError: - # Some garbage came through the channel - # E.g.: Happens at EON_HUNGARY, but only once at the start of the socket. - logger.error('Failed to parse telegram due to unicode decode error') + self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): try: diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 95f1016..44ebb82 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -92,29 +92,7 @@ obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER', obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE', obis.Q3D_EQUIPMENT_SERIALNUMBER: 'Q3D_EQUIPMENT_SERIALNUMBER', - obis.BELGIUM_MBUS2_DEVICE_TYPE: 'BELGIUM_MBUS2_DEVICE_TYPE', - obis.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3: 'EON_HU_ELECTRICITY_DELIVERED_TARIFF_3', - obis.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4: 'EON_HU_ELECTRICITY_DELIVERED_TARIFF_4', - obis.EON_HU_ELECTRICITY_USED_TARIFF_3: 'EON_HU_ELECTRICITY_USED_TARIFF_3', - obis.EON_HU_ELECTRICITY_USED_TARIFF_4: 'EON_HU_ELECTRICITY_USED_TARIFF_4', - obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1: 'EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1', - obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2: 'EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2', - obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3: 'EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3', - obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4: 'EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4', - obis.EON_HU_ELECTRICITY_COMBINED: 'EON_HU_ELECTRICITY_COMBINED', - obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL: 'EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL', - obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1: 'EON_HU_INSTANTANEOUS_POWER_FACTOR_L1', - obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2: 'EON_HU_INSTANTANEOUS_POWER_FACTOR_L2', - obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3: 'EON_HU_INSTANTANEOUS_POWER_FACTOR_L3', - obis.EON_HU_FREQUENCY: 'EON_HU_FREQUENCY', - obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1: 'EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1', - obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2: 'EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2', - obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3: 'EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3', - obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4: 'EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4', - obis.EON_HU_MAX_POWER_ON_L1: 'EON_HU_MAX_POWER_ON_L1', - obis.EON_HU_MAX_POWER_ON_L2: 'EON_HU_MAX_POWER_ON_L2', - obis.EON_HU_MAX_POWER_ON_L3: 'EON_HU_MAX_POWER_ON_L3', - obis.EON_HU_LAST_MONTH_DATA: 'EON_HU_LAST_MONTH_DATA' + obis.BELGIUM_MBUS2_DEVICE_TYPE: 'BELGIUM_MBUS2_DEVICE_TYPE' } REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 920e39e..c710504 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -122,27 +122,3 @@ 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 - -# EON Hungary -EON_HU_ELECTRICITY_USED_TARIFF_3 = r'^\d-\d:1\.8\.3.+?\r\n' -EON_HU_ELECTRICITY_USED_TARIFF_4 = r'^\d-\d:1\.8\.4.+?\r\n' -EON_HU_ELECTRICITY_DELIVERED_TARIFF_3 = r'^\d-\d:2\.8\.3.+?\r\n' -EON_HU_ELECTRICITY_DELIVERED_TARIFF_4 = r'^\d-\d:2\.8\.4.+?\r\n' -EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 = r'^\d-\d:5\.8\.0.+?\r\n' -EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 = r'^\d-\d:6\.8\.0.+?\r\n' -EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 = r'^\d-\d:7\.8\.0.+?\r\n' -EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 = r'^\d-\d:8\.8\.0.+?\r\n' -EON_HU_ELECTRICITY_COMBINED = r'^\d-\d:15\.8\.0.+?\r\n' -EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL = r'^\d-\d:13\.7\.0.+?\r\n' -EON_HU_INSTANTANEOUS_POWER_FACTOR_L1 = r'^\d-\d:33\.7\.0.+?\r\n' -EON_HU_INSTANTANEOUS_POWER_FACTOR_L2 = r'^\d-\d:53\.7\.0.+?\r\n' -EON_HU_INSTANTANEOUS_POWER_FACTOR_L3 = r'^\d-\d:73\.7\.0.+?\r\n' -EON_HU_FREQUENCY = r'^\d-\d:14\.7\.0.+?\r\n' -EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 = r'^\d-\d:5\.7\.0.+?\r\n' -EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 = r'^\d-\d:6\.7\.0.+?\r\n' -EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 = r'^\d-\d:7\.7\.0.+?\r\n' -EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 = r'^\d-\d:8\.7\.0.+?\r\n' -EON_HU_MAX_POWER_ON_L1 = r'^\d-\d:31\.4\.0.+?\r\n' -EON_HU_MAX_POWER_ON_L2 = r'^\d-\d:51\.4\.0.+?\r\n' -EON_HU_MAX_POWER_ON_L3 = r'^\d-\d:71\.4\.0.+?\r\n' -EON_HU_LAST_MONTH_DATA = r'^\d-\d:98\.1\.0.+?\r\n' diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index a5030a4..f9f8a79 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -339,60 +339,3 @@ obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), } } - -EON_HUNGARY = { - # Revision: 2023.02.10 - # Based on V5 - # Reference: https://www.eon.hu/content/dam/eon/eon-hungary/documents/Lakossagi/aram/muszaki-ugyek/p1_port%20felhaszn_interfesz_taj_%2020230210.pdf - 'checksum_support': True, - 'objects': { - obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), - obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), # "COSEM logical equipment name" - obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), # This obis is already defined, so it is not possible to "rename" it to "EQUIPMENT_SERIAL_NUMBER" - obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), - obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), # This seems to be wrong in documentation, it's not 0-0:96.50.68, but 0-0:96.3.10 - obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), # This obis is already duplicated, so it will show up as "BELGIUM_MAX_POWER_PER_PHASE" - obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.EON_HU_ELECTRICITY_USED_TARIFF_3: CosemParser(ValueParser(Decimal)), - obis.EON_HU_ELECTRICITY_USED_TARIFF_4: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3: CosemParser(ValueParser(Decimal)), - obis.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1: CosemParser(ValueParser(Decimal)), - obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2: CosemParser(ValueParser(Decimal)), - obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3: CosemParser(ValueParser(Decimal)), - obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4: CosemParser(ValueParser(Decimal)), - obis.EON_HU_ELECTRICITY_COMBINED: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters - obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters - obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters - obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters - obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL: CosemParser(ValueParser(Decimal)), - obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1: CosemParser(ValueParser(Decimal)), - obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters - obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters - obis.EON_HU_FREQUENCY: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1: CosemParser(ValueParser(Decimal)), - obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2: CosemParser(ValueParser(Decimal)), - obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3: CosemParser(ValueParser(Decimal)), - obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4: CosemParser(ValueParser(Decimal)), - obis.EON_HU_MAX_POWER_ON_L1: CosemParser(ValueParser(Decimal)), - obis.EON_HU_MAX_POWER_ON_L2: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters - obis.EON_HU_MAX_POWER_ON_L3: CosemParser(ValueParser(Decimal)), # Only with 3 phase meters - # This is a list of last month data (on last day of last month @ 23:59:59), - # But it is not clear that what are the elements of the list. - # This is not well documented enough, so it is ignored for now. - # obis.EON_HU_LAST_MONTH_DATA: - obis.TEXT_MESSAGE: CosemParser(ValueParser(str)) - } -} diff --git a/test/example_telegrams.py b/test/example_telegrams.py index 55a3dae..d59ce83 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -311,55 +311,3 @@ '0-1:96.1.1()\r\n' '!AD3B\r\n' ) - -# V5 telegram of EON in Hungary -TELEGRAM_V5_EON_HU = ( - '/SAG5SAG-METER\r\n' - '\r\n' - '0-0:1.0.0(230724150730S)\r\n' - '0-0:42.0.0(53414733303832323030303032313630)\r\n' - '0-0:96.1.0(383930303832323030303032313630)\r\n' - '0-0:96.14.0(0001)\r\n' - '0-0:96.3.10(1)\r\n' - '0-0:17.0.0(90.000*kW)\r\n' - '1-0:1.8.0(000173.640*kWh)\r\n' - '1-0:1.8.1(000047.719*kWh)\r\n' - '1-0:1.8.2(000125.921*kWh)\r\n' - '1-0:1.8.3(000000.000*kWh)\r\n' - '1-0:1.8.4(000000.000*kWh)\r\n' - '1-0:2.8.0(000627.177*kWh)\r\n' - '1-0:2.8.1(000401.829*kWh)\r\n' - '1-0:2.8.2(000225.348*kWh)\r\n' - '1-0:2.8.3(000000.000*kWh)\r\n' - '1-0:2.8.4(000000.000*kWh)\r\n' - '1-0:3.8.0(000000.123*kvarh)\r\n' - '1-0:4.8.0(000303.131*kvarh)\r\n' - '1-0:5.8.0(000000.668*kvarh)\r\n' - '1-0:6.8.0(000000.071*kvarh)\r\n' - '1-0:7.8.0(000160.487*kvarh)\r\n' - '1-0:8.8.0(000143.346*kvarh)\r\n' - '1-0:15.8.0(000800.817*kWh)\r\n' - '1-0:32.7.0(240.4*V)\r\n' - '1-0:52.7.0(239.1*V)\r\n' - '1-0:72.7.0(241.2*V)\r\n' - '1-0:31.7.0(003*A)\r\n' - '1-0:51.7.0(004*A)\r\n' - '1-0:71.7.0(003*A)\r\n' - '1-0:13.7.0(4.556)\r\n' - '1-0:33.7.0(4.591)\r\n' - '1-0:53.7.0(4.542)\r\n' - '1-0:73.7.0(4.552)\r\n' - '1-0:14.7.0(50.00*Hz)\r\n' - '1-0:1.7.0(00.000*kW)\r\n' - '1-0:2.7.0(02.601*kW)\r\n' - '1-0:5.7.0(00.000*kvar)\r\n' - '1-0:6.7.0(00.000*kvar)\r\n' - '1-0:7.7.0(00.504*kvar)\r\n' - '1-0:8.7.0(00.000*kvar)\r\n' - '1-0:31.4.0(200.00*A)\r\n' - '1-0:51.4.0(200.00*A)\r\n' - '1-0:71.4.0(200.00*A)\r\n' - '0-0:98.1.0(230701000000S)(000040.777*kWh)(000008.950*kWh)(000031.827*kWh)(000142.250*kWh)(000111.164*kWh)(000031.086*kWh)(000000.030*kvarh)(000073.988*kvarh)(000000.205*kvarh)(000000.048*kvarh)(000039.199*kvarh)(000035.020*kvarh)(000183.027*kWh)(03.564*kW)(02.156*kW)(03.564*kW)(04.104*kW)(04.104*kW)(03.400*kW)\r\n' - '0-0:96.13.0()\r\n' - '!99DA\r\n' -) \ No newline at end of file diff --git a/test/test_parse_v5_eon_hungary.py b/test/test_parse_v5_eon_hungary.py deleted file mode 100644 index 401f3dd..0000000 --- a/test/test_parse_v5_eon_hungary.py +++ /dev/null @@ -1,308 +0,0 @@ -from decimal import Decimal - -import datetime -import unittest - -import pytz - -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 -from test.example_telegrams import TELEGRAM_V5_EON_HU - - -class TelegramParserV5EONHUTest(unittest.TestCase): - """ Test parsing of a DSMR v5 EON Hungary telegram. """ - - def test_parse(self): - parser = TelegramParser(telegram_specifications.EON_HUNGARY) - try: - telegram = parser.parse(TELEGRAM_V5_EON_HU, throw_ex=True) - except Exception as ex: - assert False, f"parse trigged an exception {ex}" - - # P1_MESSAGE_TIMESTAMP (0-0:1.0.0) - assert isinstance(telegram.P1_MESSAGE_TIMESTAMP, CosemObject) - assert telegram.P1_MESSAGE_TIMESTAMP.unit is None - assert isinstance(telegram.P1_MESSAGE_TIMESTAMP.value, datetime.datetime) - assert telegram.P1_MESSAGE_TIMESTAMP.value == \ - pytz.timezone("Europe/Budapest").localize(datetime.datetime(2023, 7, 24, 15, 7, 30)) - - # LUXEMBOURG_EQUIPMENT_IDENTIFIER (0-0:42.0.0) - assert isinstance(telegram.LUXEMBOURG_EQUIPMENT_IDENTIFIER, CosemObject) - assert telegram.LUXEMBOURG_EQUIPMENT_IDENTIFIER.unit is None - assert isinstance(telegram.LUXEMBOURG_EQUIPMENT_IDENTIFIER.value, str) - assert telegram.LUXEMBOURG_EQUIPMENT_IDENTIFIER.value == '53414733303832323030303032313630' - - # EQUIPMENT_IDENTIFIER_GAS (0-0:96.1.0) - assert isinstance(telegram.EQUIPMENT_IDENTIFIER_GAS, CosemObject) - assert telegram.EQUIPMENT_IDENTIFIER_GAS.unit is None - assert isinstance(telegram.EQUIPMENT_IDENTIFIER_GAS.value, str) - assert telegram.EQUIPMENT_IDENTIFIER_GAS.value == '383930303832323030303032313630' - - # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) - assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF, CosemObject) - assert telegram.ELECTRICITY_ACTIVE_TARIFF.unit is None - assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF.value, str) - assert telegram.ELECTRICITY_ACTIVE_TARIFF.value == '0001' - - # ACTUAL_SWITCH_POSITION (0-0:96.3.10) - assert isinstance(telegram.ACTUAL_SWITCH_POSITION, CosemObject) - assert telegram.ACTUAL_SWITCH_POSITION.unit is None - assert isinstance(telegram.ACTUAL_SWITCH_POSITION.value, str) - assert telegram.ACTUAL_SWITCH_POSITION.value == '1' - - # BELGIUM_MAX_POWER_PER_PHASE (ACTUAL_TRESHOLD_ELECTRICITY) (0-0:17.0.0) - assert isinstance(telegram.BELGIUM_MAX_POWER_PER_PHASE, CosemObject) - assert telegram.BELGIUM_MAX_POWER_PER_PHASE.unit == 'kW' - assert isinstance(telegram.BELGIUM_MAX_POWER_PER_PHASE.value, Decimal) - assert telegram.BELGIUM_MAX_POWER_PER_PHASE.value == Decimal('90.000') - - # ELECTRICITY_IMPORTED_TOTAL (1-0:1.8.0) - assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL, CosemObject) - assert telegram.ELECTRICITY_IMPORTED_TOTAL.unit == 'kWh' - assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL.value, Decimal) - assert telegram.ELECTRICITY_IMPORTED_TOTAL.value == Decimal('000173.640') - - # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) - assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1, CosemObject) - assert telegram.ELECTRICITY_USED_TARIFF_1.unit == 'kWh' - assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1.value, Decimal) - assert telegram.ELECTRICITY_USED_TARIFF_1.value == Decimal('000047.719') - - # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) - assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2, CosemObject) - assert telegram.ELECTRICITY_USED_TARIFF_2.unit == 'kWh' - assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2.value, Decimal) - assert telegram.ELECTRICITY_USED_TARIFF_2.value == Decimal('000125.921') - - # EON_HU_ELECTRICITY_USED_TARIFF_3 (1-0:1.8.3) - assert isinstance(telegram.EON_HU_ELECTRICITY_USED_TARIFF_3, CosemObject) - assert telegram.EON_HU_ELECTRICITY_USED_TARIFF_3.unit == 'kWh' - assert isinstance(telegram.EON_HU_ELECTRICITY_USED_TARIFF_3.value, Decimal) - assert telegram.EON_HU_ELECTRICITY_USED_TARIFF_3.value == Decimal('000000.000') - - # EON_HU_ELECTRICITY_USED_TARIFF_4 (1-0:1.8.4) - assert isinstance(telegram.EON_HU_ELECTRICITY_USED_TARIFF_4, CosemObject) - assert telegram.EON_HU_ELECTRICITY_USED_TARIFF_4.unit == 'kWh' - assert isinstance(telegram.EON_HU_ELECTRICITY_USED_TARIFF_4.value, Decimal) - assert telegram.EON_HU_ELECTRICITY_USED_TARIFF_4.value == Decimal('000000.000') - - # ELECTRICITY_EXPORTED_TOTAL (1-0:2.8.0) - assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL, CosemObject) - assert telegram.ELECTRICITY_EXPORTED_TOTAL.unit == 'kWh' - assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL.value, Decimal) - assert telegram.ELECTRICITY_EXPORTED_TOTAL.value == Decimal('000627.177') - - # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) - assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject) - assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh' - assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal) - assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('000401.829') - - # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) - assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject) - assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh' - assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal) - assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('000225.348') - - # EON_HU_ELECTRICITY_DELIVERED_TARIFF_3 (1-0:2.8.3) - assert isinstance(telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3, CosemObject) - assert telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3.unit == 'kWh' - assert isinstance(telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3.value, Decimal) - assert telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_3.value == Decimal('000000.000') - - # EON_HU_ELECTRICITY_DELIVERED_TARIFF_4 (1-0:2.8.4) - assert isinstance(telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4, CosemObject) - assert telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4.unit == 'kWh' - assert isinstance(telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4.value, Decimal) - assert telegram.EON_HU_ELECTRICITY_DELIVERED_TARIFF_4.value == Decimal('000000.000') - - # ELECTRICITY_REACTIVE_IMPORTED_TOTAL (1-0:3.8.0) - assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL, CosemObject) - assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.unit == 'kvarh' - assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value, Decimal) - assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value == Decimal('000000.123') - - # ELECTRICITY_REACTIVE_EXPORTED_TOTAL (1-0:4.8.0) - assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL, CosemObject) - assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.unit == 'kvarh' - assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value, Decimal) - assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value == Decimal('000303.131') - - # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 (1-0:5.8.0) - assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1, CosemObject) - assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1.unit == 'kvarh' - assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1.value, Decimal) - assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1.value == Decimal('000000.668') - - # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 (1-0:6.8.0) - assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2, CosemObject) - assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2.unit == 'kvarh' - assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2.value, Decimal) - assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2.value == Decimal('000000.071') - - # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 (1-0:7.8.0) - assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3, CosemObject) - assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3.unit == 'kvarh' - assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3.value, Decimal) - assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3.value == Decimal('000160.487') - - # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 (1-0:8.8.0) - assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4, CosemObject) - assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4.unit == 'kvarh' - assert isinstance(telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4.value, Decimal) - assert telegram.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4.value == Decimal('000143.346') - - # EON_HU_ELECTRICITY_COMBINED (1-0:15.8.0) - assert isinstance(telegram.EON_HU_ELECTRICITY_COMBINED, CosemObject) - assert telegram.EON_HU_ELECTRICITY_COMBINED.unit == 'kWh' - assert isinstance(telegram.EON_HU_ELECTRICITY_COMBINED.value, Decimal) - assert telegram.EON_HU_ELECTRICITY_COMBINED.value == Decimal('000800.817') - - # INSTANTANEOUS_VOLTAGE_L2 (1-0:32.7.0) - assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1, CosemObject) - assert telegram.INSTANTANEOUS_VOLTAGE_L1.unit == 'V' - assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1.value, Decimal) - assert telegram.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('240.4') - - # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0) - assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2, CosemObject) - assert telegram.INSTANTANEOUS_VOLTAGE_L2.unit == 'V' - assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2.value, Decimal) - assert telegram.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('239.1') - - # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0) - assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3, CosemObject) - assert telegram.INSTANTANEOUS_VOLTAGE_L3.unit == 'V' - assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3.value, Decimal) - assert telegram.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('241.2') - - # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) - assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1, CosemObject) - assert telegram.INSTANTANEOUS_CURRENT_L1.unit == 'A' - assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1.value, Decimal) - assert telegram.INSTANTANEOUS_CURRENT_L1.value == Decimal('003') - - # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) - assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2, CosemObject) - assert telegram.INSTANTANEOUS_CURRENT_L2.unit == 'A' - assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2.value, Decimal) - assert telegram.INSTANTANEOUS_CURRENT_L2.value == Decimal('004') - - # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) - assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3, CosemObject) - assert telegram.INSTANTANEOUS_CURRENT_L3.unit == 'A' - assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3.value, Decimal) - assert telegram.INSTANTANEOUS_CURRENT_L3.value == Decimal('003') - - # EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL (1-0:13.7.0) - assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL, CosemObject) - assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL.unit is None - assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL.value, Decimal) - assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL.value == Decimal('4.556') - - # EON_HU_INSTANTANEOUS_POWER_FACTOR_L1 (1-0:33.7.0) - assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1, CosemObject) - assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1.unit is None - assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1.value, Decimal) - assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1.value == Decimal('4.591') - - # EON_HU_INSTANTANEOUS_POWER_FACTOR_L2 (1-0:53.7.0) - assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2, CosemObject) - assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2.unit is None - assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2.value, Decimal) - assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2.value == Decimal('4.542') - - # EON_HU_INSTANTANEOUS_POWER_FACTOR_L3 (1-0:73.7.0) - assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3, CosemObject) - assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3.unit is None - assert isinstance(telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3.value, Decimal) - assert telegram.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3.value == Decimal('4.552') - - # EON_HU_FREQUENCY (1-0:14.7.0) - assert isinstance(telegram.EON_HU_FREQUENCY, CosemObject) - assert telegram.EON_HU_FREQUENCY.unit == "Hz" - assert isinstance(telegram.EON_HU_FREQUENCY.value, Decimal) - assert telegram.EON_HU_FREQUENCY.value == Decimal('50.00') - - # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) - assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE, CosemObject) - assert telegram.CURRENT_ELECTRICITY_USAGE.unit == 'kW' - assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE.value, Decimal) - assert telegram.CURRENT_ELECTRICITY_USAGE.value == Decimal('00.000') - - # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) - assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY, CosemObject) - assert telegram.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW' - assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY.value, Decimal) - assert telegram.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('02.601') - - # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 (1-0:5.7.0) - assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1, CosemObject) - assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1.unit == 'kvar' - assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1.value, Decimal) - assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1.value == Decimal('00.000') - - # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 (1-0:6.7.0) - assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2, CosemObject) - assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2.unit == 'kvar' - assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2.value, Decimal) - assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2.value == Decimal('00.000') - - # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 (1-0:7.7.0) - assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3, CosemObject) - assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3.unit == 'kvar' - assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3.value, Decimal) - assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3.value == Decimal('00.504') - - # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 (1-0:8.7.0) - assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4, CosemObject) - assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4.unit == 'kvar' - assert isinstance(telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4.value, Decimal) - assert telegram.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4.value == Decimal('00.000') - - # EON_HU_MAX_POWER_ON_L1 (1-0:31.4.0) - assert isinstance(telegram.EON_HU_MAX_POWER_ON_L1, CosemObject) - assert telegram.EON_HU_MAX_POWER_ON_L1.unit == 'A' - assert isinstance(telegram.EON_HU_MAX_POWER_ON_L1.value, Decimal) - assert telegram.EON_HU_MAX_POWER_ON_L1.value == Decimal('200.00') - - # EON_HU_MAX_POWER_ON_L2 (1-0:31.4.0) - assert isinstance(telegram.EON_HU_MAX_POWER_ON_L2, CosemObject) - assert telegram.EON_HU_MAX_POWER_ON_L2.unit == 'A' - assert isinstance(telegram.EON_HU_MAX_POWER_ON_L2.value, Decimal) - assert telegram.EON_HU_MAX_POWER_ON_L2.value == Decimal('200.00') - - # EON_HU_MAX_POWER_ON_L3 (1-0:31.4.0) - assert isinstance(telegram.EON_HU_MAX_POWER_ON_L3, CosemObject) - assert telegram.EON_HU_MAX_POWER_ON_L3.unit == 'A' - assert isinstance(telegram.EON_HU_MAX_POWER_ON_L3.value, Decimal) - assert telegram.EON_HU_MAX_POWER_ON_L3.value == Decimal('200.00') - - # TEXT_MESSAGE (0-0:96.13.0) - assert isinstance(telegram.TEXT_MESSAGE, CosemObject) - assert telegram.TEXT_MESSAGE.unit is None - assert telegram.TEXT_MESSAGE.value is None - - def test_checksum_valid(self): - # No exception is raised. - TelegramParser.validate_checksum(TELEGRAM_V5_EON_HU) - - def test_checksum_invalid(self): - # Remove the electricty used data value. This causes the checksum to - # not match anymore. - corrupted_telegram = TELEGRAM_V5_EON_HU.replace( - '1-0:1.8.1(000047.719*kWh)\r\n', - '' - ) - - with self.assertRaises(InvalidChecksumError): - TelegramParser.validate_checksum(corrupted_telegram) - - def test_checksum_missing(self): - # Remove the checksum value causing a ParseError. - corrupted_telegram = TELEGRAM_V5_EON_HU.replace('!99DA\r\n', '') - with self.assertRaises(ParseError): - TelegramParser.validate_checksum(corrupted_telegram) From 2f1e080df35fa05f5dca8e9b082054a44b37ac58 Mon Sep 17 00:00:00 2001 From: balazs92117 Date: Tue, 1 Aug 2023 09:23:17 +0200 Subject: [PATCH 202/226] Refactor of name mapping, added E.ON Hungary (#137) * Added EON HUNGARY specification * refactoring obis name mapping --- dsmr_parser/clients/protocol.py | 7 +- dsmr_parser/clients/socket_.py | 7 +- dsmr_parser/obis_name_mapping.py | 98 -- dsmr_parser/obis_references.py | 22 + dsmr_parser/objects.py | 14 +- dsmr_parser/parsers.py | 22 +- dsmr_parser/telegram_specifications.py | 1691 ++++++++++++++++++++---- test/example_telegrams.py | 52 + test/objects/test_mbusdevice.py | 24 +- test/objects/test_telegram.py | 8 +- test/test_parse_v5_eon_hungary.py | 308 +++++ 11 files changed, 1843 insertions(+), 410 deletions(-) delete mode 100644 dsmr_parser/obis_name_mapping.py create mode 100644 test/test_parse_v5_eon_hungary.py diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index a7fb74f..4605fcf 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -21,7 +21,9 @@ 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): +# pylama noqa - because of "complex" (too long) if-elif-else. +# Match - case might be a solution but it is not available in <3.10 +def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, **kwargs): #noqa """Creates a DSMR asyncio protocol.""" if dsmr_version == '2.2': @@ -51,6 +53,9 @@ def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, elif dsmr_version == 'ISKRA_IE': specification = telegram_specifications.ISKRA_IE serial_settings = SERIAL_SETTINGS_V5 + elif dsmr_version == '5EONHU': + specification = telegram_specifications.EON_HUNGARY + serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) diff --git a/dsmr_parser/clients/socket_.py b/dsmr_parser/clients/socket_.py index b7490ec..8ee16fa 100644 --- a/dsmr_parser/clients/socket_.py +++ b/dsmr_parser/clients/socket_.py @@ -43,7 +43,12 @@ def read(self): continue for data in lines: - self.telegram_buffer.append(data.decode('ascii')) + try: + self.telegram_buffer.append(data.decode('ascii')) + except UnicodeDecodeError: + # Some garbage came through the channel + # E.g.: Happens at EON_HUNGARY, but only once at the start of the socket. + logger.error('Failed to parse telegram due to unicode decode error') for telegram in self.telegram_buffer.get_all(): try: diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py deleted file mode 100644 index 44ebb82..0000000 --- a/dsmr_parser/obis_name_mapping.py +++ /dev/null @@ -1,98 +0,0 @@ -from dsmr_parser import obis_references as obis - -""" -dsmr_parser.obis_name_mapping -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This module contains a mapping of obis references to names. -""" - -EN = { - obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', - obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', - obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL', - obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL: 'ELECTRICITY_REACTIVE_IMPORTED_TOTAL', - obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1', - obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2', - obis.ELECTRICITY_EXPORTED_TOTAL: 'ELECTRICITY_EXPORTED_TOTAL', - obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL: 'ELECTRICITY_REACTIVE_EXPORTED_TOTAL', - obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1', - obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2', - obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF', - obis.CURRENT_REACTIVE_EXPORTED: 'CURRENT_REACTIVE_EXPORTED', - obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1: 'ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1', - obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2: 'ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2', - obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1: 'ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1', - obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2: 'ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2', - obis.CURRENT_REACTIVE_IMPORTED: 'CURRENT_REACTIVE_IMPORTED', - obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER', - obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE', - obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY', - obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT', - obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT', - obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG', - obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT', - obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT', - obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT', - obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT', - obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT', - obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT', - obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1', - obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2', - obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3', - obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1', - obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2', - obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3', - obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE', - obis.TEXT_MESSAGE: 'TEXT_MESSAGE', - obis.DEVICE_TYPE: 'DEVICE_TYPE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', - obis.INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE', - obis.INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE', - obis.INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE', - obis.INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE', - obis.INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE', - obis.INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE', - obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS', - obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING', - obis.GAS_METER_READING: 'GAS_METER_READING', - obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', - obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', - obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS', - obis.BELGIUM_VERSION_INFORMATION: 'BELGIUM_VERSION_INFORMATION', - obis.BELGIUM_EQUIPMENT_IDENTIFIER: 'BELGIUM_EQUIPMENT_IDENTIFIER', - obis.BELGIUM_CURRENT_AVERAGE_DEMAND: 'BELGIUM_CURRENT_AVERAGE_DEMAND', - obis.BELGIUM_MAXIMUM_DEMAND_MONTH: 'BELGIUM_MAXIMUM_DEMAND_MONTH', - obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS: 'BELGIUM_MAXIMUM_DEMAND_13_MONTHS', - obis.BELGIUM_MAX_POWER_PER_PHASE: 'BELGIUM_MAX_POWER_PER_PHASE', - obis.BELGIUM_MAX_CURRENT_PER_PHASE: 'BELGIUM_MAX_CURRENT_PER_PHASE', - obis.BELGIUM_MBUS1_DEVICE_TYPE: 'BELGIUM_MBUS1_DEVICE_TYPE', - obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER', - obis.BELGIUM_MBUS1_VALVE_POSITION: 'BELGIUM_MBUS1_VALVE_POSITION', - obis.BELGIUM_MBUS1_METER_READING1: 'BELGIUM_MBUS1_METER_READING1', - obis.BELGIUM_MBUS1_METER_READING2: 'BELGIUM_MBUS1_METER_READING2', - obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER', - obis.BELGIUM_MBUS2_VALVE_POSITION: 'BELGIUM_MBUS2_VALVE_POSITION', - obis.BELGIUM_MBUS2_METER_READING1: 'BELGIUM_MBUS2_METER_READING1', - obis.BELGIUM_MBUS2_METER_READING2: 'BELGIUM_MBUS2_METER_READING2', - obis.BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: 'BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER', - obis.BELGIUM_MBUS3_VALVE_POSITION: 'BELGIUM_MBUS3_VALVE_POSITION', - obis.BELGIUM_MBUS3_METER_READING1: 'BELGIUM_MBUS3_METER_READING1', - obis.BELGIUM_MBUS3_METER_READING2: 'BELGIUM_MBUS3_METER_READING2', - obis.BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER: 'BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER', - obis.BELGIUM_MBUS4_VALVE_POSITION: 'BELGIUM_MBUS4_VALVE_POSITION', - obis.BELGIUM_MBUS4_METER_READING1: 'BELGIUM_MBUS4_METER_READING1', - obis.BELGIUM_MBUS4_METER_READING2: 'BELGIUM_MBUS4_METER_READING2', - obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER', - obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER', - obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE', - obis.Q3D_EQUIPMENT_SERIALNUMBER: 'Q3D_EQUIPMENT_SERIALNUMBER', - obis.BELGIUM_MBUS2_DEVICE_TYPE: 'BELGIUM_MBUS2_DEVICE_TYPE' -} - -REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index c710504..0ca9654 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -10,8 +10,12 @@ P1_MESSAGE_TIMESTAMP = r'^\d-\d:1\.0\.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_USED_TARIFF_3 = r'^\d-\d:1\.8\.3.+?\r\n' +ELECTRICITY_USED_TARIFF_4 = r'^\d-\d:1\.8\.4.+?\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_DELIVERED_TARIFF_3 = r'^\d-\d:2\.8\.3.+?\r\n' +ELECTRICITY_DELIVERED_TARIFF_4 = r'^\d-\d:2\.8\.4.+?\r\n' CURRENT_REACTIVE_IMPORTED = r'^\d-\d:3\.7\.0.+?\r\n' ELECTRICITY_REACTIVE_IMPORTED_TOTAL = r'^\d-\d:3\.8\.0.+?\r\n' ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1 = r'^\d-\d:3\.8\.1.+?\r\n' @@ -122,3 +126,21 @@ 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 + +# EON Hungary +EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 = r'^\d-\d:5\.8\.0.+?\r\n' +EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 = r'^\d-\d:6\.8\.0.+?\r\n' +EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 = r'^\d-\d:7\.8\.0.+?\r\n' +EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 = r'^\d-\d:8\.8\.0.+?\r\n' +EON_HU_ELECTRICITY_COMBINED = r'^\d-\d:15\.8\.0.+?\r\n' +EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL = r'^\d-\d:13\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_POWER_FACTOR_L1 = r'^\d-\d:33\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_POWER_FACTOR_L2 = r'^\d-\d:53\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_POWER_FACTOR_L3 = r'^\d-\d:73\.7\.0.+?\r\n' +EON_HU_FREQUENCY = r'^\d-\d:14\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 = r'^\d-\d:5\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 = r'^\d-\d:6\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 = r'^\d-\d:7\.7\.0.+?\r\n' +EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 = r'^\d-\d:8\.7\.0.+?\r\n' +EON_HU_MAX_POWER_ON_L2 = r'^\d-\d:51\.4\.0.+?\r\n' +EON_HU_MAX_POWER_ON_L3 = r'^\d-\d:71\.4\.0.+?\r\n' diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 583c7f7..98e1c5d 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -5,8 +5,6 @@ import pytz -from dsmr_parser import obis_name_mapping - class Telegram(dict): """ @@ -27,22 +25,21 @@ def __init__(self, *args, **kwargs): self._mbus_devices = [] super().__init__(*args, **kwargs) - def add(self, obis_reference, dsmr_object): + def add(self, obis_reference, dsmr_object, obis_name): # Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER - obis_name = obis_name_mapping.EN[obis_reference] setattr(self, obis_name, dsmr_object) if obis_name not in self._item_names: # TODO repeating obis references self._item_names.append(obis_name) # TODO isinstance check: MaxDemandParser (BELGIUM_MAXIMUM_DEMAND_13_MONTHS) returns a list if isinstance(dsmr_object, DSMRObject) and dsmr_object.is_mbus_reading: - self._add_mbus(obis_reference, dsmr_object) + self._add_mbus(obis_reference, dsmr_object, obis_name) # Fill dict which is only used for backwards compatibility if obis_reference not in self: self[obis_reference] = dsmr_object - def _add_mbus(self, obis_reference, dsmr_object): + def _add_mbus(self, obis_reference, dsmr_object, obis_name): """ The given DsmrObject is assumed to be Mbus related and will be grouped into a MbusDevice. Grouping is done by the DsmrObject channel ID. @@ -55,7 +52,7 @@ def _add_mbus(self, obis_reference, dsmr_object): mbus_device = MbusDevice(channel_id=channel_id) self._mbus_devices.append(mbus_device) - mbus_device.add(obis_reference, dsmr_object) + mbus_device.add(obis_reference, dsmr_object, obis_name) if not hasattr(self, 'MBUS_DEVICES'): setattr(self, 'MBUS_DEVICES', self._mbus_devices) @@ -331,10 +328,9 @@ def __init__(self, channel_id): self.channel_id = channel_id self._item_names = [] - def add(self, obis_reference, dsmr_object): + def add(self, obis_reference, dsmr_object, obis_name): # Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER # Also keep track of the added names internally - obis_name = obis_name_mapping.EN[obis_reference] setattr(self, obis_name, dsmr_object) self._item_names.append(obis_name) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 3b48cfa..61515fc 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -29,8 +29,8 @@ def __init__(self, telegram_specification, apply_checksum_validation=True): self.telegram_specification = telegram_specification # Regexes are compiled once to improve performance self.telegram_specification_regexes = { - signature: re.compile(signature, re.DOTALL | re.MULTILINE) - for signature in self.telegram_specification['objects'].keys() + object["obis_reference"]: re.compile(object["obis_reference"], re.DOTALL | re.MULTILINE) + for object in self.telegram_specification['objects'] } def parse(self, telegram_data, encryption_key="", authentication_key="", throw_ex=False): # noqa: C901 @@ -84,25 +84,31 @@ def parse(self, telegram_data, encryption_key="", authentication_key="", throw_e telegram = Telegram() - for signature, parser in self.telegram_specification['objects'].items(): - pattern = self.telegram_specification_regexes[signature] + for object in self.telegram_specification['objects']: + pattern = self.telegram_specification_regexes[object["obis_reference"]] matches = pattern.findall(telegram_data) # Some signatures are optional and may not be present, # so only parse lines that match for match in matches: try: - dsmr_object = parser.parse(match) + dsmr_object = object["value_parser"].parse(match) except ParseError: - logger.error("ignore line with signature {}, because parsing failed.".format(signature), - exc_info=True) + logger.error( + "ignore line with signature {}, because parsing failed.".format(object["obis_reference"]), + exc_info=True + ) if throw_ex: raise except Exception as err: logger.error("Unexpected {}: {}".format(type(err), err)) raise else: - telegram.add(obis_reference=signature, dsmr_object=dsmr_object) + telegram.add( + obis_reference=object["obis_reference"], + dsmr_object=dsmr_object, + obis_name=object["value_name"] + ) return telegram diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index f9f8a79..65ede2f 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -16,326 +16,1451 @@ V2_2 = { 'checksum_support': False, - 'objects': { - obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), - obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), - obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), - obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), - obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), - obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), - obis.DEVICE_TYPE: CosemParser(ValueParser(str)), - obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), - obis.GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(str), # changed to str see issue60 - ValueParser(int), - ValueParser(int), - ValueParser(str), # obis ref - ValueParser(str), # unit, position 5 - ValueParser(Decimal), # meter reading, position 6 - ), - } + 'objects': [ + { + 'obis_reference': obis.EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'EQUIPMENT_IDENTIFIER' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'ELECTRICITY_ACTIVE_TARIFF' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_USAGE' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_DELIVERY' + }, + { + 'obis_reference': obis.ACTUAL_TRESHOLD_ELECTRICITY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY' + }, + { + 'obis_reference': obis.ACTUAL_SWITCH_POSITION, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'ACTUAL_SWITCH_POSITION' + }, + { + 'obis_reference': obis.TEXT_MESSAGE_CODE, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'TEXT_MESSAGE_CODE' + }, + { + 'obis_reference': obis.TEXT_MESSAGE, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'TEXT_MESSAGE' + }, + { + 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'EQUIPMENT_IDENTIFIER_GAS' + }, + { + 'obis_reference': obis.DEVICE_TYPE, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'DEVICE_TYPE' + }, + { + 'obis_reference': obis.VALVE_POSITION_GAS, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'VALVE_POSITION_GAS' + }, + { + 'obis_reference': obis.GAS_METER_READING, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(str), # changed to str see issue60 + ValueParser(int), + ValueParser(int), + ValueParser(str), # obis ref + ValueParser(str), # unit, position 5 + ValueParser(Decimal), # meter reading, position 6 + ), + 'value_name': 'GAS_METER_READING' + }, + ] } V3 = V2_2 V4 = { 'checksum_support': True, - 'objects': { - obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), - obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), - obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), - obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: - ProfileGenericParser(BUFFER_TYPES, - PG_HEAD_PARSERS, - PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), - obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)), - obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), - obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), - obis.DEVICE_TYPE: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), - obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ) - } + 'objects': [ + { + 'obis_reference': obis.P1_MESSAGE_HEADER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'P1_MESSAGE_HEADER' + }, + { + 'obis_reference': obis.P1_MESSAGE_TIMESTAMP, + 'value_parser': CosemParser(ValueParser(timestamp)), + 'value_name': 'P1_MESSAGE_TIMESTAMP' + }, + { + 'obis_reference': obis.EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'EQUIPMENT_IDENTIFIER' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'ELECTRICITY_ACTIVE_TARIFF' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_USAGE' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_DELIVERY' + }, + { + 'obis_reference': obis.SHORT_POWER_FAILURE_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'SHORT_POWER_FAILURE_COUNT' + }, + { + 'obis_reference': obis.LONG_POWER_FAILURE_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'LONG_POWER_FAILURE_COUNT' + }, + { + 'obis_reference': obis.POWER_EVENT_FAILURE_LOG, + 'value_parser': ProfileGenericParser( + BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS + ), + 'value_name': 'POWER_EVENT_FAILURE_LOG' + }, + { + 'obis_reference': obis.VOLTAGE_SAG_L1_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SAG_L1_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SAG_L2_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SAG_L2_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SAG_L3_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SAG_L3_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SWELL_L1_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SWELL_L1_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SWELL_L2_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SWELL_L2_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SWELL_L3_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SWELL_L3_COUNT' + }, + { + 'obis_reference': obis.TEXT_MESSAGE_CODE, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'TEXT_MESSAGE_CODE' + }, + { + 'obis_reference': obis.TEXT_MESSAGE, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'TEXT_MESSAGE' + }, + { + 'obis_reference': obis.DEVICE_TYPE, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'DEVICE_TYPE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L2' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L3' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE' + }, + { + 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'EQUIPMENT_IDENTIFIER_GAS' + }, + { + 'obis_reference': obis.HOURLY_GAS_METER_READING, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'HOURLY_GAS_METER_READING' + }, + ] } V5 = { 'checksum_support': True, - 'objects': { - obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), - obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), - obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), - obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - obis.POWER_EVENT_FAILURE_LOG: - ProfileGenericParser(BUFFER_TYPES, - PG_HEAD_PARSERS, - PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), - obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)), - obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), - obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), - obis.DEVICE_TYPE: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), - obis.HOURLY_GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ) - } + 'objects': [ + { + 'obis_reference': obis.P1_MESSAGE_HEADER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'P1_MESSAGE_HEADER' + }, + { + 'obis_reference': obis.P1_MESSAGE_TIMESTAMP, + 'value_parser': CosemParser(ValueParser(timestamp)), + 'value_name': 'P1_MESSAGE_TIMESTAMP' + }, + { + 'obis_reference': obis.EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'EQUIPMENT_IDENTIFIER' + }, + { + 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_IMPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'ELECTRICITY_ACTIVE_TARIFF' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_USAGE' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_DELIVERY' + }, + { + 'obis_reference': obis.LONG_POWER_FAILURE_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'LONG_POWER_FAILURE_COUNT' + }, + { + 'obis_reference': obis.SHORT_POWER_FAILURE_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'SHORT_POWER_FAILURE_COUNT' + }, + { + 'obis_reference': obis.POWER_EVENT_FAILURE_LOG, + 'value_parser': ProfileGenericParser( + BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS + ), + 'value_name': 'POWER_EVENT_FAILURE_LOG' + }, + { + 'obis_reference': obis.VOLTAGE_SAG_L1_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SAG_L1_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SAG_L2_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SAG_L2_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SAG_L3_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SAG_L3_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SWELL_L1_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SWELL_L1_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SWELL_L2_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SWELL_L2_COUNT' + }, + { + 'obis_reference': obis.VOLTAGE_SWELL_L3_COUNT, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'VOLTAGE_SWELL_L3_COUNT' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L2' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L3' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L2' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L3' + }, + { + 'obis_reference': obis.TEXT_MESSAGE, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'TEXT_MESSAGE' + }, + { + 'obis_reference': obis.DEVICE_TYPE, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'DEVICE_TYPE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE' + }, + { + 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'EQUIPMENT_IDENTIFIER_GAS' + }, + { + 'obis_reference': obis.HOURLY_GAS_METER_READING, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'HOURLY_GAS_METER_READING' + }, + ] } ALL = (V2_2, V3, V4, V5) BELGIUM_FLUVIUS = { 'checksum_support': True, - 'objects': { - obis.BELGIUM_VERSION_INFORMATION: CosemParser(ValueParser(str)), - obis.BELGIUM_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), - obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), - obis.BELGIUM_CURRENT_AVERAGE_DEMAND: CosemParser(ValueParser(Decimal)), - obis.BELGIUM_MAXIMUM_DEMAND_MONTH: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS: MaxDemandParser(), - obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), - obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(int)), - obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), - obis.BELGIUM_MAX_POWER_PER_PHASE: CosemParser(ValueParser(Decimal)), - obis.BELGIUM_MAX_CURRENT_PER_PHASE: CosemParser(ValueParser(Decimal)), - obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), - obis.BELGIUM_MBUS1_DEVICE_TYPE: CosemParser(ValueParser(int)), - obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.BELGIUM_MBUS1_VALVE_POSITION: CosemParser(ValueParser(int)), - obis.BELGIUM_MBUS1_METER_READING1: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MBUS1_METER_READING2: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MBUS2_DEVICE_TYPE: CosemParser(ValueParser(int)), - obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.BELGIUM_MBUS2_VALVE_POSITION: CosemParser(ValueParser(int)), - obis.BELGIUM_MBUS2_METER_READING1: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MBUS2_METER_READING2: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MBUS3_DEVICE_TYPE: CosemParser(ValueParser(int)), - obis.BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.BELGIUM_MBUS3_VALVE_POSITION: CosemParser(ValueParser(int)), - obis.BELGIUM_MBUS3_METER_READING1: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MBUS3_METER_READING2: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MBUS4_DEVICE_TYPE: CosemParser(ValueParser(int)), - obis.BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.BELGIUM_MBUS4_VALVE_POSITION: CosemParser(ValueParser(int)), - obis.BELGIUM_MBUS4_METER_READING1: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - obis.BELGIUM_MBUS4_METER_READING2: MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - } + 'objects': [ + { + 'obis_reference': obis.BELGIUM_VERSION_INFORMATION, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'BELGIUM_VERSION_INFORMATION' + }, + { + 'obis_reference': obis.BELGIUM_EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'BELGIUM_EQUIPMENT_IDENTIFIER' + }, + { + 'obis_reference': obis.P1_MESSAGE_TIMESTAMP, + 'value_parser': CosemParser(ValueParser(timestamp)), + 'value_name': 'P1_MESSAGE_TIMESTAMP' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'ELECTRICITY_ACTIVE_TARIFF' + }, + { + 'obis_reference': obis.BELGIUM_CURRENT_AVERAGE_DEMAND, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'BELGIUM_CURRENT_AVERAGE_DEMAND' + }, + { + 'obis_reference': obis.BELGIUM_MAXIMUM_DEMAND_MONTH, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'BELGIUM_MAXIMUM_DEMAND_MONTH' + }, + { + 'obis_reference': obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS, + 'value_parser': MaxDemandParser(), + 'value_name': 'BELGIUM_MAXIMUM_DEMAND_13_MONTHS' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_USAGE' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_DELIVERY' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L2' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L3' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L2' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L3' + }, + { + 'obis_reference': obis.ACTUAL_SWITCH_POSITION, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'ACTUAL_SWITCH_POSITION' + }, + { + 'obis_reference': obis.ACTUAL_TRESHOLD_ELECTRICITY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY' + }, + { + 'obis_reference': obis.BELGIUM_MAX_POWER_PER_PHASE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'BELGIUM_MAX_POWER_PER_PHASE' + }, + { + 'obis_reference': obis.BELGIUM_MAX_CURRENT_PER_PHASE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'BELGIUM_MAX_CURRENT_PER_PHASE' + }, + { + 'obis_reference': obis.TEXT_MESSAGE, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'TEXT_MESSAGE' + }, + { + 'obis_reference': obis.BELGIUM_MBUS1_DEVICE_TYPE, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'BELGIUM_MBUS1_DEVICE_TYPE' + }, + { + 'obis_reference': obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER' + }, + { + 'obis_reference': obis.BELGIUM_MBUS1_VALVE_POSITION, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'BELGIUM_MBUS1_VALVE_POSITION' + }, + { + 'obis_reference': obis.BELGIUM_MBUS1_METER_READING1, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'BELGIUM_MBUS1_METER_READING1' + }, + { + 'obis_reference': obis.BELGIUM_MBUS1_METER_READING2, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'BELGIUM_MBUS1_METER_READING2' + }, + { + 'obis_reference': obis.BELGIUM_MBUS2_DEVICE_TYPE, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'BELGIUM_MBUS2_DEVICE_TYPE' + }, + { + 'obis_reference': obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER' + }, + { + 'obis_reference': obis.BELGIUM_MBUS2_VALVE_POSITION, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'BELGIUM_MBUS2_VALVE_POSITION' + }, + { + 'obis_reference': obis.BELGIUM_MBUS2_METER_READING1, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'BELGIUM_MBUS2_METER_READING1' + }, + { + 'obis_reference': obis.BELGIUM_MBUS2_METER_READING2, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'BELGIUM_MBUS2_METER_READING2' + }, + { + 'obis_reference': obis.BELGIUM_MBUS3_DEVICE_TYPE, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'BELGIUM_MBUS3_DEVICE_TYPE' + }, + { + 'obis_reference': obis.BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER' + }, + { + 'obis_reference': obis.BELGIUM_MBUS3_VALVE_POSITION, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'BELGIUM_MBUS3_VALVE_POSITION' + }, + { + 'obis_reference': obis.BELGIUM_MBUS3_METER_READING1, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'BELGIUM_MBUS3_METER_READING1' + }, + { + 'obis_reference': obis.BELGIUM_MBUS3_METER_READING2, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'BELGIUM_MBUS3_METER_READING2' + }, + { + 'obis_reference': obis.BELGIUM_MBUS4_DEVICE_TYPE, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'BELGIUM_MBUS4_DEVICE_TYPE' + }, + { + 'obis_reference': obis.BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER' + }, + { + 'obis_reference': obis.BELGIUM_MBUS4_VALVE_POSITION, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'BELGIUM_MBUS4_VALVE_POSITION' + }, + { + 'obis_reference': obis.BELGIUM_MBUS4_METER_READING1, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'BELGIUM_MBUS4_METER_READING1' + }, + { + 'obis_reference': obis.BELGIUM_MBUS4_METER_READING2, + 'value_parser': MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ), + 'value_name': 'BELGIUM_MBUS4_METER_READING2' + }, + ] } LUXEMBOURG_SMARTY = deepcopy(V5) -LUXEMBOURG_SMARTY['objects'].update({ - obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), -}) +LUXEMBOURG_SMARTY['objects'].extend([ + { + 'obis_reference': obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'LUXEMBOURG_EQUIPMENT_IDENTIFIER' + }, + # This is already presented in V5, with the same data + # { + # 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL, + # 'value_parser': CosemParser(ValueParser(Decimal)), + # 'value_name': 'ELECTRICITY_IMPORTED_TOTAL' + # }, + { + 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_EXPORTED_TOTAL' + } +]) # Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/ # branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf SWEDEN = { 'checksum_support': True, - 'objects': { - obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), - obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), - obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_REACTIVE_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)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), - } + 'objects': [ + { + 'obis_reference': obis.P1_MESSAGE_HEADER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'P1_MESSAGE_HEADER' + }, + { + 'obis_reference': obis.P1_MESSAGE_TIMESTAMP, + 'value_parser': CosemParser(ValueParser(timestamp)), + 'value_name': 'P1_MESSAGE_TIMESTAMP' + }, + { + 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_IMPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_EXPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TOTAL' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_USAGE' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_DELIVERY' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L2' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L3' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L2' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L3' + } + ] } Q3D = { "checksum_support": False, - "objects": { - obis.Q3D_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - obis.Q3D_EQUIPMENT_STATE: CosemParser(ValueParser(str)), - obis.Q3D_EQUIPMENT_SERIALNUMBER: CosemParser(ValueParser(str)), - }, + "objects": [ + { + 'obis_reference': obis.Q3D_EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'Q3D_EQUIPMENT_IDENTIFIER' + }, + { + 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_IMPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_EXPORTED_TOTAL' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_USAGE' + }, + { + 'obis_reference': obis.Q3D_EQUIPMENT_STATE, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'Q3D_EQUIPMENT_STATE' + }, + { + 'obis_reference': obis.Q3D_EQUIPMENT_SERIALNUMBER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'Q3D_EQUIPMENT_SERIALNUMBER' + }, + ] } SAGEMCOM_T210_D_R = { "general_global_cipher": True, "checksum_support": True, - 'objects': { - obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), - obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), - obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - - obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.CURRENT_REACTIVE_IMPORTED: CosemParser(ValueParser(Decimal)), - - obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - - obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.CURRENT_REACTIVE_EXPORTED: CosemParser(ValueParser(Decimal)), - } + 'objects': [ + { + 'obis_reference': obis.P1_MESSAGE_HEADER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'P1_MESSAGE_HEADER' + }, + { + 'obis_reference': obis.P1_MESSAGE_TIMESTAMP, + 'value_parser': CosemParser(ValueParser(timestamp)), + 'value_name': 'P1_MESSAGE_TIMESTAMP' + }, + { + 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_IMPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_2' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_USAGE' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2' + }, + { + 'obis_reference': obis.CURRENT_REACTIVE_IMPORTED, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_REACTIVE_IMPORTED' + }, + { + 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_EXPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_DELIVERY' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2' + }, + { + 'obis_reference': obis.CURRENT_REACTIVE_EXPORTED, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_REACTIVE_EXPORTED' + } + ] } AUSTRIA_ENERGIENETZE_STEIERMARK = SAGEMCOM_T210_D_R ISKRA_IE = { "checksum_support": False, - 'objects': { - obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), - obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), - obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), - obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), - obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), - obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), - obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), - obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - } + 'objects': [ + { + 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'EQUIPMENT_IDENTIFIER_GAS' + }, + { + 'obis_reference': obis.P1_MESSAGE_TIMESTAMP, + 'value_parser': CosemParser(ValueParser(timestamp)), + 'value_name': 'P1_MESSAGE_TIMESTAMP' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'ELECTRICITY_ACTIVE_TARIFF' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_USAGE' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_DELIVERY' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L2' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L3' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L2' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L3' + }, + { + 'obis_reference': obis.ACTUAL_SWITCH_POSITION, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'ACTUAL_SWITCH_POSITION' + }, + { + 'obis_reference': obis.TEXT_MESSAGE, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'TEXT_MESSAGE' + }, + { + 'obis_reference': obis.EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'EQUIPMENT_IDENTIFIER' + }, + ] +} + +EON_HUNGARY = { + # Revision: 2023.02.10 + # Based on V5 + # Reference: https://www.eon.hu/content/dam/eon/eon-hungary/documents/Lakossagi/aram/muszaki-ugyek/p1_port%20felhaszn_interfesz_taj_%2020230210.pdf # noqa + 'checksum_support': True, + 'objects': [ + { + 'obis_reference': obis.P1_MESSAGE_TIMESTAMP, + 'value_parser': CosemParser(ValueParser(timestamp)), + 'value_name': 'P1_MESSAGE_TIMESTAMP' + }, + { + 'obis_reference': obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'COSEM_LOGICAL_DEVICE_NAME' + }, + { + 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'EQUIPMENT_SERIAL_NUMBER' + }, + { + 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'ELECTRICITY_ACTIVE_TARIFF' + }, + { + 'obis_reference': obis.ACTUAL_SWITCH_POSITION, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'ACTUAL_SWITCH_POSITION' + # This seems to be wrong in documentation, it's not 0-0:96.50.68, but 0-0:96.3.10 + }, + { + 'obis_reference': obis.BELGIUM_MAX_POWER_PER_PHASE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY' + }, + { + 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_IMPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_3' + }, + { + 'obis_reference': obis.ELECTRICITY_USED_TARIFF_4, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_USED_TARIFF_4' + }, + { + 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_EXPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_3' + }, + { + 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_4, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_4' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TOTAL' + }, + { + 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TOTAL' + }, + { + 'obis_reference': obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_TOTAL_Q1' + }, + { + 'obis_reference': obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_TOTAL_Q2' + }, + { + 'obis_reference': obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_TOTAL_Q3' + }, + { + 'obis_reference': obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_REACTIVE_TOTAL_Q4' + }, + { + 'obis_reference': obis.EON_HU_ELECTRICITY_COMBINED, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'ELECTRICITY_COMBINED' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L2' + # Only with 3 phase meters + }, + { + 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_VOLTAGE_L3' + # Only with 3 phase meters + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L1' + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L2' + # Only with 3 phase meters + }, + { + 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_CURRENT_L3' + # Only with 3 phase meters + }, + { + 'obis_reference': obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_POWER_FACTOR_TOTAL' + }, + { + 'obis_reference': obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_POWER_FACTOR_L1' + }, + { + 'obis_reference': obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_POWER_FACTOR_L2' + # Only with 3 phase meters + }, + { + 'obis_reference': obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_POWER_FACTOR_L3' + # Only with 3 phase meters + }, + { + 'obis_reference': obis.EON_HU_FREQUENCY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'FREQUENCY' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_USAGE' + }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_DELIVERY' + }, + { + 'obis_reference': obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_Q1' + }, + { + 'obis_reference': obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_Q2' + }, + { + 'obis_reference': obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_Q3' + }, + { + 'obis_reference': obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_Q4' + }, + { + 'obis_reference': obis.BELGIUM_MAX_CURRENT_PER_PHASE, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'MAX_POWER_ON_L1' + }, + { + 'obis_reference': obis.EON_HU_MAX_POWER_ON_L2, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'MAX_POWER_ON_L2' + # Only with 3 phase meters + }, + { + 'obis_reference': obis.EON_HU_MAX_POWER_ON_L3, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'MAX_POWER_ON_L3' + # Only with 3 phase meters + }, + # I'm not sure which datas does this line containes. It should be the data of last minute of last month. + # { + # 'obis_reference': obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS, + # 'value_parser': NonExistingParser( + # ValueParser(timestamp), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal), + # ValueParser(Decimal) + # ), + # 'value_name': 'LAST_MONTH_DATA' + # }, + { + 'obis_reference': obis.TEXT_MESSAGE, + 'value_parser': CosemParser(ValueParser(str)), + 'value_name': 'TEXT_MESSAGE' + } + ] } diff --git a/test/example_telegrams.py b/test/example_telegrams.py index d59ce83..b21f6ad 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -311,3 +311,55 @@ '0-1:96.1.1()\r\n' '!AD3B\r\n' ) + +# V5 telegram of EON in Hungary +TELEGRAM_V5_EON_HU = ( + '/SAG5SAG-METER\r\n' + '\r\n' + '0-0:1.0.0(230724150730S)\r\n' + '0-0:42.0.0(53414733303832323030303032313630)\r\n' + '0-0:96.1.0(383930303832323030303032313630)\r\n' + '0-0:96.14.0(0001)\r\n' + '0-0:96.3.10(1)\r\n' + '0-0:17.0.0(90.000*kW)\r\n' + '1-0:1.8.0(000173.640*kWh)\r\n' + '1-0:1.8.1(000047.719*kWh)\r\n' + '1-0:1.8.2(000125.921*kWh)\r\n' + '1-0:1.8.3(000000.000*kWh)\r\n' + '1-0:1.8.4(000000.000*kWh)\r\n' + '1-0:2.8.0(000627.177*kWh)\r\n' + '1-0:2.8.1(000401.829*kWh)\r\n' + '1-0:2.8.2(000225.348*kWh)\r\n' + '1-0:2.8.3(000000.000*kWh)\r\n' + '1-0:2.8.4(000000.000*kWh)\r\n' + '1-0:3.8.0(000000.123*kvarh)\r\n' + '1-0:4.8.0(000303.131*kvarh)\r\n' + '1-0:5.8.0(000000.668*kvarh)\r\n' + '1-0:6.8.0(000000.071*kvarh)\r\n' + '1-0:7.8.0(000160.487*kvarh)\r\n' + '1-0:8.8.0(000143.346*kvarh)\r\n' + '1-0:15.8.0(000800.817*kWh)\r\n' + '1-0:32.7.0(240.4*V)\r\n' + '1-0:52.7.0(239.1*V)\r\n' + '1-0:72.7.0(241.2*V)\r\n' + '1-0:31.7.0(003*A)\r\n' + '1-0:51.7.0(004*A)\r\n' + '1-0:71.7.0(003*A)\r\n' + '1-0:13.7.0(4.556)\r\n' + '1-0:33.7.0(4.591)\r\n' + '1-0:53.7.0(4.542)\r\n' + '1-0:73.7.0(4.552)\r\n' + '1-0:14.7.0(50.00*Hz)\r\n' + '1-0:1.7.0(00.000*kW)\r\n' + '1-0:2.7.0(02.601*kW)\r\n' + '1-0:5.7.0(00.000*kvar)\r\n' + '1-0:6.7.0(00.000*kvar)\r\n' + '1-0:7.7.0(00.504*kvar)\r\n' + '1-0:8.7.0(00.000*kvar)\r\n' + '1-0:31.4.0(200.00*A)\r\n' + '1-0:51.4.0(200.00*A)\r\n' + '1-0:71.4.0(200.00*A)\r\n' + '0-0:98.1.0(230701000000S)(000040.777*kWh)(000008.950*kWh)(000031.827*kWh)(000142.250*kWh)(000111.164*kWh)(000031.086*kWh)(000000.030*kvarh)(000073.988*kvarh)(000000.205*kvarh)(000000.048*kvarh)(000039.199*kvarh)(000035.020*kvarh)(000183.027*kWh)(03.564*kW)(02.156*kW)(03.564*kW)(04.104*kW)(04.104*kW)(03.400*kW)\r\n' + '0-0:96.13.0()\r\n' + '!99DA\r\n' +) diff --git a/test/objects/test_mbusdevice.py b/test/objects/test_mbusdevice.py index b92d4af..03fde3f 100644 --- a/test/objects/test_mbusdevice.py +++ b/test/objects/test_mbusdevice.py @@ -12,19 +12,31 @@ class MbusDeviceTest(unittest.TestCase): def setUp(self): v5_objects = telegram_specifications.V5['objects'] - device_type_parser = v5_objects[obis_references.DEVICE_TYPE] + device_type_parser = [ + object["value_parser"] + for object in v5_objects + if object["obis_reference"] == obis_references.DEVICE_TYPE + ][0] device_type = device_type_parser.parse('0-2:24.1.0(003)\r\n') - equipment_parser = v5_objects[obis_references.EQUIPMENT_IDENTIFIER_GAS] + equipment_parser = [ + object["value_parser"] + for object in v5_objects + if object["obis_reference"] == obis_references.EQUIPMENT_IDENTIFIER_GAS + ][0] equipment = equipment_parser.parse('0-2:96.1.0(4730303339303031393336393930363139)\r\n') - gas_reading_parser = v5_objects[obis_references.HOURLY_GAS_METER_READING] + gas_reading_parser = [ + object["value_parser"] + for object in v5_objects + if object["obis_reference"] == obis_references.HOURLY_GAS_METER_READING + ][0] gas_reading = gas_reading_parser.parse('0-2:24.2.1(200426223001S)(00246.138*m3)\r\n') mbus_device = MbusDevice(channel_id=1) - mbus_device.add(obis_references.DEVICE_TYPE, device_type) - mbus_device.add(obis_references.EQUIPMENT_IDENTIFIER_GAS, equipment) - mbus_device.add(obis_references.HOURLY_GAS_METER_READING, gas_reading) + mbus_device.add(obis_references.DEVICE_TYPE, device_type, "DEVICE_TYPE") + mbus_device.add(obis_references.EQUIPMENT_IDENTIFIER_GAS, equipment, "EQUIPMENT_IDENTIFIER_GAS") + mbus_device.add(obis_references.HOURLY_GAS_METER_READING, gas_reading, "HOURLY_GAS_METER_READING") self.mbus_device = mbus_device diff --git a/test/objects/test_telegram.py b/test/objects/test_telegram.py index 17a6891..3f39d4b 100644 --- a/test/objects/test_telegram.py +++ b/test/objects/test_telegram.py @@ -4,7 +4,7 @@ import pytz from dsmr_parser import telegram_specifications, obis_references -from dsmr_parser import obis_name_mapping + from dsmr_parser.objects import CosemObject from dsmr_parser.objects import MBusObject from dsmr_parser.objects import ProfileGenericObject @@ -314,8 +314,8 @@ def test_instantiate(self): self.item_names_tested.append(testitem_name) # check if all items in telegram V4 specification are covered - V4_name_list = [obis_name_mapping.EN[signature] for signature, parser in - telegram_specifications.V4['objects'].items()] + V4_name_list = [object["value_name"] for object in + telegram_specifications.V4['objects']] V4_name_set = set(V4_name_list) item_names_tested_set = set(self.item_names_tested) @@ -329,7 +329,7 @@ def test_iter(self): break # Verify that the iterator works for at least one value - self.assertEqual(obis_name, obis_name_mapping.EN[obis_references.P1_MESSAGE_HEADER]) + self.assertEqual(obis_name, "P1_MESSAGE_HEADER") self.assertEqual(dsmr_object.value, '50') def test_mbus_devices(self): diff --git a/test/test_parse_v5_eon_hungary.py b/test/test_parse_v5_eon_hungary.py new file mode 100644 index 0000000..08dd6e1 --- /dev/null +++ b/test/test_parse_v5_eon_hungary.py @@ -0,0 +1,308 @@ +from decimal import Decimal + +import datetime +import unittest + +import pytz + +from dsmr_parser import telegram_specifications +from dsmr_parser.exceptions import InvalidChecksumError, ParseError +from dsmr_parser.objects import CosemObject +from dsmr_parser.parsers import TelegramParser +from test.example_telegrams import TELEGRAM_V5_EON_HU + + +class TelegramParserV5EONHUTest(unittest.TestCase): + """ Test parsing of a DSMR v5 EON Hungary telegram. """ + + def test_parse(self): + parser = TelegramParser(telegram_specifications.EON_HUNGARY) + try: + telegram = parser.parse(TELEGRAM_V5_EON_HU, throw_ex=True) + except Exception as ex: + assert False, f"parse trigged an exception {ex}" + + # P1_MESSAGE_TIMESTAMP (0-0:1.0.0) + assert isinstance(telegram.P1_MESSAGE_TIMESTAMP, CosemObject) + assert telegram.P1_MESSAGE_TIMESTAMP.unit is None + assert isinstance(telegram.P1_MESSAGE_TIMESTAMP.value, datetime.datetime) + assert telegram.P1_MESSAGE_TIMESTAMP.value == \ + pytz.timezone("Europe/Budapest").localize(datetime.datetime(2023, 7, 24, 15, 7, 30)) + + # EON_HU_COSEM_LOGICAL_DEVICE_NAME (0-0:42.0.0) + assert isinstance(telegram.COSEM_LOGICAL_DEVICE_NAME, CosemObject) + assert telegram.COSEM_LOGICAL_DEVICE_NAME.unit is None + assert isinstance(telegram.COSEM_LOGICAL_DEVICE_NAME.value, str) + assert telegram.COSEM_LOGICAL_DEVICE_NAME.value == '53414733303832323030303032313630' + + # EON_HU_EQUIPMENT_SERIAL_NUMBER (0-0:96.1.0) + assert isinstance(telegram.EQUIPMENT_SERIAL_NUMBER, CosemObject) + assert telegram.EQUIPMENT_SERIAL_NUMBER.unit is None + assert isinstance(telegram.EQUIPMENT_SERIAL_NUMBER.value, str) + assert telegram.EQUIPMENT_SERIAL_NUMBER.value == '383930303832323030303032313630' + + # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) + assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF, CosemObject) + assert telegram.ELECTRICITY_ACTIVE_TARIFF.unit is None + assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF.value, str) + assert telegram.ELECTRICITY_ACTIVE_TARIFF.value == '0001' + + # ACTUAL_SWITCH_POSITION (0-0:96.3.10) + assert isinstance(telegram.ACTUAL_SWITCH_POSITION, CosemObject) + assert telegram.ACTUAL_SWITCH_POSITION.unit is None + assert isinstance(telegram.ACTUAL_SWITCH_POSITION.value, str) + assert telegram.ACTUAL_SWITCH_POSITION.value == '1' + + # ACTUAL_TRESHOLD_ELECTRICITY (0-0:17.0.0) + assert isinstance(telegram.ACTUAL_TRESHOLD_ELECTRICITY, CosemObject) + assert telegram.ACTUAL_TRESHOLD_ELECTRICITY.unit == 'kW' + assert isinstance(telegram.ACTUAL_TRESHOLD_ELECTRICITY.value, Decimal) + assert telegram.ACTUAL_TRESHOLD_ELECTRICITY.value == Decimal('90.000') + + # ELECTRICITY_IMPORTED_TOTAL (1-0:1.8.0) + assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL, CosemObject) + assert telegram.ELECTRICITY_IMPORTED_TOTAL.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL.value, Decimal) + assert telegram.ELECTRICITY_IMPORTED_TOTAL.value == Decimal('000173.640') + + # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1, CosemObject) + assert telegram.ELECTRICITY_USED_TARIFF_1.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1.value, Decimal) + assert telegram.ELECTRICITY_USED_TARIFF_1.value == Decimal('000047.719') + + # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2, CosemObject) + assert telegram.ELECTRICITY_USED_TARIFF_2.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2.value, Decimal) + assert telegram.ELECTRICITY_USED_TARIFF_2.value == Decimal('000125.921') + + # EON_HU_ELECTRICITY_USED_TARIFF_3 (1-0:1.8.3) + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_3, CosemObject) + assert telegram.ELECTRICITY_USED_TARIFF_3.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_3.value, Decimal) + assert telegram.ELECTRICITY_USED_TARIFF_3.value == Decimal('000000.000') + + # EON_HU_ELECTRICITY_USED_TARIFF_4 (1-0:1.8.4) + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_4, CosemObject) + assert telegram.ELECTRICITY_USED_TARIFF_4.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_USED_TARIFF_4.value, Decimal) + assert telegram.ELECTRICITY_USED_TARIFF_4.value == Decimal('000000.000') + + # ELECTRICITY_EXPORTED_TOTAL (1-0:2.8.0) + assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL, CosemObject) + assert telegram.ELECTRICITY_EXPORTED_TOTAL.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL.value, Decimal) + assert telegram.ELECTRICITY_EXPORTED_TOTAL.value == Decimal('000627.177') + + # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('000401.829') + + # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('000225.348') + + # EON_HU_ELECTRICITY_DELIVERED_TARIFF_3 (1-0:2.8.3) + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_3, CosemObject) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_3.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_3.value, Decimal) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_3.value == Decimal('000000.000') + + # EON_HU_ELECTRICITY_DELIVERED_TARIFF_4 (1-0:2.8.4) + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_4, CosemObject) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_4.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_4.value, Decimal) + assert telegram.ELECTRICITY_DELIVERED_TARIFF_4.value == Decimal('000000.000') + + # ELECTRICITY_REACTIVE_IMPORTED_TOTAL (1-0:3.8.0) + assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL, CosemObject) + assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.unit == 'kvarh' + assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value, Decimal) + assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value == Decimal('000000.123') + + # ELECTRICITY_REACTIVE_EXPORTED_TOTAL (1-0:4.8.0) + assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL, CosemObject) + assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.unit == 'kvarh' + assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value, Decimal) + assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value == Decimal('000303.131') + + # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 (1-0:5.8.0) + assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q1, CosemObject) + assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q1.unit == 'kvarh' + assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q1.value, Decimal) + assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q1.value == Decimal('000000.668') + + # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 (1-0:6.8.0) + assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q2, CosemObject) + assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q2.unit == 'kvarh' + assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q2.value, Decimal) + assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q2.value == Decimal('000000.071') + + # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 (1-0:7.8.0) + assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q3, CosemObject) + assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q3.unit == 'kvarh' + assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q3.value, Decimal) + assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q3.value == Decimal('000160.487') + + # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 (1-0:8.8.0) + assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q4, CosemObject) + assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q4.unit == 'kvarh' + assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q4.value, Decimal) + assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q4.value == Decimal('000143.346') + + # EON_HU_ELECTRICITY_COMBINED (1-0:15.8.0) + assert isinstance(telegram.ELECTRICITY_COMBINED, CosemObject) + assert telegram.ELECTRICITY_COMBINED.unit == 'kWh' + assert isinstance(telegram.ELECTRICITY_COMBINED.value, Decimal) + assert telegram.ELECTRICITY_COMBINED.value == Decimal('000800.817') + + # INSTANTANEOUS_VOLTAGE_L2 (1-0:32.7.0) + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1, CosemObject) + assert telegram.INSTANTANEOUS_VOLTAGE_L1.unit == 'V' + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1.value, Decimal) + assert telegram.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('240.4') + + # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0) + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2, CosemObject) + assert telegram.INSTANTANEOUS_VOLTAGE_L2.unit == 'V' + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2.value, Decimal) + assert telegram.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('239.1') + + # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0) + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3, CosemObject) + assert telegram.INSTANTANEOUS_VOLTAGE_L3.unit == 'V' + assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3.value, Decimal) + assert telegram.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('241.2') + + # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1, CosemObject) + assert telegram.INSTANTANEOUS_CURRENT_L1.unit == 'A' + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1.value, Decimal) + assert telegram.INSTANTANEOUS_CURRENT_L1.value == Decimal('003') + + # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2, CosemObject) + assert telegram.INSTANTANEOUS_CURRENT_L2.unit == 'A' + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2.value, Decimal) + assert telegram.INSTANTANEOUS_CURRENT_L2.value == Decimal('004') + + # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3, CosemObject) + assert telegram.INSTANTANEOUS_CURRENT_L3.unit == 'A' + assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3.value, Decimal) + assert telegram.INSTANTANEOUS_CURRENT_L3.value == Decimal('003') + + # EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL (1-0:13.7.0) + assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL, CosemObject) + assert telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL.unit is None + assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL.value, Decimal) + assert telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL.value == Decimal('4.556') + + # EON_HU_INSTANTANEOUS_POWER_FACTOR_L1 (1-0:33.7.0) + assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L1, CosemObject) + assert telegram.INSTANTANEOUS_POWER_FACTOR_L1.unit is None + assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L1.value, Decimal) + assert telegram.INSTANTANEOUS_POWER_FACTOR_L1.value == Decimal('4.591') + + # EON_HU_INSTANTANEOUS_POWER_FACTOR_L2 (1-0:53.7.0) + assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L2, CosemObject) + assert telegram.INSTANTANEOUS_POWER_FACTOR_L2.unit is None + assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L2.value, Decimal) + assert telegram.INSTANTANEOUS_POWER_FACTOR_L2.value == Decimal('4.542') + + # EON_HU_INSTANTANEOUS_POWER_FACTOR_L3 (1-0:73.7.0) + assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L3, CosemObject) + assert telegram.INSTANTANEOUS_POWER_FACTOR_L3.unit is None + assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L3.value, Decimal) + assert telegram.INSTANTANEOUS_POWER_FACTOR_L3.value == Decimal('4.552') + + # EON_HU_FREQUENCY (1-0:14.7.0) + assert isinstance(telegram.FREQUENCY, CosemObject) + assert telegram.FREQUENCY.unit == "Hz" + assert isinstance(telegram.FREQUENCY.value, Decimal) + assert telegram.FREQUENCY.value == Decimal('50.00') + + # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) + assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE, CosemObject) + assert telegram.CURRENT_ELECTRICITY_USAGE.unit == 'kW' + assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE.value, Decimal) + assert telegram.CURRENT_ELECTRICITY_USAGE.value == Decimal('00.000') + + # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) + assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY, CosemObject) + assert telegram.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW' + assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY.value, Decimal) + assert telegram.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('02.601') + + # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 (1-0:5.7.0) + assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q1, CosemObject) + assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q1.unit == 'kvar' + assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q1.value, Decimal) + assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q1.value == Decimal('00.000') + + # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 (1-0:6.7.0) + assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q2, CosemObject) + assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q2.unit == 'kvar' + assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q2.value, Decimal) + assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q2.value == Decimal('00.000') + + # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 (1-0:7.7.0) + assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q3, CosemObject) + assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q3.unit == 'kvar' + assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q3.value, Decimal) + assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q3.value == Decimal('00.504') + + # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 (1-0:8.7.0) + assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q4, CosemObject) + assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.unit == 'kvar' + assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.value, Decimal) + assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.value == Decimal('00.000') + + # EON_HU_MAX_POWER_ON_L1 (1-0:31.4.0) + assert isinstance(telegram.MAX_POWER_ON_L1, CosemObject) + assert telegram.MAX_POWER_ON_L1.unit == 'A' + assert isinstance(telegram.MAX_POWER_ON_L1.value, Decimal) + assert telegram.MAX_POWER_ON_L1.value == Decimal('200.00') + + # EON_HU_MAX_POWER_ON_L2 (1-0:31.4.0) + assert isinstance(telegram.MAX_POWER_ON_L2, CosemObject) + assert telegram.MAX_POWER_ON_L2.unit == 'A' + assert isinstance(telegram.MAX_POWER_ON_L2.value, Decimal) + assert telegram.MAX_POWER_ON_L2.value == Decimal('200.00') + + # EON_HU_MAX_POWER_ON_L3 (1-0:31.4.0) + assert isinstance(telegram.MAX_POWER_ON_L3, CosemObject) + assert telegram.MAX_POWER_ON_L3.unit == 'A' + assert isinstance(telegram.MAX_POWER_ON_L3.value, Decimal) + assert telegram.MAX_POWER_ON_L3.value == Decimal('200.00') + + # TEXT_MESSAGE (0-0:96.13.0) + assert isinstance(telegram.TEXT_MESSAGE, CosemObject) + assert telegram.TEXT_MESSAGE.unit is None + assert telegram.TEXT_MESSAGE.value is None + + def test_checksum_valid(self): + # No exception is raised. + TelegramParser.validate_checksum(TELEGRAM_V5_EON_HU) + + def test_checksum_invalid(self): + # Remove the electricty used data value. This causes the checksum to + # not match anymore. + corrupted_telegram = TELEGRAM_V5_EON_HU.replace( + '1-0:1.8.1(000047.719*kWh)\r\n', + '' + ) + + with self.assertRaises(InvalidChecksumError): + TelegramParser.validate_checksum(corrupted_telegram) + + def test_checksum_missing(self): + # Remove the checksum value causing a ParseError. + corrupted_telegram = TELEGRAM_V5_EON_HU.replace('!99DA\r\n', '') + with self.assertRaises(ParseError): + TelegramParser.validate_checksum(corrupted_telegram) From 88923622a916877cf264de2b849bba0b94c3a347 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 1 Aug 2023 09:26:40 +0200 Subject: [PATCH 203/226] Release 1.3.0 --- CHANGELOG.rst | 5 +++++ setup.py | 2 +- test/test_parse_v5.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7d97743..b1e9648 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Change Log ---------- +**1.3.0** (2023-08-01) + +- added E.ON Hungary; refactored DSMR specifications to fix obis reference conflicts + (`PR #137 `_ by `balazs92117 `_) + **1.2.4** (2023-07-11) - EQUIPMENT IDENTIFIER is wrong for Fluvius meters when other mbus devices are present diff --git a/setup.py b/setup.py index 86d160b..14ca9b6 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.2.4', + version='1.3.0', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index f977e00..1736037 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -21,7 +21,7 @@ def test_parse(self): telegram = parser.parse(TELEGRAM_V5, throw_ex=True) except Exception as ex: assert False, f"parse trigged an exception {ex}" - + print('test: ', type(telegram.P1_MESSAGE_HEADER), telegram.P1_MESSAGE_HEADER.__dict__) # P1_MESSAGE_HEADER (1-3:0.2.8) assert isinstance(telegram.P1_MESSAGE_HEADER, CosemObject) assert telegram.P1_MESSAGE_HEADER.unit is None From 389bcefb5012d992845ff62c43b6fbbc8f2caeaf Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Thu, 2 Nov 2023 15:51:35 +0100 Subject: [PATCH 204/226] Fix parsing peak usage when value is not set Sometimes peak usage is not yet visible in the 13 months history code. It gives a 0.0kW value with timestamp (632525252525W). Also the peak usage value can be invalid, handle this case also. --- dsmr_parser/objects.py | 47 ++++++++----- test/example_telegrams.py | 46 ++++++++++++- test/test_parse_fluvius.py | 134 ++++++++++++++++++++++++++++++++++++- 3 files changed, 206 insertions(+), 21 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 98e1c5d..7d7e21c 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -140,22 +140,25 @@ def unit(self): return self.values[1]['unit'] def __str__(self): + timestamp = self.datetime + if isinstance(timestamp, datetime.datetime): + timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat() output = "{}\t[{}] at {}".format( str(self.value), str(self.unit), - str(self.datetime.astimezone().astimezone(pytz.utc).isoformat()) + str(timestamp) ) return output def to_json(self): timestamp = self.datetime - if isinstance(self.datetime, datetime.datetime): - timestamp = self.datetime.astimezone().astimezone(pytz.utc).isoformat() + if isinstance(timestamp, datetime.datetime): + timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat() value = self.value - if isinstance(self.value, datetime.datetime): - value = self.value.astimezone().astimezone(pytz.utc).isoformat() - if isinstance(self.value, Decimal): - value = float(self.value) + if isinstance(value, datetime.datetime): + value = value.astimezone().astimezone(pytz.utc).isoformat() + if isinstance(value, Decimal): + value = float(value) output = { 'datetime': timestamp, 'value': value, @@ -183,23 +186,33 @@ def unit(self): return self.values[2]['unit'] def __str__(self): + timestamp = self.datetime + if isinstance(timestamp, datetime.datetime): + timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat() + timestamp_occurred = self.occurred + if isinstance(timestamp_occurred, datetime.datetime): + timestamp_occurred = timestamp_occurred.astimezone().astimezone(pytz.utc).isoformat() + value = self.value + if isinstance(value, datetime.datetime): + value = value.astimezone().astimezone(pytz.utc).isoformat() + if isinstance(value, Decimal): + value = float(value) output = "{}\t[{}] at {} occurred {}"\ - .format(str(self.value), str(self.unit), str(self.datetime.astimezone().astimezone(pytz.utc).isoformat()), - str(self.occurred.astimezone().astimezone(pytz.utc).isoformat())) + .format(str(value), str(self.unit), str(timestamp), str(timestamp_occurred)) return output def to_json(self): timestamp = self.datetime - if isinstance(self.datetime, datetime.datetime): - timestamp = self.datetime.astimezone().astimezone(pytz.utc).isoformat() + if isinstance(timestamp, datetime.datetime): + timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat() timestamp_occurred = self.occurred - if isinstance(self.occurred, datetime.datetime): - timestamp_occurred = self.occurred.astimezone().astimezone(pytz.utc).isoformat() + if isinstance(timestamp_occurred, datetime.datetime): + timestamp_occurred = timestamp_occurred.astimezone().astimezone(pytz.utc).isoformat() value = self.value - if isinstance(self.value, datetime.datetime): - value = self.value.astimezone().astimezone(pytz.utc).isoformat() - if isinstance(self.value, Decimal): - value = float(self.value) + if isinstance(value, datetime.datetime): + value = value.astimezone().astimezone(pytz.utc).isoformat() + if isinstance(value, Decimal): + value = float(value) output = { 'datetime': timestamp, 'occurred': timestamp_occurred, diff --git a/test/example_telegrams.py b/test/example_telegrams.py index b21f6ad..871ea60 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -175,7 +175,7 @@ ) TELEGRAM_FLUVIUS_V171 = ( - '/FLU5\253769484_A\r\n' + '/FLU5\\253769484_A\r\n' '\r\n' '0-0:96.1.4(50217)\r\n' '0-0:96.1.1(3153414733313031303231363035)\r\n' @@ -213,7 +213,49 @@ '0-2:24.1.0(007)\r\n' '0-2:96.1.1(3853414731323334353637383930)\r\n' '0-2:24.2.1(200512134558S)(00872.234*m3)\r\n' - '!911C\r\n' + '!3AD7\r\n' +) + +TELEGRAM_FLUVIUS_V171_ALT = ( + '/FLU5\\253769484_A\r\n' + '\r\n' + '0-0:96.1.4(50217)\r\n' + '0-0:96.1.1(3153414733313030373231333236)\r\n' + '0-0:1.0.0(231102121548W)\r\n' + '1-0:1.8.1(000301.548*kWh)\r\n' + '1-0:1.8.2(000270.014*kWh)\r\n' + '1-0:2.8.1(000000.005*kWh)\r\n' + '1-0:2.8.2(000000.000*kWh)\r\n' + '0-0:96.14.0(0001)\r\n' + '1-0:1.4.0(00.052*kW)\r\n' + '1-0:1.6.0(231102114500W)(03.064*kW)\r\n' + '0-0:98.1.0(4)(1-0:1.6.0)(1-0:1.6.0)(230801000000S)(632525252525W)(00.000*kW)(230901000000S)(230831181500S)(01.862*kW)(231001000000S)(230910183000S)(04.229*kW)(231101000000W)(231016130000S)(04.927*kW)\r\n' + '1-0:1.7.0(00.338*kW)\r\n' + '1-0:2.7.0(00.000*kW)\r\n' + '1-0:21.7.0(00.047*kW)\r\n' + '1-0:41.7.0(00.179*kW)\r\n' + '1-0:61.7.0(00.111*kW)\r\n' + '1-0:22.7.0(00.000*kW)\r\n' + '1-0:42.7.0(00.000*kW)\r\n' + '1-0:62.7.0(00.000*kW)\r\n' + '1-0:32.7.0(232.9*V)\r\n' + '1-0:52.7.0(228.1*V)\r\n' + '1-0:72.7.0(228.1*V)\r\n' + '1-0:31.7.0(000.27*A)\r\n' + '1-0:51.7.0(000.88*A)\r\n' + '1-0:71.7.0(000.52*A)\r\n' + '0-0:96.3.10(1)\r\n' + '0-0:17.0.0(999.9*kW)\r\n' + '1-0:31.4.0(999*A)\r\n' + '0-0:96.13.0()\r\n' + '0-1:24.1.0(003)\r\n' + '0-1:96.1.1(37464C4F32313233303838303237)\r\n' + '0-1:24.4.0(1)\r\n' + '0-1:24.2.3(231102121002W)(00092.287*m3)\r\n' + '0-2:24.1.0(007)\r\n' + '0-2:96.1.1(3853455430303030393631313733)\r\n' + '0-2:24.2.1(231102121532W)(00008.579*m3)\r\n' + '!C4B0\r\n' ) # EasyMeter via COM-1 Ethernet Gateway diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py index 6969087..c8fa81d 100644 --- a/test/test_parse_fluvius.py +++ b/test/test_parse_fluvius.py @@ -1,6 +1,7 @@ from decimal import Decimal import datetime +import json import unittest import pytz @@ -9,7 +10,7 @@ from dsmr_parser.exceptions import InvalidChecksumError, ParseError from dsmr_parser.objects import CosemObject, MBusObject, MBusObjectPeak from dsmr_parser.parsers import TelegramParser -from test.example_telegrams import TELEGRAM_FLUVIUS_V171 +from test.example_telegrams import TELEGRAM_FLUVIUS_V171, TELEGRAM_FLUVIUS_V171_ALT class TelegramParserFluviusTest(unittest.TestCase): @@ -286,6 +287,135 @@ def test_checksum_invalid(self): def test_checksum_missing(self): # Remove the checksum value causing a ParseError. - corrupted_telegram = TELEGRAM_FLUVIUS_V171.replace('!911C\r\n', '') + corrupted_telegram = TELEGRAM_FLUVIUS_V171.replace('!3AD7\r\n', '') with self.assertRaises(ParseError): TelegramParser.validate_checksum(corrupted_telegram) + + def test_to_json(self): + parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS) + telegram = parser.parse(TELEGRAM_FLUVIUS_V171_ALT) + json_data = json.loads(telegram.to_json()) + + self.assertEqual( + json_data, + {'BELGIUM_VERSION_INFORMATION': {'value': '50217', 'unit': None}, + 'BELGIUM_EQUIPMENT_IDENTIFIER': {'value': '3153414733313030373231333236', 'unit': None}, + 'P1_MESSAGE_TIMESTAMP': {'value': '2023-11-02T11:15:48+00:00', 'unit': None}, + 'ELECTRICITY_USED_TARIFF_1': {'value': 301.548, 'unit': 'kWh'}, + 'ELECTRICITY_USED_TARIFF_2': {'value': 270.014, 'unit': 'kWh'}, + 'ELECTRICITY_DELIVERED_TARIFF_1': {'value': 0.005, 'unit': 'kWh'}, + 'ELECTRICITY_DELIVERED_TARIFF_2': {'value': 0.0, 'unit': 'kWh'}, + 'ELECTRICITY_ACTIVE_TARIFF': {'value': '0001', 'unit': None}, + 'BELGIUM_CURRENT_AVERAGE_DEMAND': {'value': 0.052, 'unit': 'kW'}, + 'BELGIUM_MAXIMUM_DEMAND_MONTH': {'datetime': '2023-11-02T10:45:00+00:00', + 'value': 3.064, 'unit': 'kW'}, + 'BELGIUM_MAXIMUM_DEMAND_13_MONTHS': [{'datetime': '2023-07-31T22:00:00+00:00', + 'occurred': None, 'value': 0.0, 'unit': 'kW'}, + {'datetime': '2023-08-31T22:00:00+00:00', + 'occurred': '2023-08-31T16:15:00+00:00', + 'value': 1.862, 'unit': 'kW'}, + {'datetime': '2023-09-30T22:00:00+00:00', + 'occurred': '2023-09-10T16:30:00+00:00', + 'value': 4.229, 'unit': 'kW'}, + {'datetime': '2023-10-31T23:00:00+00:00', + 'occurred': '2023-10-16T11:00:00+00:00', + 'value': 4.927, 'unit': 'kW'}], + 'CURRENT_ELECTRICITY_USAGE': {'value': 0.338, 'unit': 'kW'}, + 'CURRENT_ELECTRICITY_DELIVERY': {'value': 0.0, 'unit': 'kW'}, + 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE': {'value': 0.047, 'unit': 'kW'}, + 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE': {'value': 0.179, 'unit': 'kW'}, + 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE': {'value': 0.111, 'unit': 'kW'}, + 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE': {'value': 0.0, 'unit': 'kW'}, + 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE': {'value': 0.0, 'unit': 'kW'}, + 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE': {'value': 0.0, 'unit': 'kW'}, + 'INSTANTANEOUS_VOLTAGE_L1': {'value': 232.9, 'unit': 'V'}, + 'INSTANTANEOUS_VOLTAGE_L2': {'value': 228.1, 'unit': 'V'}, + 'INSTANTANEOUS_VOLTAGE_L3': {'value': 228.1, 'unit': 'V'}, + 'INSTANTANEOUS_CURRENT_L1': {'value': 0.27, 'unit': 'A'}, + 'INSTANTANEOUS_CURRENT_L2': {'value': 0.88, 'unit': 'A'}, + 'INSTANTANEOUS_CURRENT_L3': {'value': 0.52, 'unit': 'A'}, + 'ACTUAL_SWITCH_POSITION': {'value': 1, 'unit': None}, + 'ACTUAL_TRESHOLD_ELECTRICITY': {'value': 999.9, 'unit': 'kW'}, + 'BELGIUM_MAX_POWER_PER_PHASE': {'value': 999.9, 'unit': 'kW'}, + 'BELGIUM_MAX_CURRENT_PER_PHASE': {'value': 999.0, 'unit': 'A'}, + 'TEXT_MESSAGE': {'value': None, 'unit': None}, + 'BELGIUM_MBUS1_DEVICE_TYPE': {'value': 3, 'unit': None}, + 'MBUS_DEVICES': [{'BELGIUM_MBUS1_DEVICE_TYPE': {'value': 3, 'unit': None}, + 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER': {'value': '37464C4F32313233303838303237', + 'unit': None}, + 'BELGIUM_MBUS1_VALVE_POSITION': {'value': 1, 'unit': None}, + 'BELGIUM_MBUS1_METER_READING2': {'datetime': '2023-11-02T11:10:02+00:00', + 'value': 92.287, 'unit': 'm3'}, + 'CHANNEL_ID': 1}, + {'BELGIUM_MBUS2_DEVICE_TYPE': {'value': 7, 'unit': None}, + 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER': {'value': '3853455430303030393631313733', + 'unit': None}, + 'BELGIUM_MBUS2_METER_READING1': {'datetime': '2023-11-02T11:15:32+00:00', + 'value': 8.579, 'unit': 'm3'}, + 'CHANNEL_ID': 2}], + 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER': {'value': '37464C4F32313233303838303237', 'unit': None}, + 'BELGIUM_MBUS1_VALVE_POSITION': {'value': 1, 'unit': None}, + 'BELGIUM_MBUS1_METER_READING2': {'datetime': '2023-11-02T11:10:02+00:00', 'value': 92.287, 'unit': 'm3'}, + 'BELGIUM_MBUS2_DEVICE_TYPE': {'value': 7, 'unit': None}, + 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER': {'value': '3853455430303030393631313733', 'unit': None}, + 'BELGIUM_MBUS2_METER_READING1': {'datetime': '2023-11-02T11:15:32+00:00', 'value': 8.579, 'unit': 'm3'}} + ) + + def test_to_str(self): + parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS) + telegram = parser.parse(TELEGRAM_FLUVIUS_V171_ALT) + + self.assertEqual( + str(telegram), + ( + 'BELGIUM_VERSION_INFORMATION: 50217 [None]\n' + 'BELGIUM_EQUIPMENT_IDENTIFIER: 3153414733313030373231333236 [None]\n' + 'P1_MESSAGE_TIMESTAMP: 2023-11-02T11:15:48+00:00 [None]\n' + 'ELECTRICITY_USED_TARIFF_1: 301.548 [kWh]\n' + 'ELECTRICITY_USED_TARIFF_2: 270.014 [kWh]\n' + 'ELECTRICITY_DELIVERED_TARIFF_1: 0.005 [kWh]\n' + 'ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh]\n' + 'ELECTRICITY_ACTIVE_TARIFF: 0001 [None]\n' + 'BELGIUM_CURRENT_AVERAGE_DEMAND: 0.052 [kW]\n' + 'BELGIUM_MAXIMUM_DEMAND_MONTH: 3.064 [kW] at 2023-11-02T10:45:00+00:00\n' + '0.0 [kW] at 2023-07-31T22:00:00+00:00 occurred None' + '1.862 [kW] at 2023-08-31T22:00:00+00:00 occurred 2023-08-31T16:15:00+00:00' + '4.229 [kW] at 2023-09-30T22:00:00+00:00 occurred 2023-09-10T16:30:00+00:00' + '4.927 [kW] at 2023-10-31T23:00:00+00:00 occurred 2023-10-16T11:00:00+00:00' + 'CURRENT_ELECTRICITY_USAGE: 0.338 [kW]\n' + 'CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.047 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 0.179 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.111 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW]\n' + 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW]\n' + 'INSTANTANEOUS_VOLTAGE_L1: 232.9 [V]\n' + 'INSTANTANEOUS_VOLTAGE_L2: 228.1 [V]\n' + 'INSTANTANEOUS_VOLTAGE_L3: 228.1 [V]\n' + 'INSTANTANEOUS_CURRENT_L1: 0.27 [A]\n' + 'INSTANTANEOUS_CURRENT_L2: 0.88 [A]\n' + 'INSTANTANEOUS_CURRENT_L3: 0.52 [A]\n' + 'ACTUAL_SWITCH_POSITION: 1 [None]\n' + 'ACTUAL_TRESHOLD_ELECTRICITY: 999.9 [kW]\n' + 'BELGIUM_MAX_POWER_PER_PHASE: 999.9 [kW]\n' + 'BELGIUM_MAX_CURRENT_PER_PHASE: 999 [A]\n' + 'TEXT_MESSAGE: None [None]\n' + 'BELGIUM_MBUS1_DEVICE_TYPE: 3 [None]\n' + 'MBUS DEVICE (channel 1)\n' + ' BELGIUM_MBUS1_DEVICE_TYPE: 3 [None]\n' + ' BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: 37464C4F32313233303838303237 [None]\n' + ' BELGIUM_MBUS1_VALVE_POSITION: 1 [None]\n' + ' BELGIUM_MBUS1_METER_READING2: 92.287 [m3] at 2023-11-02T11:10:02+00:00\n' + 'MBUS DEVICE (channel 2)\n' + ' BELGIUM_MBUS2_DEVICE_TYPE: 7 [None]\n' + ' BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: 3853455430303030393631313733 [None]\n' + ' BELGIUM_MBUS2_METER_READING1: 8.579 [m3] at 2023-11-02T11:15:32+00:00\n' + 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: 37464C4F32313233303838303237 [None]\n' + 'BELGIUM_MBUS1_VALVE_POSITION: 1 [None]\n' + 'BELGIUM_MBUS1_METER_READING2: 92.287 [m3] at 2023-11-02T11:10:02+00:00\n' + 'BELGIUM_MBUS2_DEVICE_TYPE: 7 [None]\n' + 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: 3853455430303030393631313733 [None]\n' + 'BELGIUM_MBUS2_METER_READING1: 8.579 [m3] at 2023-11-02T11:15:32+00:00\n' + ) + ) From 01d7622c4030802df624c50d7f087d8a29fe6477 Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Mon, 6 Nov 2023 15:09:47 +0100 Subject: [PATCH 205/226] Release 1.3.1 --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b1e9648..9dd15b4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**1.3.1** (2023-11-06) +- Fix parsing peak usage with invalid timestamps + (`PR #143 `_ by `dupondje `_) + **1.3.0** (2023-08-01) - added E.ON Hungary; refactored DSMR specifications to fix obis reference conflicts diff --git a/setup.py b/setup.py index 14ca9b6..ae8e596 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.3.0', + version='1.3.1', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From a316d93082580c8c2ea4beb2ec734bd26f323df0 Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Wed, 18 Oct 2023 13:25:08 +0200 Subject: [PATCH 206/226] Adjust the naming of the MAX_POWER and MAX_CURRENT obis values --- dsmr_parser/obis_references.py | 6 ++--- dsmr_parser/telegram_specifications.py | 16 ++++++------ test/test_parse_fluvius.py | 10 ++++---- test/test_parse_v5_eon_hungary.py | 34 +++++++++++++------------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 0ca9654..83d3b8b 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -43,6 +43,9 @@ INSTANTANEOUS_CURRENT_L1 = r'^\d-\d:31\.7\.0.+?\r\n' INSTANTANEOUS_CURRENT_L2 = r'^\d-\d:51\.7\.0.+?\r\n' INSTANTANEOUS_CURRENT_L3 = r'^\d-\d:71\.7\.0.+?\r\n' +FUSE_THRESHOLD_L1 = r'^\d-\d:31\.4\.0.+?\r\n' # Applicable when current limitation is active +FUSE_THRESHOLD_L2 = r'^\d-\d:51\.4\.0.+?\r\n' # Applicable when current limitation is active +FUSE_THRESHOLD_L3 = r'^\d-\d:71\.4\.0.+?\r\n' # Applicable when current limitation is active TEXT_MESSAGE_CODE = r'^\d-\d:96\.13\.1.+?\r\n' TEXT_MESSAGE = r'^\d-\d:96\.13\.0.+?\r\n' DEVICE_TYPE = r'^\d-\d:24\.1\.0.+?\r\n' @@ -89,7 +92,6 @@ BELGIUM_MAXIMUM_DEMAND_MONTH = r'^\d-\d:1\.6\.0.+?\r\n' BELGIUM_MAXIMUM_DEMAND_13_MONTHS = r'^\d-\d:98\.1\.0.+?\r\n' BELGIUM_MAX_POWER_PER_PHASE = r'^\d-\d:17\.0\.0.+?\r\n' # Applicable when power limitation is active -BELGIUM_MAX_CURRENT_PER_PHASE = r'^\d-\d:31\.4\.0.+?\r\n' # Applicable when current limitation is active # Multiple 'slaves' can be linked to the main device. # Mostly MBUS1 = GAS METER with values on 24.2.3 @@ -142,5 +144,3 @@ EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 = r'^\d-\d:6\.7\.0.+?\r\n' EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 = r'^\d-\d:7\.7\.0.+?\r\n' EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 = r'^\d-\d:8\.7\.0.+?\r\n' -EON_HU_MAX_POWER_ON_L2 = r'^\d-\d:51\.4\.0.+?\r\n' -EON_HU_MAX_POWER_ON_L3 = r'^\d-\d:71\.4\.0.+?\r\n' diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 65ede2f..ee54b0a 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -630,9 +630,9 @@ 'value_name': 'BELGIUM_MAX_POWER_PER_PHASE' }, { - 'obis_reference': obis.BELGIUM_MAX_CURRENT_PER_PHASE, + 'obis_reference': obis.FUSE_THRESHOLD_L1, 'value_parser': CosemParser(ValueParser(Decimal)), - 'value_name': 'BELGIUM_MAX_CURRENT_PER_PHASE' + 'value_name': 'FUSE_THRESHOLD_L1' }, { 'obis_reference': obis.TEXT_MESSAGE, @@ -1414,20 +1414,20 @@ 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_Q4' }, { - 'obis_reference': obis.BELGIUM_MAX_CURRENT_PER_PHASE, + 'obis_reference': obis.FUSE_THRESHOLD_L1, 'value_parser': CosemParser(ValueParser(Decimal)), - 'value_name': 'MAX_POWER_ON_L1' + 'value_name': 'FUSE_THRESHOLD_L1' }, { - 'obis_reference': obis.EON_HU_MAX_POWER_ON_L2, + 'obis_reference': obis.FUSE_THRESHOLD_L2, 'value_parser': CosemParser(ValueParser(Decimal)), - 'value_name': 'MAX_POWER_ON_L2' + 'value_name': 'FUSE_THRESHOLD_L2' # Only with 3 phase meters }, { - 'obis_reference': obis.EON_HU_MAX_POWER_ON_L3, + 'obis_reference': obis.FUSE_THRESHOLD_L3, 'value_parser': CosemParser(ValueParser(Decimal)), - 'value_name': 'MAX_POWER_ON_L3' + 'value_name': 'FUSE_THRESHOLD_L3' # Only with 3 phase meters }, # I'm not sure which datas does this line containes. It should be the data of last minute of last month. diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py index c8fa81d..6e38dd2 100644 --- a/test/test_parse_fluvius.py +++ b/test/test_parse_fluvius.py @@ -217,11 +217,11 @@ def test_parse(self): assert isinstance(result.BELGIUM_MAX_POWER_PER_PHASE.value, Decimal) assert result.BELGIUM_MAX_POWER_PER_PHASE.value == Decimal('999.9') - # BELGIUM_MAX_POWER_PER_PHASE (1-0:31.4.0) - assert isinstance(result.BELGIUM_MAX_CURRENT_PER_PHASE, CosemObject) - assert result.BELGIUM_MAX_CURRENT_PER_PHASE.unit == 'A' - assert isinstance(result.BELGIUM_MAX_CURRENT_PER_PHASE.value, Decimal) - assert result.BELGIUM_MAX_CURRENT_PER_PHASE.value == Decimal('999') + # FUSE_THRESHOLD_L1 (1-0:31.4.0) + assert isinstance(result.FUSE_THRESHOLD_L1, CosemObject) + assert result.FUSE_THRESHOLD_L1.unit == 'A' + assert isinstance(result.FUSE_THRESHOLD_L1.value, Decimal) + assert result.FUSE_THRESHOLD_L1.value == Decimal('999') # TEXT_MESSAGE (0-0:96.13.0) assert isinstance(result.TEXT_MESSAGE, CosemObject) diff --git a/test/test_parse_v5_eon_hungary.py b/test/test_parse_v5_eon_hungary.py index 08dd6e1..0862de9 100644 --- a/test/test_parse_v5_eon_hungary.py +++ b/test/test_parse_v5_eon_hungary.py @@ -263,23 +263,23 @@ def test_parse(self): assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.value, Decimal) assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.value == Decimal('00.000') - # EON_HU_MAX_POWER_ON_L1 (1-0:31.4.0) - assert isinstance(telegram.MAX_POWER_ON_L1, CosemObject) - assert telegram.MAX_POWER_ON_L1.unit == 'A' - assert isinstance(telegram.MAX_POWER_ON_L1.value, Decimal) - assert telegram.MAX_POWER_ON_L1.value == Decimal('200.00') - - # EON_HU_MAX_POWER_ON_L2 (1-0:31.4.0) - assert isinstance(telegram.MAX_POWER_ON_L2, CosemObject) - assert telegram.MAX_POWER_ON_L2.unit == 'A' - assert isinstance(telegram.MAX_POWER_ON_L2.value, Decimal) - assert telegram.MAX_POWER_ON_L2.value == Decimal('200.00') - - # EON_HU_MAX_POWER_ON_L3 (1-0:31.4.0) - assert isinstance(telegram.MAX_POWER_ON_L3, CosemObject) - assert telegram.MAX_POWER_ON_L3.unit == 'A' - assert isinstance(telegram.MAX_POWER_ON_L3.value, Decimal) - assert telegram.MAX_POWER_ON_L3.value == Decimal('200.00') + # FUSE_THRESHOLD_L1 (1-0:31.4.0) + assert isinstance(telegram.FUSE_THRESHOLD_L1, CosemObject) + assert telegram.FUSE_THRESHOLD_L1.unit == 'A' + assert isinstance(telegram.FUSE_THRESHOLD_L1.value, Decimal) + assert telegram.FUSE_THRESHOLD_L1.value == Decimal('200.00') + + # FUSE_THRESHOLD_L2 (1-0:31.4.0) + assert isinstance(telegram.FUSE_THRESHOLD_L2, CosemObject) + assert telegram.FUSE_THRESHOLD_L2.unit == 'A' + assert isinstance(telegram.FUSE_THRESHOLD_L2.value, Decimal) + assert telegram.FUSE_THRESHOLD_L2.value == Decimal('200.00') + + # FUSE_THRESHOLD_L3 (1-0:31.4.0) + assert isinstance(telegram.FUSE_THRESHOLD_L3, CosemObject) + assert telegram.FUSE_THRESHOLD_L3.unit == 'A' + assert isinstance(telegram.FUSE_THRESHOLD_L3.value, Decimal) + assert telegram.FUSE_THRESHOLD_L3.value == Decimal('200.00') # TEXT_MESSAGE (0-0:96.13.0) assert isinstance(telegram.TEXT_MESSAGE, CosemObject) From d2ff6f4bb903d9b228c1ca6301ff9c1b1beafc2b Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Mon, 6 Nov 2023 14:48:44 +0100 Subject: [PATCH 207/226] Fix tests after FUSE_TRESHOLD rename --- test/test_parse_fluvius.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py index 6e38dd2..b44ced0 100644 --- a/test/test_parse_fluvius.py +++ b/test/test_parse_fluvius.py @@ -337,7 +337,7 @@ def test_to_json(self): 'ACTUAL_SWITCH_POSITION': {'value': 1, 'unit': None}, 'ACTUAL_TRESHOLD_ELECTRICITY': {'value': 999.9, 'unit': 'kW'}, 'BELGIUM_MAX_POWER_PER_PHASE': {'value': 999.9, 'unit': 'kW'}, - 'BELGIUM_MAX_CURRENT_PER_PHASE': {'value': 999.0, 'unit': 'A'}, + 'FUSE_THRESHOLD_L1': {'value': 999.0, 'unit': 'A'}, 'TEXT_MESSAGE': {'value': None, 'unit': None}, 'BELGIUM_MBUS1_DEVICE_TYPE': {'value': 3, 'unit': None}, 'MBUS_DEVICES': [{'BELGIUM_MBUS1_DEVICE_TYPE': {'value': 3, 'unit': None}, @@ -399,7 +399,7 @@ def test_to_str(self): 'ACTUAL_SWITCH_POSITION: 1 [None]\n' 'ACTUAL_TRESHOLD_ELECTRICITY: 999.9 [kW]\n' 'BELGIUM_MAX_POWER_PER_PHASE: 999.9 [kW]\n' - 'BELGIUM_MAX_CURRENT_PER_PHASE: 999 [A]\n' + 'FUSE_THRESHOLD_L1: 999 [A]\n' 'TEXT_MESSAGE: None [None]\n' 'BELGIUM_MBUS1_DEVICE_TYPE: 3 [None]\n' 'MBUS DEVICE (channel 1)\n' From 739419e3ee0a882448cd1d966c179d46413f6c62 Mon Sep 17 00:00:00 2001 From: dupondje Date: Tue, 7 Nov 2023 12:10:42 +0100 Subject: [PATCH 208/226] Remove BELGIUM_MAX_POWER_PER_PHASE (#144) This commit removes BELGIUM_MAX_POWER_PER_PHASE and uses ACTUAL_TRESHOLD_ELECTRICITY as this has same regex. So useless to keep the BELIUM variant. --- dsmr_parser/obis_references.py | 1 - dsmr_parser/telegram_specifications.py | 6 +++--- test/test_parse_fluvius.py | 12 +++++------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 83d3b8b..78c1b55 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -91,7 +91,6 @@ BELGIUM_CURRENT_AVERAGE_DEMAND = r'^\d-\d:1\.4\.0.+?\r\n' BELGIUM_MAXIMUM_DEMAND_MONTH = r'^\d-\d:1\.6\.0.+?\r\n' BELGIUM_MAXIMUM_DEMAND_13_MONTHS = r'^\d-\d:98\.1\.0.+?\r\n' -BELGIUM_MAX_POWER_PER_PHASE = r'^\d-\d:17\.0\.0.+?\r\n' # Applicable when power limitation is active # Multiple 'slaves' can be linked to the main device. # Mostly MBUS1 = GAS METER with values on 24.2.3 diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index ee54b0a..8cbe430 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -625,9 +625,9 @@ 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY' }, { - 'obis_reference': obis.BELGIUM_MAX_POWER_PER_PHASE, + 'obis_reference': obis.ACTUAL_TRESHOLD_ELECTRICITY, 'value_parser': CosemParser(ValueParser(Decimal)), - 'value_name': 'BELGIUM_MAX_POWER_PER_PHASE' + 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY' }, { 'obis_reference': obis.FUSE_THRESHOLD_L1, @@ -1233,7 +1233,7 @@ # This seems to be wrong in documentation, it's not 0-0:96.50.68, but 0-0:96.3.10 }, { - 'obis_reference': obis.BELGIUM_MAX_POWER_PER_PHASE, + 'obis_reference': obis.ACTUAL_TRESHOLD_ELECTRICITY, 'value_parser': CosemParser(ValueParser(Decimal)), 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY' }, diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py index b44ced0..7beb272 100644 --- a/test/test_parse_fluvius.py +++ b/test/test_parse_fluvius.py @@ -211,11 +211,11 @@ def test_parse(self): assert isinstance(result.ACTUAL_SWITCH_POSITION.value, int) assert result.ACTUAL_SWITCH_POSITION.value == 1 - # BELGIUM_MAX_POWER_PER_PHASE (0-0:17.0.0) - assert isinstance(result.BELGIUM_MAX_POWER_PER_PHASE, CosemObject) - assert result.BELGIUM_MAX_POWER_PER_PHASE.unit == 'kW' - assert isinstance(result.BELGIUM_MAX_POWER_PER_PHASE.value, Decimal) - assert result.BELGIUM_MAX_POWER_PER_PHASE.value == Decimal('999.9') + # ACTUAL_TRESHOLD_ELECTRICITY (0-0:17.0.0) + assert isinstance(result.ACTUAL_TRESHOLD_ELECTRICITY, CosemObject) + assert result.ACTUAL_TRESHOLD_ELECTRICITY.unit == 'kW' + assert isinstance(result.ACTUAL_TRESHOLD_ELECTRICITY.value, Decimal) + assert result.ACTUAL_TRESHOLD_ELECTRICITY.value == Decimal('999.9') # FUSE_THRESHOLD_L1 (1-0:31.4.0) assert isinstance(result.FUSE_THRESHOLD_L1, CosemObject) @@ -336,7 +336,6 @@ def test_to_json(self): 'INSTANTANEOUS_CURRENT_L3': {'value': 0.52, 'unit': 'A'}, 'ACTUAL_SWITCH_POSITION': {'value': 1, 'unit': None}, 'ACTUAL_TRESHOLD_ELECTRICITY': {'value': 999.9, 'unit': 'kW'}, - 'BELGIUM_MAX_POWER_PER_PHASE': {'value': 999.9, 'unit': 'kW'}, 'FUSE_THRESHOLD_L1': {'value': 999.0, 'unit': 'A'}, 'TEXT_MESSAGE': {'value': None, 'unit': None}, 'BELGIUM_MBUS1_DEVICE_TYPE': {'value': 3, 'unit': None}, @@ -398,7 +397,6 @@ def test_to_str(self): 'INSTANTANEOUS_CURRENT_L3: 0.52 [A]\n' 'ACTUAL_SWITCH_POSITION: 1 [None]\n' 'ACTUAL_TRESHOLD_ELECTRICITY: 999.9 [kW]\n' - 'BELGIUM_MAX_POWER_PER_PHASE: 999.9 [kW]\n' 'FUSE_THRESHOLD_L1: 999 [A]\n' 'TEXT_MESSAGE: None [None]\n' 'BELGIUM_MBUS1_DEVICE_TYPE: 3 [None]\n' From 1680cacf49f4cbc55fda9778b51e0de297e18de0 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 29 Jan 2024 14:25:39 +0100 Subject: [PATCH 209/226] fix protocol unit test for python3.12 (#148) * fix protocol unit test for python3.12 * add workflow test for python 3.12 --- .github/workflows/tests.yml | 1 + test/test_protocol.py | 2 +- tox.ini | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 65f86d1..91cf3e9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,6 +17,7 @@ jobs: - '3.9' - '3.10' - '3.11' + - '3.12' name: Python ${{ matrix.python-version }} steps: diff --git a/test/test_protocol.py b/test/test_protocol.py index 1e7440b..7846b32 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -68,6 +68,6 @@ def test_receive_packet(self): # 2nd call of keep_alive should close the transport self.protocol.keep_alive() - assert mock_transport.close.called_once() + mock_transport.close.assert_called_once() self.protocol.connection_lost(None) diff --git a/tox.ini b/tox.ini index 533aa72..59d57e7 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ deps= pytest-asyncio pytest-mock dlms_cosem + setuptools commands= py.test --cov=dsmr_parser test {posargs} pylama dsmr_parser test From be61d4aaf9cb565b5824ea2797b3bcae3fa39926 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 29 Jan 2024 14:29:02 +0100 Subject: [PATCH 210/226] Prepare release 1.3.2 --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9dd15b4..e5bacbd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**1.3.2** (2024-01-29) +- Fix unit test for pyton 3.12 + (`PR #148 `_ by `ndokter `_) + **1.3.1** (2023-11-06) - Fix parsing peak usage with invalid timestamps (`PR #143 `_ by `dupondje `_) diff --git a/setup.py b/setup.py index ae8e596..1e7c15a 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.3.1', + version='1.3.2', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From fcbcbb30e0038d629597800e64a49d574fc2f268 Mon Sep 17 00:00:00 2001 From: Gunnar Klauberg Date: Mon, 19 Feb 2024 14:31:09 +0100 Subject: [PATCH 211/226] Q3D add CURRENT_ELECTRICITY_DELIVERY (#149) add CURRENT_ELECTRICITY_DELIVERY sensor for Q3D --- dsmr_parser/telegram_specifications.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 8cbe430..4a08808 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -962,6 +962,11 @@ 'value_parser': CosemParser(ValueParser(Decimal)), 'value_name': 'CURRENT_ELECTRICITY_USAGE' }, + { + 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY, + 'value_parser': CosemParser(ValueParser(Decimal)), + 'value_name': 'CURRENT_ELECTRICITY_DELIVERY' + }, { 'obis_reference': obis.Q3D_EQUIPMENT_STATE, 'value_parser': CosemParser(ValueParser(str)), From bc35a051f2457599b1d1b31492fd593c45e87f1b Mon Sep 17 00:00:00 2001 From: dupondje Date: Thu, 7 Mar 2024 11:21:23 +0100 Subject: [PATCH 212/226] Change DSMR 5 and 5B to use DSMR devices (#142) At least the BE and the NL DSMR devices use MBUS. Therefore we make the MBUS obis reference generic, and convert the V5 telegram to use MBUS values. Finally set self.maxDiff = None to have a full test output on some tests. --- dsmr_parser/obis_references.py | 49 +++------ dsmr_parser/objects.py | 4 +- dsmr_parser/telegram_specifications.py | 147 ++++--------------------- test/objects/test_mbusdevice.py | 42 +++---- test/objects/test_telegram.py | 85 +++++++------- test/test_parse_fluvius.py | 138 +++++++++++------------ test/test_parse_v5.py | 51 ++++----- 7 files changed, 195 insertions(+), 321 deletions(-) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 78c1b55..4cf0906 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -69,8 +69,25 @@ ACTUAL_SWITCH_POSITION = r'^\d-\d:96\.3\.10.+?\r\n' VALVE_POSITION_GAS = r'^\d-\d:24\.4\.0.+?\r\n' +# Multiple 'slaves' can be linked to the main device. +# The type is reported on 24.1.0 +# Specifications are in EN 13757-3 +# For example: Water mater = 7, Gas meter = 3 +# Identifier is on 96.1.0 (in NL for ex) or +# on 96.1.1 (in BE for ex) +# The values are reported on 24.2.1 +# With an exception in Belgium for the GAS meter +# Be aware that for the gas volume, another OBIS-code is published +# than the one listed in section 7 of DSMR P1. +# This is due to the fact that in Belgium the not-temperature +# corrected gas volume is used while in the Netherlands, +# the temperature corrected gas volume is used. +MBUS_DEVICE_TYPE = r'^\d-[1-9]:24\.1\.0.+?\r\n' +MBUS_EQUIPMENT_IDENTIFIER = r'^\d-[1-9]:96\.1\.[01].+?\r\n' +MBUS_VALVE_POSITION = r'^\d-[1-9]:24\.4\.0.+?\r\n' +MBUS_METER_READING = r'^\d-[1-9]:24\.2\.[13].+?\r\n' + # TODO 17.0.0 -# TODO 96.3.10 ELECTRICITY_USED_TARIFF_ALL = ( ELECTRICITY_USED_TARIFF_1, @@ -92,36 +109,6 @@ BELGIUM_MAXIMUM_DEMAND_MONTH = r'^\d-\d:1\.6\.0.+?\r\n' BELGIUM_MAXIMUM_DEMAND_13_MONTHS = r'^\d-\d:98\.1\.0.+?\r\n' -# Multiple 'slaves' can be linked to the main device. -# Mostly MBUS1 = GAS METER with values on 24.2.3 -# While WATER METER reports it's values on 24.2.1 -# The GAS METER also reports its valve state on 24.4.0 -# Dev type for gas = 7 and water = 8 -BELGIUM_MBUS1_DEVICE_TYPE = r'^\d-1:24\.1\.0.+?\r\n' -BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER = r'^\d-1:96\.1\.1.+?\r\n' -BELGIUM_MBUS1_VALVE_POSITION = r'^\d-1:24\.4\.0.+?\r\n' -BELGIUM_MBUS1_METER_READING1 = r'^\d-1:24\.2\.1.+?\r\n' -BELGIUM_MBUS1_METER_READING2 = r'^\d-1:24\.2\.3.+?\r\n' - -BELGIUM_MBUS2_DEVICE_TYPE = r'^\d-2:24\.1\.0.+?\r\n' -BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER = r'^\d-2:96\.1\.1.+?\r\n' -BELGIUM_MBUS2_VALVE_POSITION = r'^\d-2:24\.4\.0.+?\r\n' -BELGIUM_MBUS2_METER_READING1 = r'^\d-2:24\.2\.1.+?\r\n' -BELGIUM_MBUS2_METER_READING2 = r'^\d-2:24\.2\.3.+?\r\n' - -BELGIUM_MBUS3_DEVICE_TYPE = r'^\d-3:24\.1\.0.+?\r\n' -BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER = r'^\d-3:96\.1\.1.+?\r\n' -BELGIUM_MBUS3_VALVE_POSITION = r'^\d-3:24\.4\.0.+?\r\n' -BELGIUM_MBUS3_METER_READING1 = r'^\d-3:24\.2\.1.+?\r\n' -BELGIUM_MBUS3_METER_READING2 = r'^\d-3:24\.2\.3.+?\r\n' - -BELGIUM_MBUS4_DEVICE_TYPE = r'^\d-4:24\.1\.0.+?\r\n' -BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER = r'^\d-4:96\.1\.1.+?\r\n' -BELGIUM_MBUS4_VALVE_POSITION = r'^\d-4:24\.4\.0.+?\r\n' -BELGIUM_MBUS4_METER_READING1 = r'^\d-4:24\.2\.1.+?\r\n' -BELGIUM_MBUS4_METER_READING2 = r'^\d-4:24\.2\.3.+?\r\n' - - LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'^\d-\d:42\.0\.0.+?\r\n' # Logical device name Q3D_EQUIPMENT_IDENTIFIER = r'^\d-\d:0\.0\.0.+?\r\n' # Logical device name diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 7d7e21c..ff6de21 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -28,12 +28,12 @@ def __init__(self, *args, **kwargs): def add(self, obis_reference, dsmr_object, obis_name): # Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER setattr(self, obis_name, dsmr_object) - if obis_name not in self._item_names: # TODO repeating obis references - self._item_names.append(obis_name) # TODO isinstance check: MaxDemandParser (BELGIUM_MAXIMUM_DEMAND_13_MONTHS) returns a list if isinstance(dsmr_object, DSMRObject) and dsmr_object.is_mbus_reading: self._add_mbus(obis_reference, dsmr_object, obis_name) + elif obis_name not in self._item_names: # TODO repeating obis references + self._item_names.append(obis_name) # Fill dict which is only used for backwards compatibility if obis_reference not in self: diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 4a08808..cee76d7 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -430,11 +430,6 @@ 'value_parser': CosemParser(ValueParser(str)), 'value_name': 'TEXT_MESSAGE' }, - { - 'obis_reference': obis.DEVICE_TYPE, - 'value_parser': CosemParser(ValueParser(int)), - 'value_name': 'DEVICE_TYPE' - }, { 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, 'value_parser': CosemParser(ValueParser(Decimal)), @@ -466,17 +461,27 @@ 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE' }, { - 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS, + 'obis_reference': obis.MBUS_DEVICE_TYPE, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'MBUS_DEVICE_TYPE' + }, + { + 'obis_reference': obis.MBUS_EQUIPMENT_IDENTIFIER, 'value_parser': CosemParser(ValueParser(str)), - 'value_name': 'EQUIPMENT_IDENTIFIER_GAS' + 'value_name': 'MBUS_EQUIPMENT_IDENTIFIER' }, { - 'obis_reference': obis.HOURLY_GAS_METER_READING, - 'value_parser': MBusParser( + 'obis_reference': obis.MBUS_VALVE_POSITION, + 'value_parser': CosemParser(ValueParser(int)), + 'value_name': 'MBUS_VALVE_POSITION' + }, + { + 'obis_reference': obis.MBUS_METER_READING, + 'value_parser': MBusParser( ValueParser(timestamp), ValueParser(Decimal) ), - 'value_name': 'HOURLY_GAS_METER_READING' + 'value_name': 'MBUS_METER_READING' }, ] } @@ -624,11 +629,6 @@ 'value_parser': CosemParser(ValueParser(Decimal)), 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY' }, - { - 'obis_reference': obis.ACTUAL_TRESHOLD_ELECTRICITY, - 'value_parser': CosemParser(ValueParser(Decimal)), - 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY' - }, { 'obis_reference': obis.FUSE_THRESHOLD_L1, 'value_parser': CosemParser(ValueParser(Decimal)), @@ -640,128 +640,27 @@ 'value_name': 'TEXT_MESSAGE' }, { - 'obis_reference': obis.BELGIUM_MBUS1_DEVICE_TYPE, - 'value_parser': CosemParser(ValueParser(int)), - 'value_name': 'BELGIUM_MBUS1_DEVICE_TYPE' - }, - { - 'obis_reference': obis.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, - 'value_parser': CosemParser(ValueParser(str)), - 'value_name': 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER' - }, - { - 'obis_reference': obis.BELGIUM_MBUS1_VALVE_POSITION, + 'obis_reference': obis.MBUS_DEVICE_TYPE, 'value_parser': CosemParser(ValueParser(int)), - 'value_name': 'BELGIUM_MBUS1_VALVE_POSITION' - }, - { - 'obis_reference': obis.BELGIUM_MBUS1_METER_READING1, - 'value_parser': MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - 'value_name': 'BELGIUM_MBUS1_METER_READING1' + 'value_name': 'MBUS_DEVICE_TYPE' }, { - 'obis_reference': obis.BELGIUM_MBUS1_METER_READING2, - 'value_parser': MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - 'value_name': 'BELGIUM_MBUS1_METER_READING2' - }, - { - 'obis_reference': obis.BELGIUM_MBUS2_DEVICE_TYPE, - 'value_parser': CosemParser(ValueParser(int)), - 'value_name': 'BELGIUM_MBUS2_DEVICE_TYPE' - }, - { - 'obis_reference': obis.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + 'obis_reference': obis.MBUS_EQUIPMENT_IDENTIFIER, 'value_parser': CosemParser(ValueParser(str)), - 'value_name': 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER' + 'value_name': 'MBUS_EQUIPMENT_IDENTIFIER' }, { - 'obis_reference': obis.BELGIUM_MBUS2_VALVE_POSITION, + 'obis_reference': obis.MBUS_VALVE_POSITION, 'value_parser': CosemParser(ValueParser(int)), - 'value_name': 'BELGIUM_MBUS2_VALVE_POSITION' - }, - { - 'obis_reference': obis.BELGIUM_MBUS2_METER_READING1, - 'value_parser': MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - 'value_name': 'BELGIUM_MBUS2_METER_READING1' - }, - { - 'obis_reference': obis.BELGIUM_MBUS2_METER_READING2, - 'value_parser': MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - 'value_name': 'BELGIUM_MBUS2_METER_READING2' - }, - { - 'obis_reference': obis.BELGIUM_MBUS3_DEVICE_TYPE, - 'value_parser': CosemParser(ValueParser(int)), - 'value_name': 'BELGIUM_MBUS3_DEVICE_TYPE' - }, - { - 'obis_reference': obis.BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, - 'value_parser': CosemParser(ValueParser(str)), - 'value_name': 'BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER' - }, - { - 'obis_reference': obis.BELGIUM_MBUS3_VALVE_POSITION, - 'value_parser': CosemParser(ValueParser(int)), - 'value_name': 'BELGIUM_MBUS3_VALVE_POSITION' - }, - { - 'obis_reference': obis.BELGIUM_MBUS3_METER_READING1, - 'value_parser': MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - 'value_name': 'BELGIUM_MBUS3_METER_READING1' - }, - { - 'obis_reference': obis.BELGIUM_MBUS3_METER_READING2, - 'value_parser': MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - 'value_name': 'BELGIUM_MBUS3_METER_READING2' - }, - { - 'obis_reference': obis.BELGIUM_MBUS4_DEVICE_TYPE, - 'value_parser': CosemParser(ValueParser(int)), - 'value_name': 'BELGIUM_MBUS4_DEVICE_TYPE' - }, - { - 'obis_reference': obis.BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, - 'value_parser': CosemParser(ValueParser(str)), - 'value_name': 'BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER' - }, - { - 'obis_reference': obis.BELGIUM_MBUS4_VALVE_POSITION, - 'value_parser': CosemParser(ValueParser(int)), - 'value_name': 'BELGIUM_MBUS4_VALVE_POSITION' - }, - { - 'obis_reference': obis.BELGIUM_MBUS4_METER_READING1, - 'value_parser': MBusParser( - ValueParser(timestamp), - ValueParser(Decimal) - ), - 'value_name': 'BELGIUM_MBUS4_METER_READING1' + 'value_name': 'MBUS_VALVE_POSITION' }, { - 'obis_reference': obis.BELGIUM_MBUS4_METER_READING2, + 'obis_reference': obis.MBUS_METER_READING, 'value_parser': MBusParser( ValueParser(timestamp), ValueParser(Decimal) ), - 'value_name': 'BELGIUM_MBUS4_METER_READING2' + 'value_name': 'MBUS_METER_READING' }, ] } diff --git a/test/objects/test_mbusdevice.py b/test/objects/test_mbusdevice.py index 03fde3f..03358dd 100644 --- a/test/objects/test_mbusdevice.py +++ b/test/objects/test_mbusdevice.py @@ -15,59 +15,59 @@ def setUp(self): device_type_parser = [ object["value_parser"] for object in v5_objects - if object["obis_reference"] == obis_references.DEVICE_TYPE + if object["obis_reference"] == obis_references.MBUS_DEVICE_TYPE ][0] device_type = device_type_parser.parse('0-2:24.1.0(003)\r\n') equipment_parser = [ object["value_parser"] for object in v5_objects - if object["obis_reference"] == obis_references.EQUIPMENT_IDENTIFIER_GAS + if object["obis_reference"] == obis_references.MBUS_EQUIPMENT_IDENTIFIER ][0] equipment = equipment_parser.parse('0-2:96.1.0(4730303339303031393336393930363139)\r\n') gas_reading_parser = [ object["value_parser"] for object in v5_objects - if object["obis_reference"] == obis_references.HOURLY_GAS_METER_READING + if object["obis_reference"] == obis_references.MBUS_METER_READING ][0] gas_reading = gas_reading_parser.parse('0-2:24.2.1(200426223001S)(00246.138*m3)\r\n') - mbus_device = MbusDevice(channel_id=1) - mbus_device.add(obis_references.DEVICE_TYPE, device_type, "DEVICE_TYPE") - mbus_device.add(obis_references.EQUIPMENT_IDENTIFIER_GAS, equipment, "EQUIPMENT_IDENTIFIER_GAS") - mbus_device.add(obis_references.HOURLY_GAS_METER_READING, gas_reading, "HOURLY_GAS_METER_READING") + mbus_device = MbusDevice(channel_id=2) + mbus_device.add(obis_references.MBUS_DEVICE_TYPE, device_type, "MBUS_DEVICE_TYPE") + mbus_device.add(obis_references.MBUS_EQUIPMENT_IDENTIFIER, equipment, "MBUS_EQUIPMENT_IDENTIFIER") + mbus_device.add(obis_references.MBUS_METER_READING, gas_reading, "MBUS_METER_READING") self.mbus_device = mbus_device def test_attributes(self): - self.assertEqual(self.mbus_device.DEVICE_TYPE.value, 3) - self.assertEqual(self.mbus_device.DEVICE_TYPE.unit, None) + self.assertEqual(self.mbus_device.MBUS_DEVICE_TYPE.value, 3) + self.assertEqual(self.mbus_device.MBUS_DEVICE_TYPE.unit, None) - self.assertEqual(self.mbus_device.EQUIPMENT_IDENTIFIER_GAS.value, + self.assertEqual(self.mbus_device.MBUS_EQUIPMENT_IDENTIFIER.value, '4730303339303031393336393930363139') - self.assertEqual(self.mbus_device.EQUIPMENT_IDENTIFIER_GAS.unit, None) + self.assertEqual(self.mbus_device.MBUS_EQUIPMENT_IDENTIFIER.unit, None) - self.assertEqual(self.mbus_device.HOURLY_GAS_METER_READING.value, Decimal('246.138')) - self.assertEqual(self.mbus_device.HOURLY_GAS_METER_READING.unit, 'm3') + self.assertEqual(self.mbus_device.MBUS_METER_READING.value, Decimal('246.138')) + self.assertEqual(self.mbus_device.MBUS_METER_READING.unit, 'm3') def test_to_json(self): self.assertEqual( json.loads(self.mbus_device.to_json()), { - 'CHANNEL_ID': 1, - 'DEVICE_TYPE': {'value': 3, 'unit': None}, - 'EQUIPMENT_IDENTIFIER_GAS': {'value': '4730303339303031393336393930363139', 'unit': None}, - 'HOURLY_GAS_METER_READING': {'datetime': '2020-04-26T20:30:01+00:00', 'value': 246.138, 'unit': 'm3'}} + 'CHANNEL_ID': 2, + 'MBUS_DEVICE_TYPE': {'value': 3, 'unit': None}, + 'MBUS_EQUIPMENT_IDENTIFIER': {'value': '4730303339303031393336393930363139', 'unit': None}, + 'MBUS_METER_READING': {'datetime': '2020-04-26T20:30:01+00:00', 'value': 246.138, 'unit': 'm3'}} ) def test_str(self): self.assertEqual( str(self.mbus_device), ( - 'MBUS DEVICE (channel 1)\n' - '\tDEVICE_TYPE: 3 [None]\n' - '\tEQUIPMENT_IDENTIFIER_GAS: 4730303339303031393336393930363139 [None]\n' - '\tHOURLY_GAS_METER_READING: 246.138 [m3] at 2020-04-26T20:30:01+00:00\n' + 'MBUS DEVICE (channel 2)\n' + '\tMBUS_DEVICE_TYPE: 3 [None]\n' + '\tMBUS_EQUIPMENT_IDENTIFIER: 4730303339303031393336393930363139 [None]\n' + '\tMBUS_METER_READING: 246.138 [m3] at 2020-04-26T20:30:01+00:00\n' ) ) diff --git a/test/objects/test_telegram.py b/test/objects/test_telegram.py index 3f39d4b..164a0ae 100644 --- a/test/objects/test_telegram.py +++ b/test/objects/test_telegram.py @@ -216,14 +216,6 @@ def test_instantiate(self): value_type=Decimal, value_val=Decimal('2')) - # DEVICE_TYPE (0-x:24.1.0) - self.verify_telegram_item(telegram, - 'DEVICE_TYPE', - object_type=CosemObject, - unit_val=None, - value_type=int, - value_val=3) - # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) self.verify_telegram_item(telegram, 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', @@ -272,7 +264,15 @@ def test_instantiate(self): value_type=Decimal, value_val=Decimal('0')) - # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) + # DEVICE_TYPE (0-1:24.1.0) + self.verify_telegram_item(telegram, + 'DEVICE_TYPE', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=3) + + # EQUIPMENT_IDENTIFIER_GAS (0-1:96.1.0) self.verify_telegram_item(telegram, 'EQUIPMENT_IDENTIFIER_GAS', object_type=CosemObject, @@ -340,28 +340,28 @@ def test_mbus_devices(self): self.assertEqual(len(mbus_devices), 2) mbus_device_1 = mbus_devices[0] - self.assertEqual(mbus_device_1.DEVICE_TYPE.value, 3) - self.assertEqual(mbus_device_1.EQUIPMENT_IDENTIFIER_GAS.value, None) - self.assertEqual(mbus_device_1.HOURLY_GAS_METER_READING.value, Decimal('0')) + self.assertEqual(mbus_device_1.MBUS_DEVICE_TYPE.value, 3) + self.assertEqual(mbus_device_1.MBUS_EQUIPMENT_IDENTIFIER.value, None) + self.assertEqual(mbus_device_1.MBUS_METER_READING.value, Decimal('0')) mbus_device_2 = mbus_devices[1] - self.assertEqual(mbus_device_2.DEVICE_TYPE.value, 3) - self.assertEqual(mbus_device_2.EQUIPMENT_IDENTIFIER_GAS.value, '4730303339303031393336393930363139') - self.assertEqual(mbus_device_2.HOURLY_GAS_METER_READING.value, Decimal('246.138')) + self.assertEqual(mbus_device_2.MBUS_DEVICE_TYPE.value, 3) + self.assertEqual(mbus_device_2.MBUS_EQUIPMENT_IDENTIFIER.value, '4730303339303031393336393930363139') + self.assertEqual(mbus_device_2.MBUS_METER_READING.value, Decimal('246.138')) def test_get_mbus_device_by_channel(self): parser = TelegramParser(telegram_specifications.V5) telegram = parser.parse(TELEGRAM_V5_TWO_MBUS) mbus_device_1 = telegram.get_mbus_device_by_channel(1) - self.assertEqual(mbus_device_1.DEVICE_TYPE.value, 3) - self.assertEqual(mbus_device_1.EQUIPMENT_IDENTIFIER_GAS.value, None) - self.assertEqual(mbus_device_1.HOURLY_GAS_METER_READING.value, Decimal('0')) + self.assertEqual(mbus_device_1.MBUS_DEVICE_TYPE.value, 3) + self.assertEqual(mbus_device_1.MBUS_EQUIPMENT_IDENTIFIER.value, None) + self.assertEqual(mbus_device_1.MBUS_METER_READING.value, Decimal('0')) mbus_device_2 = telegram.get_mbus_device_by_channel(2) - self.assertEqual(mbus_device_2.DEVICE_TYPE.value, 3) - self.assertEqual(mbus_device_2.EQUIPMENT_IDENTIFIER_GAS.value, '4730303339303031393336393930363139') - self.assertEqual(mbus_device_2.HOURLY_GAS_METER_READING.value, Decimal('246.138')) + self.assertEqual(mbus_device_2.MBUS_DEVICE_TYPE.value, 3) + self.assertEqual(mbus_device_2.MBUS_EQUIPMENT_IDENTIFIER.value, '4730303339303031393336393930363139') + self.assertEqual(mbus_device_2.MBUS_METER_READING.value, Decimal('246.138')) def test_without_mbus_devices(self): parser = TelegramParser(telegram_specifications.V5, apply_checksum_validation=False) @@ -375,11 +375,12 @@ def test_to_json(self): telegram = parser.parse(TELEGRAM_V5) json_data = json.loads(telegram.to_json()) + self.maxDiff = None + self.assertEqual( json_data, {'CURRENT_ELECTRICITY_DELIVERY': {'unit': 'kW', 'value': 0.0}, 'CURRENT_ELECTRICITY_USAGE': {'unit': 'kW', 'value': 0.244}, - 'DEVICE_TYPE': {'unit': None, 'value': 3}, 'ELECTRICITY_ACTIVE_TARIFF': {'unit': None, 'value': '0002'}, 'ELECTRICITY_DELIVERED_TARIFF_1': {'unit': 'kWh', 'value': 2.444}, 'ELECTRICITY_DELIVERED_TARIFF_2': {'unit': 'kWh', 'value': 0.0}, @@ -387,10 +388,6 @@ def test_to_json(self): 'ELECTRICITY_USED_TARIFF_2': {'unit': 'kWh', 'value': 2.399}, 'EQUIPMENT_IDENTIFIER': {'unit': None, 'value': '4B384547303034303436333935353037'}, - 'EQUIPMENT_IDENTIFIER_GAS': {'unit': None, 'value': None}, - 'HOURLY_GAS_METER_READING': {'datetime': '2017-01-02T15:10:05+00:00', - 'unit': 'm3', - 'value': 0.107}, 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE': {'unit': 'kW', 'value': 0.0}, 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE': {'unit': 'kW', 'value': 0.07}, 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE': {'unit': 'kW', 'value': 0.0}, @@ -405,15 +402,16 @@ def test_to_json(self): 'INSTANTANEOUS_VOLTAGE_L3': {'unit': 'V', 'value': 229.0}, 'LONG_POWER_FAILURE_COUNT': {'unit': None, 'value': 0}, 'MBUS_DEVICES': [{'CHANNEL_ID': 1, - 'DEVICE_TYPE': {'unit': None, 'value': 3}, - 'EQUIPMENT_IDENTIFIER_GAS': {'unit': None, - 'value': '3232323241424344313233343536373839'}, - 'HOURLY_GAS_METER_READING': {'datetime': '2017-01-02T15:10:05+00:00', - 'unit': 'm3', - 'value': 0.107}}, + 'MBUS_DEVICE_TYPE': {'unit': None, 'value': 3}, + 'MBUS_EQUIPMENT_IDENTIFIER': {'unit': None, + 'value': '3232323241424344313233343536373839'}, + 'MBUS_METER_READING': {'datetime': '2017-01-02T15:10:05+00:00', + 'unit': 'm3', + 'value': 0.107}}, {'CHANNEL_ID': 2, - 'DEVICE_TYPE': {'unit': None, 'value': 3}, - 'EQUIPMENT_IDENTIFIER_GAS': {'unit': None, 'value': None}}], + 'MBUS_DEVICE_TYPE': {'unit': None, 'value': 3}, + 'MBUS_EQUIPMENT_IDENTIFIER': {'unit': None, + 'value': None}}], 'P1_MESSAGE_HEADER': {'unit': None, 'value': '50'}, 'P1_MESSAGE_TIMESTAMP': {'unit': None, 'value': '2017-01-02T18:20:02+00:00'}, 'POWER_EVENT_FAILURE_LOG': {'buffer': [], @@ -433,6 +431,8 @@ def test_to_str(self): parser = TelegramParser(telegram_specifications.V5) telegram = parser.parse(TELEGRAM_V5) + self.maxDiff = None + self.assertEqual( str(telegram), ( @@ -463,22 +463,19 @@ def test_to_str(self): 'INSTANTANEOUS_CURRENT_L2: 0.44 [A]\n' 'INSTANTANEOUS_CURRENT_L3: 0.86 [A]\n' 'TEXT_MESSAGE: None [None]\n' - 'DEVICE_TYPE: 3 [None]\n' - 'MBUS DEVICE (channel 1)\n' - ' DEVICE_TYPE: 3 [None]\n' - ' EQUIPMENT_IDENTIFIER_GAS: 3232323241424344313233343536373839 [None]\n' - ' HOURLY_GAS_METER_READING: 0.107 [m3] at 2017-01-02T15:10:05+00:00\n' - 'MBUS DEVICE (channel 2)\n' - ' DEVICE_TYPE: 3 [None]\n' - ' EQUIPMENT_IDENTIFIER_GAS: None [None]\n' 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.070 [kW]\n' 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 0.032 [kW]\n' 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.142 [kW]\n' 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW]\n' 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW]\n' 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW]\n' - 'EQUIPMENT_IDENTIFIER_GAS: None [None]\n' - 'HOURLY_GAS_METER_READING: 0.107 [m3] at 2017-01-02T15:10:05+00:00\n' + 'MBUS DEVICE (channel 1)\n' + ' MBUS_DEVICE_TYPE: 3 [None]\n' + ' MBUS_EQUIPMENT_IDENTIFIER: 3232323241424344313233343536373839 [None]\n' + ' MBUS_METER_READING: 0.107 [m3] at 2017-01-02T15:10:05+00:00\n' + 'MBUS DEVICE (channel 2)\n' + ' MBUS_DEVICE_TYPE: 3 [None]\n' + ' MBUS_EQUIPMENT_IDENTIFIER: None [None]\n' ) ) diff --git a/test/test_parse_fluvius.py b/test/test_parse_fluvius.py index 7beb272..23d166f 100644 --- a/test/test_parse_fluvius.py +++ b/test/test_parse_fluvius.py @@ -228,47 +228,53 @@ def test_parse(self): assert result.TEXT_MESSAGE.unit is None assert result.TEXT_MESSAGE.value is None - # BELGIUM_MBUS1_DEVICE_TYPE (0-1:24.1.0) - assert isinstance(result.BELGIUM_MBUS1_DEVICE_TYPE, CosemObject) - assert result.BELGIUM_MBUS1_DEVICE_TYPE.unit is None - assert isinstance(result.BELGIUM_MBUS1_DEVICE_TYPE.value, int) - assert result.BELGIUM_MBUS1_DEVICE_TYPE.value == 3 - - # BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER (0-1:96.1.1) - assert isinstance(result.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, CosemObject) - assert result.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER.unit is None - assert isinstance(result.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER.value, str) - assert result.BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER.value == '37464C4F32313139303333373333' - - # BELGIUM_MBUS1_VALVE_POSITION (0-1:24.4.0) - assert isinstance(result.BELGIUM_MBUS1_VALVE_POSITION, CosemObject) - assert result.BELGIUM_MBUS1_VALVE_POSITION.unit is None - assert isinstance(result.BELGIUM_MBUS1_VALVE_POSITION.value, int) - assert result.BELGIUM_MBUS1_VALVE_POSITION.value == 1 - - # BELGIUM_MBUS1_METER_READING2 (0-1:24.2.3) - assert isinstance(result.BELGIUM_MBUS1_METER_READING2, MBusObject) - assert result.BELGIUM_MBUS1_METER_READING2.unit == 'm3' - assert isinstance(result.BELGIUM_MBUS1_METER_READING2.value, Decimal) - assert result.BELGIUM_MBUS1_METER_READING2.value == Decimal('112.384') - - # BELGIUM_MBUS2_DEVICE_TYPE (0-2:24.1.0) - assert isinstance(result.BELGIUM_MBUS2_DEVICE_TYPE, CosemObject) - assert result.BELGIUM_MBUS2_DEVICE_TYPE.unit is None - assert isinstance(result.BELGIUM_MBUS2_DEVICE_TYPE.value, int) - assert result.BELGIUM_MBUS2_DEVICE_TYPE.value == 7 - - # BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER (0-2:96.1.1) - assert isinstance(result.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, CosemObject) - assert result.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER.unit is None - assert isinstance(result.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER.value, str) - assert result.BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER.value == '3853414731323334353637383930' - - # BELGIUM_MBUS2_METER_READING1 (0-1:24.2.1) - assert isinstance(result.BELGIUM_MBUS2_METER_READING1, MBusObject) - assert result.BELGIUM_MBUS2_METER_READING1.unit == 'm3' - assert isinstance(result.BELGIUM_MBUS2_METER_READING1.value, Decimal) - assert result.BELGIUM_MBUS2_METER_READING1.value == Decimal('872.234') + # MBUS DEVICE 1 + mbus1 = result.get_mbus_device_by_channel(1) + + # MBUS_DEVICE_TYPE (0-1:24.1.0) + assert isinstance(mbus1.MBUS_DEVICE_TYPE, CosemObject) + assert mbus1.MBUS_DEVICE_TYPE.unit is None + assert isinstance(mbus1.MBUS_DEVICE_TYPE.value, int) + assert mbus1.MBUS_DEVICE_TYPE.value == 3 + + # MBUS_EQUIPMENT_IDENTIFIER (0-1:96.1.1) + assert isinstance(mbus1.MBUS_EQUIPMENT_IDENTIFIER, CosemObject) + assert mbus1.MBUS_EQUIPMENT_IDENTIFIER.unit is None + assert isinstance(mbus1.MBUS_EQUIPMENT_IDENTIFIER.value, str) + assert mbus1.MBUS_EQUIPMENT_IDENTIFIER.value == '37464C4F32313139303333373333' + + # MBUS_VALVE_POSITION (0-1:24.4.0) + assert isinstance(result.MBUS_VALVE_POSITION, CosemObject) + assert result.MBUS_VALVE_POSITION.unit is None + assert isinstance(result.MBUS_VALVE_POSITION.value, int) + assert result.MBUS_VALVE_POSITION.value == 1 + + # MBUS_METER_READING (0-1:24.2.3) + assert isinstance(mbus1.MBUS_METER_READING, MBusObject) + assert mbus1.MBUS_METER_READING.unit == 'm3' + assert isinstance(mbus1.MBUS_METER_READING.value, Decimal) + assert mbus1.MBUS_METER_READING.value == Decimal('112.384') + + # MBUS DEVICE 2 + mbus2 = result.get_mbus_device_by_channel(2) + + # MBUS_DEVICE_TYPE (0-2:24.1.0) + assert isinstance(mbus2.MBUS_DEVICE_TYPE, CosemObject) + assert mbus2.MBUS_DEVICE_TYPE.unit is None + assert isinstance(mbus2.MBUS_DEVICE_TYPE.value, int) + assert mbus2.MBUS_DEVICE_TYPE.value == 7 + + # MBUS_EQUIPMENT_IDENTIFIER (0-2:96.1.1) + assert isinstance(mbus2.MBUS_EQUIPMENT_IDENTIFIER, CosemObject) + assert mbus2.MBUS_EQUIPMENT_IDENTIFIER.unit is None + assert isinstance(mbus2.MBUS_EQUIPMENT_IDENTIFIER.value, str) + assert mbus2.MBUS_EQUIPMENT_IDENTIFIER.value == '3853414731323334353637383930' + + # MBUS_METER_READING (0-1:24.2.1) + assert isinstance(mbus2.MBUS_METER_READING, MBusObject) + assert mbus2.MBUS_METER_READING.unit == 'm3' + assert isinstance(mbus2.MBUS_METER_READING.value, Decimal) + assert mbus2.MBUS_METER_READING.value == Decimal('872.234') def test_checksum_valid(self): # No exception is raised. @@ -296,6 +302,8 @@ def test_to_json(self): telegram = parser.parse(TELEGRAM_FLUVIUS_V171_ALT) json_data = json.loads(telegram.to_json()) + self.maxDiff = None + self.assertEqual( json_data, {'BELGIUM_VERSION_INFORMATION': {'value': '50217', 'unit': None}, @@ -338,26 +346,19 @@ def test_to_json(self): 'ACTUAL_TRESHOLD_ELECTRICITY': {'value': 999.9, 'unit': 'kW'}, 'FUSE_THRESHOLD_L1': {'value': 999.0, 'unit': 'A'}, 'TEXT_MESSAGE': {'value': None, 'unit': None}, - 'BELGIUM_MBUS1_DEVICE_TYPE': {'value': 3, 'unit': None}, - 'MBUS_DEVICES': [{'BELGIUM_MBUS1_DEVICE_TYPE': {'value': 3, 'unit': None}, - 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER': {'value': '37464C4F32313233303838303237', + 'MBUS_DEVICES': [{'MBUS_DEVICE_TYPE': {'value': 3, 'unit': None}, + 'MBUS_EQUIPMENT_IDENTIFIER': {'value': '37464C4F32313233303838303237', 'unit': None}, - 'BELGIUM_MBUS1_VALVE_POSITION': {'value': 1, 'unit': None}, - 'BELGIUM_MBUS1_METER_READING2': {'datetime': '2023-11-02T11:10:02+00:00', - 'value': 92.287, 'unit': 'm3'}, + 'MBUS_VALVE_POSITION': {'value': 1, 'unit': None}, + 'MBUS_METER_READING': {'datetime': '2023-11-02T11:10:02+00:00', + 'value': 92.287, 'unit': 'm3'}, 'CHANNEL_ID': 1}, - {'BELGIUM_MBUS2_DEVICE_TYPE': {'value': 7, 'unit': None}, - 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER': {'value': '3853455430303030393631313733', + {'MBUS_DEVICE_TYPE': {'value': 7, 'unit': None}, + 'MBUS_EQUIPMENT_IDENTIFIER': {'value': '3853455430303030393631313733', 'unit': None}, - 'BELGIUM_MBUS2_METER_READING1': {'datetime': '2023-11-02T11:15:32+00:00', - 'value': 8.579, 'unit': 'm3'}, - 'CHANNEL_ID': 2}], - 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER': {'value': '37464C4F32313233303838303237', 'unit': None}, - 'BELGIUM_MBUS1_VALVE_POSITION': {'value': 1, 'unit': None}, - 'BELGIUM_MBUS1_METER_READING2': {'datetime': '2023-11-02T11:10:02+00:00', 'value': 92.287, 'unit': 'm3'}, - 'BELGIUM_MBUS2_DEVICE_TYPE': {'value': 7, 'unit': None}, - 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER': {'value': '3853455430303030393631313733', 'unit': None}, - 'BELGIUM_MBUS2_METER_READING1': {'datetime': '2023-11-02T11:15:32+00:00', 'value': 8.579, 'unit': 'm3'}} + 'MBUS_METER_READING': {'datetime': '2023-11-02T11:15:32+00:00', + 'value': 8.579, 'unit': 'm3'}, + 'CHANNEL_ID': 2}]} ) def test_to_str(self): @@ -399,21 +400,14 @@ def test_to_str(self): 'ACTUAL_TRESHOLD_ELECTRICITY: 999.9 [kW]\n' 'FUSE_THRESHOLD_L1: 999 [A]\n' 'TEXT_MESSAGE: None [None]\n' - 'BELGIUM_MBUS1_DEVICE_TYPE: 3 [None]\n' 'MBUS DEVICE (channel 1)\n' - ' BELGIUM_MBUS1_DEVICE_TYPE: 3 [None]\n' - ' BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: 37464C4F32313233303838303237 [None]\n' - ' BELGIUM_MBUS1_VALVE_POSITION: 1 [None]\n' - ' BELGIUM_MBUS1_METER_READING2: 92.287 [m3] at 2023-11-02T11:10:02+00:00\n' + ' MBUS_DEVICE_TYPE: 3 [None]\n' + ' MBUS_EQUIPMENT_IDENTIFIER: 37464C4F32313233303838303237 [None]\n' + ' MBUS_VALVE_POSITION: 1 [None]\n' + ' MBUS_METER_READING: 92.287 [m3] at 2023-11-02T11:10:02+00:00\n' 'MBUS DEVICE (channel 2)\n' - ' BELGIUM_MBUS2_DEVICE_TYPE: 7 [None]\n' - ' BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: 3853455430303030393631313733 [None]\n' - ' BELGIUM_MBUS2_METER_READING1: 8.579 [m3] at 2023-11-02T11:15:32+00:00\n' - 'BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: 37464C4F32313233303838303237 [None]\n' - 'BELGIUM_MBUS1_VALVE_POSITION: 1 [None]\n' - 'BELGIUM_MBUS1_METER_READING2: 92.287 [m3] at 2023-11-02T11:10:02+00:00\n' - 'BELGIUM_MBUS2_DEVICE_TYPE: 7 [None]\n' - 'BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: 3853455430303030393631313733 [None]\n' - 'BELGIUM_MBUS2_METER_READING1: 8.579 [m3] at 2023-11-02T11:15:32+00:00\n' + ' MBUS_DEVICE_TYPE: 7 [None]\n' + ' MBUS_EQUIPMENT_IDENTIFIER: 3853455430303030393631313733 [None]\n' + ' MBUS_METER_READING: 8.579 [m3] at 2023-11-02T11:15:32+00:00\n' ) ) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index 1736037..a321b21 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -172,12 +172,6 @@ def test_parse(self): assert telegram.TEXT_MESSAGE.unit is None assert telegram.TEXT_MESSAGE.value is None - # DEVICE_TYPE (0-x:24.1.0) - assert isinstance(telegram.DEVICE_TYPE, CosemObject) - assert telegram.DEVICE_TYPE.unit is None - assert isinstance(telegram.DEVICE_TYPE.value, int) - assert telegram.DEVICE_TYPE.value == 3 - # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, CosemObject) assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.unit == 'kW' @@ -215,27 +209,27 @@ def test_parse(self): assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value == Decimal('0') # There's only one Mbus device (gas meter) in this case. Alternatively - # use get_mbget_mbus_device_by_channel + # use get_mbus_device_by_channel gas_meter_devices = telegram.MBUS_DEVICES gas_meter_device = gas_meter_devices[0] - # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) - assert isinstance(gas_meter_device.DEVICE_TYPE, CosemObject) - assert gas_meter_device.DEVICE_TYPE.unit is None - assert isinstance(gas_meter_device.DEVICE_TYPE.value, int) - assert gas_meter_device.DEVICE_TYPE.value == 3 + # MBUS_DEVICE_TYPE (0-1:96.1.0) + assert isinstance(gas_meter_device.MBUS_DEVICE_TYPE, CosemObject) + assert gas_meter_device.MBUS_DEVICE_TYPE.unit is None + assert isinstance(gas_meter_device.MBUS_DEVICE_TYPE.value, int) + assert gas_meter_device.MBUS_DEVICE_TYPE.value == 3 - # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) - assert isinstance(gas_meter_device.EQUIPMENT_IDENTIFIER_GAS, CosemObject) - assert gas_meter_device.EQUIPMENT_IDENTIFIER_GAS.unit is None - assert isinstance(gas_meter_device.EQUIPMENT_IDENTIFIER_GAS.value, str) - assert gas_meter_device.EQUIPMENT_IDENTIFIER_GAS.value == '3232323241424344313233343536373839' + # MBUS_EQUIPMENT_IDENTIFIER (0-1:96.1.0) + assert isinstance(gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER, CosemObject) + assert gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER.unit is None + assert isinstance(gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER.value, str) + assert gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER.value == '3232323241424344313233343536373839' - # HOURLY_GAS_METER_READING (0-1:24.2.1) - assert isinstance(gas_meter_device.HOURLY_GAS_METER_READING, MBusObject) - assert gas_meter_device.HOURLY_GAS_METER_READING.unit == 'm3' - assert isinstance(telegram.HOURLY_GAS_METER_READING.value, Decimal) - assert gas_meter_device.HOURLY_GAS_METER_READING.value == Decimal('0.107') + # MBUS_METER_READING (0-1:24.2.1) + assert isinstance(gas_meter_device.MBUS_METER_READING, MBusObject) + assert gas_meter_device.MBUS_METER_READING.unit == 'm3' + assert isinstance(telegram.MBUS_METER_READING.value, Decimal) + assert gas_meter_device.MBUS_METER_READING.value == Decimal('0.107') def test_checksum_valid(self): # No exception is raised. @@ -270,8 +264,11 @@ def test_gas_timestamp_invalid(self): parser = TelegramParser(telegram_specifications.V5) telegram = parser.parse(invalid_date_telegram) - # HOURLY_GAS_METER_READING (0-1:24.2.1) - assert isinstance(telegram.HOURLY_GAS_METER_READING, MBusObject) - assert telegram.HOURLY_GAS_METER_READING.unit is None - assert isinstance(telegram.HOURLY_GAS_METER_READING.value, Decimal) - assert telegram.HOURLY_GAS_METER_READING.value == Decimal('0.000') + # MBUS DEVICE 1 + mbus1 = telegram.get_mbus_device_by_channel(1) + + # MBUS_METER_READING (0-1:24.2.1) + assert isinstance(mbus1.MBUS_METER_READING, MBusObject) + assert mbus1.MBUS_METER_READING.unit is None + assert isinstance(mbus1.MBUS_METER_READING.value, Decimal) + assert mbus1.MBUS_METER_READING.value == Decimal('0.000') From 1ce0925b6108e3dc77855a0e6767db988bb2c36d Mon Sep 17 00:00:00 2001 From: dupondje Date: Tue, 12 Mar 2024 11:14:43 +0100 Subject: [PATCH 213/226] Copy head_parsers list on construct. (#150) We need to create a copy of the head_parsers. Otherwise the self.value_formats contains just a reference to head_parsers. But the self.value_formats is modified in the _parse_values, causing invalid values in the head_parsers list. --- dsmr_parser/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 61515fc..d49d2bd 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -335,7 +335,7 @@ class ProfileGenericParser(DSMRObjectParser): """ def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): - self.value_formats = head_parsers + self.value_formats = head_parsers.copy() self.buffer_types = buffer_types self.parsers_for_unidentified = parsers_for_unidentified From 88aaa2d30fcd1ca47599f17e84ffff00742cd085 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 12 Mar 2024 12:50:36 +0100 Subject: [PATCH 214/226] Release 1.4.0 --- CHANGELOG.rst | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e5bacbd..e4e9e67 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Change Log ---------- +**1.4.0** (2024-03-12) +- Q3D add CURRENT_ELECTRICITY_DELIVERY + (`PR #149 `_ by `Aeroid `_) +- Copy head_parsers list on construct. + (`PR #150 `_ by `dupondje `_) + + **1.3.2** (2024-01-29) - Fix unit test for pyton 3.12 (`PR #148 `_ by `ndokter `_) diff --git a/setup.py b/setup.py index 1e7c15a..effe553 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.3.2', + version='1.4.0', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 61985c5c891d2927425c71ce1527fd9aa93f1606 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 12 Mar 2024 12:54:28 +0100 Subject: [PATCH 215/226] Changelog formatting --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e4e9e67..3f7fe0c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,7 @@ Change Log ---------- **1.4.0** (2024-03-12) + - Q3D add CURRENT_ELECTRICITY_DELIVERY (`PR #149 `_ by `Aeroid `_) - Copy head_parsers list on construct. @@ -9,10 +10,12 @@ Change Log **1.3.2** (2024-01-29) + - Fix unit test for pyton 3.12 (`PR #148 `_ by `ndokter `_) **1.3.1** (2023-11-06) + - Fix parsing peak usage with invalid timestamps (`PR #143 `_ by `dupondje `_) From 6387cc4440d7b41fefe63e2fc6685425d0722203 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 12 Mar 2024 12:56:13 +0100 Subject: [PATCH 216/226] CHANGELOG formatting --- CHANGELOG.rst | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3f7fe0c..478dcfa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,31 +3,25 @@ Change Log **1.4.0** (2024-03-12) -- Q3D add CURRENT_ELECTRICITY_DELIVERY - (`PR #149 `_ by `Aeroid `_) -- Copy head_parsers list on construct. - (`PR #150 `_ by `dupondje `_) +- Q3D add CURRENT_ELECTRICITY_DELIVERY (`PR #149 `_ by `Aeroid `_) +- Copy head_parsers list on construct. (`PR #150 `_ by `dupondje `_) **1.3.2** (2024-01-29) -- Fix unit test for pyton 3.12 - (`PR #148 `_ by `ndokter `_) +- Fix unit test for pyton 3.12 (`PR #148 `_ by `ndokter `_) **1.3.1** (2023-11-06) -- Fix parsing peak usage with invalid timestamps - (`PR #143 `_ by `dupondje `_) +- Fix parsing peak usage with invalid timestamps (`PR #143 `_ by `dupondje `_) **1.3.0** (2023-08-01) -- added E.ON Hungary; refactored DSMR specifications to fix obis reference conflicts - (`PR #137 `_ by `balazs92117 `_) +- added E.ON Hungary; refactored DSMR specifications to fix obis reference conflicts (`PR #137 `_ by `balazs92117 `_) **1.2.4** (2023-07-11) -- EQUIPMENT IDENTIFIER is wrong for Fluvius meters when other mbus devices are present - (`PR #133 `_ by `ejpalacios `_) +- EQUIPMENT IDENTIFIER is wrong for Fluvius meters when other mbus devices are present (`PR #133 `_ by `ejpalacios `_) **1.2.3** (2023-04-18) From d680d446831a2f47f4b0d5f1cefc7395c7a835be Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 12 Mar 2024 13:26:04 +0100 Subject: [PATCH 217/226] Update CHANGELOG.rst --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 478dcfa..5cb0f9b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,10 +3,10 @@ Change Log **1.4.0** (2024-03-12) +- Mbus alt (`PR #142 `_ by `dupondje `_) - Q3D add CURRENT_ELECTRICITY_DELIVERY (`PR #149 `_ by `Aeroid `_) - Copy head_parsers list on construct. (`PR #150 `_ by `dupondje `_) - **1.3.2** (2024-01-29) - Fix unit test for pyton 3.12 (`PR #148 `_ by `ndokter `_) From 3cf627eedcbb5ed6371a8aca3f9c8ca5f1d0cacc Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Thu, 28 Mar 2024 12:40:17 +0100 Subject: [PATCH 218/226] Change checksum log level (#151) * Update filereader.py * Update socket_.py * Update serial_.py * Update protocol.py * Update socket_.py --- dsmr_parser/clients/filereader.py | 2 +- dsmr_parser/clients/protocol.py | 2 +- dsmr_parser/clients/serial_.py | 2 +- dsmr_parser/clients/socket_.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index a2ab525..4769f16 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -73,7 +73,7 @@ def read_as_object(self): try: yield self.telegram_parser.parse(telegram) except InvalidChecksumError as e: - logger.warning(str(e)) + logger.info(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 4605fcf..73ad9d5 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -158,7 +158,7 @@ def handle_telegram(self, telegram): try: parsed_telegram = self.telegram_parser.parse(telegram) except InvalidChecksumError as e: - self.log.warning(str(e)) + self.log.info(str(e)) except ParseError: self.log.exception("failed to parse telegram") else: diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 945c4e7..08b69d8 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -37,7 +37,7 @@ def read(self): try: yield self.telegram_parser.parse(telegram) except InvalidChecksumError as e: - logger.warning(str(e)) + logger.info(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e) diff --git a/dsmr_parser/clients/socket_.py b/dsmr_parser/clients/socket_.py index 8ee16fa..968a582 100644 --- a/dsmr_parser/clients/socket_.py +++ b/dsmr_parser/clients/socket_.py @@ -54,7 +54,7 @@ def read(self): try: yield self.telegram_parser.parse(telegram) except InvalidChecksumError as e: - logger.warning(str(e)) + logger.info(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e) From b42ceb6555b1610c0f588d1296b2dbe1d53205d6 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 4 Jun 2024 15:51:03 +0200 Subject: [PATCH 219/226] Avoid loading timezone at runtime (#157) * Avoid loading timezone at runtime Pytz will load timezone info from files in a lazy fashion on first access. This triggers warnings in HA due to it blocking the event loop. Pre-load the needed timezone info at module import instead, which will run in executor in HA. * Update value_types.py --- dsmr_parser/value_types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/value_types.py b/dsmr_parser/value_types.py index 487e98c..faf10b9 100644 --- a/dsmr_parser/value_types.py +++ b/dsmr_parser/value_types.py @@ -2,6 +2,10 @@ import pytz +# TODO : Use system timezone +# Preload timezone to avoid loading in event loop later +local_tz = pytz.timezone('Europe/Amsterdam') + def timestamp(value): try: @@ -20,8 +24,6 @@ def timestamp(value): else: is_dst = False - # TODO : Use system timezone - local_tz = pytz.timezone('Europe/Amsterdam') localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst) return localized_datetime.astimezone(pytz.utc) From d7f30b506901053a3841203ecc7218bb4a6b363c Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 4 Jun 2024 15:59:32 +0200 Subject: [PATCH 220/226] Release 1.4.1 --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5cb0f9b..494e41e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**1.4.1** (2024-03-12) + +- Avoid loading timezone at runtime (`PR #157 `_ by `dupondje `_) + **1.4.0** (2024-03-12) - Mbus alt (`PR #142 `_ by `dupondje `_) diff --git a/setup.py b/setup.py index effe553..ec93e45 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.4.0', + version='1.4.1', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 059c0802ad6c08f31b76f7a85077768b538df065 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 4 Jun 2024 16:06:38 +0200 Subject: [PATCH 221/226] Fix changelog --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 494e41e..09cdb47 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,9 @@ Change Log ---------- -**1.4.1** (2024-03-12) +**1.4.1** (2024-06-04) -- Avoid loading timezone at runtime (`PR #157 `_ by `dupondje `_) +- Avoid loading timezone at runtime (`PR #157 `_ by `elupus `_) **1.4.0** (2024-03-12) From fc653263700b3f87bf23a5277cbf57bf07d0bb66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jun 2024 09:08:35 -0500 Subject: [PATCH 222/226] Swap pyserial-asyncio for pyserial-asyncio-fast (#158) * Swap pyserial-asyncio for pyserial-asyncio-fast fixes #154 * actually commit the import change --- dsmr_parser/clients/protocol.py | 2 +- dsmr_parser/clients/rfxtrx_protocol.py | 2 +- dsmr_parser/clients/serial_.py | 6 +++--- setup.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 73ad9d5..70be235 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -4,7 +4,7 @@ import asyncio import logging -from serial_asyncio import create_serial_connection +from serial_asyncio_fast import create_serial_connection from dsmr_parser import telegram_specifications from dsmr_parser.clients.telegram_buffer import TelegramBuffer diff --git a/dsmr_parser/clients/rfxtrx_protocol.py b/dsmr_parser/clients/rfxtrx_protocol.py index 848de71..0ebcafd 100644 --- a/dsmr_parser/clients/rfxtrx_protocol.py +++ b/dsmr_parser/clients/rfxtrx_protocol.py @@ -2,7 +2,7 @@ import asyncio -from serial_asyncio import create_serial_connection +from serial_asyncio_fast import create_serial_connection from .protocol import DSMRProtocol, _create_dsmr_protocol diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 08b69d8..a084c53 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -1,6 +1,6 @@ import logging import serial -import serial_asyncio +import serial_asyncio_fast from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError @@ -77,7 +77,7 @@ async def read(self, queue): :rtype: None """ # create Serial StreamReader - conn = serial_asyncio.open_serial_connection(**self.serial_settings) + conn = serial_asyncio_fast.open_serial_connection(**self.serial_settings) reader, _ = await conn while True: @@ -107,7 +107,7 @@ async def read_as_object(self, queue): """ # create Serial StreamReader - conn = serial_asyncio.open_serial_connection(**self.serial_settings) + conn = serial_asyncio_fast.open_serial_connection(**self.serial_settings) reader, _ = await conn while True: diff --git a/setup.py b/setup.py index ec93e45..631b3d6 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', - 'pyserial-asyncio<1', + 'pyserial-asyncio-fast>=0.11', 'pytz', 'Tailer==0.4.1', 'dlms_cosem==21.3.2' From 55ac551a2a0a37f5134cddbb89f553927e89c7a1 Mon Sep 17 00:00:00 2001 From: Ido Szargel <78739764+ido-szargel@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:19:01 +0200 Subject: [PATCH 223/226] Add socket timeout (#155) * Add socket timeout --- dsmr_parser/clients/socket_.py | 8 ++++++-- tox.ini | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/dsmr_parser/clients/socket_.py b/dsmr_parser/clients/socket_.py index 968a582..8976aaf 100644 --- a/dsmr_parser/clients/socket_.py +++ b/dsmr_parser/clients/socket_.py @@ -31,11 +31,15 @@ def read(self): buffer = b"" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle: - + socket_handle.settimeout(60) socket_handle.connect((self.host, self.port)) while True: - buffer += socket_handle.recv(self.BUFFER_SIZE) + try: + buffer += socket_handle.recv(self.BUFFER_SIZE) + except socket.timeout: + logger.error("Socket timeout occurred, exiting") + break lines = buffer.splitlines(keepends=True) diff --git a/tox.ini b/tox.ini index 59d57e7..8f20f71 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,9 @@ commands= [pylama:dsmr_parser/clients/__init__.py] ignore = W0611 +[pylama:dsmr_parser/clients/socket_.py] +ignore = C901 + [pylama:dsmr_parser/parsers.py] ignore = W605 @@ -24,4 +27,4 @@ ignore = E501 max_line_length = 120 [pylama:pycodestyle] -max_line_length = 120 \ No newline at end of file +max_line_length = 120 From 8829260b153e8a73287e76ffdb217242770c584f Mon Sep 17 00:00:00 2001 From: Dennis Siemensma <16581744+dennissiemensma@users.noreply.github.com> Date: Sat, 15 Jun 2024 11:33:15 +0200 Subject: [PATCH 224/226] Bump Github Actions to latest versions in favor of Node deprecations (#159) --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 91cf3e9..467be3c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,15 +21,15 @@ jobs: name: Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Cached PIP dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.cache/pip @@ -44,4 +44,4 @@ jobs: run: tox - name: Code coverage upload - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 From 94189b6b63e12554c866bc2964ffb7d5c7e8b2c4 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 14 Jul 2024 14:21:13 +0200 Subject: [PATCH 225/226] Preparing release 1.4.2 --- CHANGELOG.rst | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 09cdb47..fcba834 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Change Log ---------- +**1.4.2** (2024-07-14) + +- Bump Github Actions to latest versions in favor of Node deprecations (`PR #159 `_ by `dennissiemensma `_) +- Swap pyserial-asyncio for pyserial-asyncio-fast (`PR #158 `_ by `bdraco `_) + **1.4.1** (2024-06-04) - Avoid loading timezone at runtime (`PR #157 `_ by `elupus `_) diff --git a/setup.py b/setup.py index 631b3d6..0a18cbd 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author_email='mail@nldr.net', license='MIT', url='https://github.com/ndokter/dsmr_parser', - version='1.4.1', + version='1.4.2', packages=find_packages(exclude=('test', 'test.*')), install_requires=[ 'pyserial>=3,<4', From 99ad21c89628c547379f7f872ec9b5732404bde4 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 25 Aug 2024 14:55:22 +0200 Subject: [PATCH 226/226] added a bit of documentation on how to run tests (#165) * added a bit of documentation on how to run tests * formatting * formatting * formatting * formatting * formatting * formatting --- README.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.rst b/README.rst index 9d80b96..5aa9524 100644 --- a/README.rst +++ b/README.rst @@ -293,6 +293,36 @@ To install DSMR Parser: $ pip install dsmr-parser +Development +----------- + +Create a virtualenv and activate it followed by the installation of the dsmr-parser: + +.. code-block:: bash + + python3 -m venv venv + source venv/bin/activate + pip install -e . + +Install tox and run it: + +.. code-block:: bash + + pip install tox + tox + +You should see that the tests have succeeded: + +.. code-block:: text + + ======================================================================================================== 59 passed in 0.91s ======================================================================================================== + py: commands[1]> pylama dsmr_parser test + py: OK (11.55=setup[9.73]+cmd[1.29,0.53] seconds) + congratulations :) (11.69 seconds) + + +Now you can make changes by editing the code and rerunning tox to verify your changes. + Known issues ------------