From de167c89b6b4c01f166cbf745ae431665b6443e7 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 19 Feb 2023 12:24:44 +0100 Subject: [PATCH] =?UTF-8?q?issue-51-telegram=20refactored=20TelegramParser?= =?UTF-8?q?.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 | 174 +++-------- 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 | 191 +++++++++--- 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.2.0** (2023-02-18) + +- Improved gas meter (mbus devices) support and replaced Telegram dictionary with backwards compatible object (`PR #121 `_ by `ndokter `_) + **1.1.0** (2023-02-08) -- Add instantaneous reactive power + fixed swapped reactive total import export (`pull request #124 `_ by `yada75 `_) - +- 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] - - # See dsmr_reader.obis_references for all readable telegram values. - # Note that the available values differ per DSMR version. - -Telegram as an Object +Telegram object --------------------- -An object version of the telegram is available as well. +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'. + +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) + # 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') - for telegram in serial_reader.read_as_object(): - os.system('clear') - print(telegram) + # DEPRECATED: the dictionary approach of getting the values by key or `.items()' or '.get() is deprecated + telegram[obis_references.CURRENT_ELECTRICITY_USAGE] -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 - 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 @@ import tailer 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 @@ class FileReader(object): 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 @@ class FileInputReader(object): 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 @@ class FileTailReader(object): 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 @@ import serial_asyncio 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 @@ class SerialReader(object): 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 @@ class AsyncSerialReader(SerialReader): 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 @@ 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__) @@ -81,7 +80,7 @@ class SocketReader(object): 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 @@ EN = { 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 @@ EN = { 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 @@ 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' +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 -import datetime -import json from decimal import Decimal +import datetime +import json -class Telegram(object): +import pytz + +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 __init__(self, *args, **kwargs): + self._item_names = [] + self._mbus_devices = [] + super().__init__(*args, **kwargs) - 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 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) - def _get_item_names(self): - return [self._obis_name_mapping[k] for k, v in self._dictionary.items()] + # 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 @@ class Telegram(object): 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 @@ class MBusObject(DSMRObject): 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 @@ class MBusObjectPeak(DSMRObject): 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 @@ class CosemObject(DSMRObject): 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 @@ class ProfileGenericObject(DSMRObject): 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 @@ class ProfileGenericObject(DSMRObject): 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 @@ class ProfileGenericObject(DSMRObject): 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 decimal import Decimal 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 @@ class TelegramParser(object): ('!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 @@ class TelegramParser(object): 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 @@ class DSMRObjectParser(object): 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 @@ class MaxDemandParser(DSMRObjectParser): 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 @@ class MaxDemandParser(DSMRObjectParser): 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 @@ class ProfileGenericParser(DSMRObjectParser): 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 @@ setup( 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 @@ TELEGRAM_V5 = ( '!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 @@ 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 @@ -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 @@ class TelegramTest(unittest.TestCase): 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 @@ class TelegramTest(unittest.TestCase): 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 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 @@ -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(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.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(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 @@ class TelegramParserV5Test(unittest.TestCase): ) 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 @@ import unittest 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 @@ class ProtocolTest(unittest.TestCase): 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 @@ import unittest 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 @@ class RFXtrxProtocolTest(unittest.TestCase): 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'