diff --git a/README.md b/README.md new file mode 100644 index 0000000..e20bb78 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +This is a review regarding https://github.com/ndokter/dsmr_parser. + +## Review + +tialize and clean up resources for multiple test cases, which can reduce code repetition. + +## Good Points +- Thoroughly tests various aspects of the DSMR v4.2 telegram, including both successful parsing and error scenarios. +- Effectively leverages the `unittest` framework for organizing test cases. +- Consistently uses appropriate data types like `Decimal` for numeric precision. + +## Summary +This test suite is thorough and effectively covers the parsing of DSMR v4.2 telegrams. It includes tests for successful parsing, as well as handling invalid and missing checksum scenarios. The suite could be further improved by optimizing repetitive code and adopting more descriptive assertion methods provided by the `unittest` framework. Overall, it is a well-structured and comprehensive set of tests that offers good coverage. + +## Open Source Alternatives +- **Pytest**: A popular testing framework that can simplify complex test cases and offers more advanced features compared to `unittest`. +- **Testcontainers**: A library for integration and end-to-end testing using Docker containers, which might be useful if the project scales to include more external services and components. diff --git a/README.rst b/README2.rst similarity index 100% rename from README.rst rename to README2.rst diff --git a/reviews/dsmr_parser/__main__.py.json b/reviews/dsmr_parser/__main__.py.json new file mode 100644 index 0000000..b583a4b --- /dev/null +++ b/reviews/dsmr_parser/__main__.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from functools import partial\nimport argparse\nimport asyncio\nimport logging\n\nfrom dsmr_parser.clients import create_dsmr_reader, create_tcp_dsmr_reader\n\n\ndef console():\n \"\"\"Output DSMR data to console.\"\"\"\n\n parser = argparse.ArgumentParser(description=console.__doc__)\n parser.add_argument('--device', default='/dev/ttyUSB0',\n help='port to read DSMR data from')\n parser.add_argument('--host', default=None,\n help='alternatively connect using TCP host.')\n parser.add_argument('--port', default=None,\n help='TCP port to use for connection')\n parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L', '5S', 'Q3D'],\n help='DSMR version (2.2, 4, 5, 5B, 5L, 5S, Q3D)')\n parser.add_argument('--verbose', '-v', action='count')\n\n args = parser.parse_args()\n\n if args.verbose:\n level = logging.DEBUG\n else:\n level = logging.ERROR\n logging.basicConfig(level=level)\n\n loop = asyncio.get_event_loop()\n\n def print_callback(telegram):\n \"\"\"Callback that prints telegram values.\"\"\"\n for obiref, obj in telegram.items():\n if obj:\n print(obj.value, obj.unit)\n print()\n\n # create tcp or serial connection depending on args\n if args.host and args.port:\n create_connection = partial(create_tcp_dsmr_reader,\n args.host, args.port, args.version,\n print_callback, loop=loop)\n else:\n create_connection = partial(create_dsmr_reader,\n args.device, args.version,\n print_callback, loop=loop)\n\n try:\n # connect and keep connected until interrupted by ctrl-c\n while True:\n # create serial or tcp connection\n conn = create_connection()\n transport, protocol = loop.run_until_complete(conn)\n # wait until connection it closed\n loop.run_until_complete(protocol.wait_closed())\n # wait 5 seconds before attempting reconnect\n loop.run_until_complete(asyncio.sleep(5))\n except KeyboardInterrupt:\n # cleanup connection after user initiated shutdown\n transport.close()\n loop.run_until_complete(asyncio.sleep(0))\n finally:\n loop.close()\n", + "review": "**Grade: 7**\n\n### Bugs\n\n- There are no identifiable bugs causing the script to malfunction, but use of exceptions could be refined.\n\n### Optimizations\n\n- Use `asyncio.run()` instead of `loop.run_until_complete()` for simplicity and better structure in modern Python versions.\n- Consider using a specific logger instead of the root logger for more flexible logging configurations.\n- Add exception handling around network or serial connection to manage specific exceptions like `ConnectionError`.\n\n### Good points\n\n- The use of `argparse` makes the script flexible and easy to configure via command-line arguments.\n- Persistent reconnection strategy in case of connection drops is well-implemented.\n- Callback function (`print_callback`) is defined clearly and is easily extensible.\n\n### Summary\n\nThe script uses Python's asyncio feature to continuously read data from a DSMR device, either over a network or serial port, and outputs the data to the console. It is flexible due to the use of `argparse` for command-line argument parsing and handles reconnection attempts upon connection loss effectively. However, the script would benefit from some modernization using the latest asyncio practices and improved logging management.\n\n### Open source alternatives\n\n- **Home Assistant**: An open-source platform that supports DSMR readers among many other devices, offering a more comprehensive home automation solution.\n- **Domoticz**: Another home automation system that includes support for DSMR as one of its features for monitoring energy consumption.", + "filename": "__main__.py", + "path": "dsmr_parser/__main__.py", + "directory": "dsmr_parser", + "grade": 7, + "size": 2406, + "line_count": 66 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/__main__.py.md b/reviews/dsmr_parser/__main__.py.md new file mode 100644 index 0000000..c5fcd03 --- /dev/null +++ b/reviews/dsmr_parser/__main__.py.md @@ -0,0 +1,26 @@ +**Grade: 7** + +### Bugs + +- There are no identifiable bugs causing the script to malfunction, but use of exceptions could be refined. + +### Optimizations + +- Use `asyncio.run()` instead of `loop.run_until_complete()` for simplicity and better structure in modern Python versions. +- Consider using a specific logger instead of the root logger for more flexible logging configurations. +- Add exception handling around network or serial connection to manage specific exceptions like `ConnectionError`. + +### Good points + +- The use of `argparse` makes the script flexible and easy to configure via command-line arguments. +- Persistent reconnection strategy in case of connection drops is well-implemented. +- Callback function (`print_callback`) is defined clearly and is easily extensible. + +### Summary + +The script uses Python's asyncio feature to continuously read data from a DSMR device, either over a network or serial port, and outputs the data to the console. It is flexible due to the use of `argparse` for command-line argument parsing and handles reconnection attempts upon connection loss effectively. However, the script would benefit from some modernization using the latest asyncio practices and improved logging management. + +### Open source alternatives + +- **Home Assistant**: An open-source platform that supports DSMR readers among many other devices, offering a more comprehensive home automation solution. +- **Domoticz**: Another home automation system that includes support for DSMR as one of its features for monitoring energy consumption. \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/__init__.py.json b/reviews/dsmr_parser/clients/__init__.py.json new file mode 100644 index 0000000..e9bc5b5 --- /dev/null +++ b/reviews/dsmr_parser/clients/__init__.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \\\n SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5\nfrom dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader\nfrom dsmr_parser.clients.socket_ import SocketReader\nfrom dsmr_parser.clients.protocol import create_dsmr_protocol, \\\n create_dsmr_reader, create_tcp_dsmr_reader\n", + "review": "**8**\n\n### Bugs\n- No bugs detected in the provided code snippet.\n\n### Optimizations\n- Consider adding comments or docstrings to improve code readability and maintainability.\n- Ensure that the necessary exception handling is in place when using these imports in the main logic to prevent runtime errors.\n \n### Good points\n- The code is well-organized with logical grouping of related imports.\n- Efficient use of line continuation with backslashes for better readability.\n- The imports seem to imply a modular program structure which is beneficial for maintenance and testing.\n\n### Summary\nThe code is clean and well-organized, with a logical structure in grouping related imports. Although the snippet doesn't reveal implementation details, it suggests a modular design that is beneficial for maintenance and testing. Improvements in documentation such as comments and docstrings would enhance the comprehensibility of this snippet when integrated into a larger codebase. It is essential to ensure exception handling when these imports are used in practice.\n\n### Open source alternatives\n- **Home Assistant**: It is an open-source platform designed to help you manage your smart home devices. Home Assistant integrates with devices and services like DSMR (Dutch Smart Meter Reading).\n- **OpenEnergyMonitor**: A tool for monitoring energy usage using open-source blueprints for energy monitoring systems and can be adapted for smart meter readings.", + "filename": "__init__.py", + "path": "dsmr_parser/clients/__init__.py", + "directory": "clients", + "grade": 8, + "size": 345, + "line_count": 7 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/__init__.py.md b/reviews/dsmr_parser/clients/__init__.py.md new file mode 100644 index 0000000..948da1f --- /dev/null +++ b/reviews/dsmr_parser/clients/__init__.py.md @@ -0,0 +1,20 @@ +**8** + +### Bugs +- No bugs detected in the provided code snippet. + +### Optimizations +- Consider adding comments or docstrings to improve code readability and maintainability. +- Ensure that the necessary exception handling is in place when using these imports in the main logic to prevent runtime errors. + +### Good points +- The code is well-organized with logical grouping of related imports. +- Efficient use of line continuation with backslashes for better readability. +- The imports seem to imply a modular program structure which is beneficial for maintenance and testing. + +### Summary +The code is clean and well-organized, with a logical structure in grouping related imports. Although the snippet doesn't reveal implementation details, it suggests a modular design that is beneficial for maintenance and testing. Improvements in documentation such as comments and docstrings would enhance the comprehensibility of this snippet when integrated into a larger codebase. It is essential to ensure exception handling when these imports are used in practice. + +### Open source alternatives +- **Home Assistant**: It is an open-source platform designed to help you manage your smart home devices. Home Assistant integrates with devices and services like DSMR (Dutch Smart Meter Reading). +- **OpenEnergyMonitor**: A tool for monitoring energy usage using open-source blueprints for energy monitoring systems and can be adapted for smart meter readings. \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/filereader.py.json b/reviews/dsmr_parser/clients/filereader.py.json new file mode 100644 index 0000000..919a6c4 --- /dev/null +++ b/reviews/dsmr_parser/clients/filereader.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import logging\nimport fileinput\nimport tailer\n\nfrom dsmr_parser.clients.telegram_buffer import TelegramBuffer\nfrom dsmr_parser.exceptions import ParseError, InvalidChecksumError\nfrom dsmr_parser.parsers import TelegramParser\n\nlogger = logging.getLogger(__name__)\n\n\nclass FileReader(object):\n \"\"\"\n Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects\n for each read telegram.\n Usage:\n from dsmr_parser import telegram_specifications\n from dsmr_parser.clients.filereader import FileReader\n\n if __name__== \"__main__\":\n\n infile = '/data/smartmeter/readings.txt'\n\n file_reader = FileReader(\n file = infile,\n telegram_specification = telegram_specifications.V4\n )\n\n for telegram in file_reader.read_as_object():\n print(telegram)\n\n The file can be created like:\n from dsmr_parser import telegram_specifications\n from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5\n\n if __name__== \"__main__\":\n\n outfile = '/data/smartmeter/readings.txt'\n\n serial_reader = SerialReader(\n device='/dev/ttyUSB0',\n serial_settings=SERIAL_SETTINGS_V5,\n telegram_specification=telegram_specifications.V4\n )\n\n for telegram in serial_reader.read_as_object():\n f=open(outfile,\"ab+\")\n f.write(telegram._telegram_data.encode())\n f.close()\n \"\"\"\n\n def __init__(self, file, telegram_specification):\n self._file = file\n self.telegram_parser = TelegramParser(telegram_specification)\n self.telegram_buffer = TelegramBuffer()\n self.telegram_specification = telegram_specification\n\n def read_as_object(self):\n \"\"\"\n Read complete DSMR telegram's from a file and return a Telegram object.\n :rtype: generator\n \"\"\"\n with open(self._file, \"rb\") as file_handle:\n while True:\n data = file_handle.readline()\n\n if not data:\n break\n\n self.telegram_buffer.append(data.decode())\n\n for telegram in self.telegram_buffer.get_all():\n try:\n yield self.telegram_parser.parse(telegram)\n except InvalidChecksumError as e:\n logger.info(str(e))\n except ParseError as e:\n logger.error('Failed to parse telegram: %s', e)\n\n\nclass FileInputReader(object):\n \"\"\"\n Filereader to read and parse raw telegram strings from stdin or files specified at the commandline\n and instantiate Telegram objects for each read telegram.\n Usage python script \"syphon_smartmeter_readings_stdin.py\":\n from dsmr_parser import telegram_specifications\n from dsmr_parser.clients.filereader import FileInputReader\n\n if __name__== \"__main__\":\n\n fileinput_reader = FileReader(\n file = infile,\n telegram_specification = telegram_specifications.V4\n )\n\n for telegram in fileinput_reader.read_as_object():\n print(telegram)\n\n Command line:\n tail -f /data/smartmeter/readings.txt | python3 syphon_smartmeter_readings_stdin.py\n\n \"\"\"\n\n def __init__(self, telegram_specification):\n self.telegram_parser = TelegramParser(telegram_specification)\n self.telegram_buffer = TelegramBuffer()\n self.telegram_specification = telegram_specification\n\n def read_as_object(self):\n \"\"\"\n Read complete DSMR telegram's from stdin of filearguments specified on teh command line\n and return a Telegram object.\n :rtype: generator\n \"\"\"\n with fileinput.input(mode='rb') as file_handle:\n while True:\n data = file_handle.readline()\n str = data.decode()\n self.telegram_buffer.append(str)\n\n for telegram in self.telegram_buffer.get_all():\n try:\n yield self.telegram_parser.parse(telegram)\n except InvalidChecksumError as e:\n logger.warning(str(e))\n except ParseError as e:\n logger.error('Failed to parse telegram: %s', e)\n\n\nclass FileTailReader(object):\n \"\"\"\n Filereader to read and parse raw telegram strings from the tail of a\n given file and instantiate Telegram objects for each read telegram.\n Usage python script \"syphon_smartmeter_readings_stdin.py\":\n from dsmr_parser import telegram_specifications\n from dsmr_parser.clients.filereader import FileTailReader\n\n if __name__== \"__main__\":\n\n infile = '/data/smartmeter/readings.txt'\n\n filetail_reader = FileTailReader(\n file = infile,\n telegram_specification = telegram_specifications.V5\n )\n\n for telegram in filetail_reader.read_as_object():\n print(telegram)\n \"\"\"\n\n def __init__(self, file, telegram_specification):\n self._file = file\n self.telegram_parser = TelegramParser(telegram_specification)\n self.telegram_buffer = TelegramBuffer()\n self.telegram_specification = telegram_specification\n\n def read_as_object(self):\n \"\"\"\n Read complete DSMR telegram's from a files tail and return a Telegram object.\n :rtype: generator\n \"\"\"\n with open(self._file, \"rb\") as file_handle:\n for data in tailer.follow(file_handle):\n str = data.decode()\n self.telegram_buffer.append(str)\n\n for telegram in self.telegram_buffer.get_all():\n try:\n yield self.telegram_parser.parse(telegram)\n except InvalidChecksumError as e:\n logger.warning(str(e))\n except ParseError as e:\n logger.error('Failed to parse telegram: %s', e)\n", + "review": "# 7\n\n## Bugs\n- In the `FileInputReader.read_as_object` method, a local variable named `str` is used, which shadows the built-in `str` type.\n- There are inconsistencies with the use of exception logging levels between classes (e.g., `logger.info` vs `logger.warning`).\n- The code comments mention the usage of `FileReader` instead of `FileInputReader` in the `FileInputReader` class documentation.\n\n## Optimizations\n- Consider using context managers `with open()` for writing to the file in the commented documentation for better resource handling.\n- Refactor repetitive code patterns in different classes into a shared function or base class to follow the DRY principle.\n- Use more explicit exception messages for easier debugging inside the exception handling.\n- Ensure `telegram_buffer` and `telegram_parser` are adequately validated on initialization to handle potential errors gracefully.\n\n## Good points\n- The code is well-structured and organized with each class having a specific responsibility.\n- Detailed documentation is provided for usage of these classes in different contexts.\n- Makes effective use of Python's generator pattern offering optimal memory usage.\n\n## Summary\nThe code effectively reads and parses DSMR telegram data from files and standard input, offering structured class-based utilities for different ways of input. There are a few inconsistencies in variable naming and logging levels, which could potentially lead to maintenance challenges in the future. The repeated code could benefit from a refactor to improve readability and reduce duplication. However, the code is well-documented and designed with clear intentions, which helps subsequent developers understand the workflow.\n\n## Open source alternatives\n- **OpenDSMR**: An open-source DSMR reading platform with extensive support for various formats and devices.\n- **dsmr-reader**: A web application that reads and processes DSMR telegram data, provides dashboards and API access.\n", + "filename": "filereader.py", + "path": "dsmr_parser/clients/filereader.py", + "directory": "clients", + "grade": 7, + "size": 6144, + "line_count": 174 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/filereader.py.md b/reviews/dsmr_parser/clients/filereader.py.md new file mode 100644 index 0000000..cd95057 --- /dev/null +++ b/reviews/dsmr_parser/clients/filereader.py.md @@ -0,0 +1,24 @@ +# 7 + +## Bugs +- In the `FileInputReader.read_as_object` method, a local variable named `str` is used, which shadows the built-in `str` type. +- There are inconsistencies with the use of exception logging levels between classes (e.g., `logger.info` vs `logger.warning`). +- The code comments mention the usage of `FileReader` instead of `FileInputReader` in the `FileInputReader` class documentation. + +## Optimizations +- Consider using context managers `with open()` for writing to the file in the commented documentation for better resource handling. +- Refactor repetitive code patterns in different classes into a shared function or base class to follow the DRY principle. +- Use more explicit exception messages for easier debugging inside the exception handling. +- Ensure `telegram_buffer` and `telegram_parser` are adequately validated on initialization to handle potential errors gracefully. + +## Good points +- The code is well-structured and organized with each class having a specific responsibility. +- Detailed documentation is provided for usage of these classes in different contexts. +- Makes effective use of Python's generator pattern offering optimal memory usage. + +## Summary +The code effectively reads and parses DSMR telegram data from files and standard input, offering structured class-based utilities for different ways of input. There are a few inconsistencies in variable naming and logging levels, which could potentially lead to maintenance challenges in the future. The repeated code could benefit from a refactor to improve readability and reduce duplication. However, the code is well-documented and designed with clear intentions, which helps subsequent developers understand the workflow. + +## Open source alternatives +- **OpenDSMR**: An open-source DSMR reading platform with extensive support for various formats and devices. +- **dsmr-reader**: A web application that reads and processes DSMR telegram data, provides dashboards and API access. diff --git a/reviews/dsmr_parser/clients/protocol.py.json b/reviews/dsmr_parser/clients/protocol.py.json new file mode 100644 index 0000000..fe426fb --- /dev/null +++ b/reviews/dsmr_parser/clients/protocol.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "\"\"\"Asyncio protocol implementation for handling telegrams.\"\"\"\n\nfrom functools import partial\nimport asyncio\nimport logging\n\nfrom serial_asyncio_fast import create_serial_connection\n\nfrom dsmr_parser import telegram_specifications\nfrom dsmr_parser.clients.telegram_buffer import TelegramBuffer\nfrom dsmr_parser.exceptions import ParseError, InvalidChecksumError\nfrom dsmr_parser.parsers import TelegramParser\nfrom dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \\\n SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5\n\n\ndef create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs):\n \"\"\"Creates a DSMR asyncio protocol.\"\"\"\n protocol = _create_dsmr_protocol(dsmr_version, telegram_callback,\n DSMRProtocol, loop, **kwargs)\n return protocol\n\n\n# pylama noqa - because of \"complex\" (too long) if-elif-else.\n# Match - case might be a solution but it is not available in <3.10\ndef _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, **kwargs): #noqa\n \"\"\"Creates a DSMR asyncio protocol.\"\"\"\n\n if dsmr_version == '2.2':\n specification = telegram_specifications.V2_2\n serial_settings = SERIAL_SETTINGS_V2_2\n elif dsmr_version == '4':\n specification = telegram_specifications.V4\n serial_settings = SERIAL_SETTINGS_V4\n elif dsmr_version == '4+':\n specification = telegram_specifications.V5\n serial_settings = SERIAL_SETTINGS_V4\n elif dsmr_version == '5':\n specification = telegram_specifications.V5\n serial_settings = SERIAL_SETTINGS_V5\n elif dsmr_version == '5B':\n specification = telegram_specifications.BELGIUM_FLUVIUS\n serial_settings = SERIAL_SETTINGS_V5\n elif dsmr_version == \"5L\":\n specification = telegram_specifications.LUXEMBOURG_SMARTY\n serial_settings = SERIAL_SETTINGS_V5\n elif dsmr_version == \"5S\":\n specification = telegram_specifications.SWEDEN\n serial_settings = SERIAL_SETTINGS_V5\n elif dsmr_version == \"Q3D\":\n specification = telegram_specifications.Q3D\n serial_settings = SERIAL_SETTINGS_V5\n elif dsmr_version == 'ISKRA_IE':\n specification = telegram_specifications.ISKRA_IE\n serial_settings = SERIAL_SETTINGS_V5\n elif dsmr_version == '5EONHU':\n specification = telegram_specifications.EON_HUNGARY\n serial_settings = SERIAL_SETTINGS_V5\n else:\n raise NotImplementedError(\"No telegram parser found for version: %s\",\n dsmr_version)\n\n protocol = partial(protocol, loop, TelegramParser(specification),\n telegram_callback=telegram_callback, **kwargs)\n\n return protocol, serial_settings\n\n\ndef create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):\n \"\"\"Creates a DSMR asyncio protocol coroutine using serial port.\"\"\"\n protocol, serial_settings = create_dsmr_protocol(\n dsmr_version, telegram_callback, loop=None)\n serial_settings['url'] = port\n\n conn = create_serial_connection(loop, protocol, **serial_settings)\n return conn\n\n\ndef create_tcp_dsmr_reader(host, port, dsmr_version,\n telegram_callback, loop=None,\n keep_alive_interval=None):\n \"\"\"Creates a DSMR asyncio protocol coroutine using TCP connection.\"\"\"\n if not loop:\n loop = asyncio.get_event_loop()\n protocol, _ = create_dsmr_protocol(\n dsmr_version, telegram_callback, loop=loop,\n keep_alive_interval=keep_alive_interval)\n conn = loop.create_connection(protocol, host, port)\n return conn\n\n\nclass DSMRProtocol(asyncio.Protocol):\n \"\"\"Assemble and handle incoming data into complete DSM telegrams.\"\"\"\n\n transport = None\n telegram_callback = None\n\n def __init__(self, loop, telegram_parser,\n telegram_callback=None, keep_alive_interval=None):\n \"\"\"Initialize class.\"\"\"\n self.loop = loop\n self.log = logging.getLogger(__name__)\n self.telegram_parser = telegram_parser\n # callback to call on complete telegram\n self.telegram_callback = telegram_callback\n # buffer to keep incomplete incoming data\n self.telegram_buffer = TelegramBuffer()\n # keep a lock until the connection is closed\n self._closed = asyncio.Event()\n self._keep_alive_interval = keep_alive_interval\n self._active = True\n\n def connection_made(self, transport):\n \"\"\"Just logging for now.\"\"\"\n self.transport = transport\n self.log.debug('connected')\n self._active = False\n if self.loop and self._keep_alive_interval:\n self.loop.call_later(self._keep_alive_interval, self.keep_alive)\n\n def data_received(self, data):\n \"\"\"Add incoming data to buffer.\"\"\"\n\n # accept latin-1 (8-bit) on the line, to allow for non-ascii transport or padding\n data = data.decode(\"latin1\")\n self._active = True\n self.log.debug('received data: %s', data)\n self.telegram_buffer.append(data)\n\n for telegram in self.telegram_buffer.get_all():\n # ensure actual telegram is ascii (7-bit) only (ISO 646:1991 IRV required in section 5.5 of IEC 62056-21)\n telegram = telegram.encode(\"latin1\").decode(\"ascii\")\n self.handle_telegram(telegram)\n\n def keep_alive(self):\n if self._active:\n self.log.debug('keep-alive checked')\n self._active = False\n if self.loop:\n self.loop.call_later(self._keep_alive_interval, self.keep_alive)\n else:\n self.log.warning('keep-alive check failed')\n if self.transport:\n self.transport.close()\n\n def connection_lost(self, exc):\n \"\"\"Stop when connection is lost.\"\"\"\n if exc:\n self.log.exception('disconnected due to exception', exc_info=exc)\n else:\n self.log.info('disconnected because of close/abort.')\n self._closed.set()\n\n def handle_telegram(self, telegram):\n \"\"\"Send off parsed telegram to handling callback.\"\"\"\n self.log.debug('got telegram: %s', telegram)\n\n try:\n parsed_telegram = self.telegram_parser.parse(telegram)\n except InvalidChecksumError as e:\n self.log.info(str(e))\n except ParseError:\n self.log.exception(\"failed to parse telegram\")\n else:\n self.telegram_callback(parsed_telegram)\n\n async def wait_closed(self):\n \"\"\"Wait until connection is closed.\"\"\"\n await self._closed.wait()\n", + "review": "# 6\n\n## Bugs\n- Potential memory leak if large amounts of data are appended to `telegram_buffer` without being processed and cleared.\n- The `SERIAL_SETTINGS` are specified but not provided, making it unclear if the implementation correctly interfaces with serial connections.\n- Exception messages are logged but not raised or handled in any way that could allow upstream error handling.\n- The `loop=None` argument in `create_dsmr_reader` is not used when invoking `create_dsmr_protocol`.\n\n## Optimizations\n- Consider using the Python `async`/`await` syntax more extensively to simplify structure and improve readability, especially around network connection management.\n- The lengthy if-elif chain in `_create_dsmr_protocol` could be replaced with a dictionary mapping DSMR versions to their specifications and serial settings, which would simplify readability and maintenance.\n- Implement `match-case` for cleaner and more efficient branching when Python 3.10+ become a baseline.\n- Include error recovery mechanisms or retries in case of connection failures.\n- Use a timeout or limit for the connection methods to avoid indefinite blocking.\n\n## Good points\n- The use of logging helps in debugging and tracking the flow of data and errors.\n- Protocol separation caters well to both serial and TCP connection methods.\n- Asyncio `Protocol` based design is a good choice for handling asynchronous data over IO streams.\n\n## Summary\nThe code provides an asyncio-based DSMR protocol implementation aimed at handling telegram data via various connection types. While it effectively utilizes asyncio for asynchronous operations, the structure would benefit from using modern Python features for cleaner and more maintainable code. Several issues, such as potential memory leaks and incomplete error handling, would require attention. The use of logging is well-implemented, facilitating better debugging and monitoring during runtime. \n\n## Open source alternatives\n- **dsmr-reader**: A software tool for reading and visualizing DSMR data which uses Django as its web framework and has a rich set of features including long-term data storage.\n- **home-Assistant DSMR**: A popular home automation system that supports DSMR protocol for Dutch smart meters as part of its core or as an integration.\n- **pySerial**: A library offering serial port bindings for Python, which can be used as a foundation for handling serial communications.", + "filename": "protocol.py", + "path": "dsmr_parser/clients/protocol.py", + "directory": "clients", + "grade": 6, + "size": 6565, + "line_count": 170 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/protocol.py.md b/reviews/dsmr_parser/clients/protocol.py.md new file mode 100644 index 0000000..17de6be --- /dev/null +++ b/reviews/dsmr_parser/clients/protocol.py.md @@ -0,0 +1,27 @@ +# 6 + +## Bugs +- Potential memory leak if large amounts of data are appended to `telegram_buffer` without being processed and cleared. +- The `SERIAL_SETTINGS` are specified but not provided, making it unclear if the implementation correctly interfaces with serial connections. +- Exception messages are logged but not raised or handled in any way that could allow upstream error handling. +- The `loop=None` argument in `create_dsmr_reader` is not used when invoking `create_dsmr_protocol`. + +## Optimizations +- Consider using the Python `async`/`await` syntax more extensively to simplify structure and improve readability, especially around network connection management. +- The lengthy if-elif chain in `_create_dsmr_protocol` could be replaced with a dictionary mapping DSMR versions to their specifications and serial settings, which would simplify readability and maintenance. +- Implement `match-case` for cleaner and more efficient branching when Python 3.10+ become a baseline. +- Include error recovery mechanisms or retries in case of connection failures. +- Use a timeout or limit for the connection methods to avoid indefinite blocking. + +## Good points +- The use of logging helps in debugging and tracking the flow of data and errors. +- Protocol separation caters well to both serial and TCP connection methods. +- Asyncio `Protocol` based design is a good choice for handling asynchronous data over IO streams. + +## Summary +The code provides an asyncio-based DSMR protocol implementation aimed at handling telegram data via various connection types. While it effectively utilizes asyncio for asynchronous operations, the structure would benefit from using modern Python features for cleaner and more maintainable code. Several issues, such as potential memory leaks and incomplete error handling, would require attention. The use of logging is well-implemented, facilitating better debugging and monitoring during runtime. + +## Open source alternatives +- **dsmr-reader**: A software tool for reading and visualizing DSMR data which uses Django as its web framework and has a rich set of features including long-term data storage. +- **home-Assistant DSMR**: A popular home automation system that supports DSMR protocol for Dutch smart meters as part of its core or as an integration. +- **pySerial**: A library offering serial port bindings for Python, which can be used as a foundation for handling serial communications. \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/rfxtrx_protocol.py.json b/reviews/dsmr_parser/clients/rfxtrx_protocol.py.json new file mode 100644 index 0000000..3de0b1c --- /dev/null +++ b/reviews/dsmr_parser/clients/rfxtrx_protocol.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "\"\"\"Asyncio protocol implementation for handling telegrams over a RFXtrx connection .\"\"\"\n\nimport asyncio\n\nfrom serial_asyncio_fast import create_serial_connection\nfrom .protocol import DSMRProtocol, _create_dsmr_protocol\n\n\ndef create_rfxtrx_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs):\n \"\"\"Creates a RFXtrxDSMR asyncio protocol.\"\"\"\n protocol = _create_dsmr_protocol(dsmr_version, telegram_callback,\n RFXtrxDSMRProtocol, loop, **kwargs)\n return protocol\n\n\ndef create_rfxtrx_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):\n \"\"\"Creates a DSMR asyncio protocol coroutine using a RFXtrx serial port.\"\"\"\n protocol, serial_settings = create_rfxtrx_dsmr_protocol(\n dsmr_version, telegram_callback, loop=None)\n serial_settings['url'] = port\n\n conn = create_serial_connection(loop, protocol, **serial_settings)\n return conn\n\n\ndef create_rfxtrx_tcp_dsmr_reader(host, port, dsmr_version,\n telegram_callback, loop=None,\n keep_alive_interval=None):\n \"\"\"Creates a DSMR asyncio protocol coroutine using a RFXtrx TCP connection.\"\"\"\n if not loop:\n loop = asyncio.get_event_loop()\n protocol, _ = create_rfxtrx_dsmr_protocol(\n dsmr_version, telegram_callback, loop=loop,\n keep_alive_interval=keep_alive_interval)\n conn = loop.create_connection(protocol, host, port)\n return conn\n\n\nPACKETTYPE_DSMR = 0x62\nSUBTYPE_P1 = 0x01\n\n\nclass RFXtrxDSMRProtocol(DSMRProtocol):\n\n remaining_data = b''\n\n def data_received(self, data):\n \"\"\"Add incoming data to buffer.\"\"\"\n\n data = self.remaining_data + data\n\n packetlength = data[0] + 1 if len(data) > 0 else 1\n while packetlength <= len(data):\n packettype = data[1]\n subtype = data[2]\n if (packettype == PACKETTYPE_DSMR and subtype == SUBTYPE_P1):\n dsmr_data = data[4:packetlength]\n super().data_received(dsmr_data)\n data = data[packetlength:]\n packetlength = data[0] + 1 if len(data) > 0 else 1\n\n self.remaining_data = data\n", + "review": "**Grade: 7**\n\n### Bugs\n- Missing error handling or verification when unpacking the data could lead to potential runtime errors if the data source sends malformed packets.\n\n### Optimizations\n- Consider adding logging for connection attempts and data receipt, which would aid in debugging and monitoring the system.\n- Potentially utilize the modern `asyncio.run()` function or `async/await` syntax to handle the coroutine for Python 3.7+ for a more readable and concise structure.\n- The method `create_rfxtrx_dsmr_reader` redundantly sets `loop=None` in the `create_rfxtrx_dsmr_protocol()` call, as `None` is already the default value.\n- Checking packet integrity and validating specifically sized packets before processing would enhance robustness.\n\n### Good Points\n- The code leverages `asyncio` for asynchronous operations, making it suitable for I/O-bound tasks without blocking the executing thread.\n- The modular design by using factory functions (`create_rfxtrx_dsmr_protocol`, `create_rfxtrx_dsmr_reader`, etc.) enhances reusability and clarity.\n- Clear separation of concerns; handling of data reception and protocol setup are well-defined and focused.\n\n### Summary\nThe given asyncio protocol implementation is well-structured, with a focus on establishing and handling RFXtrx connections. While the code handles the basic requirements adequately, it lacks error checking and logging, which are crucial in network programming for reliability and troubleshooting. Updating to more modern asynchronous patterns could enhance its readability and ease of use.\n\n### Open source alternatives\n- **pyRFXtrx**: A library specifically designed for RFXtrx connections and communications, with broader device support and robust functionalities.\n- **home-assistant-rfxtrx**: A component of Home Assistant, although more comprehensive, it provides extended utilities and is actively maintained by the community.", + "filename": "rfxtrx_protocol.py", + "path": "dsmr_parser/clients/rfxtrx_protocol.py", + "directory": "clients", + "grade": 7, + "size": 2173, + "line_count": 63 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/rfxtrx_protocol.py.md b/reviews/dsmr_parser/clients/rfxtrx_protocol.py.md new file mode 100644 index 0000000..11a4db5 --- /dev/null +++ b/reviews/dsmr_parser/clients/rfxtrx_protocol.py.md @@ -0,0 +1,22 @@ +**Grade: 7** + +### Bugs +- Missing error handling or verification when unpacking the data could lead to potential runtime errors if the data source sends malformed packets. + +### Optimizations +- Consider adding logging for connection attempts and data receipt, which would aid in debugging and monitoring the system. +- Potentially utilize the modern `asyncio.run()` function or `async/await` syntax to handle the coroutine for Python 3.7+ for a more readable and concise structure. +- The method `create_rfxtrx_dsmr_reader` redundantly sets `loop=None` in the `create_rfxtrx_dsmr_protocol()` call, as `None` is already the default value. +- Checking packet integrity and validating specifically sized packets before processing would enhance robustness. + +### Good Points +- The code leverages `asyncio` for asynchronous operations, making it suitable for I/O-bound tasks without blocking the executing thread. +- The modular design by using factory functions (`create_rfxtrx_dsmr_protocol`, `create_rfxtrx_dsmr_reader`, etc.) enhances reusability and clarity. +- Clear separation of concerns; handling of data reception and protocol setup are well-defined and focused. + +### Summary +The given asyncio protocol implementation is well-structured, with a focus on establishing and handling RFXtrx connections. While the code handles the basic requirements adequately, it lacks error checking and logging, which are crucial in network programming for reliability and troubleshooting. Updating to more modern asynchronous patterns could enhance its readability and ease of use. + +### Open source alternatives +- **pyRFXtrx**: A library specifically designed for RFXtrx connections and communications, with broader device support and robust functionalities. +- **home-assistant-rfxtrx**: A component of Home Assistant, although more comprehensive, it provides extended utilities and is actively maintained by the community. \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/serial_.py.json b/reviews/dsmr_parser/clients/serial_.py.json new file mode 100644 index 0000000..0f995b6 --- /dev/null +++ b/reviews/dsmr_parser/clients/serial_.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import logging\nimport serial\nimport serial_asyncio_fast\n\nfrom dsmr_parser.clients.telegram_buffer import TelegramBuffer\nfrom dsmr_parser.exceptions import ParseError, InvalidChecksumError\nfrom dsmr_parser.parsers import TelegramParser\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass SerialReader(object):\n PORT_KEY = 'port'\n\n def __init__(self, device, serial_settings, telegram_specification):\n self.serial_settings = serial_settings\n self.serial_settings[self.PORT_KEY] = device\n\n self.telegram_parser = TelegramParser(telegram_specification)\n self.telegram_buffer = TelegramBuffer()\n self.telegram_specification = telegram_specification\n\n def read(self):\n \"\"\"\n Read complete DSMR telegram's from the serial interface and parse it\n into CosemObject's and MbusObject's\n\n :rtype: generator\n \"\"\"\n with serial.Serial(**self.serial_settings) as serial_handle:\n while True:\n data = serial_handle.read(max(1, min(1024, serial_handle.in_waiting)))\n self.telegram_buffer.append(data.decode('ascii'))\n\n for telegram in self.telegram_buffer.get_all():\n try:\n yield self.telegram_parser.parse(telegram)\n except InvalidChecksumError as e:\n logger.info(str(e))\n except ParseError as e:\n logger.error('Failed to parse telegram: %s', e)\n\n def read_as_object(self):\n \"\"\"\n Read complete DSMR telegram's from the serial interface and return a Telegram object.\n\n :rtype: generator\n \"\"\"\n with serial.Serial(**self.serial_settings) as serial_handle:\n while True:\n data = serial_handle.readline()\n self.telegram_buffer.append(data.decode('ascii'))\n\n for telegram in self.telegram_buffer.get_all():\n try:\n yield self.telegram_parser.parse(telegram)\n except InvalidChecksumError as e:\n logger.warning(str(e))\n except ParseError as e:\n logger.error('Failed to parse telegram: %s', e)\n\n\nclass AsyncSerialReader(SerialReader):\n \"\"\"Serial reader using asyncio pyserial.\"\"\"\n\n PORT_KEY = 'url'\n\n async def read(self, queue):\n \"\"\"\n Read complete DSMR telegram's from the serial interface and parse it\n into CosemObject's and MbusObject's.\n\n Instead of being a generator, values are pushed to provided queue for\n asynchronous processing.\n\n :rtype: None\n \"\"\"\n # create Serial StreamReader\n conn = serial_asyncio_fast.open_serial_connection(**self.serial_settings)\n reader, _ = await conn\n\n while True:\n # Read line if available or give control back to loop until new\n # data has arrived.\n data = await reader.readline()\n self.telegram_buffer.append(data.decode('ascii'))\n\n for telegram in self.telegram_buffer.get_all():\n try:\n # Push new parsed telegram onto queue.\n queue.put_nowait(\n self.telegram_parser.parse(telegram)\n )\n except ParseError as e:\n logger.warning('Failed to parse telegram: %s', e)\n\n async def read_as_object(self, queue):\n \"\"\"\n Read complete DSMR telegram's from the serial interface\n and return a Telegram object.\n\n Instead of being a generator, Telegram objects are pushed\n to provided queue for asynchronous processing.\n\n :rtype: None\n \"\"\"\n\n # create Serial StreamReader\n conn = serial_asyncio_fast.open_serial_connection(**self.serial_settings)\n reader, _ = await conn\n\n while True:\n\n # Read line if available or give control back to loop until new\n # data has arrived.\n data = await reader.readline()\n self.telegram_buffer.append(data.decode('ascii'))\n\n for telegram in self.telegram_buffer.get_all():\n try:\n queue.put_nowait(\n self.telegram_parser.parse(telegram)\n )\n except InvalidChecksumError as e:\n logger.warning(str(e))\n except ParseError as e:\n logger.error('Failed to parse telegram: %s', e)\n", + "review": "# 7\n\n## Bugs\n- Potentially, `serial_asyncio_fast` is a non-standard library as there is no common knowledge about it, which might lead to compatibility issues or conflicts.\n- The repeated use of `data.decode('ascii')` could fail if non-ASCII bytes are received, causing an exception.\n- `serial_asyncio_fast.open_serial_connection` usage might be problematic if `serial_asyncio_fast` has different configurations or parameters for `open_serial_connection`.\n\n## Optimizations\n- Consider using a context manager for managing the async serial connection in `AsyncSerialReader`.\n- Add error handling to manage unexpected EOF or IOError during serial reads.\n- Use a more robust and universal encoding like `utf-8` rather than `ascii`.\n- Replace synchronous sleep loops with asyncio-compatible sleep methods for better event loop performance.\n\n## Good points\n- The code is structured into clear and concise classes, facilitating object-oriented approach.\n- It uses logging effectively to capture and report errors or warnings.\n- Asynchronous handling with `asyncio` is utilized for potentially better performance and integration in async environments.\n- The code provides separate synchronous and asynchronous classes, offering flexibility depending on the use case.\n\n## Summary\nThe provided code adeptly handles DSMR telegram reading, parsing, and processing, with solid separation of concerns through its class design. However, it could be improved by addressing potential bugs concerning byte decoding, the unconventional import of `serial_asyncio_fast`, and further optimizing asynchronous serial operations. Additionally, some encoding errors might arise due to non-ASCII byte communication, highlighting the need for safer encoding practices. Finally, leveraging context managers for asynchronous operations could result in more elegant and error-free code.\n\n## Open source alternatives\n- [Pyserial](https://github.com/pyserial/pyserial): Offers both synchronous and asynchronous methods for serial communication in Python.\n- [PySerial-Asyncio](https://github.com/pyserial/pyserial-asyncio): Provides asyncio support for pyserial, suitable for async serial communication in Python environments.\n- [PyDSM](https://github.com/ndokter/pyDSM): Specifically aimed at parsing DSMR protocols, which can be used for similar functionalities.", + "filename": "serial_.py", + "path": "dsmr_parser/clients/serial_.py", + "directory": "clients", + "grade": 7, + "size": 4532, + "line_count": 129 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/serial_.py.md b/reviews/dsmr_parser/clients/serial_.py.md new file mode 100644 index 0000000..7359917 --- /dev/null +++ b/reviews/dsmr_parser/clients/serial_.py.md @@ -0,0 +1,26 @@ +# 7 + +## Bugs +- Potentially, `serial_asyncio_fast` is a non-standard library as there is no common knowledge about it, which might lead to compatibility issues or conflicts. +- The repeated use of `data.decode('ascii')` could fail if non-ASCII bytes are received, causing an exception. +- `serial_asyncio_fast.open_serial_connection` usage might be problematic if `serial_asyncio_fast` has different configurations or parameters for `open_serial_connection`. + +## Optimizations +- Consider using a context manager for managing the async serial connection in `AsyncSerialReader`. +- Add error handling to manage unexpected EOF or IOError during serial reads. +- Use a more robust and universal encoding like `utf-8` rather than `ascii`. +- Replace synchronous sleep loops with asyncio-compatible sleep methods for better event loop performance. + +## Good points +- The code is structured into clear and concise classes, facilitating object-oriented approach. +- It uses logging effectively to capture and report errors or warnings. +- Asynchronous handling with `asyncio` is utilized for potentially better performance and integration in async environments. +- The code provides separate synchronous and asynchronous classes, offering flexibility depending on the use case. + +## Summary +The provided code adeptly handles DSMR telegram reading, parsing, and processing, with solid separation of concerns through its class design. However, it could be improved by addressing potential bugs concerning byte decoding, the unconventional import of `serial_asyncio_fast`, and further optimizing asynchronous serial operations. Additionally, some encoding errors might arise due to non-ASCII byte communication, highlighting the need for safer encoding practices. Finally, leveraging context managers for asynchronous operations could result in more elegant and error-free code. + +## Open source alternatives +- [Pyserial](https://github.com/pyserial/pyserial): Offers both synchronous and asynchronous methods for serial communication in Python. +- [PySerial-Asyncio](https://github.com/pyserial/pyserial-asyncio): Provides asyncio support for pyserial, suitable for async serial communication in Python environments. +- [PyDSM](https://github.com/ndokter/pyDSM): Specifically aimed at parsing DSMR protocols, which can be used for similar functionalities. \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/settings.py.json b/reviews/dsmr_parser/clients/settings.py.json new file mode 100644 index 0000000..f50403e --- /dev/null +++ b/reviews/dsmr_parser/clients/settings.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import serial\n\n\nSERIAL_SETTINGS_V2_2 = {\n 'baudrate': 9600,\n 'bytesize': serial.SEVENBITS,\n 'parity': serial.PARITY_EVEN,\n 'stopbits': serial.STOPBITS_ONE,\n 'xonxoff': 0,\n 'rtscts': 0,\n 'timeout': 20\n}\n\nSERIAL_SETTINGS_V4 = {\n 'baudrate': 115200,\n 'bytesize': serial.SEVENBITS,\n 'parity': serial.PARITY_EVEN,\n 'stopbits': serial.STOPBITS_ONE,\n 'xonxoff': 0,\n 'rtscts': 0,\n 'timeout': 20\n}\n\nSERIAL_SETTINGS_V5 = {\n 'baudrate': 115200,\n 'bytesize': serial.EIGHTBITS,\n 'parity': serial.PARITY_NONE,\n 'stopbits': serial.STOPBITS_ONE,\n 'xonxoff': 0,\n 'rtscts': 0,\n 'timeout': 20\n}\n", + "review": "# 6\n\n## Bugs\n- There are no apparent bugs in the given code snippet as it consists of properly defined dictionaries with settings.\n\n## Optimizations\n- **DRY Principle**: Consider refactoring the settings dictionaries to reduce redundancy. Common configurations could be maintained separately, with only differing settings defined in each version.\n- **Constants for Magic Numbers**: Define constants for repetitive values like the timeout and flow control settings (xonxoff, rtscts) which are consistently set to 0.\n- **Inline Comments**: Add comments explaining the context or purpose of each serial setting to improve readability.\n\n## Good points\n- **Clarity**: The code is clear and easy to understand, with settings being neatly partitioned by version.\n- **Use of Python's Serial Library Constants**: Proper use of constants from the `serial` library ensures settings are standardized and readable.\n\n## Summary\nThe code snippet sets serial connection settings for different versions, demonstrating clean and straightforward use of Python dictionaries. While there aren't any direct errors present, the design could be improved by reducing repetitive values through refactoring. Additionally, explaining the context of these configurations with comments might aid future developers in understanding the code's purpose.\n\n## Open source alternatives\n- **PySerial**: An open-source library for serial communication. The settings here could serve as configurations for creating or managing serial connections using this library.", + "filename": "settings.py", + "path": "dsmr_parser/clients/settings.py", + "directory": "clients", + "grade": 6, + "size": 639, + "line_count": 33 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/settings.py.md b/reviews/dsmr_parser/clients/settings.py.md new file mode 100644 index 0000000..d750aa3 --- /dev/null +++ b/reviews/dsmr_parser/clients/settings.py.md @@ -0,0 +1,19 @@ +# 6 + +## Bugs +- There are no apparent bugs in the given code snippet as it consists of properly defined dictionaries with settings. + +## Optimizations +- **DRY Principle**: Consider refactoring the settings dictionaries to reduce redundancy. Common configurations could be maintained separately, with only differing settings defined in each version. +- **Constants for Magic Numbers**: Define constants for repetitive values like the timeout and flow control settings (xonxoff, rtscts) which are consistently set to 0. +- **Inline Comments**: Add comments explaining the context or purpose of each serial setting to improve readability. + +## Good points +- **Clarity**: The code is clear and easy to understand, with settings being neatly partitioned by version. +- **Use of Python's Serial Library Constants**: Proper use of constants from the `serial` library ensures settings are standardized and readable. + +## Summary +The code snippet sets serial connection settings for different versions, demonstrating clean and straightforward use of Python dictionaries. While there aren't any direct errors present, the design could be improved by reducing repetitive values through refactoring. Additionally, explaining the context of these configurations with comments might aid future developers in understanding the code's purpose. + +## Open source alternatives +- **PySerial**: An open-source library for serial communication. The settings here could serve as configurations for creating or managing serial connections using this library. \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/socket_.py.json b/reviews/dsmr_parser/clients/socket_.py.json new file mode 100644 index 0000000..bccdc56 --- /dev/null +++ b/reviews/dsmr_parser/clients/socket_.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import logging\nimport socket\n\nfrom dsmr_parser.clients.telegram_buffer import TelegramBuffer\nfrom dsmr_parser.exceptions import ParseError, InvalidChecksumError\nfrom dsmr_parser.parsers import TelegramParser\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass SocketReader(object):\n\n BUFFER_SIZE = 256\n\n def __init__(self, host, port, telegram_specification):\n self.host = host\n self.port = port\n\n self.telegram_parser = TelegramParser(telegram_specification)\n self.telegram_buffer = TelegramBuffer()\n self.telegram_specification = telegram_specification\n\n def read(self):\n \"\"\"\n Read complete DSMR telegram's from remote interface and parse it\n into CosemObject's and MbusObject's\n\n :rtype: generator\n \"\"\"\n buffer = b\"\"\n\n with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:\n socket_handle.settimeout(60)\n socket_handle.connect((self.host, self.port))\n\n while True:\n try:\n buffer += socket_handle.recv(self.BUFFER_SIZE)\n except socket.timeout:\n logger.error(\"Socket timeout occurred, exiting\")\n break\n\n lines = buffer.splitlines(keepends=True)\n\n if len(lines) == 0:\n continue\n\n for data in lines:\n try:\n self.telegram_buffer.append(data.decode('ascii'))\n except UnicodeDecodeError:\n # Some garbage came through the channel\n # E.g.: Happens at EON_HUNGARY, but only once at the start of the socket.\n logger.error('Failed to parse telegram due to unicode decode error')\n\n for telegram in self.telegram_buffer.get_all():\n try:\n yield self.telegram_parser.parse(telegram)\n except InvalidChecksumError as e:\n logger.info(str(e))\n except ParseError as e:\n logger.error('Failed to parse telegram: %s', e)\n\n buffer = b\"\"\n\n def read_as_object(self):\n \"\"\"\n Read complete DSMR telegram's from remote and return a Telegram object.\n\n :rtype: generator\n \"\"\"\n buffer = b\"\"\n\n with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:\n\n socket_handle.connect((self.host, self.port))\n\n while True:\n buffer += socket_handle.recv(self.BUFFER_SIZE)\n\n lines = buffer.splitlines(keepends=True)\n\n if len(lines) == 0:\n continue\n\n for data in lines:\n self.telegram_buffer.append(data.decode('ascii'))\n\n for telegram in self.telegram_buffer.get_all():\n try:\n yield self.telegram_parser.parse(telegram)\n except InvalidChecksumError as e:\n logger.warning(str(e))\n except ParseError as e:\n logger.error('Failed to parse telegram: %s', e)\n\n buffer = b\"\"\n", + "review": "# Grade: 7\n\n## Bugs\n- The `socket.timeout` exception is handled in `read()` method but not in `read_as_object()`, leading to possible unhandled exceptions.\n- Potential UnicodeDecodeError is logged but not handled properly in the `read_as_object()` method.\n\n## Optimizations\n- Repeated code in methods `read()` and `read_as_object()` should be refactored to enhance maintainability and reduce duplication.\n- The `BUFFER_SIZE` could potentially be increased for more efficient data transfer if network constraints allow.\n- Log messages could be made more descriptive to aid debugging.\n- Add exception handling for other socket-related errors, like `socket.error`, to ensure robustness.\n- In `read_as_object`, setting a socket timeout would make the code more consistent and prevent indefinite blocking.\n\n## Good points\n- Use of `with` statement to ensure sockets are properly closed.\n- Logging is used to record errors and exceptions which helps in diagnosing issues.\n- Usage of generator functions provides memory efficiency.\n\n## Summary\nThe code is generally structured well, with proper logging mechanisms and efficient resource management using context managers. However, there are areas for improvement, particularly regarding code repetition, exception handling enhancements, and potentially optimizing socket read operations for efficiency. Refactoring the repeated code would make the codebase more maintainable and robust. Exception handling could be more comprehensive to manage all potential errors that might occur during socket communication.\n\n## Open source alternatives\n- **dsmr-reader**: A free and open source project to visually track and show all your smart meter data, directly from your own database.\n- **pysml**: A library for parsing smart meter language (SML) from electronic meters, commonly used in various energy sectors.", + "filename": "socket_.py", + "path": "dsmr_parser/clients/socket_.py", + "directory": "clients", + "grade": 7, + "size": 3265, + "line_count": 99 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/socket_.py.md b/reviews/dsmr_parser/clients/socket_.py.md new file mode 100644 index 0000000..c2f8fec --- /dev/null +++ b/reviews/dsmr_parser/clients/socket_.py.md @@ -0,0 +1,24 @@ +# Grade: 7 + +## Bugs +- The `socket.timeout` exception is handled in `read()` method but not in `read_as_object()`, leading to possible unhandled exceptions. +- Potential UnicodeDecodeError is logged but not handled properly in the `read_as_object()` method. + +## Optimizations +- Repeated code in methods `read()` and `read_as_object()` should be refactored to enhance maintainability and reduce duplication. +- The `BUFFER_SIZE` could potentially be increased for more efficient data transfer if network constraints allow. +- Log messages could be made more descriptive to aid debugging. +- Add exception handling for other socket-related errors, like `socket.error`, to ensure robustness. +- In `read_as_object`, setting a socket timeout would make the code more consistent and prevent indefinite blocking. + +## Good points +- Use of `with` statement to ensure sockets are properly closed. +- Logging is used to record errors and exceptions which helps in diagnosing issues. +- Usage of generator functions provides memory efficiency. + +## Summary +The code is generally structured well, with proper logging mechanisms and efficient resource management using context managers. However, there are areas for improvement, particularly regarding code repetition, exception handling enhancements, and potentially optimizing socket read operations for efficiency. Refactoring the repeated code would make the codebase more maintainable and robust. Exception handling could be more comprehensive to manage all potential errors that might occur during socket communication. + +## Open source alternatives +- **dsmr-reader**: A free and open source project to visually track and show all your smart meter data, directly from your own database. +- **pysml**: A library for parsing smart meter language (SML) from electronic meters, commonly used in various energy sectors. \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/telegram_buffer.py.json b/reviews/dsmr_parser/clients/telegram_buffer.py.json new file mode 100644 index 0000000..7723fd4 --- /dev/null +++ b/reviews/dsmr_parser/clients/telegram_buffer.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import re\n\n# - Match all characters after start of telegram except for the start\n# itself again '^\\/]+', which eliminates incomplete preceding telegrams.\n# - Do non greedy match using '?' so start is matched up to the first\n# checksum that's found.\n# - The checksum is optional '{0,4}' because not all telegram versions\n# support it.\n_FIND_TELEGRAMS_REGEX = re.compile(r\"\\/[^\\/]+?\\![A-F0-9]{0,4}\\0?\\r\\n\", re.DOTALL)\n\n\nclass TelegramBuffer(object):\n \"\"\"\n Used as a buffer for a stream of telegram data. Constructs full telegram\n strings from the buffered data and returns it.\n \"\"\"\n\n def __init__(self):\n self._buffer = \"\"\n\n def get_all(self):\n \"\"\"\n Remove complete telegrams from buffer and yield them.\n :rtype generator:\n \"\"\"\n for telegram in _FIND_TELEGRAMS_REGEX.findall(self._buffer):\n self._remove(telegram)\n yield telegram\n\n def append(self, data):\n \"\"\"\n Add telegram data to buffer.\n :param str data: chars, lines or full telegram strings of telegram data\n \"\"\"\n self._buffer += data\n\n def _remove(self, telegram):\n \"\"\"\n Remove telegram from buffer and incomplete data preceding it. This\n is easier than validating the data before adding it to the buffer.\n :param str telegram:\n :return:\n \"\"\"\n # Remove data leading up to the telegram and the telegram itself.\n index = self._buffer.index(telegram) + len(telegram)\n\n self._buffer = self._buffer[index:]\n", + "review": "6\n\n### Bugs\n- The regular expression may miss some valid telegrams or mistakenly identify invalid ones due to the pattern being overly broad.\n- Using the `index()` method without precaution could throw a `ValueError` if the telegram is not present in the buffer, though this is unlikely due to the design.\n- It seems the `\\0` in the regex has no apparent purpose as a null character isn't commonly part of telegram line endings.\n\n### Optimizations\n- Could utilize `deque` for efficient popping from the left.\n- Use `str.startswith()` when matching telegram headers for checks instead of regex when modifications are needed.\n- Consider more detailed regex to prevent capturing unwanted characters or malformed strings if particular telegram structure is known.\n- Implement error handling to make the code more robust especially around index checks and regex matching.\n- Use more descriptive comments or docstrings to enhance understanding, especially on the `append` method.\n\n### Good points\n- The code is well-structured and follows an object-oriented design.\n- Use of regular expressions to identify specific data patterns is efficient for parsing logs or streams.\n- Buffers telegram messages correctly to separate complete telegrams from incomplete input.\n\n### Summary\nThe code displays a good initial implementation of a buffer for telegram message strings. It is efficient in its approach using regex to parse and dissect data. However, robustness can be improved with better error handling and more optimized data structures. There may also be room for refinement in the regex to avoid potential edge cases which could lead to runtime errors.\n\n### Open source alternatives\n- **mqtt**: An open-source implementation for MQTT protocol, useful for messaging applications.\n- **Telethon**: A Python library for interacting with Telegram's API directly, beneficial for secure telegram processing.", + "filename": "telegram_buffer.py", + "path": "dsmr_parser/clients/telegram_buffer.py", + "directory": "clients", + "grade": 6, + "size": 1543, + "line_count": 48 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/clients/telegram_buffer.py.md b/reviews/dsmr_parser/clients/telegram_buffer.py.md new file mode 100644 index 0000000..7d99cee --- /dev/null +++ b/reviews/dsmr_parser/clients/telegram_buffer.py.md @@ -0,0 +1,25 @@ +6 + +### Bugs +- The regular expression may miss some valid telegrams or mistakenly identify invalid ones due to the pattern being overly broad. +- Using the `index()` method without precaution could throw a `ValueError` if the telegram is not present in the buffer, though this is unlikely due to the design. +- It seems the `\0` in the regex has no apparent purpose as a null character isn't commonly part of telegram line endings. + +### Optimizations +- Could utilize `deque` for efficient popping from the left. +- Use `str.startswith()` when matching telegram headers for checks instead of regex when modifications are needed. +- Consider more detailed regex to prevent capturing unwanted characters or malformed strings if particular telegram structure is known. +- Implement error handling to make the code more robust especially around index checks and regex matching. +- Use more descriptive comments or docstrings to enhance understanding, especially on the `append` method. + +### Good points +- The code is well-structured and follows an object-oriented design. +- Use of regular expressions to identify specific data patterns is efficient for parsing logs or streams. +- Buffers telegram messages correctly to separate complete telegrams from incomplete input. + +### Summary +The code displays a good initial implementation of a buffer for telegram message strings. It is efficient in its approach using regex to parse and dissect data. However, robustness can be improved with better error handling and more optimized data structures. There may also be room for refinement in the regex to avoid potential edge cases which could lead to runtime errors. + +### Open source alternatives +- **mqtt**: An open-source implementation for MQTT protocol, useful for messaging applications. +- **Telethon**: A Python library for interacting with Telegram's API directly, beneficial for secure telegram processing. \ No newline at end of file diff --git a/reviews/dsmr_parser/exceptions.py.json b/reviews/dsmr_parser/exceptions.py.json new file mode 100644 index 0000000..63920c0 --- /dev/null +++ b/reviews/dsmr_parser/exceptions.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "class ParseError(Exception):\n pass\n\n\nclass InvalidChecksumError(ParseError):\n pass\n", + "review": "5\n\n### Bugs\n- No explicit code functionality is given, so no bugs are currently identifiable.\n- The classes do not include any methods, attributes, or detailed documentation.\n\n### Optimizations\n- Add docstrings to explain the purpose of each class and how they should be used.\n- Implement methods or attributes, if applicable, to provide functionality related to parsing and checksum validation.\n- Consider leveraging built-in exception-handling features in Python to reduce code maintenance.\n\n### Good points\n- The use of custom exception classes (`ParseError` and `InvalidChecksumError`) is a good practice for handling specific errors.\n- Both classes inherit from Python's `Exception` class, which is a correct use of inheritance for error handling.\n \n### Summary\nThe code defines two custom exception classes for handling parse errors and invalid checksum errors, but there is no additional functionality provided. It serves as a basic template for extending error handling in a larger application. To improve it, consider expanding on its functionality and documentation for clarity and usability.\n\n### Open source alternatives\n- [pyparsing](https://github.com/pyparsing/pyparsing): A general parsing module for creating and executing parsers.\n- [lxml](https://github.com/lxml/lxml): Useful for XML parsing which inherently deals with checksum and parse validation.", + "filename": "exceptions.py", + "path": "dsmr_parser/exceptions.py", + "directory": "dsmr_parser", + "grade": 5, + "size": 89, + "line_count": 7 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/exceptions.py.md b/reviews/dsmr_parser/exceptions.py.md new file mode 100644 index 0000000..f29e2b9 --- /dev/null +++ b/reviews/dsmr_parser/exceptions.py.md @@ -0,0 +1,21 @@ +5 + +### Bugs +- No explicit code functionality is given, so no bugs are currently identifiable. +- The classes do not include any methods, attributes, or detailed documentation. + +### Optimizations +- Add docstrings to explain the purpose of each class and how they should be used. +- Implement methods or attributes, if applicable, to provide functionality related to parsing and checksum validation. +- Consider leveraging built-in exception-handling features in Python to reduce code maintenance. + +### Good points +- The use of custom exception classes (`ParseError` and `InvalidChecksumError`) is a good practice for handling specific errors. +- Both classes inherit from Python's `Exception` class, which is a correct use of inheritance for error handling. + +### Summary +The code defines two custom exception classes for handling parse errors and invalid checksum errors, but there is no additional functionality provided. It serves as a basic template for extending error handling in a larger application. To improve it, consider expanding on its functionality and documentation for clarity and usability. + +### Open source alternatives +- [pyparsing](https://github.com/pyparsing/pyparsing): A general parsing module for creating and executing parsers. +- [lxml](https://github.com/lxml/lxml): Useful for XML parsing which inherently deals with checksum and parse validation. \ No newline at end of file diff --git a/reviews/dsmr_parser/obis_references.py.json b/reviews/dsmr_parser/obis_references.py.json new file mode 100644 index 0000000..89e1dda --- /dev/null +++ b/reviews/dsmr_parser/obis_references.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "\"\"\"\nContains the signatures of each telegram line.\n\nPreviously contained the channel + obis reference signatures, but has been\nrefactored to full line signatures to maintain backwards compatibility.\nMight be refactored in a backwards incompatible way as soon as proper telegram\nobjects are introduced.\n\"\"\"\nP1_MESSAGE_HEADER = r'^\\d-\\d:0\\.2\\.8.+?\\r\\n'\nP1_MESSAGE_TIMESTAMP = r'^\\d-\\d:1\\.0\\.0.+?\\r\\n'\nELECTRICITY_USED_TARIFF_1 = r'^\\d-\\d:1\\.8\\.1.+?\\r\\n'\nELECTRICITY_USED_TARIFF_2 = r'^\\d-\\d:1\\.8\\.2.+?\\r\\n'\nELECTRICITY_USED_TARIFF_3 = r'^\\d-\\d:1\\.8\\.3.+?\\r\\n'\nELECTRICITY_USED_TARIFF_4 = r'^\\d-\\d:1\\.8\\.4.+?\\r\\n'\nELECTRICITY_DELIVERED_TARIFF_1 = r'^\\d-\\d:2\\.8\\.1.+?\\r\\n'\nELECTRICITY_DELIVERED_TARIFF_2 = r'^\\d-\\d:2\\.8\\.2.+?\\r\\n'\nELECTRICITY_DELIVERED_TARIFF_3 = r'^\\d-\\d:2\\.8\\.3.+?\\r\\n'\nELECTRICITY_DELIVERED_TARIFF_4 = r'^\\d-\\d:2\\.8\\.4.+?\\r\\n'\nCURRENT_REACTIVE_IMPORTED = r'^\\d-\\d:3\\.7\\.0.+?\\r\\n'\nELECTRICITY_REACTIVE_IMPORTED_TOTAL = r'^\\d-\\d:3\\.8\\.0.+?\\r\\n'\nELECTRICITY_REACTIVE_IMPORTED_TARIFF_1 = r'^\\d-\\d:3\\.8\\.1.+?\\r\\n'\nELECTRICITY_REACTIVE_IMPORTED_TARIFF_2 = r'^\\d-\\d:3\\.8\\.2.+?\\r\\n'\nCURRENT_REACTIVE_EXPORTED = r'^\\d-\\d:4\\.7\\.0.+?\\r\\n'\nELECTRICITY_REACTIVE_EXPORTED_TOTAL = r'^\\d-\\d:4\\.8\\.0.+?\\r\\n'\nELECTRICITY_REACTIVE_EXPORTED_TARIFF_1 = r'^\\d-\\d:4\\.8\\.1.+?\\r\\n'\nELECTRICITY_REACTIVE_EXPORTED_TARIFF_2 = r'^\\d-\\d:4\\.8\\.2.+?\\r\\n'\nELECTRICITY_ACTIVE_TARIFF = r'^\\d-\\d:96\\.14\\.0.+?\\r\\n'\nEQUIPMENT_IDENTIFIER = r'^\\d-\\d:96\\.1\\.1.+?\\r\\n'\nCURRENT_ELECTRICITY_USAGE = r'^\\d-\\d:1\\.7\\.0.+?\\r\\n'\nCURRENT_ELECTRICITY_DELIVERY = r'^\\d-\\d:2\\.7\\.0.+?\\r\\n'\nLONG_POWER_FAILURE_COUNT = r'^\\d-\\d:96\\.7\\.9.+?\\r\\n'\nSHORT_POWER_FAILURE_COUNT = r'^\\d-\\d:96\\.7\\.21.+?\\r\\n'\nPOWER_EVENT_FAILURE_LOG = r'^\\d-\\d:99\\.97\\.0.+?\\r\\n'\nVOLTAGE_SAG_L1_COUNT = r'^\\d-\\d:32\\.32\\.0.+?\\r\\n'\nVOLTAGE_SAG_L2_COUNT = r'^\\d-\\d:52\\.32\\.0.+?\\r\\n'\nVOLTAGE_SAG_L3_COUNT = r'^\\d-\\d:72\\.32\\.0.+?\\r\\n'\nVOLTAGE_SWELL_L1_COUNT = r'^\\d-\\d:32\\.36\\.0.+?\\r\\n'\nVOLTAGE_SWELL_L2_COUNT = r'^\\d-\\d:52\\.36\\.0.+?\\r\\n'\nVOLTAGE_SWELL_L3_COUNT = r'^\\d-\\d:72\\.36\\.0.+?\\r\\n'\nINSTANTANEOUS_VOLTAGE_L1 = r'^\\d-\\d:32\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_VOLTAGE_L2 = r'^\\d-\\d:52\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_VOLTAGE_L3 = r'^\\d-\\d:72\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_CURRENT_L1 = r'^\\d-\\d:31\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_CURRENT_L2 = r'^\\d-\\d:51\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_CURRENT_L3 = r'^\\d-\\d:71\\.7\\.0.+?\\r\\n'\nFUSE_THRESHOLD_L1 = r'^\\d-\\d:31\\.4\\.0.+?\\r\\n' # Applicable when current limitation is active\nFUSE_THRESHOLD_L2 = r'^\\d-\\d:51\\.4\\.0.+?\\r\\n' # Applicable when current limitation is active\nFUSE_THRESHOLD_L3 = r'^\\d-\\d:71\\.4\\.0.+?\\r\\n' # Applicable when current limitation is active\nTEXT_MESSAGE_CODE = r'^\\d-\\d:96\\.13\\.1.+?\\r\\n'\nTEXT_MESSAGE = r'^\\d-\\d:96\\.13\\.0.+?\\r\\n'\nDEVICE_TYPE = r'^\\d-\\d:24\\.1\\.0.+?\\r\\n'\nINSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'^\\d-\\d:21\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'^\\d-\\d:41\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'^\\d-\\d:61\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'^\\d-\\d:22\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'^\\d-\\d:42\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'^\\d-\\d:62\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE = r'^\\d-\\d:23\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE = r'^\\d-\\d:24\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE = r'^\\d-\\d:43\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE = r'^\\d-\\d:44\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE = r'^\\d-\\d:63\\.7\\.0.+?\\r\\n'\nINSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE = r'^\\d-\\d:64\\.7\\.0.+?\\r\\n'\nEQUIPMENT_IDENTIFIER_GAS = r'^\\d-\\d:96\\.1\\.0.+?\\r\\n'\n# TODO differences between gas meter readings in v3 and lower and v4 and up\nHOURLY_GAS_METER_READING = r'^\\d-\\d:24\\.2\\.1.+?\\r\\n'\nGAS_METER_READING = r'^\\d-\\d:24\\.3\\.0.+?\\r\\n.+?\\r\\n'\nACTUAL_TRESHOLD_ELECTRICITY = r'^\\d-\\d:17\\.0\\.0.+?\\r\\n'\nACTUAL_SWITCH_POSITION = r'^\\d-\\d:96\\.3\\.10.+?\\r\\n'\nVALVE_POSITION_GAS = r'^\\d-\\d:24\\.4\\.0.+?\\r\\n'\n\n# Multiple 'slaves' can be linked to the main device.\n# The type is reported on 24.1.0\n# Specifications are in EN 13757-3\n# For example: Water mater = 7, Gas meter = 3\n# Identifier is on 96.1.0 (in NL for ex) or\n# on 96.1.1 (in BE for ex)\n# The values are reported on 24.2.1\n# With an exception in Belgium for the GAS meter\n# Be aware that for the gas volume, another OBIS-code is published\n# than the one listed in section 7 of DSMR P1.\n# This is due to the fact that in Belgium the not-temperature\n# corrected gas volume is used while in the Netherlands,\n# the temperature corrected gas volume is used.\nMBUS_DEVICE_TYPE = r'^\\d-[1-9]:24\\.1\\.0.+?\\r\\n'\nMBUS_EQUIPMENT_IDENTIFIER = r'^\\d-[1-9]:96\\.1\\.[01].+?\\r\\n'\nMBUS_VALVE_POSITION = r'^\\d-[1-9]:24\\.4\\.0.+?\\r\\n'\nMBUS_METER_READING = r'^\\d-[1-9]:24\\.2\\.[13].+?\\r\\n'\n\n# TODO 17.0.0\n\nELECTRICITY_USED_TARIFF_ALL = (\n ELECTRICITY_USED_TARIFF_1,\n ELECTRICITY_USED_TARIFF_2\n)\nELECTRICITY_DELIVERED_TARIFF_ALL = (\n ELECTRICITY_DELIVERED_TARIFF_1,\n ELECTRICITY_DELIVERED_TARIFF_2\n)\n\n# International generalized additions\nELECTRICITY_IMPORTED_TOTAL = r'^\\d-\\d:1\\.8\\.0.+?\\r\\n' # Total imported energy register (P+)\nELECTRICITY_EXPORTED_TOTAL = r'^\\d-\\d:2\\.8\\.0.+?\\r\\n' # Total exported energy register (P-)\n\n# International non generalized additions (country specific) / risk for necessary refactoring\nBELGIUM_VERSION_INFORMATION = r'^\\d-\\d:96\\.1\\.4.+?\\r\\n'\nBELGIUM_EQUIPMENT_IDENTIFIER = r'^\\d-0:96\\.1\\.1.+?\\r\\n'\nBELGIUM_CURRENT_AVERAGE_DEMAND = r'^\\d-\\d:1\\.4\\.0.+?\\r\\n'\nBELGIUM_MAXIMUM_DEMAND_MONTH = r'^\\d-\\d:1\\.6\\.0.+?\\r\\n'\nBELGIUM_MAXIMUM_DEMAND_13_MONTHS = r'^\\d-\\d:98\\.1\\.0.+?\\r\\n'\n\nLUXEMBOURG_EQUIPMENT_IDENTIFIER = r'^\\d-\\d:42\\.0\\.0.+?\\r\\n' # Logical device name\n\nQ3D_EQUIPMENT_IDENTIFIER = r'^\\d-\\d:0\\.0\\.0.+?\\r\\n' # Logical device name\nQ3D_EQUIPMENT_STATE = r'^\\d-\\d:96\\.5\\.5.+?\\r\\n' # Device state (hexadecimal)\nQ3D_EQUIPMENT_SERIALNUMBER = r'^\\d-\\d:96\\.1\\.255.+?\\r\\n' # Device Serialnumber\n\n# EON Hungary\nEON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 = r'^\\d-\\d:5\\.8\\.0.+?\\r\\n'\nEON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 = r'^\\d-\\d:6\\.8\\.0.+?\\r\\n'\nEON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 = r'^\\d-\\d:7\\.8\\.0.+?\\r\\n'\nEON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 = r'^\\d-\\d:8\\.8\\.0.+?\\r\\n'\nEON_HU_ELECTRICITY_COMBINED = r'^\\d-\\d:15\\.8\\.0.+?\\r\\n'\nEON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL = r'^\\d-\\d:13\\.7\\.0.+?\\r\\n'\nEON_HU_INSTANTANEOUS_POWER_FACTOR_L1 = r'^\\d-\\d:33\\.7\\.0.+?\\r\\n'\nEON_HU_INSTANTANEOUS_POWER_FACTOR_L2 = r'^\\d-\\d:53\\.7\\.0.+?\\r\\n'\nEON_HU_INSTANTANEOUS_POWER_FACTOR_L3 = r'^\\d-\\d:73\\.7\\.0.+?\\r\\n'\nEON_HU_FREQUENCY = r'^\\d-\\d:14\\.7\\.0.+?\\r\\n'\nEON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 = r'^\\d-\\d:5\\.7\\.0.+?\\r\\n'\nEON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 = r'^\\d-\\d:6\\.7\\.0.+?\\r\\n'\nEON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 = r'^\\d-\\d:7\\.7\\.0.+?\\r\\n'\nEON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 = r'^\\d-\\d:8\\.7\\.0.+?\\r\\n'\n", + "review": "# 8\n\n## Bugs\n- No major bugs detected as the code mainly consists of constant definitions for regex patterns, serving as configuration or supportive code. \n\n## Optimizations\n- Consider organizing constants into categories or groups for better readability and maintainability, possibly grouping them into dictionaries or classes.\n- Utilize comments more effectively to clarify complex regex patterns.\n- If some of these regex patterns are used frequently in different parts of the program, encapsulate them in a function or class to avoid redundant code and improve manageability.\n\n## Good Points\n- Clarity in the structure of the patterns with consistent naming conventions for variables, making it easier to understand the purpose of each pattern.\n- Well-commented sections that provide context and guide the reader about potential refactoring, specifically for future enhancements or changes.\n\n## Summary\nThe provided code demonstrates consistent use of descriptive variable names in defining regex patterns, primarily purposed for parsing telegram messages related to power and gas metering. While generally clean and organized, it can be further optimized by categorizing related patterns. This will enhance maintainability, particularly when the codebase scales or undergoes changes. Overall, it's a solid foundation for handling meter data but would benefit from thoughtful refactoring for broader usage beyond configuration purposes.\n\n## Open source alternatives\n- **Open Energy Monitoring**: A suite of open-source tools for energy monitoring which might include similar functionalities.\n- **GridLAB-D**: An open-source simulation and analysis tool that might handle similar data parsing for utility data.\n- **Home Assistant**: An open-source platform that supports home automation, which could work with similar data patterns.", + "filename": "obis_references.py", + "path": "dsmr_parser/obis_references.py", + "directory": "dsmr_parser", + "grade": 8, + "size": 6817, + "line_count": 133 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/obis_references.py.md b/reviews/dsmr_parser/obis_references.py.md new file mode 100644 index 0000000..b02b764 --- /dev/null +++ b/reviews/dsmr_parser/obis_references.py.md @@ -0,0 +1,21 @@ +# 8 + +## Bugs +- No major bugs detected as the code mainly consists of constant definitions for regex patterns, serving as configuration or supportive code. + +## Optimizations +- Consider organizing constants into categories or groups for better readability and maintainability, possibly grouping them into dictionaries or classes. +- Utilize comments more effectively to clarify complex regex patterns. +- If some of these regex patterns are used frequently in different parts of the program, encapsulate them in a function or class to avoid redundant code and improve manageability. + +## Good Points +- Clarity in the structure of the patterns with consistent naming conventions for variables, making it easier to understand the purpose of each pattern. +- Well-commented sections that provide context and guide the reader about potential refactoring, specifically for future enhancements or changes. + +## Summary +The provided code demonstrates consistent use of descriptive variable names in defining regex patterns, primarily purposed for parsing telegram messages related to power and gas metering. While generally clean and organized, it can be further optimized by categorizing related patterns. This will enhance maintainability, particularly when the codebase scales or undergoes changes. Overall, it's a solid foundation for handling meter data but would benefit from thoughtful refactoring for broader usage beyond configuration purposes. + +## Open source alternatives +- **Open Energy Monitoring**: A suite of open-source tools for energy monitoring which might include similar functionalities. +- **GridLAB-D**: An open-source simulation and analysis tool that might handle similar data parsing for utility data. +- **Home Assistant**: An open-source platform that supports home automation, which could work with similar data patterns. \ No newline at end of file diff --git a/reviews/dsmr_parser/objects.py.json b/reviews/dsmr_parser/objects.py.json new file mode 100644 index 0000000..df3ca77 --- /dev/null +++ b/reviews/dsmr_parser/objects.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from decimal import Decimal\n\nimport datetime\nimport json\n\nimport pytz\n\n\nclass Telegram(dict):\n \"\"\"\n Container for parsed telegram data.\n\n Attributes can be accessed on a telegram object by addressing by their english name, for example:\n telegram.ELECTRICITY_USED_TARIFF_1\n\n All attributes in a telegram can be iterated over, for example:\n [k for k,v in telegram]\n yields:\n ['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...]\n\n Note: Dict like usage is deprecated. The inheritance from dict is because of backwards compatibility.\n \"\"\"\n def __init__(self, *args, **kwargs):\n self._item_names = []\n self._mbus_devices = []\n super().__init__(*args, **kwargs)\n\n def add(self, obis_reference, dsmr_object, obis_name):\n # Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER\n setattr(self, obis_name, dsmr_object)\n\n # TODO isinstance check: MaxDemandParser (BELGIUM_MAXIMUM_DEMAND_13_MONTHS) returns a list\n if isinstance(dsmr_object, DSMRObject) and dsmr_object.is_mbus_reading:\n self._add_mbus(obis_reference, dsmr_object, obis_name)\n elif obis_name not in self._item_names: # TODO repeating obis references\n self._item_names.append(obis_name)\n\n # Fill dict which is only used for backwards compatibility\n if obis_reference not in self:\n self[obis_reference] = dsmr_object\n\n def _add_mbus(self, obis_reference, dsmr_object, obis_name):\n \"\"\"\n The given DsmrObject is assumed to be Mbus related and will be grouped into a MbusDevice.\n Grouping is done by the DsmrObject channel ID.\n \"\"\"\n channel_id = dsmr_object.obis_id_code[1]\n\n # Create new MbusDevice or update existing one as it's records are being added one by one.\n mbus_device = self.get_mbus_device_by_channel(channel_id)\n if not mbus_device:\n mbus_device = MbusDevice(channel_id=channel_id)\n self._mbus_devices.append(mbus_device)\n\n mbus_device.add(obis_reference, dsmr_object, obis_name)\n\n if not hasattr(self, 'MBUS_DEVICES'):\n setattr(self, 'MBUS_DEVICES', self._mbus_devices)\n self._item_names.append('MBUS_DEVICES')\n\n def get_mbus_device_by_channel(self, channel_id):\n \"\"\"\n :rtype: MbusDevice|None\n \"\"\"\n for mbus_device in self._mbus_devices:\n if mbus_device.channel_id == channel_id:\n return mbus_device\n\n def __iter__(self):\n for attr in self._item_names:\n value = getattr(self, attr)\n yield attr, value\n\n def __str__(self):\n output = \"\"\n for attr, value in self:\n if isinstance(value, list):\n output += ''.join(map(str, value))\n else:\n output += \"{}: \\t {}\\n\".format(attr, str(value))\n\n return output\n\n def to_json(self):\n json_data = {}\n\n for attr, value in self:\n if isinstance(value, list):\n json_data[attr] = [json.loads(item.to_json() if hasattr(item, 'to_json') else item)\n for item in value]\n elif hasattr(value, 'to_json'):\n json_data[attr] = json.loads(value.to_json())\n\n return json.dumps(json_data)\n\n\nclass DSMRObject(object):\n \"\"\"\n Represents all data from a single telegram line.\n \"\"\"\n def __init__(self, obis_id_code, values):\n self.obis_id_code = obis_id_code\n self.values = values\n\n @property\n def is_mbus_reading(self):\n \"\"\" Detect Mbus related readings using obis id + channel. \"\"\"\n obis_id, channel_id = self.obis_id_code\n\n return obis_id == 0 and channel_id != 0\n\n def to_json(self):\n raise NotImplementedError\n\n\nclass MBusObject(DSMRObject):\n\n @property\n def datetime(self):\n return self.values[0]['value']\n\n @property\n def value(self):\n # TODO temporary workaround for DSMR v2.2. Maybe use the same type of\n # TODO object, but let the parse set them differently? So don't use\n # TODO hardcoded indexes here.\n if len(self.values) != 2: # v2\n return self.values[6]['value']\n else:\n return self.values[1]['value']\n\n @property\n def unit(self):\n # TODO temporary workaround for DSMR v2.2. Maybe use the same type of\n # TODO object, but let the parse set them differently? So don't use\n # TODO hardcoded indexes here.\n if len(self.values) != 2: # v2\n return self.values[5]['value']\n else:\n return self.values[1]['unit']\n\n def __str__(self):\n timestamp = self.datetime\n if isinstance(timestamp, datetime.datetime):\n timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat()\n output = \"{}\\t[{}] at {}\".format(\n str(self.value),\n str(self.unit),\n str(timestamp)\n )\n return output\n\n def to_json(self):\n timestamp = self.datetime\n if isinstance(timestamp, datetime.datetime):\n timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat()\n value = self.value\n if isinstance(value, datetime.datetime):\n value = value.astimezone().astimezone(pytz.utc).isoformat()\n if isinstance(value, Decimal):\n value = float(value)\n output = {\n 'datetime': timestamp,\n 'value': value,\n 'unit': self.unit\n }\n return json.dumps(output)\n\n\nclass MBusObjectPeak(DSMRObject):\n\n @property\n def datetime(self):\n return self.values[0]['value']\n\n @property\n def occurred(self):\n return self.values[1]['value']\n\n @property\n def value(self):\n return self.values[2]['value']\n\n @property\n def unit(self):\n return self.values[2]['unit']\n\n def __str__(self):\n timestamp = self.datetime\n if isinstance(timestamp, datetime.datetime):\n timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat()\n timestamp_occurred = self.occurred\n if isinstance(timestamp_occurred, datetime.datetime):\n timestamp_occurred = timestamp_occurred.astimezone().astimezone(pytz.utc).isoformat()\n value = self.value\n if isinstance(value, datetime.datetime):\n value = value.astimezone().astimezone(pytz.utc).isoformat()\n if isinstance(value, Decimal):\n value = float(value)\n output = \"{}\\t[{}] at {} occurred {}\"\\\n .format(str(value), str(self.unit), str(timestamp), str(timestamp_occurred))\n return output\n\n def to_json(self):\n timestamp = self.datetime\n if isinstance(timestamp, datetime.datetime):\n timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat()\n timestamp_occurred = self.occurred\n if isinstance(timestamp_occurred, datetime.datetime):\n timestamp_occurred = timestamp_occurred.astimezone().astimezone(pytz.utc).isoformat()\n value = self.value\n if isinstance(value, datetime.datetime):\n value = value.astimezone().astimezone(pytz.utc).isoformat()\n if isinstance(value, Decimal):\n value = float(value)\n output = {\n 'datetime': timestamp,\n 'occurred': timestamp_occurred,\n 'value': value,\n 'unit': self.unit\n }\n return json.dumps(output)\n\n\nclass CosemObject(DSMRObject):\n\n @property\n def value(self):\n return self.values[0]['value']\n\n @property\n def unit(self):\n return self.values[0]['unit']\n\n def __str__(self):\n print_value = self.value\n if isinstance(self.value, datetime.datetime):\n print_value = self.value.astimezone().astimezone(pytz.utc).isoformat()\n output = \"{}\\t[{}]\".format(str(print_value), str(self.unit))\n return output\n\n def to_json(self):\n json_value = self.value\n if isinstance(self.value, datetime.datetime):\n json_value = self.value.astimezone().astimezone(pytz.utc).isoformat()\n if isinstance(self.value, Decimal):\n json_value = float(self.value)\n output = {\n 'value': json_value,\n 'unit': self.unit\n }\n return json.dumps(output)\n\n\nclass ProfileGenericObject(DSMRObject):\n \"\"\"\n Represents all data in a GenericProfile value.\n All buffer values are returned as a list of MBusObjects,\n containing the datetime (timestamp) and the value.\n \"\"\"\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self._buffer_list = None\n\n @property\n def value(self):\n # value is added to make sure the telegram iterator does not break\n return self.values\n\n @property\n def unit(self):\n # value is added to make sure all items have a unit so code that relies on that does not break\n return None\n\n @property\n def buffer_length(self):\n return self.values[0]['value']\n\n @property\n def buffer_type(self):\n return self.values[1]['value']\n\n @property\n def buffer(self):\n if self._buffer_list is None:\n self._buffer_list = []\n values_offset = 2\n\n for i in range(self.buffer_length):\n offset = values_offset + i * 2\n self._buffer_list.append(\n MBusObject(\n obis_id_code=self.obis_id_code,\n values=[self.values[offset], self.values[offset + 1]]\n )\n )\n\n return self._buffer_list\n\n def __str__(self):\n output = \"\\t buffer length: {}\\n\".format(self.buffer_length)\n output += \"\\t buffer type: {}\".format(self.buffer_type)\n for buffer_value in self.buffer:\n timestamp = buffer_value.datetime\n if isinstance(timestamp, datetime.datetime):\n timestamp = str(timestamp.astimezone().astimezone(pytz.utc).isoformat())\n output += \"\\n\\t event occured at: {}\".format(timestamp)\n output += \"\\t for: {} [{}]\".format(buffer_value.value, buffer_value.unit)\n return output\n\n def to_json(self):\n \"\"\"\n :return: A json of all values in the GenericProfileObject , with the following structure\n {'buffer_length': n,\n 'buffer_type': obis_ref,\n 'buffer': [{'datetime': d1,\n 'value': v1,\n 'unit': u1},\n ...\n {'datetime': dn,\n 'value': vn,\n 'unit': un}\n ]\n }\n \"\"\"\n list = [['buffer_length', self.buffer_length]]\n list.append(['buffer_type', self.buffer_type])\n buffer_repr = [json.loads(buffer_item.to_json()) for buffer_item in self.buffer]\n list.append(['buffer', buffer_repr])\n output = dict(list)\n return json.dumps(output)\n\n\nclass MbusDevice:\n \"\"\"\n This object is similar to the Telegram except that it only contains readings related to the same mbus device.\n \"\"\"\n\n def __init__(self, channel_id):\n self.channel_id = channel_id\n self._item_names = []\n\n def add(self, obis_reference, dsmr_object, obis_name):\n # Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER\n # Also keep track of the added names internally\n setattr(self, obis_name, dsmr_object)\n self._item_names.append(obis_name)\n\n def __len__(self):\n return len(self._item_names)\n\n def __iter__(self):\n for attr in self._item_names:\n value = getattr(self, attr)\n yield attr, value\n\n def __str__(self):\n output = \"MBUS DEVICE (channel {})\\n\".format(self.channel_id)\n for attr, value in self:\n output += \"\\t{}: \\t {}\\n\".format(attr, str(value))\n return output\n\n def to_json(self):\n data = {obis_name: json.loads(value.to_json()) for obis_name, value in self}\n data['CHANNEL_ID'] = self.channel_id\n\n return json.dumps(data)\n", + "review": "# 6\n\n## Bugs\n- Potentially unsafe handling of JSON serialization, especially if any of the objects have complex types.\n- The handling of `Decimal` to `float` conversion within JSON methods may lead to precision loss.\n- Missed implementation of the `to_json` method in `DSMRObject`, which is raising `NotImplementedError`.\n- Direct manipulation of timezone might be incorrect depending on the input timezone given that the offsets are called twice consecutively without any reason.\n\n## Optimizations\n- Instead of hard-coded indexes for accessing values, consider more dynamic or descriptive access methods.\n- Handling of strings within `__str__` methods can be optimized via `str.join` rather than string concatenation.\n- Utilize more efficient data structures or iterators for attribute storage and retrieval, such as collections.OrderedDict.\n- Error handling around timezone conversions and JSON manipulations should be improved.\n- Introduce lazy loading for buffer processing in `ProfileGenericObject` with property decorators.\n- The structure could be modularized further by separating responsibilities into different modules/classes.\n\n## Good points\n- Use of class-based design organizes functionality by logical grouping.\n- The transition from backward-compatible to new code structure appears to be documented and considered.\n- Proper use of Python's `property` decorators for value encapsulation.\n- Inclusion of detailed object representations via `__str__`.\n\n## Summary\nThe code is a well-designed hierarchical structure for processing and managing DSMR telegram data. It makes good use of object-oriented principles and provides a clear interface for interacting with telegram attributes. However, a few improvements can be made, particularly regarding handling serialization, hard-coded indices and ensuring robust timezone handling. Additionally, optimization around inefficiencies in code execution, such as using more appropriate data structures or methods, would enhance performance.\n\n## Open source alternatives\n- **DSMR-Reader**: An open-source application for reading DSMR telegrams with more features, including more detailed data processing and UI components.\n- **Home Assistant DSMR Slimme Meter**: Integrates with Home Assistant to visualize and log energy consumption.\n- **Open Energy Monitor**: For broader energy monitoring solutions that can integrate with DSMR readers for analytics and monitoring.\n", + "filename": "objects.py", + "path": "dsmr_parser/objects.py", + "directory": "dsmr_parser", + "grade": 6, + "size": 12332, + "line_count": 369 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/objects.py.md b/reviews/dsmr_parser/objects.py.md new file mode 100644 index 0000000..cf791fa --- /dev/null +++ b/reviews/dsmr_parser/objects.py.md @@ -0,0 +1,29 @@ +# 6 + +## Bugs +- Potentially unsafe handling of JSON serialization, especially if any of the objects have complex types. +- The handling of `Decimal` to `float` conversion within JSON methods may lead to precision loss. +- Missed implementation of the `to_json` method in `DSMRObject`, which is raising `NotImplementedError`. +- Direct manipulation of timezone might be incorrect depending on the input timezone given that the offsets are called twice consecutively without any reason. + +## Optimizations +- Instead of hard-coded indexes for accessing values, consider more dynamic or descriptive access methods. +- Handling of strings within `__str__` methods can be optimized via `str.join` rather than string concatenation. +- Utilize more efficient data structures or iterators for attribute storage and retrieval, such as collections.OrderedDict. +- Error handling around timezone conversions and JSON manipulations should be improved. +- Introduce lazy loading for buffer processing in `ProfileGenericObject` with property decorators. +- The structure could be modularized further by separating responsibilities into different modules/classes. + +## Good points +- Use of class-based design organizes functionality by logical grouping. +- The transition from backward-compatible to new code structure appears to be documented and considered. +- Proper use of Python's `property` decorators for value encapsulation. +- Inclusion of detailed object representations via `__str__`. + +## Summary +The code is a well-designed hierarchical structure for processing and managing DSMR telegram data. It makes good use of object-oriented principles and provides a clear interface for interacting with telegram attributes. However, a few improvements can be made, particularly regarding handling serialization, hard-coded indices and ensuring robust timezone handling. Additionally, optimization around inefficiencies in code execution, such as using more appropriate data structures or methods, would enhance performance. + +## Open source alternatives +- **DSMR-Reader**: An open-source application for reading DSMR telegrams with more features, including more detailed data processing and UI components. +- **Home Assistant DSMR Slimme Meter**: Integrates with Home Assistant to visualize and log energy consumption. +- **Open Energy Monitor**: For broader energy monitoring solutions that can integrate with DSMR readers for analytics and monitoring. diff --git a/reviews/dsmr_parser/parsers.py.json b/reviews/dsmr_parser/parsers.py.json new file mode 100644 index 0000000..98f94a5 --- /dev/null +++ b/reviews/dsmr_parser/parsers.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import logging\nimport re\nfrom binascii import unhexlify\n\nfrom ctypes import c_ushort\nfrom decimal import Decimal\n\nfrom dlms_cosem.connection import XDlmsApduFactory\nfrom dlms_cosem.protocol.xdlms import GeneralGlobalCipher\n\nfrom dsmr_parser.objects import MBusObject, MBusObjectPeak, CosemObject, ProfileGenericObject, Telegram\nfrom dsmr_parser.exceptions import ParseError, InvalidChecksumError\nfrom dsmr_parser.value_types import timestamp\n\nlogger = logging.getLogger(__name__)\n\n\nclass TelegramParser(object):\n crc16_tab = []\n\n def __init__(self, telegram_specification, apply_checksum_validation=True):\n \"\"\"\n :param telegram_specification: determines how the telegram is parsed\n :param apply_checksum_validation: validate checksum if applicable for\n telegram DSMR version (v4 and up).\n :type telegram_specification: dict\n \"\"\"\n self.apply_checksum_validation = apply_checksum_validation\n self.telegram_specification = telegram_specification\n # Regexes are compiled once to improve performance\n self.telegram_specification_regexes = {\n object[\"obis_reference\"]: re.compile(object[\"obis_reference\"], re.DOTALL | re.MULTILINE)\n for object in self.telegram_specification['objects']\n }\n\n def parse(self, telegram_data, encryption_key=\"\", authentication_key=\"\", throw_ex=False): # noqa: C901\n \"\"\"\n Parse telegram from string to dict.\n The telegram str type makes python 2.x integration easier.\n\n :param str telegram_data: full telegram from start ('/') to checksum\n ('!ABCD') including line endings in between the telegram's lines\n :param str encryption_key: encryption key\n :param str authentication_key: authentication key\n :rtype: Telegram\n :raises ParseError:\n :raises InvalidChecksumError:\n \"\"\"\n\n if \"general_global_cipher\" in self.telegram_specification:\n if self.telegram_specification[\"general_global_cipher\"]:\n enc_key = unhexlify(encryption_key)\n auth_key = unhexlify(authentication_key)\n telegram_data = unhexlify(telegram_data)\n apdu = XDlmsApduFactory.apdu_from_bytes(apdu_bytes=telegram_data)\n if apdu.security_control.security_suite != 0:\n logger.warning(\"Untested security suite\")\n if apdu.security_control.authenticated and not apdu.security_control.encrypted:\n logger.warning(\"Untested authentication only\")\n if not apdu.security_control.authenticated and not apdu.security_control.encrypted:\n logger.warning(\"Untested not encrypted or authenticated\")\n if apdu.security_control.compressed:\n logger.warning(\"Untested compression\")\n if apdu.security_control.broadcast_key:\n logger.warning(\"Untested broadcast key\")\n telegram_data = apdu.to_plain_apdu(enc_key, auth_key).decode(\"ascii\")\n else:\n try:\n if unhexlify(telegram_data[0:2])[0] == GeneralGlobalCipher.TAG:\n raise RuntimeError(\"Looks like a general_global_cipher frame \"\n \"but telegram specification is not matching!\")\n except Exception:\n pass\n else:\n try:\n if unhexlify(telegram_data[0:2])[0] == GeneralGlobalCipher.TAG:\n raise RuntimeError(\n \"Looks like a general_global_cipher frame but telegram specification is not matching!\")\n except Exception:\n pass\n\n if self.apply_checksum_validation and self.telegram_specification['checksum_support']:\n self.validate_checksum(telegram_data)\n\n telegram = Telegram()\n\n for object in self.telegram_specification['objects']:\n pattern = self.telegram_specification_regexes[object[\"obis_reference\"]]\n matches = pattern.findall(telegram_data)\n\n # Some signatures are optional and may not be present,\n # so only parse lines that match\n for match in matches:\n try:\n dsmr_object = object[\"value_parser\"].parse(match)\n except ParseError:\n logger.error(\n \"ignore line with signature {}, because parsing failed.\".format(object[\"obis_reference\"]),\n exc_info=True\n )\n if throw_ex:\n raise\n except Exception as err:\n logger.error(\"Unexpected {}: {}\".format(type(err), err))\n raise\n else:\n telegram.add(\n obis_reference=object[\"obis_reference\"],\n dsmr_object=dsmr_object,\n obis_name=object[\"value_name\"]\n )\n\n return telegram\n\n @staticmethod\n def validate_checksum(telegram):\n \"\"\"\n :param str telegram:\n :raises ParseError:\n :raises InvalidChecksumError:\n \"\"\"\n\n # Extract the part for which the checksum applies.\n checksum_contents = re.search(r'\\/.+\\!', telegram, re.DOTALL)\n\n # Extract the hexadecimal checksum value itself.\n # The line ending '\\r\\n' for the checksum line can be ignored.\n checksum_hex = re.search(r'((?<=\\!)[0-9A-Z]{4})+', telegram)\n\n if not checksum_contents or not checksum_hex:\n raise ParseError(\n 'Failed to perform CRC validation because the telegram is '\n 'incomplete. The checksum and/or content values are missing.'\n )\n\n calculated_crc = TelegramParser.crc16(checksum_contents.group(0))\n expected_crc = int(checksum_hex.group(0), base=16)\n\n if calculated_crc != expected_crc:\n raise InvalidChecksumError(\n \"Invalid telegram. The CRC checksum '{}' does not match the \"\n \"expected '{}'\".format(\n calculated_crc,\n expected_crc\n )\n )\n\n @staticmethod\n def crc16(telegram):\n \"\"\"\n Calculate the CRC16 value for the given telegram\n\n :param str telegram:\n \"\"\"\n crcValue = 0x0000\n\n if len(TelegramParser.crc16_tab) == 0:\n for i in range(0, 256):\n crc = c_ushort(i).value\n for j in range(0, 8):\n if (crc & 0x0001):\n crc = c_ushort(crc >> 1).value ^ 0xA001\n else:\n crc = c_ushort(crc >> 1).value\n TelegramParser.crc16_tab.append(hex(crc))\n\n for c in telegram:\n d = ord(c)\n tmp = crcValue ^ d\n rotated = c_ushort(crcValue >> 8).value\n crcValue = rotated ^ int(TelegramParser.crc16_tab[(tmp & 0x00ff)], 0)\n\n return crcValue\n\n\nclass DSMRObjectParser(object):\n \"\"\"\n Parses an object (can also be see as a 'line') from a telegram.\n \"\"\"\n\n def __init__(self, *value_formats):\n self.value_formats = value_formats\n\n def _is_line_wellformed(self, line, values):\n # allows overriding by child class\n return (values and (len(values) == len(self.value_formats)))\n\n def _parse_values(self, values):\n # allows overriding by child class\n return [self.value_formats[i].parse(value)\n for i, value in enumerate(values)]\n\n def _parse_obis_id_code(self, line):\n \"\"\"\n Get the OBIS ID code\n\n Example line:\n '0-2:24.2.1(200426223001S)(00246.138*m3)'\n\n OBIS ID code = 0-2 returned as tuple\n \"\"\"\n try:\n return int(line[0]), int(line[2])\n except ValueError:\n raise ParseError(\"Invalid OBIS ID code for line '%s' in '%s'\", line, self)\n\n def _parse(self, line):\n # Match value groups, but exclude the parentheses\n pattern = re.compile(r'((?<=\\()[0-9a-zA-Z\\.\\*\\-\\:]{0,}(?=\\)))')\n\n values = re.findall(pattern, line)\n\n if not self._is_line_wellformed(line, values):\n raise ParseError(\"Invalid '%s' line for '%s'\", line, self)\n\n # Convert empty value groups to None for clarity.\n values = [None if value == '' else value for value in values]\n\n return self._parse_values(values)\n\n\nclass MBusParser(DSMRObjectParser):\n \"\"\"\n Gas meter value parser.\n\n These are lines with a timestamp and gas meter value.\n\n Line format:\n 'ID (TST) (Mv1*U1)'\n\n 1 2 3 4\n\n 1) OBIS Reduced ID-code\n 2) Time Stamp (TST) of capture time of measurement value\n 3) Measurement value 1 (most recent entry of buffer attribute without unit)\n 4) Unit of measurement values (Unit of capture objects attribute)\n \"\"\"\n\n def parse(self, line):\n return MBusObject(\n obis_id_code=self._parse_obis_id_code(line),\n values=self._parse(line)\n )\n\n\nclass MaxDemandParser(DSMRObjectParser):\n \"\"\"\n Max demand history parser.\n\n These are lines with multiple values. Each containing 2 timestamps and a value\n\n Line format:\n 'ID (Count) (ID) (ID) (TST) (TST) (Mv1*U1)'\n\n 1 2 3 4 5 6 7\n\n 1) OBIS Reduced ID-code\n 2) Amount of values in the response\n 3) ID of the source\n 4) ^^\n 5) Time Stamp (TST) of the month\n 6) Time Stamp (TST) when the max demand occured\n 6) Measurement value 1 (most recent entry of buffer attribute without unit)\n 7) Unit of measurement values (Unit of capture objects attribute)\n \"\"\"\n\n def parse(self, line):\n pattern = re.compile(r'((?<=\\()[0-9a-zA-Z\\.\\*\\-\\:]{0,}(?=\\)))')\n values = re.findall(pattern, line)\n\n obis_id_code = self._parse_obis_id_code(line)\n\n objects = []\n\n count = int(values[0])\n for i in range(1, count + 1):\n timestamp_month = ValueParser(timestamp).parse(values[i * 3 + 0])\n timestamp_occurred = ValueParser(timestamp).parse(values[i * 3 + 1])\n value = ValueParser(Decimal).parse(values[i * 3 + 2])\n objects.append(MBusObjectPeak(\n obis_id_code=obis_id_code,\n values=[timestamp_month, timestamp_occurred, value]\n ))\n\n return objects\n\n\nclass CosemParser(DSMRObjectParser):\n \"\"\"\n Cosem object parser.\n\n These are data objects with a single value that optionally have a unit of\n measurement.\n\n Line format:\n ID (Mv*U)\n\n 1 23 45\n\n 1) OBIS Reduced ID-code\n 2) Separator \"(\", ASCII 28h\n 3) COSEM object attribute value\n 4) Unit of measurement values (Unit of capture objects attribute) - only if\n applicable\n 5) Separator \")\", ASCII 29h\n \"\"\"\n\n def parse(self, line):\n return CosemObject(\n obis_id_code=self._parse_obis_id_code(line),\n values=self._parse(line)\n )\n\n\nclass ProfileGenericParser(DSMRObjectParser):\n \"\"\"\n Power failure log parser.\n\n These are data objects with multiple repeating groups of values.\n\n Line format:\n ID (z) (ID1) (TST) (Bv1*U1) (TST) (Bvz*Uz)\n\n 1 2 3 4 5 6 7 8 9\n\n 1) OBIS Reduced ID-code\n 2) Number of values z (max 10).\n 3) Identifications of buffer values (OBIS Reduced ID codes of capture objects attribute)\n 4) Time Stamp (TST) of power failure end time\n 5) Buffer value 1 (most recent entry of buffer attribute without unit)\n 6) Unit of buffer values (Unit of capture objects attribute)\n 7) Time Stamp (TST) of power failure end time\n 8) Buffer value 2 (oldest entry of buffer attribute without unit)\n 9) Unit of buffer values (Unit of capture objects attribute)\n \"\"\"\n\n def __init__(self, buffer_types, head_parsers, parsers_for_unidentified):\n self.value_formats = head_parsers.copy()\n self.buffer_types = buffer_types\n self.parsers_for_unidentified = parsers_for_unidentified\n\n def _is_line_wellformed(self, line, values):\n if values and (len(values) == 1) and (values[0] == ''):\n # special case: single empty parentheses (indicated by empty string)\n return True\n\n if values and (len(values) >= 2) and (values[0].isdigit()):\n buffer_length = int(values[0])\n return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2))\n else:\n return False\n\n def _parse_values(self, values):\n if values and (len(values) == 1) and (values[0] is None):\n # special case: single empty parentheses; make sure empty ProfileGenericObject is created\n values = [0, None] # buffer_length=0, buffer_value_obis_ID=None\n buffer_length = int(values[0])\n buffer_value_obis_ID = values[1]\n if (buffer_length > 0):\n if buffer_value_obis_ID in self.buffer_types:\n bufferValueParsers = self.buffer_types[buffer_value_obis_ID]\n else:\n bufferValueParsers = self.parsers_for_unidentified\n # add the parsers for the encountered value type z times\n for _ in range(buffer_length):\n self.value_formats.extend(bufferValueParsers)\n\n return [self.value_formats[i].parse(value) for i, value in enumerate(values)]\n\n def parse(self, line):\n return ProfileGenericObject(\n obis_id_code=self._parse_obis_id_code(line),\n values=self._parse(line)\n )\n\n\nclass ValueParser(object):\n \"\"\"\n Parses a single value from DSMRObject's.\n\n Example with coerce_type being int:\n (002*A) becomes {'value': 2, 'unit': 'A'}\n\n Example with coerce_type being str:\n (42) becomes {'value': '42', 'unit': None}\n \"\"\"\n\n def __init__(self, coerce_type):\n self.coerce_type = coerce_type\n\n def parse(self, value):\n unit_of_measurement = None\n\n if value and '*' in value:\n value, unit_of_measurement = value.split('*')\n\n # A value group is not required to have a value, and then coercing does\n # not apply.\n value = self.coerce_type(value) if value is not None else value\n\n return {\n 'value': value,\n 'unit': unit_of_measurement\n }\n", + "review": "# 7\n\n## Bugs\n- The `crc16_tab` is mutable but populated only once, thus it is vulnerable to potential side-effects or race conditions if accessed in a multi-threaded context.\n- In the `ProfileGenericParser`, the logic assumes `values` length is correctly calculated; however, inadequate handling when regex fails to capture suitable groups is present.\n- The exception handling for hex operations could provide more specific cases rather than catching all Exceptions.\n\n## Optimizations\n- Preloading and reusing the compiled regex patterns across other parsing methods could improve performance and consistency.\n- The `crc16` method tabulation could be optimized or replaced with an alternative optimized library for CRC calculation.\n- Repeated warning checks for unhandled `security_suite` cases could be refactored into a helper method for better code reuse and maintainability.\n- Consider using a more modern Python class inheritance structure by extending directly from `object`.\n\n## Good Points\n- The use of logging to aid error-tracking and debugging.\n- Modular approach with multiple parsing classes, each with a specific responsibility.\n\n## Summary\nThe overall design of the code is modular, which is advantageous for maintainability. However, there are areas where optimizations, especially related to performance and security handling, could be implemented. Bugs are mostly related to error handling and potential issues when operating in a multi-threaded environment. Refactoring for more modern Python practices could enhance code readability and performance.\n\n## Open source alternatives\n- **PyCRC**: It offers common CRC algorithms like CRC16, which can be used instead of manually performing the CRC calculations.\n- **OpenDSS**: Data parsing and manipulation for smart grids and DSMR-related purposes.\n- **pyOBIS**: A library specifically for decoding OBIS (Object Identification System) codes.", + "filename": "parsers.py", + "path": "dsmr_parser/parsers.py", + "directory": "dsmr_parser", + "grade": 7, + "size": 14360, + "line_count": 405 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/parsers.py.md b/reviews/dsmr_parser/parsers.py.md new file mode 100644 index 0000000..3cb6aa3 --- /dev/null +++ b/reviews/dsmr_parser/parsers.py.md @@ -0,0 +1,24 @@ +# 7 + +## Bugs +- The `crc16_tab` is mutable but populated only once, thus it is vulnerable to potential side-effects or race conditions if accessed in a multi-threaded context. +- In the `ProfileGenericParser`, the logic assumes `values` length is correctly calculated; however, inadequate handling when regex fails to capture suitable groups is present. +- The exception handling for hex operations could provide more specific cases rather than catching all Exceptions. + +## Optimizations +- Preloading and reusing the compiled regex patterns across other parsing methods could improve performance and consistency. +- The `crc16` method tabulation could be optimized or replaced with an alternative optimized library for CRC calculation. +- Repeated warning checks for unhandled `security_suite` cases could be refactored into a helper method for better code reuse and maintainability. +- Consider using a more modern Python class inheritance structure by extending directly from `object`. + +## Good Points +- The use of logging to aid error-tracking and debugging. +- Modular approach with multiple parsing classes, each with a specific responsibility. + +## Summary +The overall design of the code is modular, which is advantageous for maintainability. However, there are areas where optimizations, especially related to performance and security handling, could be implemented. Bugs are mostly related to error handling and potential issues when operating in a multi-threaded environment. Refactoring for more modern Python practices could enhance code readability and performance. + +## Open source alternatives +- **PyCRC**: It offers common CRC algorithms like CRC16, which can be used instead of manually performing the CRC calculations. +- **OpenDSS**: Data parsing and manipulation for smart grids and DSMR-related purposes. +- **pyOBIS**: A library specifically for decoding OBIS (Object Identification System) codes. \ No newline at end of file diff --git a/reviews/dsmr_parser/profile_generic_specifications.py.json b/reviews/dsmr_parser/profile_generic_specifications.py.json new file mode 100644 index 0000000..e34c9b8 --- /dev/null +++ b/reviews/dsmr_parser/profile_generic_specifications.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from dsmr_parser.parsers import ValueParser\nfrom dsmr_parser.value_types import timestamp\n\nPG_FAILURE_EVENT = r'0-0:96.7.19'\n\nPG_HEAD_PARSERS = [ValueParser(int), ValueParser(str)]\nPG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)]\nBUFFER_TYPES = {\n PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)]\n}\n", + "review": "**7**\n\n### Bugs\n- No known bugs are evident from the code snippet.\n\n### Optimizations\n- Consider using a more descriptive variable name than `PG_HEAD_PARSERS` and `PG_UNIDENTIFIED_BUFFERTYPE_PARSERS` to make the code self-documenting.\n- If `timestamp` is a function or a complex class, ensure it is being used correctly with `ValueParser`.\n\n### Good points\n- The use of regular expressions like `r'0-0:96.7.19'` for identifying buffer types appears effective.\n- The code is structured neatly with clear separation of constants and mapping.\n\n### Summary\nThe code defines a set of value parsers and maps certain parser configurations to specific string identifiers. It appears well-organized and straightforward. Naming conventions could be improved to increase code maintainability and readability. No immediate bugs are apparent, but some minor enhancements could further polish the code.\n\n### Open source alternatives\n- **pySerial**: A library for serial communication in Python, which can also interpret specific data structures.\n- **DSMR-reader**: An open-source project that reads and interprets smart meter data, similar to what `dsmr_parser` aims to achieve.", + "filename": "profile_generic_specifications.py", + "path": "dsmr_parser/profile_generic_specifications.py", + "directory": "dsmr_parser", + "grade": 7, + "size": 340, + "line_count": 11 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/profile_generic_specifications.py.md b/reviews/dsmr_parser/profile_generic_specifications.py.md new file mode 100644 index 0000000..431decc --- /dev/null +++ b/reviews/dsmr_parser/profile_generic_specifications.py.md @@ -0,0 +1,19 @@ +**7** + +### Bugs +- No known bugs are evident from the code snippet. + +### Optimizations +- Consider using a more descriptive variable name than `PG_HEAD_PARSERS` and `PG_UNIDENTIFIED_BUFFERTYPE_PARSERS` to make the code self-documenting. +- If `timestamp` is a function or a complex class, ensure it is being used correctly with `ValueParser`. + +### Good points +- The use of regular expressions like `r'0-0:96.7.19'` for identifying buffer types appears effective. +- The code is structured neatly with clear separation of constants and mapping. + +### Summary +The code defines a set of value parsers and maps certain parser configurations to specific string identifiers. It appears well-organized and straightforward. Naming conventions could be improved to increase code maintainability and readability. No immediate bugs are apparent, but some minor enhancements could further polish the code. + +### Open source alternatives +- **pySerial**: A library for serial communication in Python, which can also interpret specific data structures. +- **DSMR-reader**: An open-source project that reads and interprets smart meter data, similar to what `dsmr_parser` aims to achieve. \ No newline at end of file diff --git a/reviews/dsmr_parser/telegram_specifications.py.json b/reviews/dsmr_parser/telegram_specifications.py.json new file mode 100644 index 0000000..a1fd9e3 --- /dev/null +++ b/reviews/dsmr_parser/telegram_specifications.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from decimal import Decimal\nfrom copy import deepcopy\n\nfrom dsmr_parser import obis_references as obis\nfrom dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser, MaxDemandParser\nfrom dsmr_parser.value_types import timestamp\nfrom dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS\n\n\"\"\"\ndsmr_parser.telegram_specifications\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThis module contains DSMR telegram specifications. Each specifications describes\nhow the telegram lines are parsed.\n\"\"\"\n\nV2_2 = {\n 'checksum_support': False,\n 'objects': [\n {\n 'obis_reference': obis.EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'EQUIPMENT_IDENTIFIER'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'ELECTRICITY_ACTIVE_TARIFF'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_USAGE'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_DELIVERY'\n },\n {\n 'obis_reference': obis.ACTUAL_TRESHOLD_ELECTRICITY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY'\n },\n {\n 'obis_reference': obis.ACTUAL_SWITCH_POSITION,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'ACTUAL_SWITCH_POSITION'\n },\n {\n 'obis_reference': obis.TEXT_MESSAGE_CODE,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'TEXT_MESSAGE_CODE'\n },\n {\n 'obis_reference': obis.TEXT_MESSAGE,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'TEXT_MESSAGE'\n },\n {\n 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'EQUIPMENT_IDENTIFIER_GAS'\n },\n {\n 'obis_reference': obis.DEVICE_TYPE,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'DEVICE_TYPE'\n },\n {\n 'obis_reference': obis.VALVE_POSITION_GAS,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'VALVE_POSITION_GAS'\n },\n {\n 'obis_reference': obis.GAS_METER_READING,\n 'value_parser': MBusParser(\n ValueParser(timestamp),\n ValueParser(str), # changed to str see issue60\n ValueParser(int),\n ValueParser(int),\n ValueParser(str), # obis ref\n ValueParser(str), # unit, position 5\n ValueParser(Decimal), # meter reading, position 6\n ),\n 'value_name': 'GAS_METER_READING'\n },\n ]\n}\n\nV3 = V2_2\n\nV4 = {\n 'checksum_support': True,\n 'objects': [\n {\n 'obis_reference': obis.P1_MESSAGE_HEADER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'P1_MESSAGE_HEADER'\n },\n {\n 'obis_reference': obis.P1_MESSAGE_TIMESTAMP,\n 'value_parser': CosemParser(ValueParser(timestamp)),\n 'value_name': 'P1_MESSAGE_TIMESTAMP'\n },\n {\n 'obis_reference': obis.EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'EQUIPMENT_IDENTIFIER'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'ELECTRICITY_ACTIVE_TARIFF'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_USAGE'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_DELIVERY'\n },\n {\n 'obis_reference': obis.SHORT_POWER_FAILURE_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'SHORT_POWER_FAILURE_COUNT'\n },\n {\n 'obis_reference': obis.LONG_POWER_FAILURE_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'LONG_POWER_FAILURE_COUNT'\n },\n {\n 'obis_reference': obis.POWER_EVENT_FAILURE_LOG,\n 'value_parser': ProfileGenericParser(\n BUFFER_TYPES,\n PG_HEAD_PARSERS,\n PG_UNIDENTIFIED_BUFFERTYPE_PARSERS\n ),\n 'value_name': 'POWER_EVENT_FAILURE_LOG'\n },\n {\n 'obis_reference': obis.VOLTAGE_SAG_L1_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SAG_L1_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SAG_L2_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SAG_L2_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SAG_L3_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SAG_L3_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SWELL_L1_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SWELL_L1_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SWELL_L2_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SWELL_L2_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SWELL_L3_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SWELL_L3_COUNT'\n },\n {\n 'obis_reference': obis.TEXT_MESSAGE_CODE,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'TEXT_MESSAGE_CODE'\n },\n {\n 'obis_reference': obis.TEXT_MESSAGE,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'TEXT_MESSAGE'\n },\n {\n 'obis_reference': obis.DEVICE_TYPE,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'DEVICE_TYPE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L2'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L3'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE'\n },\n {\n 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'EQUIPMENT_IDENTIFIER_GAS'\n },\n {\n 'obis_reference': obis.HOURLY_GAS_METER_READING,\n 'value_parser': MBusParser(\n ValueParser(timestamp),\n ValueParser(Decimal)\n ),\n 'value_name': 'HOURLY_GAS_METER_READING'\n },\n ]\n}\n\nV5 = {\n 'checksum_support': True,\n 'objects': [\n {\n 'obis_reference': obis.P1_MESSAGE_HEADER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'P1_MESSAGE_HEADER'\n },\n {\n 'obis_reference': obis.P1_MESSAGE_TIMESTAMP,\n 'value_parser': CosemParser(ValueParser(timestamp)),\n 'value_name': 'P1_MESSAGE_TIMESTAMP'\n },\n {\n 'obis_reference': obis.EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'EQUIPMENT_IDENTIFIER'\n },\n {\n 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_IMPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'ELECTRICITY_ACTIVE_TARIFF'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_USAGE'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_DELIVERY'\n },\n {\n 'obis_reference': obis.LONG_POWER_FAILURE_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'LONG_POWER_FAILURE_COUNT'\n },\n {\n 'obis_reference': obis.SHORT_POWER_FAILURE_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'SHORT_POWER_FAILURE_COUNT'\n },\n {\n 'obis_reference': obis.POWER_EVENT_FAILURE_LOG,\n 'value_parser': ProfileGenericParser(\n BUFFER_TYPES,\n PG_HEAD_PARSERS,\n PG_UNIDENTIFIED_BUFFERTYPE_PARSERS\n ),\n 'value_name': 'POWER_EVENT_FAILURE_LOG'\n },\n {\n 'obis_reference': obis.VOLTAGE_SAG_L1_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SAG_L1_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SAG_L2_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SAG_L2_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SAG_L3_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SAG_L3_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SWELL_L1_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SWELL_L1_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SWELL_L2_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SWELL_L2_COUNT'\n },\n {\n 'obis_reference': obis.VOLTAGE_SWELL_L3_COUNT,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'VOLTAGE_SWELL_L3_COUNT'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L2'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L3'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L2'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L3'\n },\n {\n 'obis_reference': obis.TEXT_MESSAGE,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'TEXT_MESSAGE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE'\n },\n {\n 'obis_reference': obis.MBUS_DEVICE_TYPE,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'MBUS_DEVICE_TYPE'\n },\n {\n 'obis_reference': obis.MBUS_EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'MBUS_EQUIPMENT_IDENTIFIER'\n },\n {\n 'obis_reference': obis.MBUS_VALVE_POSITION,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'MBUS_VALVE_POSITION'\n },\n {\n 'obis_reference': obis.MBUS_METER_READING,\n 'value_parser': MBusParser(\n ValueParser(timestamp),\n ValueParser(Decimal)\n ),\n 'value_name': 'MBUS_METER_READING'\n },\n ]\n}\n\nALL = (V2_2, V3, V4, V5)\n\nBELGIUM_FLUVIUS = {\n 'checksum_support': True,\n 'objects': [\n {\n 'obis_reference': obis.BELGIUM_VERSION_INFORMATION,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'BELGIUM_VERSION_INFORMATION'\n },\n {\n 'obis_reference': obis.BELGIUM_EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'BELGIUM_EQUIPMENT_IDENTIFIER'\n },\n {\n 'obis_reference': obis.P1_MESSAGE_TIMESTAMP,\n 'value_parser': CosemParser(ValueParser(timestamp)),\n 'value_name': 'P1_MESSAGE_TIMESTAMP'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'ELECTRICITY_ACTIVE_TARIFF'\n },\n {\n 'obis_reference': obis.BELGIUM_CURRENT_AVERAGE_DEMAND,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'BELGIUM_CURRENT_AVERAGE_DEMAND'\n },\n {\n 'obis_reference': obis.BELGIUM_MAXIMUM_DEMAND_MONTH,\n 'value_parser': MBusParser(\n ValueParser(timestamp),\n ValueParser(Decimal)\n ),\n 'value_name': 'BELGIUM_MAXIMUM_DEMAND_MONTH'\n },\n {\n 'obis_reference': obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS,\n 'value_parser': MaxDemandParser(),\n 'value_name': 'BELGIUM_MAXIMUM_DEMAND_13_MONTHS'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_USAGE'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_DELIVERY'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L2'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L3'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L2'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L3'\n },\n {\n 'obis_reference': obis.ACTUAL_SWITCH_POSITION,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'ACTUAL_SWITCH_POSITION'\n },\n {\n 'obis_reference': obis.ACTUAL_TRESHOLD_ELECTRICITY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY'\n },\n {\n 'obis_reference': obis.FUSE_THRESHOLD_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'FUSE_THRESHOLD_L1'\n },\n {\n 'obis_reference': obis.TEXT_MESSAGE,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'TEXT_MESSAGE'\n },\n {\n 'obis_reference': obis.MBUS_DEVICE_TYPE,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'MBUS_DEVICE_TYPE'\n },\n {\n 'obis_reference': obis.MBUS_EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'MBUS_EQUIPMENT_IDENTIFIER'\n },\n {\n 'obis_reference': obis.MBUS_VALVE_POSITION,\n 'value_parser': CosemParser(ValueParser(int)),\n 'value_name': 'MBUS_VALVE_POSITION'\n },\n {\n 'obis_reference': obis.MBUS_METER_READING,\n 'value_parser': MBusParser(\n ValueParser(timestamp),\n ValueParser(Decimal)\n ),\n 'value_name': 'MBUS_METER_READING'\n },\n ]\n}\n\nLUXEMBOURG_SMARTY = deepcopy(V5)\nLUXEMBOURG_SMARTY['objects'].extend([\n {\n 'obis_reference': obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'LUXEMBOURG_EQUIPMENT_IDENTIFIER'\n },\n # This is already presented in V5, with the same data\n # {\n # 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL,\n # 'value_parser': CosemParser(ValueParser(Decimal)),\n # 'value_name': 'ELECTRICITY_IMPORTED_TOTAL'\n # },\n {\n 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_EXPORTED_TOTAL'\n }\n])\n\n# Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/\n# branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf\nSWEDEN = {\n 'checksum_support': True,\n 'objects': [\n {\n 'obis_reference': obis.P1_MESSAGE_HEADER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'P1_MESSAGE_HEADER'\n },\n {\n 'obis_reference': obis.P1_MESSAGE_TIMESTAMP,\n 'value_parser': CosemParser(ValueParser(timestamp)),\n 'value_name': 'P1_MESSAGE_TIMESTAMP'\n },\n {\n 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_IMPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_EXPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_USAGE'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_DELIVERY'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L2'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L3'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L2'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L3'\n }\n ]\n}\n\nQ3D = {\n \"checksum_support\": False,\n \"objects\": [\n {\n 'obis_reference': obis.Q3D_EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'Q3D_EQUIPMENT_IDENTIFIER'\n },\n {\n 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_IMPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_EXPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_USAGE'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_DELIVERY'\n },\n {\n 'obis_reference': obis.Q3D_EQUIPMENT_STATE,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'Q3D_EQUIPMENT_STATE'\n },\n {\n 'obis_reference': obis.Q3D_EQUIPMENT_SERIALNUMBER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'Q3D_EQUIPMENT_SERIALNUMBER'\n },\n ]\n}\n\n\nSAGEMCOM_T210_D_R = {\n \"general_global_cipher\": True,\n \"checksum_support\": True,\n 'objects': [\n {\n 'obis_reference': obis.P1_MESSAGE_HEADER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'P1_MESSAGE_HEADER'\n },\n {\n 'obis_reference': obis.P1_MESSAGE_TIMESTAMP,\n 'value_parser': CosemParser(ValueParser(timestamp)),\n 'value_name': 'P1_MESSAGE_TIMESTAMP'\n },\n {\n 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_IMPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_2'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_USAGE'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2'\n },\n {\n 'obis_reference': obis.CURRENT_REACTIVE_IMPORTED,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_REACTIVE_IMPORTED'\n },\n {\n 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_EXPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_DELIVERY'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2'\n },\n {\n 'obis_reference': obis.CURRENT_REACTIVE_EXPORTED,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_REACTIVE_EXPORTED'\n }\n ]\n}\nAUSTRIA_ENERGIENETZE_STEIERMARK = SAGEMCOM_T210_D_R\n\nISKRA_IE = {\n \"checksum_support\": False,\n 'objects': [\n {\n 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'EQUIPMENT_IDENTIFIER_GAS'\n },\n {\n 'obis_reference': obis.P1_MESSAGE_TIMESTAMP,\n 'value_parser': CosemParser(ValueParser(timestamp)),\n 'value_name': 'P1_MESSAGE_TIMESTAMP'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'ELECTRICITY_ACTIVE_TARIFF'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_USAGE'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_DELIVERY'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L2'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L3'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L2'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L3'\n },\n {\n 'obis_reference': obis.ACTUAL_SWITCH_POSITION,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'ACTUAL_SWITCH_POSITION'\n },\n {\n 'obis_reference': obis.TEXT_MESSAGE,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'TEXT_MESSAGE'\n },\n {\n 'obis_reference': obis.EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'EQUIPMENT_IDENTIFIER'\n },\n ]\n}\n\nEON_HUNGARY = {\n # Revision: 2023.02.10\n # Based on V5\n # Reference: https://www.eon.hu/content/dam/eon/eon-hungary/documents/Lakossagi/aram/muszaki-ugyek/p1_port%20felhaszn_interfesz_taj_%2020230210.pdf # noqa\n 'checksum_support': True,\n 'objects': [\n {\n 'obis_reference': obis.P1_MESSAGE_TIMESTAMP,\n 'value_parser': CosemParser(ValueParser(timestamp)),\n 'value_name': 'P1_MESSAGE_TIMESTAMP'\n },\n {\n 'obis_reference': obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'COSEM_LOGICAL_DEVICE_NAME'\n },\n {\n 'obis_reference': obis.EQUIPMENT_IDENTIFIER_GAS,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'EQUIPMENT_SERIAL_NUMBER'\n },\n {\n 'obis_reference': obis.ELECTRICITY_ACTIVE_TARIFF,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'ELECTRICITY_ACTIVE_TARIFF'\n },\n {\n 'obis_reference': obis.ACTUAL_SWITCH_POSITION,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'ACTUAL_SWITCH_POSITION'\n # This seems to be wrong in documentation, it's not 0-0:96.50.68, but 0-0:96.3.10\n },\n {\n 'obis_reference': obis.ACTUAL_TRESHOLD_ELECTRICITY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ACTUAL_TRESHOLD_ELECTRICITY'\n },\n {\n 'obis_reference': obis.ELECTRICITY_IMPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_IMPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_3'\n },\n {\n 'obis_reference': obis.ELECTRICITY_USED_TARIFF_4,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_USED_TARIFF_4'\n },\n {\n 'obis_reference': obis.ELECTRICITY_EXPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_EXPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_1'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_2'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_3'\n },\n {\n 'obis_reference': obis.ELECTRICITY_DELIVERED_TARIFF_4,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_DELIVERED_TARIFF_4'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_IMPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_IMPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.ELECTRICITY_REACTIVE_EXPORTED_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_EXPORTED_TOTAL'\n },\n {\n 'obis_reference': obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_TOTAL_Q1'\n },\n {\n 'obis_reference': obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_TOTAL_Q2'\n },\n {\n 'obis_reference': obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_TOTAL_Q3'\n },\n {\n 'obis_reference': obis.EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_REACTIVE_TOTAL_Q4'\n },\n {\n 'obis_reference': obis.EON_HU_ELECTRICITY_COMBINED,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'ELECTRICITY_COMBINED'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L2'\n # Only with 3 phase meters\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_VOLTAGE_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_VOLTAGE_L3'\n # Only with 3 phase meters\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L1'\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L2'\n # Only with 3 phase meters\n },\n {\n 'obis_reference': obis.INSTANTANEOUS_CURRENT_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_CURRENT_L3'\n # Only with 3 phase meters\n },\n {\n 'obis_reference': obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_POWER_FACTOR_TOTAL'\n },\n {\n 'obis_reference': obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_POWER_FACTOR_L1'\n },\n {\n 'obis_reference': obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_POWER_FACTOR_L2'\n # Only with 3 phase meters\n },\n {\n 'obis_reference': obis.EON_HU_INSTANTANEOUS_POWER_FACTOR_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_POWER_FACTOR_L3'\n # Only with 3 phase meters\n },\n {\n 'obis_reference': obis.EON_HU_FREQUENCY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'FREQUENCY'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_USAGE,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_USAGE'\n },\n {\n 'obis_reference': obis.CURRENT_ELECTRICITY_DELIVERY,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'CURRENT_ELECTRICITY_DELIVERY'\n },\n {\n 'obis_reference': obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_Q1'\n },\n {\n 'obis_reference': obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_Q2'\n },\n {\n 'obis_reference': obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_Q3'\n },\n {\n 'obis_reference': obis.EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'INSTANTANEOUS_REACTIVE_POWER_Q4'\n },\n {\n 'obis_reference': obis.FUSE_THRESHOLD_L1,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'FUSE_THRESHOLD_L1'\n },\n {\n 'obis_reference': obis.FUSE_THRESHOLD_L2,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'FUSE_THRESHOLD_L2'\n # Only with 3 phase meters\n },\n {\n 'obis_reference': obis.FUSE_THRESHOLD_L3,\n 'value_parser': CosemParser(ValueParser(Decimal)),\n 'value_name': 'FUSE_THRESHOLD_L3'\n # Only with 3 phase meters\n },\n # I'm not sure which datas does this line containes. It should be the data of last minute of last month.\n # {\n # 'obis_reference': obis.BELGIUM_MAXIMUM_DEMAND_13_MONTHS,\n # 'value_parser': NonExistingParser(\n # ValueParser(timestamp),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal),\n # ValueParser(Decimal)\n # ),\n # 'value_name': 'LAST_MONTH_DATA'\n # },\n {\n 'obis_reference': obis.TEXT_MESSAGE,\n 'value_parser': CosemParser(ValueParser(str)),\n 'value_name': 'TEXT_MESSAGE'\n }\n ]\n}\n", + "review": "# 8\n\n## Bugs\n- Inconsistent trailing commas within list and dictionary entries.\n- Typo in `'ACTUAL_TRESHOLD_ELECTRICITY'`. The correct spelling should be `'THRESHOLD'` instead of `'TRESHOLD'`.\n- Redundant definition of `V3` as `V2_2` without any modifications.\n- Obsolete references or undefined elements in some comments, e.g., `NonExistingParser`.\n\n## Optimizations\n- Remove the redundant assignment of `V3 = V2_2` as they are identical.\n- Use a helper function to reduce redundancy when defining similar dictionary objects for different versions.\n- Consolidate or document repetitive structures/options like `checksum_support` which appear across different dictionaries with explanations or use variables for descriptions.\n- Improve comments for clarity, specifying which parts are unique to certain regions or versions.\n\n## Good points\n- The code is well-structured, clearly defining DSMR specifications and neatly organizing protocol versions.\n- Use of descriptive names for the objects and dictionaries adds to code readability.\n- The utilization of specific parser classes demonstrates good understanding and application of object-oriented principles.\n\n## Summary\nThe source code provided defines DSMR telegram specifications using structured dictionaries to map out OBIS references with appropriate parsers and value names. Although generally well-organized, the code contains some redundancies and minor typos that could be refined. The parser mappings are comprehensive and structured clearly, with potential optimizations in eliminating unnecessary repetition. Ensuring consistent syntax and improving documentation would further enhance the code's quality.\n\n## Open source alternatives\n- **dsmr_parser**: The library this code corresponds to is itself an open-source solution for parsing DSMR telegrams.\n- **Home Assistant DSMR Reader**: An integration that can read DSMR telegrams and provide a user-friendly interface for monitoring energy data.", + "filename": "telegram_specifications.py", + "path": "dsmr_parser/telegram_specifications.py", + "directory": "dsmr_parser", + "grade": 8, + "size": 53601, + "line_count": 1371 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/telegram_specifications.py.md b/reviews/dsmr_parser/telegram_specifications.py.md new file mode 100644 index 0000000..6e893f1 --- /dev/null +++ b/reviews/dsmr_parser/telegram_specifications.py.md @@ -0,0 +1,25 @@ +# 8 + +## Bugs +- Inconsistent trailing commas within list and dictionary entries. +- Typo in `'ACTUAL_TRESHOLD_ELECTRICITY'`. The correct spelling should be `'THRESHOLD'` instead of `'TRESHOLD'`. +- Redundant definition of `V3` as `V2_2` without any modifications. +- Obsolete references or undefined elements in some comments, e.g., `NonExistingParser`. + +## Optimizations +- Remove the redundant assignment of `V3 = V2_2` as they are identical. +- Use a helper function to reduce redundancy when defining similar dictionary objects for different versions. +- Consolidate or document repetitive structures/options like `checksum_support` which appear across different dictionaries with explanations or use variables for descriptions. +- Improve comments for clarity, specifying which parts are unique to certain regions or versions. + +## Good points +- The code is well-structured, clearly defining DSMR specifications and neatly organizing protocol versions. +- Use of descriptive names for the objects and dictionaries adds to code readability. +- The utilization of specific parser classes demonstrates good understanding and application of object-oriented principles. + +## Summary +The source code provided defines DSMR telegram specifications using structured dictionaries to map out OBIS references with appropriate parsers and value names. Although generally well-organized, the code contains some redundancies and minor typos that could be refined. The parser mappings are comprehensive and structured clearly, with potential optimizations in eliminating unnecessary repetition. Ensuring consistent syntax and improving documentation would further enhance the code's quality. + +## Open source alternatives +- **dsmr_parser**: The library this code corresponds to is itself an open-source solution for parsing DSMR telegrams. +- **Home Assistant DSMR Reader**: An integration that can read DSMR telegrams and provide a user-friendly interface for monitoring energy data. \ No newline at end of file diff --git a/reviews/dsmr_parser/value_types.py.json b/reviews/dsmr_parser/value_types.py.json new file mode 100644 index 0000000..9e45de4 --- /dev/null +++ b/reviews/dsmr_parser/value_types.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import datetime\n\nimport pytz\n\n# TODO : Use system timezone\n# Preload timezone to avoid loading in event loop later\nlocal_tz = pytz.timezone('Europe/Amsterdam')\n\n\ndef timestamp(value):\n try:\n naive_datetime = datetime.datetime.strptime(value[:-1], '%y%m%d%H%M%S')\n except ValueError:\n return None\n\n # Timestamp has the following format:\n # YYMMDDhhmmssX\n # ASCII presentation of Time stamp with\n # Year, Month, Day, Hour, Minute, Second,\n # and an indication whether DST is active\n # (X=S) or DST is not active (X=W)\n if len(value) == 13:\n is_dst = value[12] == 'S' # assume format 160322150000W\n else:\n is_dst = False\n\n localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst)\n\n return localized_datetime.astimezone(pytz.utc)\n", + "review": "# 6\n\n## Bugs\n- The function might incorrectly handle timestamps if another timezone is used without modification.\n- Hardcoded to 'Europe/Amsterdam' timezone, disregards the system timezone or caller's needs.\n\n## Optimizations\n- Allow dynamic use of system or parameter-specified timezone instead of hardcoding 'Europe/Amsterdam'.\n- Handle invalid timestamp length gracefully, ensuring function does not break with unexpected inputs.\n- Consider more efficient error handling mechanisms, like logging, apart from returning None.\n- Reuse the `localize` function with updated methods from `pytz` if they become deprecated.\n\n## Good Points\n- Utilizes `pytz` for timezone management, which is a common library for such tasks.\n- Includes detailed comments explaining the timestamp format and handling.\n- The function handles DST (Daylight Saving Time) correctly based on the input.\n\n## Summary\nThe code is functional but limited due to its hardcoded timezone and lack of flexibility. While the approach to time conversion and DST handling is correct, there's room for improvement in adding robustness and versatility to the function. Allowing specification of the timezone, and handling errors more comprehensively, can enhance its utility and resilience.\n\n## Open source alternatives\n- **dateutil module**: Offers timezone and time conversion functions with more flexibility and less dependency on hardcoded values. \n- **Arrow library**: Provides better UTC and timezone management and human-friendly datetime manipulation.", + "filename": "value_types.py", + "path": "dsmr_parser/value_types.py", + "directory": "dsmr_parser", + "grade": 6, + "size": 804, + "line_count": 30 +} \ No newline at end of file diff --git a/reviews/dsmr_parser/value_types.py.md b/reviews/dsmr_parser/value_types.py.md new file mode 100644 index 0000000..9706c3c --- /dev/null +++ b/reviews/dsmr_parser/value_types.py.md @@ -0,0 +1,23 @@ +# 6 + +## Bugs +- The function might incorrectly handle timestamps if another timezone is used without modification. +- Hardcoded to 'Europe/Amsterdam' timezone, disregards the system timezone or caller's needs. + +## Optimizations +- Allow dynamic use of system or parameter-specified timezone instead of hardcoding 'Europe/Amsterdam'. +- Handle invalid timestamp length gracefully, ensuring function does not break with unexpected inputs. +- Consider more efficient error handling mechanisms, like logging, apart from returning None. +- Reuse the `localize` function with updated methods from `pytz` if they become deprecated. + +## Good Points +- Utilizes `pytz` for timezone management, which is a common library for such tasks. +- Includes detailed comments explaining the timestamp format and handling. +- The function handles DST (Daylight Saving Time) correctly based on the input. + +## Summary +The code is functional but limited due to its hardcoded timezone and lack of flexibility. While the approach to time conversion and DST handling is correct, there's room for improvement in adding robustness and versatility to the function. Allowing specification of the timezone, and handling errors more comprehensively, can enhance its utility and resilience. + +## Open source alternatives +- **dateutil module**: Offers timezone and time conversion functions with more flexibility and less dependency on hardcoded values. +- **Arrow library**: Provides better UTC and timezone management and human-friendly datetime manipulation. \ No newline at end of file diff --git a/reviews/setup.py.json b/reviews/setup.py.json new file mode 100644 index 0000000..e6fceeb --- /dev/null +++ b/reviews/setup.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from setuptools import setup, find_packages\n\nsetup(\n name='dsmr-parser',\n description='Library to parse Dutch Smart Meter Requirements (DSMR)',\n author='Nigel Dokter and many others',\n author_email='mail@nldr.net',\n license='MIT',\n url='https://github.com/ndokter/dsmr_parser',\n version='1.4.2',\n packages=find_packages(exclude=('test', 'test.*')),\n install_requires=[\n 'pyserial>=3,<4',\n 'pyserial-asyncio-fast>=0.11',\n 'pytz',\n 'Tailer==0.4.1',\n 'dlms_cosem==21.3.2'\n ],\n entry_points={\n 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']\n },\n)\n", + "review": "# 8\n\n## Bugs\n- No apparent bugs in the setup configuration.\n\n## Optimizations\n- Ensure that the listed dependencies are up to date and compatible with the rest of your project. Dependency constraints like `'pyserial>=3,<4'` can limit updates, missing out on potential bug fixes or improvements present in newer versions.\n- Consider using a `requirements.txt` file for better dependency handling and management or allowing pip to compile accurate dependency trees.\n- Use classifiers in the setup call to provide more metadata about the package (e.g., specifying the Python version compatibility, stable/dev status, etc.).\n\n## Good points\n- Uses `find_packages(exclude=...)`, which is efficient for including all relevant packages without needing to list each explicitly.\n- Clearly defined `entry_points` for console scripts make it easy to use the package's functionalities from the command line.\n- Comprehensive use of semantic versioning for package versioning.\n\n## Summary\nThis setup script is well-structured and adheres to standard practices found in Python packaging using `setuptools`. The use of `find_packages` and entry points are particularly well-done, offering both flexibility and ease of use. There\u2019s room for improvement regarding dependency management and metadata enhancement (such as classifiers for greater clarity).\n\n## Open source alternatives\n- **dsmr-reader**: A popular project for reading and logging data from smart meters which also has parsing capabilities.\n- **home-assistant.io**: Although primarily a home automation platform, it has integrations and parsers for DSMR data.", + "filename": "setup.py", + "path": "setup.py", + "directory": "", + "grade": 8, + "size": 639, + "line_count": 23 +} \ No newline at end of file diff --git a/reviews/setup.py.md b/reviews/setup.py.md new file mode 100644 index 0000000..54b0be5 --- /dev/null +++ b/reviews/setup.py.md @@ -0,0 +1,21 @@ +# 8 + +## Bugs +- No apparent bugs in the setup configuration. + +## Optimizations +- Ensure that the listed dependencies are up to date and compatible with the rest of your project. Dependency constraints like `'pyserial>=3,<4'` can limit updates, missing out on potential bug fixes or improvements present in newer versions. +- Consider using a `requirements.txt` file for better dependency handling and management or allowing pip to compile accurate dependency trees. +- Use classifiers in the setup call to provide more metadata about the package (e.g., specifying the Python version compatibility, stable/dev status, etc.). + +## Good points +- Uses `find_packages(exclude=...)`, which is efficient for including all relevant packages without needing to list each explicitly. +- Clearly defined `entry_points` for console scripts make it easy to use the package's functionalities from the command line. +- Comprehensive use of semantic versioning for package versioning. + +## Summary +This setup script is well-structured and adheres to standard practices found in Python packaging using `setuptools`. The use of `find_packages` and entry points are particularly well-done, offering both flexibility and ease of use. There’s room for improvement regarding dependency management and metadata enhancement (such as classifiers for greater clarity). + +## Open source alternatives +- **dsmr-reader**: A popular project for reading and logging data from smart meters which also has parsing capabilities. +- **home-assistant.io**: Although primarily a home automation platform, it has integrations and parsers for DSMR data. \ No newline at end of file diff --git a/reviews/test/example_telegrams.py.json b/reviews/test/example_telegrams.py.json new file mode 100644 index 0000000..821f442 --- /dev/null +++ b/reviews/test/example_telegrams.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "TELEGRAM_V2_2 = (\n '/ISk5\\\\2MT382-1004\\r\\n'\n '\\r\\n'\n '0-0:96.1.1(00000000000000)\\r\\n'\n '1-0:1.8.1(00001.001*kWh)\\r\\n'\n '1-0:1.8.2(00001.001*kWh)\\r\\n'\n '1-0:2.8.1(00001.001*kWh)\\r\\n'\n '1-0:2.8.2(00001.001*kWh)\\r\\n'\n '0-0:96.14.0(0001)\\r\\n'\n '1-0:1.7.0(0001.01*kW)\\r\\n'\n '1-0:2.7.0(0000.00*kW)\\r\\n'\n '0-0:17.0.0(0999.00*kW)\\r\\n'\n '0-0:96.3.10(1)\\r\\n'\n '0-0:96.13.1()\\r\\n'\n '0-0:96.13.0()\\r\\n'\n '0-1:24.1.0(3)\\r\\n'\n '0-1:96.1.0(000000000000)\\r\\n'\n '0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)\\r\\n'\n '(00001.001)\\r\\n'\n '0-1:24.4.0(1)\\r\\n'\n '!\\r\\n'\n)\n\nTELEGRAM_V3 = (\n '/ISk5\\\\2MT382-1000\\r\\n'\n '\\r\\n'\n '0-0:96.1.1(4B384547303034303436333935353037)\\r\\n'\n '1-0:1.8.1(12345.678*kWh)\\r\\n'\n '1-0:1.8.2(12345.678*kWh)\\r\\n'\n '1-0:2.8.1(12345.678*kWh)\\r\\n'\n '1-0:2.8.2(12345.678*kWh)\\r\\n'\n '0-0:96.14.0(0002)\\r\\n'\n '1-0:1.7.0(001.19*kW)\\r\\n'\n '1-0:2.7.0(000.00*kW)\\r\\n'\n '0-0:17.0.0(016*A)\\r\\n'\n '0-0:96.3.10(1)\\r\\n'\n '0-0:96.13.1(303132333435363738)\\r\\n'\n '0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E'\n '3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F30313233'\n '3435363738393A3B3C3D3E3F)\\r\\n'\n '0-1:96.1.0(3232323241424344313233343536373839)\\r\\n'\n '0-1:24.1.0(03)\\r\\n'\n '0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\\r\\n'\n '(00001.001)\\r\\n'\n '0-1:24.4.0(1)\\r\\n'\n '!\\r\\n'\n)\n\nTELEGRAM_V4_2 = (\n '/KFM5KAIFA-METER\\r\\n'\n '\\r\\n'\n '1-3:0.2.8(42)\\r\\n'\n '0-0:1.0.0(161113205757W)\\r\\n'\n '0-0:96.1.1(3960221976967177082151037881335713)\\r\\n'\n '1-0:1.8.1(001581.123*kWh)\\r\\n'\n '1-0:1.8.2(001435.706*kWh)\\r\\n'\n '1-0:2.8.1(000000.000*kWh)\\r\\n'\n '1-0:2.8.2(000000.000*kWh)\\r\\n'\n '0-0:96.14.0(0002)\\r\\n'\n '1-0:1.7.0(02.027*kW)\\r\\n'\n '1-0:2.7.0(00.000*kW)\\r\\n'\n '0-0:96.7.21(00015)\\r\\n'\n '0-0:96.7.9(00007)\\r\\n'\n '1-0:99.97.0(3)(0-0:96.7.19)(000104180320W)(0000237126*s)(000101000001W)'\n '(2147583646*s)(000102000003W)(2317482647*s)\\r\\n'\n '1-0:32.32.0(00000)\\r\\n'\n '1-0:52.32.0(00000)\\r\\n'\n '1-0:72.32.0(00000)\\r\\n'\n '1-0:32.36.0(00000)\\r\\n'\n '1-0:52.36.0(00000)\\r\\n'\n '1-0:72.36.0(00000)\\r\\n'\n '0-0:96.13.1()\\r\\n'\n '0-0:96.13.0()\\r\\n'\n '1-0:31.7.0(000*A)\\r\\n'\n '1-0:51.7.0(006*A)\\r\\n'\n '1-0:71.7.0(002*A)\\r\\n'\n '1-0:21.7.0(00.170*kW)\\r\\n'\n '1-0:22.7.0(00.000*kW)\\r\\n'\n '1-0:41.7.0(01.247*kW)\\r\\n'\n '1-0:42.7.0(00.000*kW)\\r\\n'\n '1-0:61.7.0(00.209*kW)\\r\\n'\n '1-0:62.7.0(00.000*kW)\\r\\n'\n '0-1:24.1.0(003)\\r\\n'\n '0-1:96.1.0(4819243993373755377509728609491464)\\r\\n'\n '0-1:24.2.1(161129200000W)(00981.443*m3)\\r\\n'\n '!6796\\r\\n'\n)\n\nTELEGRAM_V5 = (\n '/ISk5\\\\2MT382-1000\\r\\n'\n '\\r\\n'\n '1-3:0.2.8(50)\\r\\n'\n '0-0:1.0.0(170102192002W)\\r\\n'\n '0-0:96.1.1(4B384547303034303436333935353037)\\r\\n'\n '1-0:1.8.1(000004.426*kWh)\\r\\n'\n '1-0:1.8.2(000002.399*kWh)\\r\\n'\n '1-0:2.8.1(000002.444*kWh)\\r\\n'\n '1-0:2.8.2(000000.000*kWh)\\r\\n'\n '0-0:96.14.0(0002)\\r\\n'\n '1-0:1.7.0(00.244*kW)\\r\\n'\n '1-0:2.7.0(00.000*kW)\\r\\n'\n '0-0:96.7.21(00013)\\r\\n'\n '0-0:96.7.9(00000)\\r\\n'\n '1-0:99.97.0(0)(0-0:96.7.19)\\r\\n'\n '1-0:32.32.0(00000)\\r\\n'\n '1-0:52.32.0(00000)\\r\\n'\n '1-0:72.32.0(00000)\\r\\n'\n '1-0:32.36.0(00000)\\r\\n'\n '1-0:52.36.0(00000)\\r\\n'\n '1-0:72.36.0(00000)\\r\\n'\n '0-0:96.13.0()\\r\\n'\n '1-0:32.7.0(0230.0*V)\\r\\n'\n '1-0:52.7.0(0230.0*V)\\r\\n'\n '1-0:72.7.0(0229.0*V)\\r\\n'\n '1-0:31.7.0(0.48*A)\\r\\n'\n '1-0:51.7.0(0.44*A)\\r\\n'\n '1-0:71.7.0(0.86*A)\\r\\n'\n '1-0:21.7.0(00.070*kW)\\r\\n'\n '1-0:41.7.0(00.032*kW)\\r\\n'\n '1-0:61.7.0(00.142*kW)\\r\\n'\n '1-0:22.7.0(00.000*kW)\\r\\n'\n '1-0:42.7.0(00.000*kW)\\r\\n'\n '1-0:62.7.0(00.000*kW)\\r\\n'\n '0-1:24.1.0(003)\\r\\n'\n '0-1:96.1.0(3232323241424344313233343536373839)\\r\\n'\n '0-1:24.2.1(170102161005W)(00000.107*m3)\\r\\n'\n '0-2:24.1.0(003)\\r\\n'\n '0-2:96.1.0()\\r\\n'\n '!6EEE\\r\\n'\n)\n\n# V5 telegram with 2 MBUS devices\nTELEGRAM_V5_TWO_MBUS = (\n '/ISK5\\\\2M550T-1012\\r\\n'\n '\\r\\n'\n '1-3:0.2.8(50)\\r\\n'\n '0-0:1.0.0(200426223325S)\\r\\n'\n '0-0:96.1.1(4530303434303037333832323436303139)\\r\\n'\n '1-0:1.8.1(002130.115*kWh)\\r\\n'\n '1-0:1.8.2(000245.467*kWh)\\r\\n'\n '1-0:2.8.1(000000.000*kWh)\\r\\n'\n '1-0:2.8.2(000000.000*kWh)\\r\\n'\n '0-0:96.14.0(0001)\\r\\n'\n '1-0:1.7.0(00.111*kW)\\r\\n'\n '1-0:2.7.0(00.000*kW)\\r\\n'\n '0-0:96.7.21(00005)\\r\\n'\n '0-0:96.7.9(00003)\\r\\n'\n '1-0:99.97.0(1)(0-0:96.7.19)(190326095015W)(0000002014*s)\\r\\n'\n '1-0:32.32.0(00001)\\r\\n'\n '1-0:52.32.0(00001)\\r\\n'\n '1-0:72.32.0(00192)\\r\\n'\n '1-0:32.36.0(00001)\\r\\n'\n '1-0:52.36.0(00001)\\r\\n'\n '1-0:72.36.0(00001)\\r\\n'\n '0-0:96.13.0()\\r\\n'\n '1-0:32.7.0(229.9*V)\\r\\n'\n '1-0:52.7.0(229.2*V)\\r\\n'\n '1-0:72.7.0(222.9*V)\\r\\n'\n '1-0:31.7.0(000*A)\\r\\n'\n '1-0:51.7.0(000*A)\\r\\n'\n '1-0:71.7.0(001*A)\\r\\n'\n '1-0:21.7.0(00.056*kW)\\r\\n'\n '1-0:41.7.0(00.000*kW)\\r\\n'\n '1-0:61.7.0(00.055*kW)\\r\\n'\n '1-0:22.7.0(00.000*kW)\\r\\n'\n '1-0:42.7.0(00.000*kW)\\r\\n'\n '1-0:62.7.0(00.000*kW)\\r\\n'\n '0-1:24.1.0(003)\\r\\n'\n '0-1:96.1.0()\\r\\n'\n '0-1:24.2.1(700101010000W)(00000000)\\r\\n'\n '0-2:24.1.0(003)\\r\\n'\n '0-2:96.1.0(4730303339303031393336393930363139)\\r\\n'\n '0-2:24.2.1(200426223001S)(00246.138*m3)\\r\\n'\n '!56DD\\r\\n'\n)\n\nTELEGRAM_FLUVIUS_V171 = (\n '/FLU5\\\\253769484_A\\r\\n'\n '\\r\\n'\n '0-0:96.1.4(50217)\\r\\n'\n '0-0:96.1.1(3153414733313031303231363035)\\r\\n'\n '0-0:1.0.0(200512135409S)\\r\\n'\n '1-0:1.8.1(000000.034*kWh)\\r\\n'\n '1-0:1.8.2(000015.758*kWh)\\r\\n'\n '1-0:2.8.1(000000.000*kWh)\\r\\n'\n '1-0:2.8.2(000000.011*kWh)\\r\\n'\n '1-0:1.4.0(02.351*kW)\\r\\n'\n '1-0:1.6.0(200509134558S)(02.589*kW)\\r\\n'\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'\n '0-0:96.14.0(0001)\\r\\n'\n '1-0:1.7.0(00.000*kW)\\r\\n'\n '1-0:2.7.0(00.000*kW)\\r\\n'\n '1-0:21.7.0(00.000*kW)\\r\\n'\n '1-0:41.7.0(00.000*kW)\\r\\n'\n '1-0:61.7.0(00.000*kW)\\r\\n'\n '1-0:22.7.0(00.000*kW)\\r\\n'\n '1-0:42.7.0(00.000*kW)\\r\\n'\n '1-0:62.7.0(00.000*kW)\\r\\n'\n '1-0:32.7.0(234.7*V)\\r\\n'\n '1-0:52.7.0(234.7*V)\\r\\n'\n '1-0:72.7.0(234.7*V)\\r\\n'\n '1-0:31.7.0(000.00*A)\\r\\n'\n '1-0:51.7.0(000.00*A)\\r\\n'\n '1-0:71.7.0(000.00*A)\\r\\n'\n '0-0:96.3.10(1)\\r\\n'\n '0-0:17.0.0(999.9*kW)\\r\\n'\n '1-0:31.4.0(999*A)\\r\\n'\n '0-0:96.13.0()\\r\\n'\n '0-1:24.1.0(003)\\r\\n'\n '0-1:96.1.1(37464C4F32313139303333373333)\\r\\n'\n '0-1:24.4.0(1)\\r\\n'\n '0-1:24.2.3(200512134558S)(00112.384*m3)\\r\\n'\n '0-2:24.1.0(007)\\r\\n'\n '0-2:96.1.1(3853414731323334353637383930)\\r\\n'\n '0-2:24.2.1(200512134558S)(00872.234*m3)\\r\\n'\n '!3AD7\\r\\n'\n)\n\nTELEGRAM_FLUVIUS_V171_ALT = (\n '/FLU5\\\\253769484_A\\r\\n'\n '\\r\\n'\n '0-0:96.1.4(50217)\\r\\n'\n '0-0:96.1.1(3153414733313030373231333236)\\r\\n'\n '0-0:1.0.0(231102121548W)\\r\\n'\n '1-0:1.8.1(000301.548*kWh)\\r\\n'\n '1-0:1.8.2(000270.014*kWh)\\r\\n'\n '1-0:2.8.1(000000.005*kWh)\\r\\n'\n '1-0:2.8.2(000000.000*kWh)\\r\\n'\n '0-0:96.14.0(0001)\\r\\n'\n '1-0:1.4.0(00.052*kW)\\r\\n'\n '1-0:1.6.0(231102114500W)(03.064*kW)\\r\\n'\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'\n '1-0:1.7.0(00.338*kW)\\r\\n'\n '1-0:2.7.0(00.000*kW)\\r\\n'\n '1-0:21.7.0(00.047*kW)\\r\\n'\n '1-0:41.7.0(00.179*kW)\\r\\n'\n '1-0:61.7.0(00.111*kW)\\r\\n'\n '1-0:22.7.0(00.000*kW)\\r\\n'\n '1-0:42.7.0(00.000*kW)\\r\\n'\n '1-0:62.7.0(00.000*kW)\\r\\n'\n '1-0:32.7.0(232.9*V)\\r\\n'\n '1-0:52.7.0(228.1*V)\\r\\n'\n '1-0:72.7.0(228.1*V)\\r\\n'\n '1-0:31.7.0(000.27*A)\\r\\n'\n '1-0:51.7.0(000.88*A)\\r\\n'\n '1-0:71.7.0(000.52*A)\\r\\n'\n '0-0:96.3.10(1)\\r\\n'\n '0-0:17.0.0(999.9*kW)\\r\\n'\n '1-0:31.4.0(999*A)\\r\\n'\n '0-0:96.13.0()\\r\\n'\n '0-1:24.1.0(003)\\r\\n'\n '0-1:96.1.1(37464C4F32313233303838303237)\\r\\n'\n '0-1:24.4.0(1)\\r\\n'\n '0-1:24.2.3(231102121002W)(00092.287*m3)\\r\\n'\n '0-2:24.1.0(007)\\r\\n'\n '0-2:96.1.1(3853455430303030393631313733)\\r\\n'\n '0-2:24.2.1(231102121532W)(00008.579*m3)\\r\\n'\n '!C4B0\\r\\n'\n)\n\n# EasyMeter via COM-1 Ethernet Gateway\n# Q3D Manual (german) https://www.easymeter.com/downloads/products/zaehler/Q3D/Easymeter_Q3D_DE_2016-06-15.pdf\n# - type code on page 8\n# - D0-Specs on page 20\n#\n# last two lines are added by the COM-1 Ethernet Gateway\n\nTELEGRAM_ESY5Q3DB1024_V304 = (\n '/ESY5Q3DB1024 V3.04\\r\\n'\n '\\r\\n'\n '1-0:0.0.0*255(0272031312565)\\r\\n'\n '1-0:1.8.0*255(00052185.7825309*kWh)\\r\\n'\n '1-0:2.8.0*255(00019949.3221493*kWh)\\r\\n'\n '1-0:21.7.0*255(000747.85*W)\\r\\n'\n '1-0:41.7.0*255(000737.28*W)\\r\\n'\n '1-0:61.7.0*255(000639.73*W)\\r\\n'\n '1-0:1.7.0*255(002124.86*W)\\r\\n'\n '1-0:96.5.5*255(80)\\r\\n'\n '0-0:96.1.255*255(1ESY1313002565)\\r\\n'\n '!\\r\\n'\n ' 25803103\\r\\n'\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'\n '\\xff\\xff\\xff\\xff\\xff\\r\\n'\n)\n\nTELEGRAM_ESY5Q3DA1004_V304 = (\n '/ESY5Q3DA1004 V3.04\\r\\n'\n '\\r\\n'\n '1-0:0.0.0*255(1336001560)\\r\\n'\n '1-0:1.8.0*255(00032549.5061662*kWh)\\r\\n'\n '1-0:21.7.0*255(000557.29*W)\\r\\n'\n '1-0:41.7.0*255(000521.62*W)\\r\\n'\n '1-0:61.7.0*255(000609.30*W)\\r\\n'\n '1-0:1.7.0*255(001688.21*W)\\r\\n'\n '1-0:96.5.5*255(80)\\r\\n'\n '0-0:96.1.255*255(1ESY1336001560)\\r\\n'\n '!\\r\\n'\n ' 25818685\\r\\n'\n 'DE0000000000000000000000000000003\\r\\n'\n)\n\nTELEGRAM_SAGEMCOM_T210_D_R = (\n '/EST5\\\\253710000_A\\r\\n'\n '\\r\\n'\n '1-3:0.2.8(50)\\r\\n'\n '0-0:1.0.0(221006155014S)\\r\\n'\n '1-0:1.8.0(006545766*Wh)\\r\\n'\n '1-0:1.8.1(005017120*Wh)\\r\\n'\n '1-0:1.8.2(001528646*Wh)\\r\\n'\n '1-0:1.7.0(000000286*W)\\r\\n'\n '1-0:2.8.0(000000058*Wh)\\r\\n'\n '1-0:2.8.1(000000000*Wh)\\r\\n'\n '1-0:2.8.2(000000058*Wh)\\r\\n'\n '1-0:2.7.0(000000000*W)\\r\\n'\n '1-0:3.8.0(000000747*varh)\\r\\n'\n '1-0:3.8.1(000000000*varh)\\r\\n'\n '1-0:3.8.2(000000747*varh)\\r\\n'\n '1-0:3.7.0(000000000*var)\\r\\n'\n '1-0:4.8.0(003897726*varh)\\r\\n'\n '1-0:4.8.1(002692848*varh)\\r\\n'\n '1-0:4.8.2(001204878*varh)\\r\\n'\n '1-0:4.7.0(000000166*var)\\r\\n'\n '!7EF9\\r\\n'\n)\n\nTELEGRAM_ISKRA_IE = (\n '/ISk5\\2MIE5T-200\\r\\n'\n '\\r\\n'\n '1-0:0.0.0(00000000)\\r\\n'\n '0-0:96.1.0(09610)\\r\\n'\n '0-0:1.0.0(230202132747S)\\r\\n'\n '1-0:1.8.1(000010.181*kWh)\\r\\n'\n '1-0:1.8.2(000010.182*kWh)\\r\\n'\n '1-0:2.8.1(000010.281*kWh)\\r\\n'\n '1-0:2.8.2(000010.282*kWh)\\r\\n'\n '0-0:96.14.0(0001)\\r\\n'\n '1-0:1.7.0(00.170*kW)\\r\\n'\n '1-0:2.7.0(00.270*kW)\\r\\n'\n '1-0:21.7.0(00.217*kW)\\r\\n'\n '1-0:41.7.0(00.417*kW)\\r\\n'\n '1-0:61.7.0(00.617*kW)\\r\\n'\n '1-0:22.7.0(00.227*kW)\\r\\n'\n '1-0:42.7.0(00.427*kW)\\r\\n'\n '1-0:62.7.0(00.627*kW)\\r\\n'\n '1-0:32.7.0(242.5*V)\\r\\n'\n '1-0:52.7.0(241.7*V)\\r\\n'\n '1-0:72.7.0(243.3*V)\\r\\n'\n '1-0:31.7.0(000*A)\\r\\n'\n '1-0:51.7.0(000*A)\\r\\n'\n '1-0:71.7.0(000*A)\\r\\n'\n '0-0:96.3.10(1)\\r\\n'\n '0-0:96.13.0()\\r\\n'\n '0-1:96.1.1()\\r\\n'\n '!AD3B\\r\\n'\n)\n\n# V5 telegram of EON in Hungary\nTELEGRAM_V5_EON_HU = (\n '/SAG5SAG-METER\\r\\n'\n '\\r\\n'\n '0-0:1.0.0(230724150730S)\\r\\n'\n '0-0:42.0.0(53414733303832323030303032313630)\\r\\n'\n '0-0:96.1.0(383930303832323030303032313630)\\r\\n'\n '0-0:96.14.0(0001)\\r\\n'\n '0-0:96.3.10(1)\\r\\n'\n '0-0:17.0.0(90.000*kW)\\r\\n'\n '1-0:1.8.0(000173.640*kWh)\\r\\n'\n '1-0:1.8.1(000047.719*kWh)\\r\\n'\n '1-0:1.8.2(000125.921*kWh)\\r\\n'\n '1-0:1.8.3(000000.000*kWh)\\r\\n'\n '1-0:1.8.4(000000.000*kWh)\\r\\n'\n '1-0:2.8.0(000627.177*kWh)\\r\\n'\n '1-0:2.8.1(000401.829*kWh)\\r\\n'\n '1-0:2.8.2(000225.348*kWh)\\r\\n'\n '1-0:2.8.3(000000.000*kWh)\\r\\n'\n '1-0:2.8.4(000000.000*kWh)\\r\\n'\n '1-0:3.8.0(000000.123*kvarh)\\r\\n'\n '1-0:4.8.0(000303.131*kvarh)\\r\\n'\n '1-0:5.8.0(000000.668*kvarh)\\r\\n'\n '1-0:6.8.0(000000.071*kvarh)\\r\\n'\n '1-0:7.8.0(000160.487*kvarh)\\r\\n'\n '1-0:8.8.0(000143.346*kvarh)\\r\\n'\n '1-0:15.8.0(000800.817*kWh)\\r\\n'\n '1-0:32.7.0(240.4*V)\\r\\n'\n '1-0:52.7.0(239.1*V)\\r\\n'\n '1-0:72.7.0(241.2*V)\\r\\n'\n '1-0:31.7.0(003*A)\\r\\n'\n '1-0:51.7.0(004*A)\\r\\n'\n '1-0:71.7.0(003*A)\\r\\n'\n '1-0:13.7.0(4.556)\\r\\n'\n '1-0:33.7.0(4.591)\\r\\n'\n '1-0:53.7.0(4.542)\\r\\n'\n '1-0:73.7.0(4.552)\\r\\n'\n '1-0:14.7.0(50.00*Hz)\\r\\n'\n '1-0:1.7.0(00.000*kW)\\r\\n'\n '1-0:2.7.0(02.601*kW)\\r\\n'\n '1-0:5.7.0(00.000*kvar)\\r\\n'\n '1-0:6.7.0(00.000*kvar)\\r\\n'\n '1-0:7.7.0(00.504*kvar)\\r\\n'\n '1-0:8.7.0(00.000*kvar)\\r\\n'\n '1-0:31.4.0(200.00*A)\\r\\n'\n '1-0:51.4.0(200.00*A)\\r\\n'\n '1-0:71.4.0(200.00*A)\\r\\n'\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'\n '0-0:96.13.0()\\r\\n'\n '!99DA\\r\\n'\n)\n", + "review": "**Grade: 5**\n\n### Bugs\n- None observed, but there may be potential for validation issues since there isn't any parsing or validation logic provided here.\n\n### Optimizations\n- Consider encapsulating telegrams in a data structure or class to provide methods for parsing and validation.\n- Add comments to explain the purpose of each telegram or important sections for better readability.\n- Use a consistent naming convention for variables to improve maintainability.\n\n### Good points\n- Data is well-organized in distinct variables for each telegram type.\n- Usage of constant variables to hold telegrams, which promotes immutability and safer code practices.\n\n### Summary\nThe given code contains multiple datasets representing telegrams for different versions and types of smart meter readings. While the structure is clear and data is organized, the absence of any processing, parsing, or validation function limits its current utility in application beyond storage. Improvements can be made by providing encapsulation, adding functions for parsing telegrams, and ensuring consistency in naming and styling practices. Overall, the code serves as a baseline data source but lacks interactive features or error handling that might be necessary for real-world applications.\n\n### Open source alternatives\n- **Open Energy Monitor**: Provides tools and platforms to monitor and analyze electricity usage.\n- **Home Assistant**: A widely used open-source home automation platform that can integrate energy monitoring.\n- **OpenEMS**: Provides a framework for energy management systems, including support for various protocols and integrations.", + "filename": "example_telegrams.py", + "path": "test/example_telegrams.py", + "directory": "test", + "grade": 5, + "size": 13068, + "line_count": 408 +} \ No newline at end of file diff --git a/reviews/test/example_telegrams.py.md b/reviews/test/example_telegrams.py.md new file mode 100644 index 0000000..0d4aefa --- /dev/null +++ b/reviews/test/example_telegrams.py.md @@ -0,0 +1,21 @@ +**Grade: 5** + +### Bugs +- None observed, but there may be potential for validation issues since there isn't any parsing or validation logic provided here. + +### Optimizations +- Consider encapsulating telegrams in a data structure or class to provide methods for parsing and validation. +- Add comments to explain the purpose of each telegram or important sections for better readability. +- Use a consistent naming convention for variables to improve maintainability. + +### Good points +- Data is well-organized in distinct variables for each telegram type. +- Usage of constant variables to hold telegrams, which promotes immutability and safer code practices. + +### Summary +The given code contains multiple datasets representing telegrams for different versions and types of smart meter readings. While the structure is clear and data is organized, the absence of any processing, parsing, or validation function limits its current utility in application beyond storage. Improvements can be made by providing encapsulation, adding functions for parsing telegrams, and ensuring consistency in naming and styling practices. Overall, the code serves as a baseline data source but lacks interactive features or error handling that might be necessary for real-world applications. + +### Open source alternatives +- **Open Energy Monitor**: Provides tools and platforms to monitor and analyze electricity usage. +- **Home Assistant**: A widely used open-source home automation platform that can integrate energy monitoring. +- **OpenEMS**: Provides a framework for energy management systems, including support for various protocols and integrations. \ No newline at end of file diff --git a/reviews/test/experiment_telegram.py.json b/reviews/test/experiment_telegram.py.json new file mode 100644 index 0000000..65b0dca --- /dev/null +++ b/reviews/test/experiment_telegram.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from dsmr_parser import telegram_specifications\nfrom dsmr_parser.parsers import TelegramParser\nfrom test.example_telegrams import TELEGRAM_V4_2\nparser = TelegramParser(telegram_specifications.V4)\ntelegram = parser.parse(TELEGRAM_V4_2)\n\nprint(telegram)\n", + "review": "8\n\n### Bugs\n- No apparent bugs in the code snippet given the current context.\n\n### Optimizations\n- Add error handling for cases where parsing might fail.\n- Consider logging for troubleshooting, especially for parsing operations.\n- If the `TELEGRAM_V4_2` data might be large, consider using a more efficient storage or processing method to handle the telegram data.\n\n### Good points\n- The code is clear and concise, directly implementing telegram parsing in a few lines.\n- Utilizes external library `dsmr_parser` effectively for parsing DSMR telegrams.\n\n### Summary\nThe provided Python code is a straightforward and effective example of how to parse DSMR telegrams using the `dsmr_parser` library. It is well-structured and uses the functionalities of the `dsmr_parser` library appropriately. Minor improvements could be made in terms of error handling and logging enhancements.\n\n### Open source alternatives\n- There are no direct alternatives to `dsmr_parser` within the specific niche of DSMR telegram parsing, but `pyserial` could be used if implementing lower-level serial communication and parsing logic manually for DSMR devices.", + "filename": "experiment_telegram.py", + "path": "test/experiment_telegram.py", + "directory": "test", + "grade": 8, + "size": 252, + "line_count": 8 +} \ No newline at end of file diff --git a/reviews/test/experiment_telegram.py.md b/reviews/test/experiment_telegram.py.md new file mode 100644 index 0000000..cdaeafd --- /dev/null +++ b/reviews/test/experiment_telegram.py.md @@ -0,0 +1,19 @@ +8 + +### Bugs +- No apparent bugs in the code snippet given the current context. + +### Optimizations +- Add error handling for cases where parsing might fail. +- Consider logging for troubleshooting, especially for parsing operations. +- If the `TELEGRAM_V4_2` data might be large, consider using a more efficient storage or processing method to handle the telegram data. + +### Good points +- The code is clear and concise, directly implementing telegram parsing in a few lines. +- Utilizes external library `dsmr_parser` effectively for parsing DSMR telegrams. + +### Summary +The provided Python code is a straightforward and effective example of how to parse DSMR telegrams using the `dsmr_parser` library. It is well-structured and uses the functionalities of the `dsmr_parser` library appropriately. Minor improvements could be made in terms of error handling and logging enhancements. + +### Open source alternatives +- There are no direct alternatives to `dsmr_parser` within the specific niche of DSMR telegram parsing, but `pyserial` could be used if implementing lower-level serial communication and parsing logic manually for DSMR devices. \ No newline at end of file diff --git a/reviews/test/objects/test_mbusdevice.py.json b/reviews/test/objects/test_mbusdevice.py.json new file mode 100644 index 0000000..f39f5ce --- /dev/null +++ b/reviews/test/objects/test_mbusdevice.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from decimal import Decimal\n\nimport json\nimport unittest\n\nfrom dsmr_parser import telegram_specifications, obis_references\nfrom dsmr_parser.objects import MbusDevice\n\n\nclass MbusDeviceTest(unittest.TestCase):\n\n def setUp(self):\n v5_objects = telegram_specifications.V5['objects']\n\n device_type_parser = [\n object[\"value_parser\"]\n for object in v5_objects\n if object[\"obis_reference\"] == obis_references.MBUS_DEVICE_TYPE\n ][0]\n device_type = device_type_parser.parse('0-2:24.1.0(003)\\r\\n')\n\n equipment_parser = [\n object[\"value_parser\"]\n for object in v5_objects\n if object[\"obis_reference\"] == obis_references.MBUS_EQUIPMENT_IDENTIFIER\n ][0]\n equipment = equipment_parser.parse('0-2:96.1.0(4730303339303031393336393930363139)\\r\\n')\n\n gas_reading_parser = [\n object[\"value_parser\"]\n for object in v5_objects\n if object[\"obis_reference\"] == obis_references.MBUS_METER_READING\n ][0]\n gas_reading = gas_reading_parser.parse('0-2:24.2.1(200426223001S)(00246.138*m3)\\r\\n')\n\n mbus_device = MbusDevice(channel_id=2)\n mbus_device.add(obis_references.MBUS_DEVICE_TYPE, device_type, \"MBUS_DEVICE_TYPE\")\n mbus_device.add(obis_references.MBUS_EQUIPMENT_IDENTIFIER, equipment, \"MBUS_EQUIPMENT_IDENTIFIER\")\n mbus_device.add(obis_references.MBUS_METER_READING, gas_reading, \"MBUS_METER_READING\")\n\n self.mbus_device = mbus_device\n\n def test_attributes(self):\n self.assertEqual(self.mbus_device.MBUS_DEVICE_TYPE.value, 3)\n self.assertEqual(self.mbus_device.MBUS_DEVICE_TYPE.unit, None)\n\n self.assertEqual(self.mbus_device.MBUS_EQUIPMENT_IDENTIFIER.value,\n '4730303339303031393336393930363139')\n self.assertEqual(self.mbus_device.MBUS_EQUIPMENT_IDENTIFIER.unit, None)\n\n self.assertEqual(self.mbus_device.MBUS_METER_READING.value, Decimal('246.138'))\n self.assertEqual(self.mbus_device.MBUS_METER_READING.unit, 'm3')\n\n def test_to_json(self):\n self.assertEqual(\n json.loads(self.mbus_device.to_json()),\n {\n 'CHANNEL_ID': 2,\n 'MBUS_DEVICE_TYPE': {'value': 3, 'unit': None},\n 'MBUS_EQUIPMENT_IDENTIFIER': {'value': '4730303339303031393336393930363139', 'unit': None},\n 'MBUS_METER_READING': {'datetime': '2020-04-26T20:30:01+00:00', 'value': 246.138, 'unit': 'm3'}}\n )\n\n def test_str(self):\n self.assertEqual(\n str(self.mbus_device),\n (\n 'MBUS DEVICE (channel 2)\\n'\n '\\tMBUS_DEVICE_TYPE: \t 3\t[None]\\n'\n '\\tMBUS_EQUIPMENT_IDENTIFIER: \t 4730303339303031393336393930363139\t[None]\\n'\n '\\tMBUS_METER_READING: \t 246.138\t[m3] at 2020-04-26T20:30:01+00:00\\n'\n )\n )\n", + "review": "# 8\n\n## Bugs\n- No explicit bug was detected in this segment of the code.\n\n## Optimizations\n- Include error handling for parsing functions to manage unexpected inputs more gracefully.\n- Consider using constants or an enum for repeated string literals to improve maintainability.\n- The parsing of date from the MBUS_METER_READING should ensure timezone consistency.\n\n## Good points\n- Makes effective use of unit tests to validate the functionality of the `MbusDevice` class.\n- Uses `unittest` which is a standard Python module for testing, showing good practice.\n- Demonstrates clarity in structuring tests into distinct methods.\n- Code adheres to PEP 8 style guidelines making it readable.\n\n## Summary\nThe code presents a unit test suite for an `MbusDevice` class, verifying its parsing and JSON serialization functionalities. The tests are well-organized, covering attributes and different string representations of the device. While the code is generally well-written and free from apparent bugs, improvements can be made in terms of error handling and simplifying repetitive structures.\n\n## Open source alternatives\n- [Home Assistant](https://www.home-assistant.io/): An open-source platform that can receive and decode DSMR data from smart meters.\n- [DSMR Reader](https://github.com/dsmrreader/dsmr-reader): An open-source application for monitoring and visualizing Dutch Smart Meter data.", + "filename": "test_mbusdevice.py", + "path": "test/objects/test_mbusdevice.py", + "directory": "objects", + "grade": 8, + "size": 2920, + "line_count": 74 +} \ No newline at end of file diff --git a/reviews/test/objects/test_mbusdevice.py.md b/reviews/test/objects/test_mbusdevice.py.md new file mode 100644 index 0000000..0f97478 --- /dev/null +++ b/reviews/test/objects/test_mbusdevice.py.md @@ -0,0 +1,22 @@ +# 8 + +## Bugs +- No explicit bug was detected in this segment of the code. + +## Optimizations +- Include error handling for parsing functions to manage unexpected inputs more gracefully. +- Consider using constants or an enum for repeated string literals to improve maintainability. +- The parsing of date from the MBUS_METER_READING should ensure timezone consistency. + +## Good points +- Makes effective use of unit tests to validate the functionality of the `MbusDevice` class. +- Uses `unittest` which is a standard Python module for testing, showing good practice. +- Demonstrates clarity in structuring tests into distinct methods. +- Code adheres to PEP 8 style guidelines making it readable. + +## Summary +The code presents a unit test suite for an `MbusDevice` class, verifying its parsing and JSON serialization functionalities. The tests are well-organized, covering attributes and different string representations of the device. While the code is generally well-written and free from apparent bugs, improvements can be made in terms of error handling and simplifying repetitive structures. + +## Open source alternatives +- [Home Assistant](https://www.home-assistant.io/): An open-source platform that can receive and decode DSMR data from smart meters. +- [DSMR Reader](https://github.com/dsmrreader/dsmr-reader): An open-source application for monitoring and visualizing Dutch Smart Meter data. \ No newline at end of file diff --git a/reviews/test/objects/test_parser_corner_cases.py.json b/reviews/test/objects/test_parser_corner_cases.py.json new file mode 100644 index 0000000..53d023f --- /dev/null +++ b/reviews/test/objects/test_parser_corner_cases.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import unittest\n\nfrom dsmr_parser import telegram_specifications\n\nfrom dsmr_parser.objects import ProfileGenericObject\nfrom dsmr_parser.parsers import TelegramParser\nfrom dsmr_parser.parsers import ProfileGenericParser\nfrom dsmr_parser.profile_generic_specifications import BUFFER_TYPES\nfrom dsmr_parser.profile_generic_specifications import PG_HEAD_PARSERS\nfrom dsmr_parser.profile_generic_specifications import PG_UNIDENTIFIED_BUFFERTYPE_PARSERS\nfrom test.example_telegrams import TELEGRAM_V5\n\n\nclass TestParserCornerCases(unittest.TestCase):\n \"\"\" Test instantiation of Telegram object \"\"\"\n\n def test_power_event_log_empty_1(self):\n # POWER_EVENT_FAILURE_LOG (1-0:99.97.0)\n parser = TelegramParser(telegram_specifications.V5)\n telegram = parser.parse(TELEGRAM_V5)\n\n object_type = ProfileGenericObject\n testitem = telegram.POWER_EVENT_FAILURE_LOG\n assert isinstance(testitem, object_type)\n assert testitem.buffer_length == 0\n assert testitem.buffer_type == '0-0:96.7.19'\n buffer = testitem.buffer\n assert isinstance(testitem.buffer, list)\n assert len(buffer) == 0\n\n def test_power_event_log_empty_2(self):\n pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)\n object_type = ProfileGenericObject\n\n # Power Event Log with 0 items and no object type\n pefl_line = r'1-0:99.97.0(0)()\\r\\n'\n testitem = pef_parser.parse(pefl_line)\n\n assert isinstance(testitem, object_type)\n assert testitem.buffer_length == 0\n assert testitem.buffer_type is None\n buffer = testitem.buffer\n assert isinstance(testitem.buffer, list)\n assert len(buffer) == 0\n assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]\n json = testitem.to_json()\n assert json == '{\"buffer_length\": 0, \"buffer_type\": null, \"buffer\": []}'\n\n def test_power_event_log_null_values(self):\n pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)\n object_type = ProfileGenericObject\n\n # Power Event Log with 1 item and no object type and nno values for the item\n pefl_line = r'1-0:99.97.0(1)()()()\\r\\n'\n testitem = pef_parser.parse(pefl_line)\n\n assert isinstance(testitem, object_type)\n assert testitem.buffer_length == 1\n assert testitem.buffer_type is None\n buffer = testitem.buffer\n assert isinstance(testitem.buffer, list)\n assert len(buffer) == 1\n assert testitem.values == [{'value': 1, 'unit': None}, {'value': None, 'unit': None},\n {'value': None, 'unit': None}, {'value': None, 'unit': None}]\n json = testitem.to_json()\n assert json == \\\n '{\"buffer_length\": 1, \"buffer_type\": null, \"buffer\": [{\"datetime\": null, \"value\": null, \"unit\": null}]}'\n\n def test_power_event_log_brackets_only(self):\n # POWER_EVENT_FAILURE_LOG (1-0:99.97.0)\n # Issue 57\n # Test of an ill formatted empty POWER_EVENT_FAILURE_LOG, observed on some smartmeters\n # The idea is that instead of failing, the parser converts it to an empty POWER_EVENT_FAILURE_LOG\n pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)\n object_type = ProfileGenericObject\n\n pefl_line = r'1-0:99.97.0()\\r\\n'\n testitem = pef_parser.parse(pefl_line)\n\n assert isinstance(testitem, object_type)\n assert testitem.buffer_length == 0\n assert testitem.buffer_type is None\n buffer = testitem.buffer\n assert isinstance(testitem.buffer, list)\n assert len(buffer) == 0\n assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]\n json = testitem.to_json()\n assert json == '{\"buffer_length\": 0, \"buffer_type\": null, \"buffer\": []}'\n", + "review": "# 7\n\n## Bugs\n- No evident bugs found in the code provided. However, potential issues might arise if expected libraries or modules (`dsmr_parser`, `test.example_telegrams`) are missing or not compatible.\n\n## Optimizations\n- The use of hard-coded assert statements can be improved. Instead, using `self.assertEqual`, `self.assertIsInstance`, etc., would provide more robust and informative test outcomes.\n- There is a repeated pattern in obtaining the `ProfileGenericObject`; consider refactoring this into a setup method for reuse.\n- Consider improving the handling of test names to reflect the specific conditions being tested more clearly.\n\n## Good points\n- The tests cover various edge cases such as empty buffers and incorrect formatting, showing a good consideration of potential issues.\n- Usage of regular expression-like syntax to simulate data which suggests potential extensibility.\n- Converts issue-specific scenarios into actionable test cases that prevent regressions.\n\n## Summary\nThe code is a solid test suite for handling edge cases of DSMR (Dutch Smart Meter Requirements) telegram parsing. It focuses on ensuring the parser's robustness against incorrectly formatted or empty logs, and it is clear about the data format expectations. Some improvements might include refactoring to reduce repetition and improve test clarity, but overall, it provides a thorough coverage of corner cases.\n\n## Open source alternatives\n- `PyDSMR`: A Python library to parse DSMR telemetry data, which might include similar parsing and testing utilities.\n- `dsmr-reader`: An open-source project that reads DSMR data and stores it in a database, potentially including parsing capabilities.", + "filename": "test_parser_corner_cases.py", + "path": "test/objects/test_parser_corner_cases.py", + "directory": "objects", + "grade": 7, + "size": 3978, + "line_count": 89 +} \ No newline at end of file diff --git a/reviews/test/objects/test_parser_corner_cases.py.md b/reviews/test/objects/test_parser_corner_cases.py.md new file mode 100644 index 0000000..f93410f --- /dev/null +++ b/reviews/test/objects/test_parser_corner_cases.py.md @@ -0,0 +1,21 @@ +# 7 + +## Bugs +- No evident bugs found in the code provided. However, potential issues might arise if expected libraries or modules (`dsmr_parser`, `test.example_telegrams`) are missing or not compatible. + +## Optimizations +- The use of hard-coded assert statements can be improved. Instead, using `self.assertEqual`, `self.assertIsInstance`, etc., would provide more robust and informative test outcomes. +- There is a repeated pattern in obtaining the `ProfileGenericObject`; consider refactoring this into a setup method for reuse. +- Consider improving the handling of test names to reflect the specific conditions being tested more clearly. + +## Good points +- The tests cover various edge cases such as empty buffers and incorrect formatting, showing a good consideration of potential issues. +- Usage of regular expression-like syntax to simulate data which suggests potential extensibility. +- Converts issue-specific scenarios into actionable test cases that prevent regressions. + +## Summary +The code is a solid test suite for handling edge cases of DSMR (Dutch Smart Meter Requirements) telegram parsing. It focuses on ensuring the parser's robustness against incorrectly formatted or empty logs, and it is clear about the data format expectations. Some improvements might include refactoring to reduce repetition and improve test clarity, but overall, it provides a thorough coverage of corner cases. + +## Open source alternatives +- `PyDSMR`: A Python library to parse DSMR telemetry data, which might include similar parsing and testing utilities. +- `dsmr-reader`: An open-source project that reads DSMR data and stores it in a database, potentially including parsing capabilities. \ No newline at end of file diff --git a/reviews/test/objects/test_telegram.py.json b/reviews/test/objects/test_telegram.py.json new file mode 100644 index 0000000..a451010 --- /dev/null +++ b/reviews/test/objects/test_telegram.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import json\nimport unittest\nimport datetime\nimport pytz\n\nfrom dsmr_parser import telegram_specifications, obis_references\n\nfrom dsmr_parser.objects import CosemObject\nfrom dsmr_parser.objects import MBusObject\nfrom dsmr_parser.objects import ProfileGenericObject\nfrom dsmr_parser.parsers import TelegramParser\nfrom test.example_telegrams import TELEGRAM_V4_2, TELEGRAM_V5_TWO_MBUS, TELEGRAM_V5\nfrom decimal import Decimal\n\n\nclass TelegramTest(unittest.TestCase):\n \"\"\" Test instantiation of Telegram object \"\"\"\n\n def __init__(self, *args, **kwargs):\n self.item_names_tested = []\n super(TelegramTest, self).__init__(*args, **kwargs)\n\n def verify_telegram_item(self, telegram, testitem_name, object_type, unit_val, value_type, value_val):\n testitem = eval(\"telegram.{}\".format(testitem_name))\n assert isinstance(testitem, object_type)\n assert testitem.unit == unit_val\n assert isinstance(testitem.value, value_type)\n assert testitem.value == value_val\n self.item_names_tested.append(testitem_name)\n\n def test_instantiate(self):\n parser = TelegramParser(telegram_specifications.V4)\n telegram = parser.parse(TELEGRAM_V4_2)\n\n # P1_MESSAGE_HEADER (1-3:0.2.8)\n self.verify_telegram_item(telegram,\n 'P1_MESSAGE_HEADER',\n object_type=CosemObject,\n unit_val=None,\n value_type=str,\n value_val='42')\n\n # P1_MESSAGE_TIMESTAMP (0-0:1.0.0)\n self.verify_telegram_item(telegram,\n 'P1_MESSAGE_TIMESTAMP',\n CosemObject,\n unit_val=None,\n value_type=datetime.datetime,\n value_val=datetime.datetime(2016, 11, 13, 19, 57, 57, tzinfo=pytz.UTC))\n\n # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)\n self.verify_telegram_item(telegram,\n 'ELECTRICITY_USED_TARIFF_1',\n object_type=CosemObject,\n unit_val='kWh',\n value_type=Decimal,\n value_val=Decimal('1581.123'))\n\n # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)\n self.verify_telegram_item(telegram,\n 'ELECTRICITY_USED_TARIFF_2',\n object_type=CosemObject,\n unit_val='kWh',\n value_type=Decimal,\n value_val=Decimal('1435.706'))\n\n # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)\n self.verify_telegram_item(telegram,\n 'ELECTRICITY_DELIVERED_TARIFF_1',\n object_type=CosemObject,\n unit_val='kWh',\n value_type=Decimal,\n value_val=Decimal('0'))\n\n # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)\n self.verify_telegram_item(telegram,\n 'ELECTRICITY_DELIVERED_TARIFF_2',\n object_type=CosemObject,\n unit_val='kWh',\n value_type=Decimal,\n value_val=Decimal('0'))\n\n # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)\n self.verify_telegram_item(telegram,\n 'ELECTRICITY_ACTIVE_TARIFF',\n object_type=CosemObject,\n unit_val=None,\n value_type=str,\n value_val='0002')\n\n # EQUIPMENT_IDENTIFIER (0-0:96.1.1)\n self.verify_telegram_item(telegram,\n 'EQUIPMENT_IDENTIFIER',\n object_type=CosemObject,\n unit_val=None,\n value_type=str,\n value_val='3960221976967177082151037881335713')\n\n # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)\n self.verify_telegram_item(telegram,\n 'CURRENT_ELECTRICITY_USAGE',\n object_type=CosemObject,\n unit_val='kW',\n value_type=Decimal,\n value_val=Decimal('2.027'))\n\n # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)\n self.verify_telegram_item(telegram,\n 'CURRENT_ELECTRICITY_DELIVERY',\n object_type=CosemObject,\n unit_val='kW',\n value_type=Decimal,\n value_val=Decimal('0'))\n\n # SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)\n self.verify_telegram_item(telegram,\n 'SHORT_POWER_FAILURE_COUNT',\n object_type=CosemObject,\n unit_val=None,\n value_type=int,\n value_val=15)\n\n # LONG_POWER_FAILURE_COUNT (96.7.9)\n self.verify_telegram_item(telegram,\n 'LONG_POWER_FAILURE_COUNT',\n object_type=CosemObject,\n unit_val=None,\n value_type=int,\n value_val=7)\n\n # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)\n self.verify_telegram_item(telegram,\n 'VOLTAGE_SAG_L1_COUNT',\n object_type=CosemObject,\n unit_val=None,\n value_type=int,\n value_val=0)\n\n # VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)\n self.verify_telegram_item(telegram,\n 'VOLTAGE_SAG_L2_COUNT',\n object_type=CosemObject,\n unit_val=None,\n value_type=int,\n value_val=0)\n\n # VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)\n self.verify_telegram_item(telegram,\n 'VOLTAGE_SAG_L3_COUNT',\n object_type=CosemObject,\n unit_val=None,\n value_type=int,\n value_val=0)\n\n # VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)\n self.verify_telegram_item(telegram,\n 'VOLTAGE_SWELL_L1_COUNT',\n object_type=CosemObject,\n unit_val=None,\n value_type=int,\n value_val=0)\n\n # VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)\n self.verify_telegram_item(telegram,\n 'VOLTAGE_SWELL_L2_COUNT',\n object_type=CosemObject,\n unit_val=None,\n value_type=int,\n value_val=0)\n\n # VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)\n self.verify_telegram_item(telegram,\n 'VOLTAGE_SWELL_L3_COUNT',\n object_type=CosemObject,\n unit_val=None,\n value_type=int,\n value_val=0)\n\n # TEXT_MESSAGE_CODE (0-0:96.13.1)\n self.verify_telegram_item(telegram,\n 'TEXT_MESSAGE_CODE',\n object_type=CosemObject,\n unit_val=None,\n value_type=type(None),\n value_val=None)\n\n # TEXT_MESSAGE (0-0:96.13.0)\n self.verify_telegram_item(telegram,\n 'TEXT_MESSAGE',\n object_type=CosemObject,\n unit_val=None,\n value_type=type(None),\n value_val=None)\n\n # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)\n self.verify_telegram_item(telegram,\n 'INSTANTANEOUS_CURRENT_L1',\n object_type=CosemObject,\n unit_val='A',\n value_type=Decimal,\n value_val=Decimal('0'))\n\n # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)\n self.verify_telegram_item(telegram,\n 'INSTANTANEOUS_CURRENT_L2',\n object_type=CosemObject,\n unit_val='A',\n value_type=Decimal,\n value_val=Decimal('6'))\n\n # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)\n self.verify_telegram_item(telegram,\n 'INSTANTANEOUS_CURRENT_L3',\n object_type=CosemObject,\n unit_val='A',\n value_type=Decimal,\n value_val=Decimal('2'))\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)\n self.verify_telegram_item(telegram,\n 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',\n object_type=CosemObject,\n unit_val='kW',\n value_type=Decimal,\n value_val=Decimal('0.170'))\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)\n self.verify_telegram_item(telegram,\n 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',\n object_type=CosemObject,\n unit_val='kW',\n value_type=Decimal,\n value_val=Decimal('1.247'))\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)\n self.verify_telegram_item(telegram,\n 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',\n object_type=CosemObject,\n unit_val='kW',\n value_type=Decimal,\n value_val=Decimal('0.209'))\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)\n self.verify_telegram_item(telegram,\n 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',\n object_type=CosemObject,\n unit_val='kW',\n value_type=Decimal,\n value_val=Decimal('0'))\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)\n self.verify_telegram_item(telegram,\n 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',\n object_type=CosemObject,\n unit_val='kW',\n value_type=Decimal,\n value_val=Decimal('0'))\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)\n self.verify_telegram_item(telegram,\n 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',\n object_type=CosemObject,\n unit_val='kW',\n value_type=Decimal,\n value_val=Decimal('0'))\n\n # DEVICE_TYPE (0-1:24.1.0)\n self.verify_telegram_item(telegram,\n 'DEVICE_TYPE',\n object_type=CosemObject,\n unit_val=None,\n value_type=int,\n value_val=3)\n\n # EQUIPMENT_IDENTIFIER_GAS (0-1:96.1.0)\n self.verify_telegram_item(telegram,\n 'EQUIPMENT_IDENTIFIER_GAS',\n object_type=CosemObject,\n unit_val=None,\n value_type=str,\n value_val='4819243993373755377509728609491464')\n\n # HOURLY_GAS_METER_READING (0-1:24.2.1)\n self.verify_telegram_item(telegram,\n 'HOURLY_GAS_METER_READING',\n object_type=MBusObject,\n unit_val='m3',\n value_type=Decimal,\n value_val=Decimal('981.443'))\n\n # POWER_EVENT_FAILURE_LOG (1-0:99.97.0)\n testitem_name = 'POWER_EVENT_FAILURE_LOG'\n object_type = ProfileGenericObject\n testitem = eval(\"telegram.{}\".format(testitem_name))\n assert isinstance(testitem, object_type)\n assert testitem.buffer_length == 3\n assert testitem.buffer_type == '0-0:96.7.19'\n buffer = testitem.buffer\n assert isinstance(testitem.buffer, list)\n assert len(buffer) == 3\n assert all([isinstance(item, MBusObject) for item in buffer])\n date0 = datetime.datetime(2000, 1, 4, 17, 3, 20, tzinfo=datetime.timezone.utc)\n date1 = datetime.datetime(1999, 12, 31, 23, 0, 1, tzinfo=datetime.timezone.utc)\n date2 = datetime.datetime(2000, 1, 1, 23, 0, 3, tzinfo=datetime.timezone.utc)\n assert buffer[0].datetime == date0\n assert buffer[1].datetime == date1\n assert buffer[2].datetime == date2\n assert buffer[0].value == 237126\n assert buffer[1].value == 2147583646\n assert buffer[2].value == 2317482647\n assert all([isinstance(item.value, int) for item in buffer])\n assert all([isinstance(item.unit, str) for item in buffer])\n assert all([(item.unit == 's') for item in buffer])\n self.item_names_tested.append(testitem_name)\n\n # check if all items in telegram V4 specification are covered\n V4_name_list = [object[\"value_name\"] for object in\n telegram_specifications.V4['objects']]\n V4_name_set = set(V4_name_list)\n item_names_tested_set = set(self.item_names_tested)\n\n assert item_names_tested_set == V4_name_set\n\n def test_iter(self):\n parser = TelegramParser(telegram_specifications.V5)\n telegram = parser.parse(TELEGRAM_V5)\n\n for obis_name, dsmr_object in telegram:\n break\n\n # Verify that the iterator works for at least one value\n self.assertEqual(obis_name, \"P1_MESSAGE_HEADER\")\n self.assertEqual(dsmr_object.value, '50')\n\n def test_mbus_devices(self):\n parser = TelegramParser(telegram_specifications.V5)\n telegram = parser.parse(TELEGRAM_V5_TWO_MBUS)\n mbus_devices = telegram.MBUS_DEVICES\n\n self.assertEqual(len(mbus_devices), 2)\n\n mbus_device_1 = mbus_devices[0]\n self.assertEqual(mbus_device_1.MBUS_DEVICE_TYPE.value, 3)\n self.assertEqual(mbus_device_1.MBUS_EQUIPMENT_IDENTIFIER.value, None)\n self.assertEqual(mbus_device_1.MBUS_METER_READING.value, Decimal('0'))\n\n mbus_device_2 = mbus_devices[1]\n self.assertEqual(mbus_device_2.MBUS_DEVICE_TYPE.value, 3)\n self.assertEqual(mbus_device_2.MBUS_EQUIPMENT_IDENTIFIER.value, '4730303339303031393336393930363139')\n self.assertEqual(mbus_device_2.MBUS_METER_READING.value, Decimal('246.138'))\n\n def test_get_mbus_device_by_channel(self):\n parser = TelegramParser(telegram_specifications.V5)\n telegram = parser.parse(TELEGRAM_V5_TWO_MBUS)\n\n mbus_device_1 = telegram.get_mbus_device_by_channel(1)\n self.assertEqual(mbus_device_1.MBUS_DEVICE_TYPE.value, 3)\n self.assertEqual(mbus_device_1.MBUS_EQUIPMENT_IDENTIFIER.value, None)\n self.assertEqual(mbus_device_1.MBUS_METER_READING.value, Decimal('0'))\n\n mbus_device_2 = telegram.get_mbus_device_by_channel(2)\n self.assertEqual(mbus_device_2.MBUS_DEVICE_TYPE.value, 3)\n self.assertEqual(mbus_device_2.MBUS_EQUIPMENT_IDENTIFIER.value, '4730303339303031393336393930363139')\n self.assertEqual(mbus_device_2.MBUS_METER_READING.value, Decimal('246.138'))\n\n def test_without_mbus_devices(self):\n parser = TelegramParser(telegram_specifications.V5, apply_checksum_validation=False)\n telegram = parser.parse('')\n\n self.assertFalse(hasattr(telegram, 'MBUS_DEVICES'))\n self.assertIsNone(telegram.get_mbus_device_by_channel(1))\n\n def test_to_json(self):\n parser = TelegramParser(telegram_specifications.V5)\n telegram = parser.parse(TELEGRAM_V5)\n json_data = json.loads(telegram.to_json())\n\n self.maxDiff = None\n\n self.assertEqual(\n json_data,\n {'CURRENT_ELECTRICITY_DELIVERY': {'unit': 'kW', 'value': 0.0},\n 'CURRENT_ELECTRICITY_USAGE': {'unit': 'kW', 'value': 0.244},\n 'ELECTRICITY_ACTIVE_TARIFF': {'unit': None, 'value': '0002'},\n 'ELECTRICITY_DELIVERED_TARIFF_1': {'unit': 'kWh', 'value': 2.444},\n 'ELECTRICITY_DELIVERED_TARIFF_2': {'unit': 'kWh', 'value': 0.0},\n 'ELECTRICITY_USED_TARIFF_1': {'unit': 'kWh', 'value': 4.426},\n 'ELECTRICITY_USED_TARIFF_2': {'unit': 'kWh', 'value': 2.399},\n 'EQUIPMENT_IDENTIFIER': {'unit': None,\n 'value': '4B384547303034303436333935353037'},\n 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE': {'unit': 'kW', 'value': 0.0},\n 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE': {'unit': 'kW', 'value': 0.07},\n 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE': {'unit': 'kW', 'value': 0.0},\n 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE': {'unit': 'kW', 'value': 0.032},\n 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE': {'unit': 'kW', 'value': 0.0},\n 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE': {'unit': 'kW', 'value': 0.142},\n 'INSTANTANEOUS_CURRENT_L1': {'unit': 'A', 'value': 0.48},\n 'INSTANTANEOUS_CURRENT_L2': {'unit': 'A', 'value': 0.44},\n 'INSTANTANEOUS_CURRENT_L3': {'unit': 'A', 'value': 0.86},\n 'INSTANTANEOUS_VOLTAGE_L1': {'unit': 'V', 'value': 230.0},\n 'INSTANTANEOUS_VOLTAGE_L2': {'unit': 'V', 'value': 230.0},\n 'INSTANTANEOUS_VOLTAGE_L3': {'unit': 'V', 'value': 229.0},\n 'LONG_POWER_FAILURE_COUNT': {'unit': None, 'value': 0},\n 'MBUS_DEVICES': [{'CHANNEL_ID': 1,\n 'MBUS_DEVICE_TYPE': {'unit': None, 'value': 3},\n 'MBUS_EQUIPMENT_IDENTIFIER': {'unit': None,\n 'value': '3232323241424344313233343536373839'},\n 'MBUS_METER_READING': {'datetime': '2017-01-02T15:10:05+00:00',\n 'unit': 'm3',\n 'value': 0.107}},\n {'CHANNEL_ID': 2,\n 'MBUS_DEVICE_TYPE': {'unit': None, 'value': 3},\n 'MBUS_EQUIPMENT_IDENTIFIER': {'unit': None,\n 'value': None}}],\n 'P1_MESSAGE_HEADER': {'unit': None, 'value': '50'},\n 'P1_MESSAGE_TIMESTAMP': {'unit': None, 'value': '2017-01-02T18:20:02+00:00'},\n 'POWER_EVENT_FAILURE_LOG': {'buffer': [],\n 'buffer_length': 0,\n 'buffer_type': '0-0:96.7.19'},\n 'SHORT_POWER_FAILURE_COUNT': {'unit': None, 'value': 13},\n 'TEXT_MESSAGE': {'unit': None, 'value': None},\n 'VOLTAGE_SAG_L1_COUNT': {'unit': None, 'value': 0},\n 'VOLTAGE_SAG_L2_COUNT': {'unit': None, 'value': 0},\n 'VOLTAGE_SAG_L3_COUNT': {'unit': None, 'value': 0},\n 'VOLTAGE_SWELL_L1_COUNT': {'unit': None, 'value': 0},\n 'VOLTAGE_SWELL_L2_COUNT': {'unit': None, 'value': 0},\n 'VOLTAGE_SWELL_L3_COUNT': {'unit': None, 'value': 0}}\n )\n\n def test_to_str(self):\n parser = TelegramParser(telegram_specifications.V5)\n telegram = parser.parse(TELEGRAM_V5)\n\n self.maxDiff = None\n\n self.assertEqual(\n str(telegram),\n (\n 'P1_MESSAGE_HEADER: \t 50\t[None]\\n'\n 'P1_MESSAGE_TIMESTAMP: \t 2017-01-02T18:20:02+00:00\t[None]\\n'\n 'EQUIPMENT_IDENTIFIER: \t 4B384547303034303436333935353037\t[None]\\n'\n 'ELECTRICITY_USED_TARIFF_1: \t 4.426\t[kWh]\\n'\n 'ELECTRICITY_USED_TARIFF_2: \t 2.399\t[kWh]\\n'\n 'ELECTRICITY_DELIVERED_TARIFF_1: \t 2.444\t[kWh]\\n'\n 'ELECTRICITY_DELIVERED_TARIFF_2: \t 0.000\t[kWh]\\n'\n 'ELECTRICITY_ACTIVE_TARIFF: \t 0002\t[None]\\n'\n 'CURRENT_ELECTRICITY_USAGE: \t 0.244\t[kW]\\n'\n 'CURRENT_ELECTRICITY_DELIVERY: \t 0.000\t[kW]\\n'\n 'LONG_POWER_FAILURE_COUNT: \t 0\t[None]\\n'\n 'SHORT_POWER_FAILURE_COUNT: \t 13\t[None]\\n'\n 'POWER_EVENT_FAILURE_LOG: \t \t buffer length: 0\\n'\n '\t buffer type: 0-0:96.7.19\\n'\n 'VOLTAGE_SAG_L1_COUNT: \t 0\t[None]\\n'\n 'VOLTAGE_SAG_L2_COUNT: \t 0\t[None]\\n'\n 'VOLTAGE_SAG_L3_COUNT: \t 0\t[None]\\n'\n 'VOLTAGE_SWELL_L1_COUNT: \t 0\t[None]\\n'\n 'VOLTAGE_SWELL_L2_COUNT: \t 0\t[None]\\n'\n 'VOLTAGE_SWELL_L3_COUNT: \t 0\t[None]\\n'\n 'INSTANTANEOUS_VOLTAGE_L1: \t 230.0\t[V]\\n'\n 'INSTANTANEOUS_VOLTAGE_L2: \t 230.0\t[V]\\n'\n 'INSTANTANEOUS_VOLTAGE_L3: \t 229.0\t[V]\\n'\n 'INSTANTANEOUS_CURRENT_L1: \t 0.48\t[A]\\n'\n 'INSTANTANEOUS_CURRENT_L2: \t 0.44\t[A]\\n'\n 'INSTANTANEOUS_CURRENT_L3: \t 0.86\t[A]\\n'\n 'TEXT_MESSAGE: \t None\t[None]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: \t 0.070\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: \t 0.032\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: \t 0.142\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: \t 0.000\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: \t 0.000\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: \t 0.000\t[kW]\\n'\n 'MBUS DEVICE (channel 1)\\n'\n '\tMBUS_DEVICE_TYPE: \t 3\t[None]\\n'\n '\tMBUS_EQUIPMENT_IDENTIFIER: \t 3232323241424344313233343536373839\t[None]\\n'\n '\tMBUS_METER_READING: \t 0.107\t[m3] at 2017-01-02T15:10:05+00:00\\n'\n 'MBUS DEVICE (channel 2)\\n'\n '\tMBUS_DEVICE_TYPE: \t 3\t[None]\\n'\n '\tMBUS_EQUIPMENT_IDENTIFIER: \t None\t[None]\\n'\n )\n )\n\n def test_getitem(self):\n parser = TelegramParser(telegram_specifications.V5)\n telegram = parser.parse(TELEGRAM_V5)\n\n self.assertEqual(telegram[obis_references.P1_MESSAGE_HEADER].value, '50')\n", + "review": "**7**\n\n### Bugs\n- No specific bugs found in the code.\n\n### Optimizations\n- Consider replacing `eval()` with a safer alternative to access object attributes to avoid potential security issues.\n- Instead of using manual assertions, consider using unittest's built-in methods such as `assertIsInstance()`, `assertEqual()`, etc., which provide better readability and error messages.\n\n### Good points\n- Extensive use of unit tests covering various scenarios to ensure the correctness of the parser.\n- Use of helper function `verify_telegram_item` reduces code duplication and enhances readability.\n- The code follows a consistent pattern in its testing methods, making it easy to follow.\n- The use of descriptive variable names aids in understanding the purpose of the tests.\n- The tests ensure that the data is correctly parsed and transformed into the expected output formats, including JSON and string representations.\n\n### Summary\nThe code presented is a test suite for a DSMR (Dutch Smart Meter Requirements) telegram parser. The suite thoroughly tests the parsing of V4 and V5 telegram data into expected object structures, values, and types. It covers a wide range of test cases to ensure the parser accurately processes DSMR telegrams into both object attributes and JSON outputs. Although the test suite is well-structured and effective, using `eval()` can pose a security risk, and employing unittest's built-in assertion methods could improve the code's clarity and maintainability.\n\n### Open source alternatives\n- **PyDSMR**: An open-source library to parse DSMR data, focusing on extensibility and Pythonic conventions.\n- **dsmr-parser**: A simple DSMR parser library for extracting information from DSMR telegrams.", + "filename": "test_telegram.py", + "path": "test/objects/test_telegram.py", + "directory": "objects", + "grade": 7, + "size": 23715, + "line_count": 487 +} \ No newline at end of file diff --git a/reviews/test/objects/test_telegram.py.md b/reviews/test/objects/test_telegram.py.md new file mode 100644 index 0000000..86b1a22 --- /dev/null +++ b/reviews/test/objects/test_telegram.py.md @@ -0,0 +1,22 @@ +**7** + +### Bugs +- No specific bugs found in the code. + +### Optimizations +- Consider replacing `eval()` with a safer alternative to access object attributes to avoid potential security issues. +- Instead of using manual assertions, consider using unittest's built-in methods such as `assertIsInstance()`, `assertEqual()`, etc., which provide better readability and error messages. + +### Good points +- Extensive use of unit tests covering various scenarios to ensure the correctness of the parser. +- Use of helper function `verify_telegram_item` reduces code duplication and enhances readability. +- The code follows a consistent pattern in its testing methods, making it easy to follow. +- The use of descriptive variable names aids in understanding the purpose of the tests. +- The tests ensure that the data is correctly parsed and transformed into the expected output formats, including JSON and string representations. + +### Summary +The code presented is a test suite for a DSMR (Dutch Smart Meter Requirements) telegram parser. The suite thoroughly tests the parsing of V4 and V5 telegram data into expected object structures, values, and types. It covers a wide range of test cases to ensure the parser accurately processes DSMR telegrams into both object attributes and JSON outputs. Although the test suite is well-structured and effective, using `eval()` can pose a security risk, and employing unittest's built-in assertion methods could improve the code's clarity and maintainability. + +### Open source alternatives +- **PyDSMR**: An open-source library to parse DSMR data, focusing on extensibility and Pythonic conventions. +- **dsmr-parser**: A simple DSMR parser library for extracting information from DSMR telegrams. \ No newline at end of file diff --git a/reviews/test/test_filereader.py.json b/reviews/test/test_filereader.py.json new file mode 100644 index 0000000..66dfb3f --- /dev/null +++ b/reviews/test/test_filereader.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import unittest\nimport tempfile\n\nfrom dsmr_parser.clients.filereader import FileReader\nfrom dsmr_parser.telegram_specifications import V5\nfrom test.example_telegrams import TELEGRAM_V5\n\n\nclass FileReaderTest(unittest.TestCase):\n def test_read_as_object(self):\n with tempfile.NamedTemporaryFile() as file:\n with open(file.name, \"w\") as f:\n f.write(TELEGRAM_V5)\n\n telegrams = []\n reader = FileReader(file=file.name, telegram_specification=V5)\n # Call\n for telegram in reader.read_as_object():\n telegrams.append(telegram)\n\n self.assertEqual(len(telegrams), 1)\n", + "review": "**7**\n\n### Bugs\n- There are no apparent bugs in this test code snippet.\n\n### Optimizations\n- Use `self.assertListEqual` to compare the expected telegram list with the actual telegrams for more descriptive error messages.\n- Consider initializing `reader` inside the `with` statement to ensure file availability.\n- The temporary file could be managed using a `contextmanager` to make the code cleaner.\n\n### Good Points\n- The use of `tempfile.NamedTemporaryFile` ensures that test files are handled securely.\n- Clear separation of setup, execution, and assertion phases in the `test_read_as_object` method.\n- Good use of context management to handle files, reducing the risk of unclosed file handles.\n\n### Summary\nThe code is a unit test implementation that checks the reading functionality of file-based data into telegram objects using a predefined specification (`V5`). The use of temporary files for testing data handling is appropriate, even though a few improvements in comparison methods and structure can make the test more robust and expressive.\n\n### Open source alternatives\n- **pySerial**: A Python library that can be used to interface with serial ports, commonly used for reading meter data.\n- **Grid2Op**: A library to simulate and control power grid operations.\n- **Pymodbus**: A fully implemented suite of communication tools to work with the Modbus protocol.", + "filename": "test_filereader.py", + "path": "test/test_filereader.py", + "directory": "test", + "grade": 7, + "size": 663, + "line_count": 22 +} \ No newline at end of file diff --git a/reviews/test/test_filereader.py.md b/reviews/test/test_filereader.py.md new file mode 100644 index 0000000..284de6c --- /dev/null +++ b/reviews/test/test_filereader.py.md @@ -0,0 +1,22 @@ +**7** + +### Bugs +- There are no apparent bugs in this test code snippet. + +### Optimizations +- Use `self.assertListEqual` to compare the expected telegram list with the actual telegrams for more descriptive error messages. +- Consider initializing `reader` inside the `with` statement to ensure file availability. +- The temporary file could be managed using a `contextmanager` to make the code cleaner. + +### Good Points +- The use of `tempfile.NamedTemporaryFile` ensures that test files are handled securely. +- Clear separation of setup, execution, and assertion phases in the `test_read_as_object` method. +- Good use of context management to handle files, reducing the risk of unclosed file handles. + +### Summary +The code is a unit test implementation that checks the reading functionality of file-based data into telegram objects using a predefined specification (`V5`). The use of temporary files for testing data handling is appropriate, even though a few improvements in comparison methods and structure can make the test more robust and expressive. + +### Open source alternatives +- **pySerial**: A Python library that can be used to interface with serial ports, commonly used for reading meter data. +- **Grid2Op**: A library to simulate and control power grid operations. +- **Pymodbus**: A fully implemented suite of communication tools to work with the Modbus protocol. \ No newline at end of file diff --git a/reviews/test/test_parse_fluvius.py.json b/reviews/test/test_parse_fluvius.py.json new file mode 100644 index 0000000..5b514f8 --- /dev/null +++ b/reviews/test/test_parse_fluvius.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from decimal import Decimal\n\nimport datetime\nimport json\nimport unittest\n\nimport pytz\n\nfrom dsmr_parser import telegram_specifications\nfrom dsmr_parser.exceptions import InvalidChecksumError, ParseError\nfrom dsmr_parser.objects import CosemObject, MBusObject, MBusObjectPeak\nfrom dsmr_parser.parsers import TelegramParser\nfrom test.example_telegrams import TELEGRAM_FLUVIUS_V171, TELEGRAM_FLUVIUS_V171_ALT\n\n\nclass TelegramParserFluviusTest(unittest.TestCase):\n \"\"\" Test parsing of a DSMR Fluvius telegram. \"\"\"\n\n def test_parse(self):\n parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS)\n try:\n result = parser.parse(TELEGRAM_FLUVIUS_V171, throw_ex=True)\n except Exception as ex:\n assert False, f\"parse trigged an exception {ex}\"\n\n # BELGIUM_VERSION_INFORMATION (0-0:96.1.4)\n assert isinstance(result.BELGIUM_VERSION_INFORMATION, CosemObject)\n assert result.BELGIUM_VERSION_INFORMATION.unit is None\n assert isinstance(result.BELGIUM_VERSION_INFORMATION.value, str)\n assert result.BELGIUM_VERSION_INFORMATION.value == '50217'\n\n # EQUIPMENT_IDENTIFIER (0-0:96.1.1)\n assert isinstance(result.BELGIUM_EQUIPMENT_IDENTIFIER, CosemObject)\n assert result.BELGIUM_EQUIPMENT_IDENTIFIER.unit is None\n assert isinstance(result.BELGIUM_EQUIPMENT_IDENTIFIER.value, str)\n assert result.BELGIUM_EQUIPMENT_IDENTIFIER.value == '3153414733313031303231363035'\n\n # P1_MESSAGE_TIMESTAMP (0-0:1.0.0)\n assert isinstance(result.P1_MESSAGE_TIMESTAMP, CosemObject)\n assert result.P1_MESSAGE_TIMESTAMP.unit is None\n assert isinstance(result.P1_MESSAGE_TIMESTAMP.value, datetime.datetime)\n assert result.P1_MESSAGE_TIMESTAMP.value == \\\n pytz.timezone(\"Europe/Brussels\").localize(datetime.datetime(2020, 5, 12, 13, 54, 9))\n\n # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)\n assert isinstance(result.ELECTRICITY_USED_TARIFF_1, CosemObject)\n assert result.ELECTRICITY_USED_TARIFF_1.unit == 'kWh'\n assert isinstance(result.ELECTRICITY_USED_TARIFF_1.value, Decimal)\n assert result.ELECTRICITY_USED_TARIFF_1.value == Decimal('0.034')\n\n # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)\n assert isinstance(result.ELECTRICITY_USED_TARIFF_2, CosemObject)\n assert result.ELECTRICITY_USED_TARIFF_2.unit == 'kWh'\n assert isinstance(result.ELECTRICITY_USED_TARIFF_2.value, Decimal)\n assert result.ELECTRICITY_USED_TARIFF_2.value == Decimal('15.758')\n\n # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)\n assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject)\n assert result.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh'\n assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal)\n assert result.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('0.000')\n\n # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)\n assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject)\n assert result.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh'\n assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal)\n assert result.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('0.011')\n\n # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)\n assert isinstance(result.ELECTRICITY_ACTIVE_TARIFF, CosemObject)\n assert result.ELECTRICITY_ACTIVE_TARIFF.unit is None\n assert isinstance(result.ELECTRICITY_ACTIVE_TARIFF.value, str)\n assert result.ELECTRICITY_ACTIVE_TARIFF.value == '0001'\n\n # BELGIUM_CURRENT_AVERAGE_DEMAND (1-0:1.4.0)\n assert isinstance(result.BELGIUM_CURRENT_AVERAGE_DEMAND, CosemObject)\n assert result.BELGIUM_CURRENT_AVERAGE_DEMAND.unit == 'kW'\n assert isinstance(result.BELGIUM_CURRENT_AVERAGE_DEMAND.value, Decimal)\n assert result.BELGIUM_CURRENT_AVERAGE_DEMAND.value == Decimal('2.351')\n\n # BELGIUM_MAXIMUM_DEMAND_MONTH (1-0:1.6.0)\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_MONTH, MBusObject)\n assert result.BELGIUM_MAXIMUM_DEMAND_MONTH.unit == 'kW'\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_MONTH.value, Decimal)\n assert result.BELGIUM_MAXIMUM_DEMAND_MONTH.value == Decimal('2.589')\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_MONTH.datetime, datetime.datetime)\n assert result.BELGIUM_MAXIMUM_DEMAND_MONTH.datetime == \\\n pytz.timezone(\"Europe/Brussels\").localize(datetime.datetime(2020, 5, 9, 13, 45, 58))\n\n # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 0\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0], MBusObjectPeak)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].unit == 'kW'\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].value, Decimal)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].value == Decimal('3.695')\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].datetime, datetime.datetime)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].datetime == \\\n pytz.timezone(\"Europe/Brussels\").localize(datetime.datetime(2020, 5, 1, 0, 0, 0))\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].occurred, datetime.datetime)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].occurred == \\\n pytz.timezone(\"Europe/Brussels\").localize(datetime.datetime(2020, 4, 23, 19, 25, 38))\n # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 1\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1], MBusObjectPeak)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].unit == 'kW'\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].value, Decimal)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].value == Decimal('5.980')\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].datetime, datetime.datetime)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].datetime == \\\n pytz.timezone(\"Europe/Brussels\").localize(datetime.datetime(2020, 4, 1, 0, 0, 0))\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].occurred, datetime.datetime)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].occurred == \\\n pytz.timezone(\"Europe/Brussels\").localize(datetime.datetime(2020, 3, 5, 12, 21, 39))\n # BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 2\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2], MBusObjectPeak)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].unit == 'kW'\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].value, Decimal)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].value == Decimal('4.318')\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].datetime, datetime.datetime)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].datetime == \\\n pytz.timezone(\"Europe/Brussels\").localize(datetime.datetime(2020, 3, 1, 0, 0, 0))\n assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].occurred, datetime.datetime)\n assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].occurred == \\\n pytz.timezone(\"Europe/Brussels\").localize(datetime.datetime(2020, 2, 10, 3, 54, 21))\n\n # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)\n assert isinstance(result.CURRENT_ELECTRICITY_USAGE, CosemObject)\n assert result.CURRENT_ELECTRICITY_USAGE.unit == 'kW'\n assert isinstance(result.CURRENT_ELECTRICITY_USAGE.value, Decimal)\n assert result.CURRENT_ELECTRICITY_USAGE.value == Decimal('0.000')\n\n # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)\n assert isinstance(result.CURRENT_ELECTRICITY_DELIVERY, CosemObject)\n assert result.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW'\n assert isinstance(result.CURRENT_ELECTRICITY_DELIVERY.value, Decimal)\n assert result.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('0.000')\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, CosemObject)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.unit == 'kW'\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value, Decimal)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value == Decimal('0.000')\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, CosemObject)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.unit == 'kW'\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value, Decimal)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value == Decimal('0.000')\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, CosemObject)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.unit == 'kW'\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value, Decimal)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value == Decimal('0.000')\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, CosemObject)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.unit == 'kW'\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value, Decimal)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value == Decimal('0.000')\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, CosemObject)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.unit == 'kW'\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value, Decimal)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value == Decimal('0.000')\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, CosemObject)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.unit == 'kW'\n assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value, Decimal)\n assert result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value == Decimal('0.000')\n\n # INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)\n assert isinstance(result.INSTANTANEOUS_VOLTAGE_L1, CosemObject)\n assert result.INSTANTANEOUS_VOLTAGE_L1.unit == 'V'\n assert isinstance(result.INSTANTANEOUS_VOLTAGE_L1.value, Decimal)\n assert result.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('234.7')\n\n # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)\n assert isinstance(result.INSTANTANEOUS_VOLTAGE_L2, CosemObject)\n assert result.INSTANTANEOUS_VOLTAGE_L2.unit == 'V'\n assert isinstance(result.INSTANTANEOUS_VOLTAGE_L2.value, Decimal)\n assert result.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('234.7')\n\n # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)\n assert isinstance(result.INSTANTANEOUS_VOLTAGE_L3, CosemObject)\n assert result.INSTANTANEOUS_VOLTAGE_L3.unit == 'V'\n assert isinstance(result.INSTANTANEOUS_VOLTAGE_L3.value, Decimal)\n assert result.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('234.7')\n\n # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)\n assert isinstance(result.INSTANTANEOUS_CURRENT_L1, CosemObject)\n assert result.INSTANTANEOUS_CURRENT_L1.unit == 'A'\n assert isinstance(result.INSTANTANEOUS_CURRENT_L1.value, Decimal)\n assert result.INSTANTANEOUS_CURRENT_L1.value == Decimal('0.000')\n\n # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)\n assert isinstance(result.INSTANTANEOUS_CURRENT_L2, CosemObject)\n assert result.INSTANTANEOUS_CURRENT_L2.unit == 'A'\n assert isinstance(result.INSTANTANEOUS_CURRENT_L2.value, Decimal)\n assert result.INSTANTANEOUS_CURRENT_L2.value == Decimal('0.000')\n\n # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)\n assert isinstance(result.INSTANTANEOUS_CURRENT_L3, CosemObject)\n assert result.INSTANTANEOUS_CURRENT_L3.unit == 'A'\n assert isinstance(result.INSTANTANEOUS_CURRENT_L3.value, Decimal)\n assert result.INSTANTANEOUS_CURRENT_L3.value == Decimal('0.000')\n\n # ACTUAL_SWITCH_POSITION (0-0:96.3.10)\n assert isinstance(result.ACTUAL_SWITCH_POSITION, CosemObject)\n assert result.ACTUAL_SWITCH_POSITION.unit is None\n assert isinstance(result.ACTUAL_SWITCH_POSITION.value, int)\n assert result.ACTUAL_SWITCH_POSITION.value == 1\n\n # ACTUAL_TRESHOLD_ELECTRICITY (0-0:17.0.0)\n assert isinstance(result.ACTUAL_TRESHOLD_ELECTRICITY, CosemObject)\n assert result.ACTUAL_TRESHOLD_ELECTRICITY.unit == 'kW'\n assert isinstance(result.ACTUAL_TRESHOLD_ELECTRICITY.value, Decimal)\n assert result.ACTUAL_TRESHOLD_ELECTRICITY.value == Decimal('999.9')\n\n # FUSE_THRESHOLD_L1 (1-0:31.4.0)\n assert isinstance(result.FUSE_THRESHOLD_L1, CosemObject)\n assert result.FUSE_THRESHOLD_L1.unit == 'A'\n assert isinstance(result.FUSE_THRESHOLD_L1.value, Decimal)\n assert result.FUSE_THRESHOLD_L1.value == Decimal('999')\n\n # TEXT_MESSAGE (0-0:96.13.0)\n assert isinstance(result.TEXT_MESSAGE, CosemObject)\n assert result.TEXT_MESSAGE.unit is None\n assert result.TEXT_MESSAGE.value is None\n\n # MBUS DEVICE 1\n mbus1 = result.get_mbus_device_by_channel(1)\n\n # MBUS_DEVICE_TYPE (0-1:24.1.0)\n assert isinstance(mbus1.MBUS_DEVICE_TYPE, CosemObject)\n assert mbus1.MBUS_DEVICE_TYPE.unit is None\n assert isinstance(mbus1.MBUS_DEVICE_TYPE.value, int)\n assert mbus1.MBUS_DEVICE_TYPE.value == 3\n\n # MBUS_EQUIPMENT_IDENTIFIER (0-1:96.1.1)\n assert isinstance(mbus1.MBUS_EQUIPMENT_IDENTIFIER, CosemObject)\n assert mbus1.MBUS_EQUIPMENT_IDENTIFIER.unit is None\n assert isinstance(mbus1.MBUS_EQUIPMENT_IDENTIFIER.value, str)\n assert mbus1.MBUS_EQUIPMENT_IDENTIFIER.value == '37464C4F32313139303333373333'\n\n # MBUS_VALVE_POSITION (0-1:24.4.0)\n assert isinstance(result.MBUS_VALVE_POSITION, CosemObject)\n assert result.MBUS_VALVE_POSITION.unit is None\n assert isinstance(result.MBUS_VALVE_POSITION.value, int)\n assert result.MBUS_VALVE_POSITION.value == 1\n\n # MBUS_METER_READING (0-1:24.2.3)\n assert isinstance(mbus1.MBUS_METER_READING, MBusObject)\n assert mbus1.MBUS_METER_READING.unit == 'm3'\n assert isinstance(mbus1.MBUS_METER_READING.value, Decimal)\n assert mbus1.MBUS_METER_READING.value == Decimal('112.384')\n\n # MBUS DEVICE 2\n mbus2 = result.get_mbus_device_by_channel(2)\n\n # MBUS_DEVICE_TYPE (0-2:24.1.0)\n assert isinstance(mbus2.MBUS_DEVICE_TYPE, CosemObject)\n assert mbus2.MBUS_DEVICE_TYPE.unit is None\n assert isinstance(mbus2.MBUS_DEVICE_TYPE.value, int)\n assert mbus2.MBUS_DEVICE_TYPE.value == 7\n\n # MBUS_EQUIPMENT_IDENTIFIER (0-2:96.1.1)\n assert isinstance(mbus2.MBUS_EQUIPMENT_IDENTIFIER, CosemObject)\n assert mbus2.MBUS_EQUIPMENT_IDENTIFIER.unit is None\n assert isinstance(mbus2.MBUS_EQUIPMENT_IDENTIFIER.value, str)\n assert mbus2.MBUS_EQUIPMENT_IDENTIFIER.value == '3853414731323334353637383930'\n\n # MBUS_METER_READING (0-1:24.2.1)\n assert isinstance(mbus2.MBUS_METER_READING, MBusObject)\n assert mbus2.MBUS_METER_READING.unit == 'm3'\n assert isinstance(mbus2.MBUS_METER_READING.value, Decimal)\n assert mbus2.MBUS_METER_READING.value == Decimal('872.234')\n\n def test_checksum_valid(self):\n # No exception is raised.\n TelegramParser.validate_checksum(TELEGRAM_FLUVIUS_V171)\n\n def test_checksum_invalid(self):\n # Remove the electricty used data value. This causes the checksum to\n # not match anymore.\n corrupted_telegram = TELEGRAM_FLUVIUS_V171.replace(\n '1-0:1.8.1(000000.034*kWh)\\r\\n',\n ''\n )\n\n with self.assertRaises(InvalidChecksumError):\n TelegramParser.validate_checksum(corrupted_telegram)\n\n def test_checksum_missing(self):\n # Remove the checksum value causing a ParseError.\n corrupted_telegram = TELEGRAM_FLUVIUS_V171.replace('!3AD7\\r\\n', '')\n with self.assertRaises(ParseError):\n TelegramParser.validate_checksum(corrupted_telegram)\n\n def test_to_json(self):\n parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS)\n telegram = parser.parse(TELEGRAM_FLUVIUS_V171_ALT)\n json_data = json.loads(telegram.to_json())\n\n self.maxDiff = None\n\n self.assertEqual(\n json_data,\n {'BELGIUM_VERSION_INFORMATION': {'value': '50217', 'unit': None},\n 'BELGIUM_EQUIPMENT_IDENTIFIER': {'value': '3153414733313030373231333236', 'unit': None},\n 'P1_MESSAGE_TIMESTAMP': {'value': '2023-11-02T11:15:48+00:00', 'unit': None},\n 'ELECTRICITY_USED_TARIFF_1': {'value': 301.548, 'unit': 'kWh'},\n 'ELECTRICITY_USED_TARIFF_2': {'value': 270.014, 'unit': 'kWh'},\n 'ELECTRICITY_DELIVERED_TARIFF_1': {'value': 0.005, 'unit': 'kWh'},\n 'ELECTRICITY_DELIVERED_TARIFF_2': {'value': 0.0, 'unit': 'kWh'},\n 'ELECTRICITY_ACTIVE_TARIFF': {'value': '0001', 'unit': None},\n 'BELGIUM_CURRENT_AVERAGE_DEMAND': {'value': 0.052, 'unit': 'kW'},\n 'BELGIUM_MAXIMUM_DEMAND_MONTH': {'datetime': '2023-11-02T10:45:00+00:00',\n 'value': 3.064, 'unit': 'kW'},\n 'BELGIUM_MAXIMUM_DEMAND_13_MONTHS': [{'datetime': '2023-07-31T22:00:00+00:00',\n 'occurred': None, 'value': 0.0, 'unit': 'kW'},\n {'datetime': '2023-08-31T22:00:00+00:00',\n 'occurred': '2023-08-31T16:15:00+00:00',\n 'value': 1.862, 'unit': 'kW'},\n {'datetime': '2023-09-30T22:00:00+00:00',\n 'occurred': '2023-09-10T16:30:00+00:00',\n 'value': 4.229, 'unit': 'kW'},\n {'datetime': '2023-10-31T23:00:00+00:00',\n 'occurred': '2023-10-16T11:00:00+00:00',\n 'value': 4.927, 'unit': 'kW'}],\n 'CURRENT_ELECTRICITY_USAGE': {'value': 0.338, 'unit': 'kW'},\n 'CURRENT_ELECTRICITY_DELIVERY': {'value': 0.0, 'unit': 'kW'},\n 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE': {'value': 0.047, 'unit': 'kW'},\n 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE': {'value': 0.179, 'unit': 'kW'},\n 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE': {'value': 0.111, 'unit': 'kW'},\n 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE': {'value': 0.0, 'unit': 'kW'},\n 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE': {'value': 0.0, 'unit': 'kW'},\n 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE': {'value': 0.0, 'unit': 'kW'},\n 'INSTANTANEOUS_VOLTAGE_L1': {'value': 232.9, 'unit': 'V'},\n 'INSTANTANEOUS_VOLTAGE_L2': {'value': 228.1, 'unit': 'V'},\n 'INSTANTANEOUS_VOLTAGE_L3': {'value': 228.1, 'unit': 'V'},\n 'INSTANTANEOUS_CURRENT_L1': {'value': 0.27, 'unit': 'A'},\n 'INSTANTANEOUS_CURRENT_L2': {'value': 0.88, 'unit': 'A'},\n 'INSTANTANEOUS_CURRENT_L3': {'value': 0.52, 'unit': 'A'},\n 'ACTUAL_SWITCH_POSITION': {'value': 1, 'unit': None},\n 'ACTUAL_TRESHOLD_ELECTRICITY': {'value': 999.9, 'unit': 'kW'},\n 'FUSE_THRESHOLD_L1': {'value': 999.0, 'unit': 'A'},\n 'TEXT_MESSAGE': {'value': None, 'unit': None},\n 'MBUS_DEVICES': [{'MBUS_DEVICE_TYPE': {'value': 3, 'unit': None},\n 'MBUS_EQUIPMENT_IDENTIFIER': {'value': '37464C4F32313233303838303237',\n 'unit': None},\n 'MBUS_VALVE_POSITION': {'value': 1, 'unit': None},\n 'MBUS_METER_READING': {'datetime': '2023-11-02T11:10:02+00:00',\n 'value': 92.287, 'unit': 'm3'},\n 'CHANNEL_ID': 1},\n {'MBUS_DEVICE_TYPE': {'value': 7, 'unit': None},\n 'MBUS_EQUIPMENT_IDENTIFIER': {'value': '3853455430303030393631313733',\n 'unit': None},\n 'MBUS_METER_READING': {'datetime': '2023-11-02T11:15:32+00:00',\n 'value': 8.579, 'unit': 'm3'},\n 'CHANNEL_ID': 2}]}\n )\n\n def test_to_str(self):\n parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS)\n telegram = parser.parse(TELEGRAM_FLUVIUS_V171_ALT)\n\n self.assertEqual(\n str(telegram),\n (\n 'BELGIUM_VERSION_INFORMATION: \t 50217\t[None]\\n'\n 'BELGIUM_EQUIPMENT_IDENTIFIER: \t 3153414733313030373231333236\t[None]\\n'\n 'P1_MESSAGE_TIMESTAMP: \t 2023-11-02T11:15:48+00:00\t[None]\\n'\n 'ELECTRICITY_USED_TARIFF_1: \t 301.548\t[kWh]\\n'\n 'ELECTRICITY_USED_TARIFF_2: \t 270.014\t[kWh]\\n'\n 'ELECTRICITY_DELIVERED_TARIFF_1: \t 0.005\t[kWh]\\n'\n 'ELECTRICITY_DELIVERED_TARIFF_2: \t 0.000\t[kWh]\\n'\n 'ELECTRICITY_ACTIVE_TARIFF: \t 0001\t[None]\\n'\n 'BELGIUM_CURRENT_AVERAGE_DEMAND: \t 0.052\t[kW]\\n'\n 'BELGIUM_MAXIMUM_DEMAND_MONTH: \t 3.064\t[kW] at 2023-11-02T10:45:00+00:00\\n'\n '0.0\t[kW] at 2023-07-31T22:00:00+00:00 occurred None'\n '1.862\t[kW] at 2023-08-31T22:00:00+00:00 occurred 2023-08-31T16:15:00+00:00'\n '4.229\t[kW] at 2023-09-30T22:00:00+00:00 occurred 2023-09-10T16:30:00+00:00'\n '4.927\t[kW] at 2023-10-31T23:00:00+00:00 occurred 2023-10-16T11:00:00+00:00'\n 'CURRENT_ELECTRICITY_USAGE: \t 0.338\t[kW]\\n'\n 'CURRENT_ELECTRICITY_DELIVERY: \t 0.000\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: \t 0.047\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: \t 0.179\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: \t 0.111\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: \t 0.000\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: \t 0.000\t[kW]\\n'\n 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: \t 0.000\t[kW]\\n'\n 'INSTANTANEOUS_VOLTAGE_L1: \t 232.9\t[V]\\n'\n 'INSTANTANEOUS_VOLTAGE_L2: \t 228.1\t[V]\\n'\n 'INSTANTANEOUS_VOLTAGE_L3: \t 228.1\t[V]\\n'\n 'INSTANTANEOUS_CURRENT_L1: \t 0.27\t[A]\\n'\n 'INSTANTANEOUS_CURRENT_L2: \t 0.88\t[A]\\n'\n 'INSTANTANEOUS_CURRENT_L3: \t 0.52\t[A]\\n'\n 'ACTUAL_SWITCH_POSITION: \t 1\t[None]\\n'\n 'ACTUAL_TRESHOLD_ELECTRICITY: \t 999.9\t[kW]\\n'\n 'FUSE_THRESHOLD_L1: \t 999\t[A]\\n'\n 'TEXT_MESSAGE: \t None\t[None]\\n'\n 'MBUS DEVICE (channel 1)\\n'\n '\tMBUS_DEVICE_TYPE: \t 3\t[None]\\n'\n '\tMBUS_EQUIPMENT_IDENTIFIER: \t 37464C4F32313233303838303237\t[None]\\n'\n '\tMBUS_VALVE_POSITION: \t 1\t[None]\\n'\n '\tMBUS_METER_READING: \t 92.287\t[m3] at 2023-11-02T11:10:02+00:00\\n'\n 'MBUS DEVICE (channel 2)\\n'\n '\tMBUS_DEVICE_TYPE: \t 7\t[None]\\n'\n '\tMBUS_EQUIPMENT_IDENTIFIER: \t 3853455430303030393631313733\t[None]\\n'\n '\tMBUS_METER_READING: \t 8.579\t[m3] at 2023-11-02T11:15:32+00:00\\n'\n )\n )\n", + "review": "**Grade: 8**\n\n### Bugs\n- None observed in this segment.\n\n### Optimizations\n- Rather than numerous inline assertions, consider using table-driven tests to streamline data processing and verification.\n- Consider creating separate validation methods for common validation steps to DRY (Don't Repeat Yourself) the code.\n- Utilize `unittest.TestCase` methods like `self.assertIsInstance`, `self.assertEqual` for more readable and better error-reported assertions.\n \n### Good Points\n- Comprehensive test coverage for multiple aspects of parsing a DSMR Fluvius telegram.\n- Any raised exception in `test_parse` is caught, ensuring graceful error handling.\n- Utilizes the `unittest` framework for structured and systematic testing.\n- Tests multiple lines of a telegram, providing confidence in broader coverage and reliability of parsing implementation.\n\n### Summary\nThe provided code efficiently validates and parses DSMR Fluvius telegrams, with robust unit tests that cover numerous aspects and edge cases of the parsing process. The tests ensure that the various components of the telegram are accurately parsed and checked against expected values. Opportunities for improvement include reducing code repetition and utilizing Python's unittest asserts more thoroughly for readability and maintainability improvements.\n\n### Open Source Alternatives\n- **Home Assistant DSMR integration**: An integration that reads out DSMR telegram data as sensors in the Home Assistant ecosystem.\n- **DSMR Reader**: An application to display and graph data from the Dutch Smart Meter Requirement (DSMR) in a user-friendly manner.\n- **datalogger**: A Python library to read data from different types of smart meters and log them.", + "filename": "test_parse_fluvius.py", + "path": "test/test_parse_fluvius.py", + "directory": "test", + "grade": 8, + "size": 24155, + "line_count": 414 +} \ No newline at end of file diff --git a/reviews/test/test_parse_fluvius.py.md b/reviews/test/test_parse_fluvius.py.md new file mode 100644 index 0000000..e2ab318 --- /dev/null +++ b/reviews/test/test_parse_fluvius.py.md @@ -0,0 +1,23 @@ +**Grade: 8** + +### Bugs +- None observed in this segment. + +### Optimizations +- Rather than numerous inline assertions, consider using table-driven tests to streamline data processing and verification. +- Consider creating separate validation methods for common validation steps to DRY (Don't Repeat Yourself) the code. +- Utilize `unittest.TestCase` methods like `self.assertIsInstance`, `self.assertEqual` for more readable and better error-reported assertions. + +### Good Points +- Comprehensive test coverage for multiple aspects of parsing a DSMR Fluvius telegram. +- Any raised exception in `test_parse` is caught, ensuring graceful error handling. +- Utilizes the `unittest` framework for structured and systematic testing. +- Tests multiple lines of a telegram, providing confidence in broader coverage and reliability of parsing implementation. + +### Summary +The provided code efficiently validates and parses DSMR Fluvius telegrams, with robust unit tests that cover numerous aspects and edge cases of the parsing process. The tests ensure that the various components of the telegram are accurately parsed and checked against expected values. Opportunities for improvement include reducing code repetition and utilizing Python's unittest asserts more thoroughly for readability and maintainability improvements. + +### Open Source Alternatives +- **Home Assistant DSMR integration**: An integration that reads out DSMR telegram data as sensors in the Home Assistant ecosystem. +- **DSMR Reader**: An application to display and graph data from the Dutch Smart Meter Requirement (DSMR) in a user-friendly manner. +- **datalogger**: A Python library to read data from different types of smart meters and log them. \ No newline at end of file diff --git a/reviews/test/test_parse_iskra_ie.py.json b/reviews/test/test_parse_iskra_ie.py.json new file mode 100644 index 0000000..1e1fc5b --- /dev/null +++ b/reviews/test/test_parse_iskra_ie.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import unittest\n\nfrom decimal import Decimal\n\nfrom dsmr_parser.exceptions import InvalidChecksumError, ParseError\nfrom dsmr_parser.objects import CosemObject\nfrom dsmr_parser.parsers import TelegramParser\nfrom dsmr_parser import telegram_specifications\nfrom dsmr_parser import obis_references as obis\nfrom test.example_telegrams import TELEGRAM_ISKRA_IE\n\n\nclass TelegramParserIskraIETest(unittest.TestCase):\n \"\"\" Test parsing of a Iskra IE5 telegram. \"\"\"\n\n def test_parse(self):\n parser = TelegramParser(telegram_specifications.ISKRA_IE)\n try:\n result = parser.parse(TELEGRAM_ISKRA_IE, throw_ex=True)\n except Exception as ex:\n assert False, f\"parse trigged an exception {ex}\"\n\n # EQUIPMENT_IDENTIFIER_GAS (0-0:96.1.0)\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject)\n assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str)\n assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '09610'\n\n # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)\n assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal)\n assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('10.181')\n\n # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject)\n assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal)\n assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('10.182')\n\n # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('10.281')\n\n # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('10.282')\n\n # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)\n assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject)\n assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None\n assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str)\n assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0001'\n\n # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)\n assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject)\n assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'\n assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal)\n assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.170')\n\n # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)\n assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject)\n assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW'\n assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)\n assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0.270')\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.217')\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.417')\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.617')\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0.227')\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0.427')\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0.627')\n\n # INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject)\n assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V'\n assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal)\n assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('242.5')\n\n # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject)\n assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V'\n assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal)\n assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('241.7')\n\n # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject)\n assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V'\n assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal)\n assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('243.3')\n\n # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)\n assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)\n assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.000')\n\n # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)\n assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)\n assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.000')\n\n # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)\n assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)\n assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.000')\n\n # ACTUAL_SWITCH_POSITION (0-0:96.3.10)\n assert isinstance(result[obis.ACTUAL_SWITCH_POSITION], CosemObject)\n assert result[obis.ACTUAL_SWITCH_POSITION].unit is None\n assert isinstance(result[obis.ACTUAL_SWITCH_POSITION].value, str)\n assert result[obis.ACTUAL_SWITCH_POSITION].value == '1'\n\n # TEXT_MESSAGE (0-0:96.13.0)\n assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)\n assert result[obis.TEXT_MESSAGE].unit is None\n assert result[obis.TEXT_MESSAGE].value is None\n\n # EQUIPMENT_IDENTIFIER (0-0:96.1.1)\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject)\n assert result[obis.EQUIPMENT_IDENTIFIER].unit is None\n assert result[obis.EQUIPMENT_IDENTIFIER].value is None\n\n def test_checksum_valid(self):\n # No exception is raised.\n TelegramParser.validate_checksum(TELEGRAM_ISKRA_IE)\n\n def test_checksum_invalid(self):\n # Remove the electricty used data value. This causes the checksum to not match anymore.\n corrupted_telegram = TELEGRAM_ISKRA_IE.replace(\n '1-0:1.8.1(000010.181*kWh)\\r\\n',\n ''\n )\n\n with self.assertRaises(InvalidChecksumError):\n TelegramParser.validate_checksum(corrupted_telegram)\n\n def test_checksum_missing(self):\n # Remove the checksum value causing a ParseError.\n corrupted_telegram = TELEGRAM_ISKRA_IE.replace('!AD3B\\r\\n', '')\n with self.assertRaises(ParseError):\n TelegramParser.validate_checksum(corrupted_telegram)\n", + "review": "# 8\n\n## Bugs\n- No significant bugs identified in the test code.\n\n## Optimizations\n- Use `self.assertIsInstance` and `self.assertEqual` instead of `assert` statements for better test framework integration.\n- Consider using `setUp` method to initialize common variables to reduce repetition.\n- Group similar assertions together for better readability and performance where possible.\n\n## Good points\n- The code is well-structured and highly readable.\n- Comprehensive test coverage for various scenarios including valid parsing and checksum cases as well as invalid ones.\n- Use of specific exceptions (`InvalidChecksumError`, `ParseError`) enhances clarity and debugging.\n- Efficient use of the `unittest` framework for structured testing.\n\n## Summary\nThe code is a well-developed test suite for the `TelegramParser` class related to parsing Iskra IE5 telegrams. It effectively checks both normal and edge cases, including parsing and validation operations. Optimization could be done by using more features from the `unittest` module to standardize and simplify assertion checks, thereby improving maintainability.\n\n## Open source alternatives\n- **PyDSM**: A Python library providing an interface for reading DSMR telegrams, capturing similar functionality.\n- **dsmr-parser-python**: Another Python parser specifically designed for DSMR telegrams, very similar to the approach used here.", + "filename": "test_parse_iskra_ie.py", + "path": "test/test_parse_iskra_ie.py", + "directory": "test", + "grade": 8, + "size": 9825, + "line_count": 178 +} \ No newline at end of file diff --git a/reviews/test/test_parse_iskra_ie.py.md b/reviews/test/test_parse_iskra_ie.py.md new file mode 100644 index 0000000..ac1a88b --- /dev/null +++ b/reviews/test/test_parse_iskra_ie.py.md @@ -0,0 +1,22 @@ +# 8 + +## Bugs +- No significant bugs identified in the test code. + +## Optimizations +- Use `self.assertIsInstance` and `self.assertEqual` instead of `assert` statements for better test framework integration. +- Consider using `setUp` method to initialize common variables to reduce repetition. +- Group similar assertions together for better readability and performance where possible. + +## Good points +- The code is well-structured and highly readable. +- Comprehensive test coverage for various scenarios including valid parsing and checksum cases as well as invalid ones. +- Use of specific exceptions (`InvalidChecksumError`, `ParseError`) enhances clarity and debugging. +- Efficient use of the `unittest` framework for structured testing. + +## Summary +The code is a well-developed test suite for the `TelegramParser` class related to parsing Iskra IE5 telegrams. It effectively checks both normal and edge cases, including parsing and validation operations. Optimization could be done by using more features from the `unittest` module to standardize and simplify assertion checks, thereby improving maintainability. + +## Open source alternatives +- **PyDSM**: A Python library providing an interface for reading DSMR telegrams, capturing similar functionality. +- **dsmr-parser-python**: Another Python parser specifically designed for DSMR telegrams, very similar to the approach used here. \ No newline at end of file diff --git a/reviews/test/test_parse_sagemcom_t210_d_r.py.json b/reviews/test/test_parse_sagemcom_t210_d_r.py.json new file mode 100644 index 0000000..de53f44 --- /dev/null +++ b/reviews/test/test_parse_sagemcom_t210_d_r.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from binascii import unhexlify\nfrom copy import deepcopy\n\nimport unittest\n\nfrom dlms_cosem.exceptions import DecryptionError\nfrom dlms_cosem.protocol.xdlms import GeneralGlobalCipher\nfrom dlms_cosem.security import SecurityControlField, encrypt\n\nfrom dsmr_parser import telegram_specifications\nfrom dsmr_parser.exceptions import ParseError\nfrom dsmr_parser.parsers import TelegramParser\nfrom test.example_telegrams import TELEGRAM_SAGEMCOM_T210_D_R\n\n\nclass TelegramParserEncryptedTest(unittest.TestCase):\n \"\"\" Test parsing of a DSML encypted DSMR v5.x telegram. \"\"\"\n DUMMY_ENCRYPTION_KEY = \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n DUMMY_AUTHENTICATION_KEY = \"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\"\n\n def __generate_encrypted(self, security_suite=0, authenticated=True, encrypted=True):\n security_control = SecurityControlField(\n security_suite=security_suite, authenticated=authenticated, encrypted=encrypted\n )\n encryption_key = unhexlify(self.DUMMY_ENCRYPTION_KEY)\n authentication_key = unhexlify(self.DUMMY_AUTHENTICATION_KEY)\n system_title = \"SYSTEMID\".encode(\"ascii\")\n invocation_counter = int.from_bytes(bytes.fromhex(\"10000001\"), \"big\")\n plain_data = TELEGRAM_SAGEMCOM_T210_D_R.encode(\"ascii\")\n\n encrypted = encrypt(\n security_control=security_control,\n key=encryption_key,\n auth_key=authentication_key,\n system_title=system_title,\n invocation_counter=invocation_counter,\n plain_text=plain_data,\n )\n\n full_frame = bytearray(GeneralGlobalCipher.TAG.to_bytes(1, \"big\", signed=False))\n full_frame.extend(len(system_title).to_bytes(1, \"big\", signed=False))\n full_frame.extend(system_title)\n full_frame.extend([0x82]) # Length of the following length bytes\n # https://github.com/pwitab/dlms-cosem/blob/739f81a58e5f07663a512d4a128851333a0ed5e6/dlms_cosem/a_xdr.py#L33\n\n security_control = security_control.to_bytes()\n invocation_counter = invocation_counter.to_bytes(4, \"big\", signed=False)\n full_frame.extend((len(encrypted)\n + len(invocation_counter)\n + len(security_control)).to_bytes(2, \"big\", signed=False))\n full_frame.extend(security_control)\n full_frame.extend(invocation_counter)\n full_frame.extend(encrypted)\n\n return full_frame\n\n def test_parse(self):\n parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)\n result = parser.parse(self.__generate_encrypted().hex(),\n self.DUMMY_ENCRYPTION_KEY,\n self.DUMMY_AUTHENTICATION_KEY)\n self.assertEqual(len(result), 18)\n\n def test_damaged_frame(self):\n # If the frame is damaged decrypting fails (crc is technically not needed)\n parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)\n\n generated = self.__generate_encrypted()\n generated[150] = 0x00\n generated = generated.hex()\n\n with self.assertRaises(DecryptionError):\n parser.parse(generated, self.DUMMY_ENCRYPTION_KEY, self.DUMMY_AUTHENTICATION_KEY)\n\n def test_plain(self):\n # If a plain request is parsed with \"general_global_cipher\": True it fails\n parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)\n\n with self.assertRaises(Exception):\n parser.parse(TELEGRAM_SAGEMCOM_T210_D_R, self.DUMMY_ENCRYPTION_KEY, self.DUMMY_AUTHENTICATION_KEY)\n\n def test_general_global_cipher_not_specified(self):\n # If a GGC frame is detected but general_global_cipher is not set it fails\n parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)\n parser = deepcopy(parser) # We do not want to change the module value\n parser.telegram_specification['general_global_cipher'] = False\n\n with self.assertRaises(ParseError):\n parser.parse(self.__generate_encrypted().hex(), self.DUMMY_ENCRYPTION_KEY, self.DUMMY_AUTHENTICATION_KEY)\n\n def test_only_encrypted(self):\n # Not implemented by dlms_cosem\n parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)\n\n only_auth = self.__generate_encrypted(0, authenticated=False, encrypted=True).hex()\n\n with self.assertRaises(ValueError):\n parser.parse(only_auth, self.DUMMY_ENCRYPTION_KEY)\n\n def test_only_auth(self):\n # Not implemented by dlms_cosem\n parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)\n\n only_auth = self.__generate_encrypted(0, authenticated=True, encrypted=False).hex()\n\n with self.assertRaises(ValueError):\n parser.parse(only_auth, authentication_key=self.DUMMY_AUTHENTICATION_KEY)\n", + "review": "# 7\n\n## Bugs\n- The test case method `test_only_encrypted` calls `parser.parse` without the required `self.DUMMY_AUTHENTICATION_KEY`, which leads to a `TypeError` due to a missing argument.\n- The method `__generate_encrypted` does not correctly handle dynamic lengths for the generated frame; this might lead to misinterpretations with varying-sized inputs.\n\n## Optimizations\n- Parameterize the test cases to reduce code redundancy, particularly those with similar setup and usage patterns.\n- Improve name clarity for variables and methods for better readability, like renaming `__generate_encrypted` to `generate_encrypted_frame`.\n- Use constants for repeated values (e.g., magic numbers) to provide context within the code.\n- Utilize Python's logging to capture more detailed runtime information during test failures for easier diagnosis.\n- Ensure tests are independent and reusable by abstracting common setup processes into utility functions or fixtures.\n\n## Good points\n- Test classes use meaningful docstrings that describe their purpose.\n- Thorough testing of different scenarios, including edge cases like damaged frames and only encrypted/authenticated frames, is present.\n- The code utilizes exception handling properly, asserting that expected errors are raised under specific faulty conditions.\n\n## Summary\nThe code provides a test suite for parsing encrypted DSMR v5.x telegrams with varied test conditions. While generally well-structured, there is room for improvement in terms of parameterization and error handling. The few existing bugs may affect the robustness of the test suite. Enhanced logging and variable naming could further improve code readability and maintainability.\n\n## Open source alternatives\n- **pyDSMR**: A Python library to analyze data captured from Dutch Smart Meters (DSMR). Provides parsing functionalities similar to what is attempted in the current code.\n- **pymeterreader**: Another library that supports reading and parsing meter data from DSMR devices and handles encrypted telegrams.", + "filename": "test_parse_sagemcom_t210_d_r.py", + "path": "test/test_parse_sagemcom_t210_d_r.py", + "directory": "test", + "grade": 7, + "size": 4788, + "line_count": 108 +} \ No newline at end of file diff --git a/reviews/test/test_parse_sagemcom_t210_d_r.py.md b/reviews/test/test_parse_sagemcom_t210_d_r.py.md new file mode 100644 index 0000000..60de796 --- /dev/null +++ b/reviews/test/test_parse_sagemcom_t210_d_r.py.md @@ -0,0 +1,24 @@ +# 7 + +## Bugs +- The test case method `test_only_encrypted` calls `parser.parse` without the required `self.DUMMY_AUTHENTICATION_KEY`, which leads to a `TypeError` due to a missing argument. +- The method `__generate_encrypted` does not correctly handle dynamic lengths for the generated frame; this might lead to misinterpretations with varying-sized inputs. + +## Optimizations +- Parameterize the test cases to reduce code redundancy, particularly those with similar setup and usage patterns. +- Improve name clarity for variables and methods for better readability, like renaming `__generate_encrypted` to `generate_encrypted_frame`. +- Use constants for repeated values (e.g., magic numbers) to provide context within the code. +- Utilize Python's logging to capture more detailed runtime information during test failures for easier diagnosis. +- Ensure tests are independent and reusable by abstracting common setup processes into utility functions or fixtures. + +## Good points +- Test classes use meaningful docstrings that describe their purpose. +- Thorough testing of different scenarios, including edge cases like damaged frames and only encrypted/authenticated frames, is present. +- The code utilizes exception handling properly, asserting that expected errors are raised under specific faulty conditions. + +## Summary +The code provides a test suite for parsing encrypted DSMR v5.x telegrams with varied test conditions. While generally well-structured, there is room for improvement in terms of parameterization and error handling. The few existing bugs may affect the robustness of the test suite. Enhanced logging and variable naming could further improve code readability and maintainability. + +## Open source alternatives +- **pyDSMR**: A Python library to analyze data captured from Dutch Smart Meters (DSMR). Provides parsing functionalities similar to what is attempted in the current code. +- **pymeterreader**: Another library that supports reading and parsing meter data from DSMR devices and handles encrypted telegrams. \ No newline at end of file diff --git a/reviews/test/test_parse_v2_2.py.json b/reviews/test/test_parse_v2_2.py.json new file mode 100644 index 0000000..ecf6dd5 --- /dev/null +++ b/reviews/test/test_parse_v2_2.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import unittest\n\nfrom decimal import Decimal\n\nfrom dsmr_parser.objects import MBusObject, CosemObject\nfrom dsmr_parser.parsers import TelegramParser\nfrom dsmr_parser import telegram_specifications\nfrom dsmr_parser import obis_references as obis\nfrom test.example_telegrams import TELEGRAM_V2_2\n\n\nclass TelegramParserV2_2Test(unittest.TestCase):\n \"\"\" Test parsing of a DSMR v2.2 telegram. \"\"\"\n\n def test_parse(self):\n parser = TelegramParser(telegram_specifications.V2_2)\n try:\n result = parser.parse(TELEGRAM_V2_2, throw_ex=True)\n except Exception as ex:\n assert False, f\"parse trigged an exception {ex}\"\n\n # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)\n assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal)\n assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('1.001')\n\n # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject)\n assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal)\n assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('1.001')\n\n # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('1.001')\n\n # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('1.001')\n\n # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)\n assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject)\n assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None\n assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str)\n assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0001'\n\n # EQUIPMENT_IDENTIFIER (0-0:96.1.1)\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject)\n assert result[obis.EQUIPMENT_IDENTIFIER].unit is None\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str)\n assert result[obis.EQUIPMENT_IDENTIFIER].value == '00000000000000'\n\n # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)\n assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject)\n assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'\n assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal)\n assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('1.01')\n\n # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)\n assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject)\n assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW'\n assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)\n assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0')\n\n # TEXT_MESSAGE_CODE (0-0:96.13.1)\n assert isinstance(result[obis.TEXT_MESSAGE_CODE], CosemObject)\n assert result[obis.TEXT_MESSAGE_CODE].unit is None\n\n # TEXT_MESSAGE (0-0:96.13.0)\n assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)\n assert result[obis.TEXT_MESSAGE].unit is None\n assert result[obis.TEXT_MESSAGE].value is None\n\n # DEVICE_TYPE (0-x:24.1.0)\n assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)\n assert result[obis.DEVICE_TYPE].unit is None\n assert isinstance(result[obis.DEVICE_TYPE].value, str)\n assert result[obis.DEVICE_TYPE].value == '3'\n\n # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject)\n assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str)\n assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '000000000000'\n\n # GAS_METER_READING (0-1:24.3.0)\n assert isinstance(result[obis.GAS_METER_READING], MBusObject)\n assert result[obis.GAS_METER_READING].unit == 'm3'\n assert isinstance(result[obis.GAS_METER_READING].value, Decimal)\n assert result[obis.GAS_METER_READING].value == Decimal('1.001')\n", + "review": "**Grade: 8**\n\n## Bugs\n- No apparent bugs were found in the provided code.\n\n## Optimizations\n- Use `self.assertEqual()` and similar functions from `unittest` instead of `assert` for better test case reporting and compatibility with test runners.\n- Avoid using camelCase for class names in Python unless following a specific naming convention for imports.\n\n## Good points\n- Comprehensive test coverage of various fields in the DSMR telegram.\n- Use of `Decimal` for precise fixed-point arithmetic, ensuring accuracy of test assertions.\n- Good organization and readability of test cases with clear checks for both values and units.\n\n## Summary\nThe code is a unit test for parsing DSMR v2.2 telegrams. It checks the parsing of numerous fields using the `unittest` framework, ensuring parsed values match expected data. Replacing `assert` statements with `unittest` assert methods would improve readability and compatibility with test suite tools. No bugs were identified, and the tests are well-structured for clarity.\n\n## Open source alternatives\n- [dsmr_parser](https://github.com/ndokter/dsmr_parser): The provided test cases seem to be a part of this library which handles parsing of DSMR telegrams, implying it might offer similar functionalities for DSMR telegram parsing.", + "filename": "test_parse_v2_2.py", + "path": "test/test_parse_v2_2.py", + "directory": "test", + "grade": 8, + "size": 4845, + "line_count": 96 +} \ No newline at end of file diff --git a/reviews/test/test_parse_v2_2.py.md b/reviews/test/test_parse_v2_2.py.md new file mode 100644 index 0000000..c098521 --- /dev/null +++ b/reviews/test/test_parse_v2_2.py.md @@ -0,0 +1,19 @@ +**Grade: 8** + +## Bugs +- No apparent bugs were found in the provided code. + +## Optimizations +- Use `self.assertEqual()` and similar functions from `unittest` instead of `assert` for better test case reporting and compatibility with test runners. +- Avoid using camelCase for class names in Python unless following a specific naming convention for imports. + +## Good points +- Comprehensive test coverage of various fields in the DSMR telegram. +- Use of `Decimal` for precise fixed-point arithmetic, ensuring accuracy of test assertions. +- Good organization and readability of test cases with clear checks for both values and units. + +## Summary +The code is a unit test for parsing DSMR v2.2 telegrams. It checks the parsing of numerous fields using the `unittest` framework, ensuring parsed values match expected data. Replacing `assert` statements with `unittest` assert methods would improve readability and compatibility with test suite tools. No bugs were identified, and the tests are well-structured for clarity. + +## Open source alternatives +- [dsmr_parser](https://github.com/ndokter/dsmr_parser): The provided test cases seem to be a part of this library which handles parsing of DSMR telegrams, implying it might offer similar functionalities for DSMR telegram parsing. \ No newline at end of file diff --git a/reviews/test/test_parse_v3.py.json b/reviews/test/test_parse_v3.py.json new file mode 100644 index 0000000..e1cfe5a --- /dev/null +++ b/reviews/test/test_parse_v3.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import unittest\n\nfrom decimal import Decimal\n\nfrom dsmr_parser.objects import CosemObject, MBusObject\nfrom dsmr_parser.parsers import TelegramParser\nfrom dsmr_parser import telegram_specifications\nfrom dsmr_parser import obis_references as obis\nfrom test.example_telegrams import TELEGRAM_V3\n\n\nclass TelegramParserV3Test(unittest.TestCase):\n \"\"\" Test parsing of a DSMR v3 telegram. \"\"\"\n\n def test_parse(self):\n parser = TelegramParser(telegram_specifications.V3)\n try:\n result = parser.parse(TELEGRAM_V3, throw_ex=True)\n except Exception as ex:\n assert False, f\"parse trigged an exception {ex}\"\n\n # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)\n assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal)\n assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('12345.678')\n\n # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject)\n assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal)\n assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('12345.678')\n\n # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('12345.678')\n\n # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('12345.678')\n\n # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)\n assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject)\n assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None\n assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str)\n assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0002'\n\n # EQUIPMENT_IDENTIFIER (0-0:96.1.1)\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject)\n assert result[obis.EQUIPMENT_IDENTIFIER].unit is None\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str)\n assert result[obis.EQUIPMENT_IDENTIFIER].value == '4B384547303034303436333935353037'\n\n # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)\n assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject)\n assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'\n assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal)\n assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('1.19')\n\n # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)\n assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject)\n assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW'\n assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)\n assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0')\n\n # TEXT_MESSAGE_CODE (0-0:96.13.1)\n assert isinstance(result[obis.TEXT_MESSAGE_CODE], CosemObject)\n assert result[obis.TEXT_MESSAGE_CODE].unit is None\n assert isinstance(result[obis.TEXT_MESSAGE_CODE].value, int)\n assert result[obis.TEXT_MESSAGE_CODE].value == 303132333435363738\n\n # TEXT_MESSAGE (0-0:96.13.0)\n assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)\n assert result[obis.TEXT_MESSAGE].unit is None\n assert isinstance(result[obis.TEXT_MESSAGE].value, str)\n assert result[obis.TEXT_MESSAGE].value == \\\n '303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F' \\\n '303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F' \\\n '303132333435363738393A3B3C3D3E3F'\n\n # DEVICE_TYPE (0-x:24.1.0)\n assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)\n assert result[obis.DEVICE_TYPE].unit is None\n assert isinstance(result[obis.DEVICE_TYPE].value, str)\n assert result[obis.DEVICE_TYPE].value == '03'\n\n # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject)\n assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str)\n assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '3232323241424344313233343536373839'\n\n # GAS_METER_READING (0-1:24.3.0)\n assert isinstance(result[obis.GAS_METER_READING], MBusObject)\n assert result[obis.GAS_METER_READING].unit == 'm3'\n assert isinstance(result[obis.GAS_METER_READING].value, Decimal)\n assert result[obis.GAS_METER_READING].value == Decimal('1.001')\n", + "review": "# 8\n\n## Bugs\n- None observed in the current code.\n\n## Optimizations\n- Use `self.assert*` methods from `unittest` instead of plain `assert` statements for consistency with the `unittest` framework.\n- Break down the test into smaller sub-tests using `subTest` for each section relating to different OBIS references. This improves maintainability and readability.\n- Consider using parameterized tests to reduce redundancy when asserting similar conditions across different OBIS references.\n\n## Good points\n- Comprehensive testing of various aspects of DSMR v3 telegram parsing, ensuring all critical data points are covered.\n- Use of exception handling to detect and report parsing errors during test execution.\n- Clear and logical organization of the tests, providing a good structure and flow.\n \n## Summary\nThe code provides a detailed unit test for parsing DSMR v3 telegrams, covering numerous OBIS references and possible values effectively. There are no evident bugs, but the use of the `unittest` framework could be optimized by replacing inline assertions with framework-specific methods for better integration and error reporting. The tests are well-structured and provide thorough coverage of the parsing functionality.\n\n## Open source alternatives\n- The `dmsr_parser` library seems like a specific utility for DSMR telegram parsing, and there might not be direct open-source alternatives that perform the exact function unless they're part of broader smart meter or energy management suites. Some commonly used data parsing libraries like `pandas` could potentially be adapted for similar purposes with custom function implementations.", + "filename": "test_parse_v3.py", + "path": "test/test_parse_v3.py", + "directory": "test", + "grade": 8, + "size": 5305, + "line_count": 102 +} \ No newline at end of file diff --git a/reviews/test/test_parse_v3.py.md b/reviews/test/test_parse_v3.py.md new file mode 100644 index 0000000..bfb69b9 --- /dev/null +++ b/reviews/test/test_parse_v3.py.md @@ -0,0 +1,20 @@ +# 8 + +## Bugs +- None observed in the current code. + +## Optimizations +- Use `self.assert*` methods from `unittest` instead of plain `assert` statements for consistency with the `unittest` framework. +- Break down the test into smaller sub-tests using `subTest` for each section relating to different OBIS references. This improves maintainability and readability. +- Consider using parameterized tests to reduce redundancy when asserting similar conditions across different OBIS references. + +## Good points +- Comprehensive testing of various aspects of DSMR v3 telegram parsing, ensuring all critical data points are covered. +- Use of exception handling to detect and report parsing errors during test execution. +- Clear and logical organization of the tests, providing a good structure and flow. + +## Summary +The code provides a detailed unit test for parsing DSMR v3 telegrams, covering numerous OBIS references and possible values effectively. There are no evident bugs, but the use of the `unittest` framework could be optimized by replacing inline assertions with framework-specific methods for better integration and error reporting. The tests are well-structured and provide thorough coverage of the parsing functionality. + +## Open source alternatives +- The `dmsr_parser` library seems like a specific utility for DSMR telegram parsing, and there might not be direct open-source alternatives that perform the exact function unless they're part of broader smart meter or energy management suites. Some commonly used data parsing libraries like `pandas` could potentially be adapted for similar purposes with custom function implementations. \ No newline at end of file diff --git a/reviews/test/test_parse_v4_2.py.json b/reviews/test/test_parse_v4_2.py.json new file mode 100644 index 0000000..13b302d --- /dev/null +++ b/reviews/test/test_parse_v4_2.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from decimal import Decimal\nimport datetime\nimport unittest\n\nimport pytz\n\nfrom dsmr_parser import obis_references as obis\nfrom dsmr_parser import telegram_specifications\nfrom dsmr_parser.exceptions import InvalidChecksumError, ParseError\nfrom dsmr_parser.objects import CosemObject, MBusObject\nfrom dsmr_parser.parsers import TelegramParser\nfrom test.example_telegrams import TELEGRAM_V4_2\n\n\nclass TelegramParserV4_2Test(unittest.TestCase):\n \"\"\" Test parsing of a DSMR v4.2 telegram. \"\"\"\n\n def test_parse(self):\n parser = TelegramParser(telegram_specifications.V4)\n try:\n result = parser.parse(TELEGRAM_V4_2, throw_ex=True)\n except Exception as ex:\n assert False, f\"parse trigged an exception {ex}\"\n\n # P1_MESSAGE_HEADER (1-3:0.2.8)\n assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject)\n assert result[obis.P1_MESSAGE_HEADER].unit is None\n assert isinstance(result[obis.P1_MESSAGE_HEADER].value, str)\n assert result[obis.P1_MESSAGE_HEADER].value == '42'\n\n # P1_MESSAGE_TIMESTAMP (0-0:1.0.0)\n assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP], CosemObject)\n assert result[obis.P1_MESSAGE_TIMESTAMP].unit is None\n assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP].value, datetime.datetime)\n assert result[obis.P1_MESSAGE_TIMESTAMP].value == \\\n datetime.datetime(2016, 11, 13, 19, 57, 57, tzinfo=pytz.UTC)\n\n # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)\n assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal)\n assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('1581.123')\n\n # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject)\n assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal)\n assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('1435.706')\n\n # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('0')\n\n # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh'\n assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal)\n assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('0')\n\n # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)\n assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject)\n assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None\n assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str)\n assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0002'\n\n # EQUIPMENT_IDENTIFIER (0-0:96.1.1)\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject)\n assert result[obis.EQUIPMENT_IDENTIFIER].unit is None\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str)\n assert result[obis.EQUIPMENT_IDENTIFIER].value == '3960221976967177082151037881335713'\n\n # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)\n assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject)\n assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'\n assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal)\n assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('2.027')\n\n # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)\n assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject)\n assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW'\n assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)\n assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0')\n\n # SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)\n assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject)\n assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None\n assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int)\n assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 15\n\n # LONG_POWER_FAILURE_COUNT (96.7.9)\n assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject)\n assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None\n assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int)\n assert result[obis.LONG_POWER_FAILURE_COUNT].value == 7\n\n # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)\n assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject)\n assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None\n assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT].value, int)\n assert result[obis.VOLTAGE_SAG_L1_COUNT].value == 0\n\n # VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)\n assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT], CosemObject)\n assert result[obis.VOLTAGE_SAG_L2_COUNT].unit is None\n assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT].value, int)\n assert result[obis.VOLTAGE_SAG_L2_COUNT].value == 0\n\n # VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)\n assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT], CosemObject)\n assert result[obis.VOLTAGE_SAG_L3_COUNT].unit is None\n assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT].value, int)\n assert result[obis.VOLTAGE_SAG_L3_COUNT].value == 0\n\n # VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)\n assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT], CosemObject)\n assert result[obis.VOLTAGE_SWELL_L1_COUNT].unit is None\n assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT].value, int)\n assert result[obis.VOLTAGE_SWELL_L1_COUNT].value == 0\n\n # VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)\n assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT], CosemObject)\n assert result[obis.VOLTAGE_SWELL_L2_COUNT].unit is None\n assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT].value, int)\n assert result[obis.VOLTAGE_SWELL_L2_COUNT].value == 0\n\n # VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)\n assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT], CosemObject)\n assert result[obis.VOLTAGE_SWELL_L3_COUNT].unit is None\n assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int)\n assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0\n\n # TEXT_MESSAGE_CODE (0-0:96.13.1)\n assert isinstance(result[obis.TEXT_MESSAGE_CODE], CosemObject)\n assert result[obis.TEXT_MESSAGE_CODE].unit is None\n assert result[obis.TEXT_MESSAGE_CODE].value is None\n\n # TEXT_MESSAGE (0-0:96.13.0)\n assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)\n assert result[obis.TEXT_MESSAGE].unit is None\n assert result[obis.TEXT_MESSAGE].value is None\n\n # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)\n assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)\n assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0')\n\n # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)\n assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)\n assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('6')\n\n # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)\n assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'\n assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)\n assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('2')\n\n # DEVICE_TYPE (0-x:24.1.0)\n assert isinstance(result[obis.DEVICE_TYPE], CosemObject)\n assert result[obis.DEVICE_TYPE].unit is None\n assert isinstance(result[obis.DEVICE_TYPE].value, int)\n assert result[obis.DEVICE_TYPE].value == 3\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.170')\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('1.247')\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.209')\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0')\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0')\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW'\n assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal)\n assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0')\n\n # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject)\n assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None\n assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str)\n assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '4819243993373755377509728609491464'\n\n # HOURLY_GAS_METER_READING (0-1:24.2.1)\n assert isinstance(result[obis.HOURLY_GAS_METER_READING], MBusObject)\n assert result[obis.HOURLY_GAS_METER_READING].unit == 'm3'\n assert isinstance(result[obis.HOURLY_GAS_METER_READING].value, Decimal)\n assert result[obis.HOURLY_GAS_METER_READING].value == Decimal('981.443')\n\n # POWER_EVENT_FAILURE_LOG (99.97.0)\n # TODO to be implemented\n\n # ACTUAL_TRESHOLD_ELECTRICITY (0-0:17.0.0)\n # TODO to be implemented\n\n # ACTUAL_SWITCH_POSITION (0-0:96.3.10)\n # TODO to be implemented\n\n # VALVE_POSITION_GAS (0-x:24.4.0)\n # TODO to be implemented\n\n def test_checksum_valid(self):\n # No exception is raised.\n TelegramParser.validate_checksum(TELEGRAM_V4_2)\n\n def test_checksum_invalid(self):\n # Remove the electricty used data value. This causes the checksum to\n # not match anymore.\n corrupted_telegram = TELEGRAM_V4_2.replace(\n '1-0:1.8.1(001581.123*kWh)\\r\\n',\n ''\n )\n\n with self.assertRaises(InvalidChecksumError):\n TelegramParser.validate_checksum(corrupted_telegram)\n\n def test_checksum_missing(self):\n # Remove the checksum value causing a ParseError.\n corrupted_telegram = TELEGRAM_V4_2.replace('!6796\\r\\n', '')\n\n with self.assertRaises(ParseError):\n TelegramParser.validate_checksum(corrupted_telegram)\n", + "review": "# Grade: 8\n\n## Bugs\n- No identified bugs.\n\n## Optimizations\n- The `assert` statements could be replaced with `self.assertEqual()` or other appropriate `unittest` functions to take full advantage of Python's `unittest` capabilities.\n- Consolidate repetitive code by potentially using loops or helper functions, especially for testing similar properties across multiple data points.\n- Consider using `setUp` and `tearDown` methods in `unittest` to initialize and clean up the test environment, which can improve readability and maintenability.\n\n## Good points\n- The test comprehensively covers a wide range of data points for parsing DSMR v4.2 telegrams.\n- Includes tests for both valid and corrupted checksums, enhancing the robustness of the test suite.\n- Uses comprehensive assertions to check the correctness of parsed values against expected results.\n- Proper use of `Decimal` for financial and energy data, ensuring precision.\n\n## Summary\nThe `TelegramParserV4_2Test` class provides a thorough suite of tests for parsing DSMR v4.2 telegrams. The integration with obis references and assertions for each data field ensures data integrity in parsing test cases. Corrections are suggested for employing `unittest` assert methods for improved diagnostics and potentially restructuring for common test logic to enhance maintainability.\n\n## Open source alternatives\n- **dsmr-reader**: A web application that reads DSMR telegrams and offers a user-friendly interface to analyze energy usage.\n- **SMAP**: A Simple Measurement and Actuation Profile system to manage and process smart meter data inputs.", + "filename": "test_parse_v4_2.py", + "path": "test/test_parse_v4_2.py", + "directory": "test", + "grade": 8, + "size": 13023, + "line_count": 249 +} \ No newline at end of file diff --git a/reviews/test/test_parse_v4_2.py.md b/reviews/test/test_parse_v4_2.py.md new file mode 100644 index 0000000..b9f0a10 --- /dev/null +++ b/reviews/test/test_parse_v4_2.py.md @@ -0,0 +1,22 @@ +# Grade: 8 + +## Bugs +- No identified bugs. + +## Optimizations +- The `assert` statements could be replaced with `self.assertEqual()` or other appropriate `unittest` functions to take full advantage of Python's `unittest` capabilities. +- Consolidate repetitive code by potentially using loops or helper functions, especially for testing similar properties across multiple data points. +- Consider using `setUp` and `tearDown` methods in `unittest` to initialize and clean up the test environment, which can improve readability and maintenability. + +## Good points +- The test comprehensively covers a wide range of data points for parsing DSMR v4.2 telegrams. +- Includes tests for both valid and corrupted checksums, enhancing the robustness of the test suite. +- Uses comprehensive assertions to check the correctness of parsed values against expected results. +- Proper use of `Decimal` for financial and energy data, ensuring precision. + +## Summary +The `TelegramParserV4_2Test` class provides a thorough suite of tests for parsing DSMR v4.2 telegrams. The integration with obis references and assertions for each data field ensures data integrity in parsing test cases. Corrections are suggested for employing `unittest` assert methods for improved diagnostics and potentially restructuring for common test logic to enhance maintainability. + +## Open source alternatives +- **dsmr-reader**: A web application that reads DSMR telegrams and offers a user-friendly interface to analyze energy usage. +- **SMAP**: A Simple Measurement and Actuation Profile system to manage and process smart meter data inputs. \ No newline at end of file diff --git a/reviews/test/test_parse_v5.py.json b/reviews/test/test_parse_v5.py.json new file mode 100644 index 0000000..03fb64c --- /dev/null +++ b/reviews/test/test_parse_v5.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from decimal import Decimal\n\nimport datetime\nimport unittest\n\nimport pytz\n\nfrom dsmr_parser import telegram_specifications\nfrom dsmr_parser.exceptions import InvalidChecksumError, ParseError\nfrom dsmr_parser.objects import CosemObject, MBusObject\nfrom dsmr_parser.parsers import TelegramParser\nfrom test.example_telegrams import TELEGRAM_V5\n\n\nclass TelegramParserV5Test(unittest.TestCase):\n \"\"\" Test parsing of a DSMR v5.x telegram. \"\"\"\n\n def test_parse(self):\n parser = TelegramParser(telegram_specifications.V5)\n try:\n telegram = parser.parse(TELEGRAM_V5, throw_ex=True)\n except Exception as ex:\n assert False, f\"parse trigged an exception {ex}\"\n print('test: ', type(telegram.P1_MESSAGE_HEADER), telegram.P1_MESSAGE_HEADER.__dict__)\n # P1_MESSAGE_HEADER (1-3:0.2.8)\n assert isinstance(telegram.P1_MESSAGE_HEADER, CosemObject)\n assert telegram.P1_MESSAGE_HEADER.unit is None\n assert isinstance(telegram.P1_MESSAGE_HEADER.value, str)\n assert telegram.P1_MESSAGE_HEADER.value == '50'\n\n # P1_MESSAGE_TIMESTAMP (0-0:1.0.0)\n assert isinstance(telegram.P1_MESSAGE_TIMESTAMP, CosemObject)\n assert telegram.P1_MESSAGE_TIMESTAMP.unit is None\n assert isinstance(telegram.P1_MESSAGE_TIMESTAMP.value, datetime.datetime)\n assert telegram.P1_MESSAGE_TIMESTAMP.value == \\\n datetime.datetime(2017, 1, 2, 18, 20, 2, tzinfo=pytz.UTC)\n\n # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1, CosemObject)\n assert telegram.ELECTRICITY_USED_TARIFF_1.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1.value, Decimal)\n assert telegram.ELECTRICITY_USED_TARIFF_1.value == Decimal('4.426')\n\n # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2, CosemObject)\n assert telegram.ELECTRICITY_USED_TARIFF_2.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2.value, Decimal)\n assert telegram.ELECTRICITY_USED_TARIFF_2.value == Decimal('2.399')\n\n # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('2.444')\n\n # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('0')\n\n # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)\n assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF, CosemObject)\n assert telegram.ELECTRICITY_ACTIVE_TARIFF.unit is None\n assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF.value, str)\n assert telegram.ELECTRICITY_ACTIVE_TARIFF.value == '0002'\n\n # EQUIPMENT_IDENTIFIER (0-0:96.1.1)\n assert isinstance(telegram.EQUIPMENT_IDENTIFIER, CosemObject)\n assert telegram.EQUIPMENT_IDENTIFIER.unit is None\n assert isinstance(telegram.EQUIPMENT_IDENTIFIER.value, str)\n assert telegram.EQUIPMENT_IDENTIFIER.value == '4B384547303034303436333935353037'\n\n # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)\n assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE, CosemObject)\n assert telegram.CURRENT_ELECTRICITY_USAGE.unit == 'kW'\n assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE.value, Decimal)\n assert telegram.CURRENT_ELECTRICITY_USAGE.value == Decimal('0.244')\n\n # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)\n assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY, CosemObject)\n assert telegram.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW'\n assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY.value, Decimal)\n assert telegram.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('0')\n\n # LONG_POWER_FAILURE_COUNT (96.7.9)\n assert isinstance(telegram.LONG_POWER_FAILURE_COUNT, CosemObject)\n assert telegram.LONG_POWER_FAILURE_COUNT.unit is None\n assert isinstance(telegram.LONG_POWER_FAILURE_COUNT.value, int)\n assert telegram.LONG_POWER_FAILURE_COUNT.value == 0\n\n # SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)\n assert isinstance(telegram.SHORT_POWER_FAILURE_COUNT, CosemObject)\n assert telegram.SHORT_POWER_FAILURE_COUNT.unit is None\n assert isinstance(telegram.SHORT_POWER_FAILURE_COUNT.value, int)\n assert telegram.SHORT_POWER_FAILURE_COUNT.value == 13\n\n # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)\n assert isinstance(telegram.VOLTAGE_SAG_L1_COUNT, CosemObject)\n assert telegram.VOLTAGE_SAG_L1_COUNT.unit is None\n assert isinstance(telegram.VOLTAGE_SAG_L1_COUNT.value, int)\n assert telegram.VOLTAGE_SAG_L1_COUNT.value == 0\n\n # VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)\n assert isinstance(telegram.VOLTAGE_SAG_L2_COUNT, CosemObject)\n assert telegram.VOLTAGE_SAG_L2_COUNT.unit is None\n assert isinstance(telegram.VOLTAGE_SAG_L2_COUNT.value, int)\n assert telegram.VOLTAGE_SAG_L2_COUNT.value == 0\n\n # VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)\n assert isinstance(telegram.VOLTAGE_SAG_L3_COUNT, CosemObject)\n assert telegram.VOLTAGE_SAG_L3_COUNT.unit is None\n assert isinstance(telegram.VOLTAGE_SAG_L3_COUNT.value, int)\n assert telegram.VOLTAGE_SAG_L3_COUNT.value == 0\n\n # VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)\n assert isinstance(telegram.VOLTAGE_SWELL_L1_COUNT, CosemObject)\n assert telegram.VOLTAGE_SWELL_L1_COUNT.unit is None\n assert isinstance(telegram.VOLTAGE_SWELL_L1_COUNT.value, int)\n assert telegram.VOLTAGE_SWELL_L1_COUNT.value == 0\n\n # VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)\n assert isinstance(telegram.VOLTAGE_SWELL_L2_COUNT, CosemObject)\n assert telegram.VOLTAGE_SWELL_L2_COUNT.unit is None\n assert isinstance(telegram.VOLTAGE_SWELL_L2_COUNT.value, int)\n assert telegram.VOLTAGE_SWELL_L2_COUNT.value == 0\n\n # VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)\n assert isinstance(telegram.VOLTAGE_SWELL_L3_COUNT, CosemObject)\n assert telegram.VOLTAGE_SWELL_L3_COUNT.unit is None\n assert isinstance(telegram.VOLTAGE_SWELL_L3_COUNT.value, int)\n assert telegram.VOLTAGE_SWELL_L3_COUNT.value == 0\n\n # INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1, CosemObject)\n assert telegram.INSTANTANEOUS_VOLTAGE_L1.unit == 'V'\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1.value, Decimal)\n assert telegram.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('230.0')\n\n # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2, CosemObject)\n assert telegram.INSTANTANEOUS_VOLTAGE_L2.unit == 'V'\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2.value, Decimal)\n assert telegram.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('230.0')\n\n # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3, CosemObject)\n assert telegram.INSTANTANEOUS_VOLTAGE_L3.unit == 'V'\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3.value, Decimal)\n assert telegram.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('229.0')\n\n # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1, CosemObject)\n assert telegram.INSTANTANEOUS_CURRENT_L1.unit == 'A'\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1.value, Decimal)\n assert telegram.INSTANTANEOUS_CURRENT_L1.value == Decimal('0.48')\n\n # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2, CosemObject)\n assert telegram.INSTANTANEOUS_CURRENT_L2.unit == 'A'\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2.value, Decimal)\n assert telegram.INSTANTANEOUS_CURRENT_L2.value == Decimal('0.44')\n\n # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3, CosemObject)\n assert telegram.INSTANTANEOUS_CURRENT_L3.unit == 'A'\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3.value, Decimal)\n assert telegram.INSTANTANEOUS_CURRENT_L3.value == Decimal('0.86')\n\n # TEXT_MESSAGE (0-0:96.13.0)\n assert isinstance(telegram.TEXT_MESSAGE, CosemObject)\n assert telegram.TEXT_MESSAGE.unit is None\n assert telegram.TEXT_MESSAGE.value is None\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, CosemObject)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.unit == 'kW'\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value, Decimal)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value == Decimal('0.070')\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, CosemObject)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.unit == 'kW'\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value, Decimal)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value == Decimal('0.032')\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, CosemObject)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.unit == 'kW'\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value, Decimal)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value == Decimal('0.142')\n\n # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, CosemObject)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.unit == 'kW'\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value, Decimal)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value == Decimal('0')\n\n # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, CosemObject)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.unit == 'kW'\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value, Decimal)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value == Decimal('0')\n\n # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, CosemObject)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.unit == 'kW'\n assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value, Decimal)\n assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value == Decimal('0')\n\n # There's only one Mbus device (gas meter) in this case. Alternatively\n # use get_mbus_device_by_channel\n gas_meter_devices = telegram.MBUS_DEVICES\n gas_meter_device = gas_meter_devices[0]\n\n # MBUS_DEVICE_TYPE (0-1:96.1.0)\n assert isinstance(gas_meter_device.MBUS_DEVICE_TYPE, CosemObject)\n assert gas_meter_device.MBUS_DEVICE_TYPE.unit is None\n assert isinstance(gas_meter_device.MBUS_DEVICE_TYPE.value, int)\n assert gas_meter_device.MBUS_DEVICE_TYPE.value == 3\n\n # MBUS_EQUIPMENT_IDENTIFIER (0-1:96.1.0)\n assert isinstance(gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER, CosemObject)\n assert gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER.unit is None\n assert isinstance(gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER.value, str)\n assert gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER.value == '3232323241424344313233343536373839'\n\n # MBUS_METER_READING (0-1:24.2.1)\n assert isinstance(gas_meter_device.MBUS_METER_READING, MBusObject)\n assert gas_meter_device.MBUS_METER_READING.unit == 'm3'\n assert isinstance(telegram.MBUS_METER_READING.value, Decimal)\n assert gas_meter_device.MBUS_METER_READING.value == Decimal('0.107')\n\n def test_checksum_valid(self):\n # No exception is raised.\n TelegramParser.validate_checksum(TELEGRAM_V5)\n\n def test_checksum_invalid(self):\n # Remove the electricty used data value. This causes the checksum to\n # not match anymore.\n corrupted_telegram = TELEGRAM_V5.replace(\n '1-0:1.8.1(000004.426*kWh)\\r\\n',\n ''\n )\n\n with self.assertRaises(InvalidChecksumError):\n TelegramParser.validate_checksum(corrupted_telegram)\n\n def test_checksum_missing(self):\n # Remove the checksum value causing a ParseError.\n corrupted_telegram = TELEGRAM_V5.replace('!6EEE\\r\\n', '')\n with self.assertRaises(ParseError):\n TelegramParser.validate_checksum(corrupted_telegram)\n\n def test_gas_timestamp_invalid(self):\n # Issue 120\n # Sometimes a MBUS device (For ex a Gas Meter) returns an invalid timestamp\n # Instead of failing, we should just ignore the timestamp\n invalid_date_telegram = TELEGRAM_V5.replace(\n '0-1:24.2.1(170102161005W)(00000.107*m3)\\r\\n',\n '0-1:24.2.1(632525252525S)(00000.000)\\r\\n'\n )\n invalid_date_telegram = invalid_date_telegram.replace('!6EEE\\r\\n', '!90C2\\r\\n')\n parser = TelegramParser(telegram_specifications.V5)\n telegram = parser.parse(invalid_date_telegram)\n\n # MBUS DEVICE 1\n mbus1 = telegram.get_mbus_device_by_channel(1)\n\n # MBUS_METER_READING (0-1:24.2.1)\n assert isinstance(mbus1.MBUS_METER_READING, MBusObject)\n assert mbus1.MBUS_METER_READING.unit is None\n assert isinstance(mbus1.MBUS_METER_READING.value, Decimal)\n assert mbus1.MBUS_METER_READING.value == Decimal('0.000')\n", + "review": "**7**\n\n### Bugs\n- No direct identification of issues, but exceptions could be handled much better with more detailed information.\n- The gas meter reading asserts for unit contradiction, once as 'm3' and another time as `None`.\n \n### Optimizations\n- Consider using `self.assert` over Python's `assert` statements for better unittest integration.\n- The test could be refactored to be more DRY (Don't Repeat Yourself) by creating helper functions for shared assertion patterns.\n- Error messages in exceptions could be made more informative and specific to enhance debugging.\n- Improve exception messages to clearly communicate which part of the telegram caused the error.\n\n### Good points\n- Comprehensive range of tests covering different data points ensures thorough verification.\n- Assertions use a good combination of type checks and value comparisons.\n- Proper usage of Python's unittest framework provides a structured testing approach.\n \n### Summary\nThe provided code is robust in terms of testing a broad spectrum of possible DSMR telegram parsing scenarios and covers multiple cases such as valid data, checksum validation, and error handling for corrupted content. However, it lacks clarity in exception messages and can be refactored to reduce repetitive code. Moving from Python's generic `assert` to unittest's `self.assert` can better integrate with test runners and produce more informative output during failures.\n\n### Open source alternatives\n- **DSMR-reader**: An open source application to read and visualize DSMR data.\n- **Home Assistant DSMR integration**: A component for Home Assistant that supports parsing data from a smart meter via DSMR.", + "filename": "test_parse_v5.py", + "path": "test/test_parse_v5.py", + "directory": "test", + "grade": 7, + "size": 14292, + "line_count": 275 +} \ No newline at end of file diff --git a/reviews/test/test_parse_v5.py.md b/reviews/test/test_parse_v5.py.md new file mode 100644 index 0000000..fece4c3 --- /dev/null +++ b/reviews/test/test_parse_v5.py.md @@ -0,0 +1,23 @@ +**7** + +### Bugs +- No direct identification of issues, but exceptions could be handled much better with more detailed information. +- The gas meter reading asserts for unit contradiction, once as 'm3' and another time as `None`. + +### Optimizations +- Consider using `self.assert` over Python's `assert` statements for better unittest integration. +- The test could be refactored to be more DRY (Don't Repeat Yourself) by creating helper functions for shared assertion patterns. +- Error messages in exceptions could be made more informative and specific to enhance debugging. +- Improve exception messages to clearly communicate which part of the telegram caused the error. + +### Good points +- Comprehensive range of tests covering different data points ensures thorough verification. +- Assertions use a good combination of type checks and value comparisons. +- Proper usage of Python's unittest framework provides a structured testing approach. + +### Summary +The provided code is robust in terms of testing a broad spectrum of possible DSMR telegram parsing scenarios and covers multiple cases such as valid data, checksum validation, and error handling for corrupted content. However, it lacks clarity in exception messages and can be refactored to reduce repetitive code. Moving from Python's generic `assert` to unittest's `self.assert` can better integrate with test runners and produce more informative output during failures. + +### Open source alternatives +- **DSMR-reader**: An open source application to read and visualize DSMR data. +- **Home Assistant DSMR integration**: A component for Home Assistant that supports parsing data from a smart meter via DSMR. \ No newline at end of file diff --git a/reviews/test/test_parse_v5_eon_hungary.py.json b/reviews/test/test_parse_v5_eon_hungary.py.json new file mode 100644 index 0000000..f424cc1 --- /dev/null +++ b/reviews/test/test_parse_v5_eon_hungary.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from decimal import Decimal\n\nimport datetime\nimport unittest\n\nimport pytz\n\nfrom dsmr_parser import telegram_specifications\nfrom dsmr_parser.exceptions import InvalidChecksumError, ParseError\nfrom dsmr_parser.objects import CosemObject\nfrom dsmr_parser.parsers import TelegramParser\nfrom test.example_telegrams import TELEGRAM_V5_EON_HU\n\n\nclass TelegramParserV5EONHUTest(unittest.TestCase):\n \"\"\" Test parsing of a DSMR v5 EON Hungary telegram. \"\"\"\n\n def test_parse(self):\n parser = TelegramParser(telegram_specifications.EON_HUNGARY)\n try:\n telegram = parser.parse(TELEGRAM_V5_EON_HU, throw_ex=True)\n except Exception as ex:\n assert False, f\"parse trigged an exception {ex}\"\n\n # P1_MESSAGE_TIMESTAMP (0-0:1.0.0)\n assert isinstance(telegram.P1_MESSAGE_TIMESTAMP, CosemObject)\n assert telegram.P1_MESSAGE_TIMESTAMP.unit is None\n assert isinstance(telegram.P1_MESSAGE_TIMESTAMP.value, datetime.datetime)\n assert telegram.P1_MESSAGE_TIMESTAMP.value == \\\n pytz.timezone(\"Europe/Budapest\").localize(datetime.datetime(2023, 7, 24, 15, 7, 30))\n\n # EON_HU_COSEM_LOGICAL_DEVICE_NAME (0-0:42.0.0)\n assert isinstance(telegram.COSEM_LOGICAL_DEVICE_NAME, CosemObject)\n assert telegram.COSEM_LOGICAL_DEVICE_NAME.unit is None\n assert isinstance(telegram.COSEM_LOGICAL_DEVICE_NAME.value, str)\n assert telegram.COSEM_LOGICAL_DEVICE_NAME.value == '53414733303832323030303032313630'\n\n # EON_HU_EQUIPMENT_SERIAL_NUMBER (0-0:96.1.0)\n assert isinstance(telegram.EQUIPMENT_SERIAL_NUMBER, CosemObject)\n assert telegram.EQUIPMENT_SERIAL_NUMBER.unit is None\n assert isinstance(telegram.EQUIPMENT_SERIAL_NUMBER.value, str)\n assert telegram.EQUIPMENT_SERIAL_NUMBER.value == '383930303832323030303032313630'\n\n # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)\n assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF, CosemObject)\n assert telegram.ELECTRICITY_ACTIVE_TARIFF.unit is None\n assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF.value, str)\n assert telegram.ELECTRICITY_ACTIVE_TARIFF.value == '0001'\n\n # ACTUAL_SWITCH_POSITION (0-0:96.3.10)\n assert isinstance(telegram.ACTUAL_SWITCH_POSITION, CosemObject)\n assert telegram.ACTUAL_SWITCH_POSITION.unit is None\n assert isinstance(telegram.ACTUAL_SWITCH_POSITION.value, str)\n assert telegram.ACTUAL_SWITCH_POSITION.value == '1'\n\n # ACTUAL_TRESHOLD_ELECTRICITY (0-0:17.0.0)\n assert isinstance(telegram.ACTUAL_TRESHOLD_ELECTRICITY, CosemObject)\n assert telegram.ACTUAL_TRESHOLD_ELECTRICITY.unit == 'kW'\n assert isinstance(telegram.ACTUAL_TRESHOLD_ELECTRICITY.value, Decimal)\n assert telegram.ACTUAL_TRESHOLD_ELECTRICITY.value == Decimal('90.000')\n\n # ELECTRICITY_IMPORTED_TOTAL (1-0:1.8.0)\n assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL, CosemObject)\n assert telegram.ELECTRICITY_IMPORTED_TOTAL.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL.value, Decimal)\n assert telegram.ELECTRICITY_IMPORTED_TOTAL.value == Decimal('000173.640')\n\n # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1, CosemObject)\n assert telegram.ELECTRICITY_USED_TARIFF_1.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1.value, Decimal)\n assert telegram.ELECTRICITY_USED_TARIFF_1.value == Decimal('000047.719')\n\n # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2, CosemObject)\n assert telegram.ELECTRICITY_USED_TARIFF_2.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2.value, Decimal)\n assert telegram.ELECTRICITY_USED_TARIFF_2.value == Decimal('000125.921')\n\n # EON_HU_ELECTRICITY_USED_TARIFF_3 (1-0:1.8.3)\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_3, CosemObject)\n assert telegram.ELECTRICITY_USED_TARIFF_3.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_3.value, Decimal)\n assert telegram.ELECTRICITY_USED_TARIFF_3.value == Decimal('000000.000')\n\n # EON_HU_ELECTRICITY_USED_TARIFF_4 (1-0:1.8.4)\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_4, CosemObject)\n assert telegram.ELECTRICITY_USED_TARIFF_4.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_USED_TARIFF_4.value, Decimal)\n assert telegram.ELECTRICITY_USED_TARIFF_4.value == Decimal('000000.000')\n\n # ELECTRICITY_EXPORTED_TOTAL (1-0:2.8.0)\n assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL, CosemObject)\n assert telegram.ELECTRICITY_EXPORTED_TOTAL.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL.value, Decimal)\n assert telegram.ELECTRICITY_EXPORTED_TOTAL.value == Decimal('000627.177')\n\n # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('000401.829')\n\n # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('000225.348')\n\n # EON_HU_ELECTRICITY_DELIVERED_TARIFF_3 (1-0:2.8.3)\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_3, CosemObject)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_3.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_3.value, Decimal)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_3.value == Decimal('000000.000')\n\n # EON_HU_ELECTRICITY_DELIVERED_TARIFF_4 (1-0:2.8.4)\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_4, CosemObject)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_4.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_4.value, Decimal)\n assert telegram.ELECTRICITY_DELIVERED_TARIFF_4.value == Decimal('000000.000')\n\n # ELECTRICITY_REACTIVE_IMPORTED_TOTAL (1-0:3.8.0)\n assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL, CosemObject)\n assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.unit == 'kvarh'\n assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value, Decimal)\n assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value == Decimal('000000.123')\n\n # ELECTRICITY_REACTIVE_EXPORTED_TOTAL (1-0:4.8.0)\n assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL, CosemObject)\n assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.unit == 'kvarh'\n assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value, Decimal)\n assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value == Decimal('000303.131')\n\n # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 (1-0:5.8.0)\n assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q1, CosemObject)\n assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q1.unit == 'kvarh'\n assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q1.value, Decimal)\n assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q1.value == Decimal('000000.668')\n\n # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 (1-0:6.8.0)\n assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q2, CosemObject)\n assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q2.unit == 'kvarh'\n assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q2.value, Decimal)\n assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q2.value == Decimal('000000.071')\n\n # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 (1-0:7.8.0)\n assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q3, CosemObject)\n assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q3.unit == 'kvarh'\n assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q3.value, Decimal)\n assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q3.value == Decimal('000160.487')\n\n # EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 (1-0:8.8.0)\n assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q4, CosemObject)\n assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q4.unit == 'kvarh'\n assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q4.value, Decimal)\n assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q4.value == Decimal('000143.346')\n\n # EON_HU_ELECTRICITY_COMBINED (1-0:15.8.0)\n assert isinstance(telegram.ELECTRICITY_COMBINED, CosemObject)\n assert telegram.ELECTRICITY_COMBINED.unit == 'kWh'\n assert isinstance(telegram.ELECTRICITY_COMBINED.value, Decimal)\n assert telegram.ELECTRICITY_COMBINED.value == Decimal('000800.817')\n\n # INSTANTANEOUS_VOLTAGE_L2 (1-0:32.7.0)\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1, CosemObject)\n assert telegram.INSTANTANEOUS_VOLTAGE_L1.unit == 'V'\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1.value, Decimal)\n assert telegram.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('240.4')\n\n # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2, CosemObject)\n assert telegram.INSTANTANEOUS_VOLTAGE_L2.unit == 'V'\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2.value, Decimal)\n assert telegram.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('239.1')\n\n # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3, CosemObject)\n assert telegram.INSTANTANEOUS_VOLTAGE_L3.unit == 'V'\n assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3.value, Decimal)\n assert telegram.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('241.2')\n\n # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1, CosemObject)\n assert telegram.INSTANTANEOUS_CURRENT_L1.unit == 'A'\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1.value, Decimal)\n assert telegram.INSTANTANEOUS_CURRENT_L1.value == Decimal('003')\n\n # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2, CosemObject)\n assert telegram.INSTANTANEOUS_CURRENT_L2.unit == 'A'\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2.value, Decimal)\n assert telegram.INSTANTANEOUS_CURRENT_L2.value == Decimal('004')\n\n # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3, CosemObject)\n assert telegram.INSTANTANEOUS_CURRENT_L3.unit == 'A'\n assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3.value, Decimal)\n assert telegram.INSTANTANEOUS_CURRENT_L3.value == Decimal('003')\n\n # EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL (1-0:13.7.0)\n assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL, CosemObject)\n assert telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL.unit is None\n assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL.value, Decimal)\n assert telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL.value == Decimal('4.556')\n\n # EON_HU_INSTANTANEOUS_POWER_FACTOR_L1 (1-0:33.7.0)\n assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L1, CosemObject)\n assert telegram.INSTANTANEOUS_POWER_FACTOR_L1.unit is None\n assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L1.value, Decimal)\n assert telegram.INSTANTANEOUS_POWER_FACTOR_L1.value == Decimal('4.591')\n\n # EON_HU_INSTANTANEOUS_POWER_FACTOR_L2 (1-0:53.7.0)\n assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L2, CosemObject)\n assert telegram.INSTANTANEOUS_POWER_FACTOR_L2.unit is None\n assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L2.value, Decimal)\n assert telegram.INSTANTANEOUS_POWER_FACTOR_L2.value == Decimal('4.542')\n\n # EON_HU_INSTANTANEOUS_POWER_FACTOR_L3 (1-0:73.7.0)\n assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L3, CosemObject)\n assert telegram.INSTANTANEOUS_POWER_FACTOR_L3.unit is None\n assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L3.value, Decimal)\n assert telegram.INSTANTANEOUS_POWER_FACTOR_L3.value == Decimal('4.552')\n\n # EON_HU_FREQUENCY (1-0:14.7.0)\n assert isinstance(telegram.FREQUENCY, CosemObject)\n assert telegram.FREQUENCY.unit == \"Hz\"\n assert isinstance(telegram.FREQUENCY.value, Decimal)\n assert telegram.FREQUENCY.value == Decimal('50.00')\n\n # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)\n assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE, CosemObject)\n assert telegram.CURRENT_ELECTRICITY_USAGE.unit == 'kW'\n assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE.value, Decimal)\n assert telegram.CURRENT_ELECTRICITY_USAGE.value == Decimal('00.000')\n\n # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)\n assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY, CosemObject)\n assert telegram.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW'\n assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY.value, Decimal)\n assert telegram.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('02.601')\n\n # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 (1-0:5.7.0)\n assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q1, CosemObject)\n assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q1.unit == 'kvar'\n assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q1.value, Decimal)\n assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q1.value == Decimal('00.000')\n\n # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 (1-0:6.7.0)\n assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q2, CosemObject)\n assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q2.unit == 'kvar'\n assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q2.value, Decimal)\n assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q2.value == Decimal('00.000')\n\n # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 (1-0:7.7.0)\n assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q3, CosemObject)\n assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q3.unit == 'kvar'\n assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q3.value, Decimal)\n assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q3.value == Decimal('00.504')\n\n # EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 (1-0:8.7.0)\n assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q4, CosemObject)\n assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.unit == 'kvar'\n assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.value, Decimal)\n assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.value == Decimal('00.000')\n\n # FUSE_THRESHOLD_L1 (1-0:31.4.0)\n assert isinstance(telegram.FUSE_THRESHOLD_L1, CosemObject)\n assert telegram.FUSE_THRESHOLD_L1.unit == 'A'\n assert isinstance(telegram.FUSE_THRESHOLD_L1.value, Decimal)\n assert telegram.FUSE_THRESHOLD_L1.value == Decimal('200.00')\n\n # FUSE_THRESHOLD_L2 (1-0:31.4.0)\n assert isinstance(telegram.FUSE_THRESHOLD_L2, CosemObject)\n assert telegram.FUSE_THRESHOLD_L2.unit == 'A'\n assert isinstance(telegram.FUSE_THRESHOLD_L2.value, Decimal)\n assert telegram.FUSE_THRESHOLD_L2.value == Decimal('200.00')\n\n # FUSE_THRESHOLD_L3 (1-0:31.4.0)\n assert isinstance(telegram.FUSE_THRESHOLD_L3, CosemObject)\n assert telegram.FUSE_THRESHOLD_L3.unit == 'A'\n assert isinstance(telegram.FUSE_THRESHOLD_L3.value, Decimal)\n assert telegram.FUSE_THRESHOLD_L3.value == Decimal('200.00')\n\n # TEXT_MESSAGE (0-0:96.13.0)\n assert isinstance(telegram.TEXT_MESSAGE, CosemObject)\n assert telegram.TEXT_MESSAGE.unit is None\n assert telegram.TEXT_MESSAGE.value is None\n\n def test_checksum_valid(self):\n # No exception is raised.\n TelegramParser.validate_checksum(TELEGRAM_V5_EON_HU)\n\n def test_checksum_invalid(self):\n # Remove the electricty used data value. This causes the checksum to\n # not match anymore.\n corrupted_telegram = TELEGRAM_V5_EON_HU.replace(\n '1-0:1.8.1(000047.719*kWh)\\r\\n',\n ''\n )\n\n with self.assertRaises(InvalidChecksumError):\n TelegramParser.validate_checksum(corrupted_telegram)\n\n def test_checksum_missing(self):\n # Remove the checksum value causing a ParseError.\n corrupted_telegram = TELEGRAM_V5_EON_HU.replace('!99DA\\r\\n', '')\n with self.assertRaises(ParseError):\n TelegramParser.validate_checksum(corrupted_telegram)\n", + "review": "# 8\n\n## Bugs\n- No prominent bugs were identified, but exception handling in `test_parse` could miss specific issues due to a generic exception catch.\n\n## Optimizations\n- Use `self.assertIsInstance` instead of `assert isinstance` for consistency with unittest methods.\n- Refactor the test to separate setup, execution, and assertions for better readability and maintenance.\n- Consider using parameterized tests to avoid repetition and improve maintainability.\n- The generic exception handling in the `test_parse` method could be more specific to catch only expected exceptions.\n\n## Good points\n- Comprehensive test coverage for various fields extracted from the telegram.\n- Uses the `unittest` framework appropriately to perform unit testing.\n- Coverage includes validation for both valid and invalid checksums.\n- Correct use of `Decimal` for precise representation of numeric values.\n\n## Summary\nThe code provides thorough unittest coverage for parsing DSMR v5 EON Hungary telegrams. It ensures that each field in the telegram is correctly parsed and validated, covering cases such as valid, invalid, and missing checksums. The test structure is mostly solid, though improvements could be made by using `unittest` assertions to enhance readability and error reporting. Parameterizing repetitive assertions would also help in maintaining the code more efficiently.\n\n## Open source alternatives\n- **pytz**: This library is being used in the code for timezone localization and is a robust package for working with time zones.\n- **Decouple**: If managing environment-specific configurations becomes complex, you may consider using Decouple for configuration management.", + "filename": "test_parse_v5_eon_hungary.py", + "path": "test/test_parse_v5_eon_hungary.py", + "directory": "test", + "grade": 8, + "size": 16935, + "line_count": 309 +} \ No newline at end of file diff --git a/reviews/test/test_parse_v5_eon_hungary.py.md b/reviews/test/test_parse_v5_eon_hungary.py.md new file mode 100644 index 0000000..3917d03 --- /dev/null +++ b/reviews/test/test_parse_v5_eon_hungary.py.md @@ -0,0 +1,23 @@ +# 8 + +## Bugs +- No prominent bugs were identified, but exception handling in `test_parse` could miss specific issues due to a generic exception catch. + +## Optimizations +- Use `self.assertIsInstance` instead of `assert isinstance` for consistency with unittest methods. +- Refactor the test to separate setup, execution, and assertions for better readability and maintenance. +- Consider using parameterized tests to avoid repetition and improve maintainability. +- The generic exception handling in the `test_parse` method could be more specific to catch only expected exceptions. + +## Good points +- Comprehensive test coverage for various fields extracted from the telegram. +- Uses the `unittest` framework appropriately to perform unit testing. +- Coverage includes validation for both valid and invalid checksums. +- Correct use of `Decimal` for precise representation of numeric values. + +## Summary +The code provides thorough unittest coverage for parsing DSMR v5 EON Hungary telegrams. It ensures that each field in the telegram is correctly parsed and validated, covering cases such as valid, invalid, and missing checksums. The test structure is mostly solid, though improvements could be made by using `unittest` assertions to enhance readability and error reporting. Parameterizing repetitive assertions would also help in maintaining the code more efficiently. + +## Open source alternatives +- **pytz**: This library is being used in the code for timezone localization and is a robust package for working with time zones. +- **Decouple**: If managing environment-specific configurations becomes complex, you may consider using Decouple for configuration management. \ No newline at end of file diff --git a/reviews/test/test_protocol.py.json b/reviews/test/test_protocol.py.json new file mode 100644 index 0000000..294fdc6 --- /dev/null +++ b/reviews/test/test_protocol.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from unittest.mock import Mock\n\nimport unittest\n\nfrom dsmr_parser import obis_references as obis\nfrom dsmr_parser.clients.protocol import create_dsmr_protocol\nfrom dsmr_parser.objects import Telegram\n\nTELEGRAM_V2_2 = (\n '/ISk5\\2MT382-1004\\r\\n'\n '\\r\\n'\n '0-0:96.1.1(00000000000000)\\r\\n'\n '1-0:1.8.1(00001.001*kWh)\\r\\n'\n '1-0:1.8.2(00001.001*kWh)\\r\\n'\n '1-0:2.8.1(00001.001*kWh)\\r\\n'\n '1-0:2.8.2(00001.001*kWh)\\r\\n'\n '0-0:96.14.0(0001)\\r\\n'\n '1-0:1.7.0(0001.01*kW)\\r\\n'\n '1-0:2.7.0(0000.00*kW)\\r\\n'\n '0-0:17.0.0(0999.00*kW)\\r\\n'\n '0-0:96.3.10(1)\\r\\n'\n '0-0:96.13.1()\\r\\n'\n '0-0:96.13.0()\\r\\n'\n '0-1:24.1.0(3)\\r\\n'\n '0-1:96.1.0(000000000000)\\r\\n'\n '0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)\\r\\n'\n '(00001.001)\\r\\n'\n '0-1:24.4.0(1)\\r\\n'\n '!\\r\\n'\n)\n\n\nclass ProtocolTest(unittest.TestCase):\n\n def setUp(self):\n new_protocol, _ = create_dsmr_protocol('2.2',\n telegram_callback=Mock(),\n keep_alive_interval=1)\n self.protocol = new_protocol()\n\n def test_complete_packet(self):\n \"\"\"Protocol should assemble incoming lines into complete packet.\"\"\"\n\n self.protocol.data_received(TELEGRAM_V2_2.encode('ascii'))\n\n telegram = self.protocol.telegram_callback.call_args_list[0][0][0]\n assert isinstance(telegram, Telegram)\n\n assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01\n assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'\n\n assert float(telegram[obis.GAS_METER_READING].value) == 1.001\n assert telegram[obis.GAS_METER_READING].unit == 'm3'\n\n def test_receive_packet(self):\n \"\"\"Protocol packet reception.\"\"\"\n\n mock_transport = Mock()\n self.protocol.connection_made(mock_transport)\n assert not self.protocol._active\n\n self.protocol.data_received(TELEGRAM_V2_2.encode('ascii'))\n assert self.protocol._active\n\n # 1st call of keep_alive resets 'active' flag\n self.protocol.keep_alive()\n assert not self.protocol._active\n\n # 2nd call of keep_alive should close the transport\n self.protocol.keep_alive()\n mock_transport.close.assert_called_once()\n\n self.protocol.connection_lost(None)\n", + "review": "**Grade: 7**\n\n### Bugs\n- No significant bugs were found in the code.\n\n### Optimizations\n- Use `self.assertIsInstance` instead of the `assert` statement for consistency and better error messages within unittest framework.\n- Instead of `assert`, use `self.assertEqual`, `self.assertAlmostEqual`, and `self.assertFalse` for assertions to fit within the unittest framework's conventions, which provide more informative output.\n- Applying consistent naming conventions to better clarify method purposes, such as prefixing test methods with `test_`.\n\n### Good points\n- The use of `unittest` framework which is widely accepted and provides a structured way to test in Python.\n- Code is organized with setup and teardown functionalities method which ensures each test runs in isolation, maintaining test integrity.\n- Mocks are appropriately used to simulate and assert interactions in tests.\n\n### Summary\nThe code presents a well-structured unit test for a protocol that processes data telegrams. It leverages Python's `unittest` library to ensure the functionalities of receiving and processing data packets are verified. While it is mostly well-written, it could benefit from adhering more rigorously to unittest's built-in methods for making assertions versus using base `assert` statements, as well as slightly improving method naming conventions for clarity. \n\n### Open source alternatives\n- `pytest`: A powerful alternative to `unittest`, known for its simple syntax and ability to use fixtures, which can simplify setup and teardown processes.\n- `nose2`: Another testing framework that extends unittest and offers plugin support for extensibility.", + "filename": "test_protocol.py", + "path": "test/test_protocol.py", + "directory": "test", + "grade": 7, + "size": 2327, + "line_count": 74 +} \ No newline at end of file diff --git a/reviews/test/test_protocol.py.md b/reviews/test/test_protocol.py.md new file mode 100644 index 0000000..a2faf90 --- /dev/null +++ b/reviews/test/test_protocol.py.md @@ -0,0 +1,21 @@ +**Grade: 7** + +### Bugs +- No significant bugs were found in the code. + +### Optimizations +- Use `self.assertIsInstance` instead of the `assert` statement for consistency and better error messages within unittest framework. +- Instead of `assert`, use `self.assertEqual`, `self.assertAlmostEqual`, and `self.assertFalse` for assertions to fit within the unittest framework's conventions, which provide more informative output. +- Applying consistent naming conventions to better clarify method purposes, such as prefixing test methods with `test_`. + +### Good points +- The use of `unittest` framework which is widely accepted and provides a structured way to test in Python. +- Code is organized with setup and teardown functionalities method which ensures each test runs in isolation, maintaining test integrity. +- Mocks are appropriately used to simulate and assert interactions in tests. + +### Summary +The code presents a well-structured unit test for a protocol that processes data telegrams. It leverages Python's `unittest` library to ensure the functionalities of receiving and processing data packets are verified. While it is mostly well-written, it could benefit from adhering more rigorously to unittest's built-in methods for making assertions versus using base `assert` statements, as well as slightly improving method naming conventions for clarity. + +### Open source alternatives +- `pytest`: A powerful alternative to `unittest`, known for its simple syntax and ability to use fixtures, which can simplify setup and teardown processes. +- `nose2`: Another testing framework that extends unittest and offers plugin support for extensibility. \ No newline at end of file diff --git a/reviews/test/test_rfxtrx_protocol.py.json b/reviews/test/test_rfxtrx_protocol.py.json new file mode 100644 index 0000000..890b4f6 --- /dev/null +++ b/reviews/test/test_rfxtrx_protocol.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "from unittest.mock import Mock\n\nimport unittest\n\nfrom dsmr_parser import obis_references as obis\nfrom dsmr_parser.clients.rfxtrx_protocol import create_rfxtrx_dsmr_protocol, PACKETTYPE_DSMR, SUBTYPE_P1\nfrom dsmr_parser.objects import Telegram\n\nTELEGRAM_V2_2 = (\n '/ISk5\\2MT382-1004\\r\\n'\n '\\r\\n'\n '0-0:96.1.1(00000000000000)\\r\\n'\n '1-0:1.8.1(00001.001*kWh)\\r\\n'\n '1-0:1.8.2(00001.001*kWh)\\r\\n'\n '1-0:2.8.1(00001.001*kWh)\\r\\n'\n '1-0:2.8.2(00001.001*kWh)\\r\\n'\n '0-0:96.14.0(0001)\\r\\n'\n '1-0:1.7.0(0001.01*kW)\\r\\n'\n '1-0:2.7.0(0000.00*kW)\\r\\n'\n '0-0:17.0.0(0999.00*kW)\\r\\n'\n '0-0:96.3.10(1)\\r\\n'\n '0-0:96.13.1()\\r\\n'\n '0-0:96.13.0()\\r\\n'\n '0-1:24.1.0(3)\\r\\n'\n '0-1:96.1.0(000000000000)\\r\\n'\n '0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)\\r\\n'\n '(00001.001)\\r\\n'\n '0-1:24.4.0(1)\\r\\n'\n '!\\r\\n'\n)\n\nOTHER_RF_PACKET = b'\\x03\\x01\\x02\\x03'\n\n\ndef encode_telegram_as_RF_packets(telegram):\n data = b''\n\n for line in telegram.split('\\n'):\n packet_data = (line + '\\n').encode('ascii')\n packet_header = bytes(bytearray([\n len(packet_data) + 3, # excluding length byte\n PACKETTYPE_DSMR,\n SUBTYPE_P1,\n 0 # seq num (ignored)\n ]))\n\n data += packet_header + packet_data\n # other RF packets can pass by on the line\n data += OTHER_RF_PACKET\n\n return data\n\n\nclass RFXtrxProtocolTest(unittest.TestCase):\n\n def setUp(self):\n new_protocol, _ = create_rfxtrx_dsmr_protocol('2.2',\n telegram_callback=Mock(),\n keep_alive_interval=1)\n self.protocol = new_protocol()\n\n def test_complete_packet(self):\n \"\"\"Protocol should assemble incoming lines into complete packet.\"\"\"\n\n data = encode_telegram_as_RF_packets(TELEGRAM_V2_2)\n # send data broken up in two parts\n self.protocol.data_received(data[0:200])\n self.protocol.data_received(data[200:])\n\n telegram = self.protocol.telegram_callback.call_args_list[0][0][0]\n assert isinstance(telegram, Telegram)\n\n assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01\n assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'\n\n assert float(telegram[obis.GAS_METER_READING].value) == 1.001\n assert telegram[obis.GAS_METER_READING].unit == 'm3'\n", + "review": "# Grade: 7\n\n## Bugs\n- None detected\n\n## Optimizations\n- Use `self.assertIsInstance` instead of `assert isinstance` for better unittest integration.\n- Consider proper asynchronous function handling since `data_received` might be designed to work in an async environment.\n- Instead of hardcoding values, consider defining constants for repeating values, such as `3` in `packet_header`.\n- The expression `(line + '\\n')` can be optimized by using formatted strings.\n\n## Good points\n- The code has proper use of unittest framework with clear test structure.\n- Effective use of Mock to simulate external dependencies.\n- Code is modular, breaking logic into clearly defined functions.\n\n## Summary\nThe code is a unit test for the RFXtrx DSMR protocol using the unittest framework. It includes a helper function to encode a DSMR telegram into RF packets and uses Mock to simulate the telegram callback. The test checks that incoming data is correctly interpreted as a telegram object with the expected electricity and gas readings. The code is clean and follows good practices in terms of structure and decomposition, although minor optimizations could improve performance and readability.\n\n## Open source alternatives\n- [dsmr_parser](https://github.com/ndokter/dsmr_parser): While this is more focused on parsing DSMR readings, related functionalities of DSMR processing are relevant.\n- [Home Assistant](https://github.com/home-assistant/core): Offers broader smart home integrations, including DSMR integrations.\n- [pyRFXtrx](https://github.com/Danielhiversen/pyRFXtrx): A Python library that controls RFXtrx chips and can contribute to understanding RF protocol integration.", + "filename": "test_rfxtrx_protocol.py", + "path": "test/test_rfxtrx_protocol.py", + "directory": "test", + "grade": 7, + "size": 2436, + "line_count": 78 +} \ No newline at end of file diff --git a/reviews/test/test_rfxtrx_protocol.py.md b/reviews/test/test_rfxtrx_protocol.py.md new file mode 100644 index 0000000..f1a7a53 --- /dev/null +++ b/reviews/test/test_rfxtrx_protocol.py.md @@ -0,0 +1,23 @@ +# Grade: 7 + +## Bugs +- None detected + +## Optimizations +- Use `self.assertIsInstance` instead of `assert isinstance` for better unittest integration. +- Consider proper asynchronous function handling since `data_received` might be designed to work in an async environment. +- Instead of hardcoding values, consider defining constants for repeating values, such as `3` in `packet_header`. +- The expression `(line + '\n')` can be optimized by using formatted strings. + +## Good points +- The code has proper use of unittest framework with clear test structure. +- Effective use of Mock to simulate external dependencies. +- Code is modular, breaking logic into clearly defined functions. + +## Summary +The code is a unit test for the RFXtrx DSMR protocol using the unittest framework. It includes a helper function to encode a DSMR telegram into RF packets and uses Mock to simulate the telegram callback. The test checks that incoming data is correctly interpreted as a telegram object with the expected electricity and gas readings. The code is clean and follows good practices in terms of structure and decomposition, although minor optimizations could improve performance and readability. + +## Open source alternatives +- [dsmr_parser](https://github.com/ndokter/dsmr_parser): While this is more focused on parsing DSMR readings, related functionalities of DSMR processing are relevant. +- [Home Assistant](https://github.com/home-assistant/core): Offers broader smart home integrations, including DSMR integrations. +- [pyRFXtrx](https://github.com/Danielhiversen/pyRFXtrx): A Python library that controls RFXtrx chips and can contribute to understanding RF protocol integration. \ No newline at end of file diff --git a/reviews/test/test_telegram_buffer.py.json b/reviews/test/test_telegram_buffer.py.json new file mode 100644 index 0000000..0fd06b3 --- /dev/null +++ b/reviews/test/test_telegram_buffer.py.json @@ -0,0 +1,11 @@ +{ + "extension": ".py", + "source": "import unittest\n\nfrom dsmr_parser.clients.telegram_buffer import TelegramBuffer\nfrom test.example_telegrams import TELEGRAM_V2_2, TELEGRAM_V4_2\n\n\nclass TelegramBufferTest(unittest.TestCase):\n\n def setUp(self):\n self.telegram_buffer = TelegramBuffer()\n\n def test_v22_telegram(self):\n self.telegram_buffer.append(TELEGRAM_V2_2)\n\n telegram = next(self.telegram_buffer.get_all())\n\n self.assertEqual(telegram, TELEGRAM_V2_2)\n self.assertEqual(self.telegram_buffer._buffer, '')\n\n def test_v42_telegram(self):\n self.telegram_buffer.append(TELEGRAM_V4_2)\n\n telegram = next(self.telegram_buffer.get_all())\n\n self.assertEqual(telegram, TELEGRAM_V4_2)\n self.assertEqual(self.telegram_buffer._buffer, '')\n\n def test_multiple_mixed_telegrams(self):\n self.telegram_buffer.append(\n ''.join((TELEGRAM_V2_2, TELEGRAM_V4_2, TELEGRAM_V2_2))\n )\n\n telegrams = list(self.telegram_buffer.get_all())\n\n self.assertListEqual(\n telegrams,\n [\n TELEGRAM_V2_2,\n TELEGRAM_V4_2,\n TELEGRAM_V2_2\n ]\n )\n\n self.assertEqual(self.telegram_buffer._buffer, '')\n\n def test_v42_telegram_preceded_with_unclosed_telegram(self):\n # There are unclosed telegrams at the start of the buffer.\n incomplete_telegram = TELEGRAM_V4_2[:-1]\n\n self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2)\n\n telegram = next(self.telegram_buffer.get_all())\n\n self.assertEqual(telegram, TELEGRAM_V4_2)\n self.assertEqual(self.telegram_buffer._buffer, '')\n\n def test_v42_telegram_preceded_with_unopened_telegram(self):\n # There is unopened telegrams at the start of the buffer indicating that\n # the buffer was being filled while the telegram was outputted halfway.\n incomplete_telegram = TELEGRAM_V4_2[1:]\n\n self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2)\n\n telegram = next(self.telegram_buffer.get_all())\n\n self.assertEqual(telegram, TELEGRAM_V4_2)\n self.assertEqual(self.telegram_buffer._buffer, '')\n\n def test_v42_telegram_trailed_by_unclosed_telegram(self):\n incomplete_telegram = TELEGRAM_V4_2[:-1]\n\n self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram)\n\n telegram = next(self.telegram_buffer.get_all())\n\n self.assertEqual(telegram, TELEGRAM_V4_2)\n self.assertEqual(self.telegram_buffer._buffer, incomplete_telegram)\n\n def test_v42_telegram_trailed_by_unopened_telegram(self):\n incomplete_telegram = TELEGRAM_V4_2[1:]\n\n self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram)\n\n telegram = next(self.telegram_buffer.get_all())\n\n self.assertEqual(telegram, TELEGRAM_V4_2)\n self.assertEqual(self.telegram_buffer._buffer, incomplete_telegram)\n\n def test_v42_telegram_adding_line_by_line(self):\n for line in TELEGRAM_V4_2.splitlines(keepends=True):\n self.telegram_buffer.append(line)\n\n telegram = next(self.telegram_buffer.get_all())\n\n self.assertEqual(telegram, TELEGRAM_V4_2)\n self.assertEqual(self.telegram_buffer._buffer, '')\n\n def test_v42_telegram_adding_char_by_char(self):\n for char in TELEGRAM_V4_2:\n self.telegram_buffer.append(char)\n\n telegram = next(self.telegram_buffer.get_all())\n\n self.assertEqual(telegram, TELEGRAM_V4_2)\n self.assertEqual(self.telegram_buffer._buffer, '')\n", + "review": "**8**\n\n### Bugs\n- No explicit bugs were found in the provided code snippet.\n\n### Optimizations\n- Consider using a mock library to handle the TELGRAM data instead of importing it directly to allow more flexible testing.\n- Add more detailed assertions to handle potential edge cases or unexpected states in the TelegramBuffer methods.\n- Consider using the `with` statement with unittest (e.g., `with self.subTest(...):`) to better manage and report multiple test cases within the same test method.\n\n### Good points\n- Comprehensive test cases covering various scenarios.\n- Use of `unittest.TestCase` for a structured testing approach.\n- Use of descriptive method names that clearly state the intent of each test case.\n- The tests seem well-organized and appear to cover a wide variety of buffer states.\n\n### Summary\nThe provided code is a unit test suite for the `TelegramBuffer` class, which handles the parsing and storage of telegrams in a buffer. The suite includes tests for normal operation, boundary conditions, and malformed telegrams, providing solid coverage. There's room for improvement with the flexibility and granularity of the tests using mocks and more assertive testing of edge cases. No significant bugs were identified, indicating a stable code base.\n\n### Open source alternatives\n- `pytest` - A more feature-rich and flexible testing framework than unittest.\n- `nose2` - Extends unittest for additional functionality and easier test organization.\n- `hypothesis` - Provides property-based testing, useful for edge cases not explicitly defined in the test scenarios.", + "filename": "test_telegram_buffer.py", + "path": "test/test_telegram_buffer.py", + "directory": "test", + "grade": 8, + "size": 3535, + "line_count": 106 +} \ No newline at end of file diff --git a/reviews/test/test_telegram_buffer.py.md b/reviews/test/test_telegram_buffer.py.md new file mode 100644 index 0000000..ce000d1 --- /dev/null +++ b/reviews/test/test_telegram_buffer.py.md @@ -0,0 +1,23 @@ +**8** + +### Bugs +- No explicit bugs were found in the provided code snippet. + +### Optimizations +- Consider using a mock library to handle the TELGRAM data instead of importing it directly to allow more flexible testing. +- Add more detailed assertions to handle potential edge cases or unexpected states in the TelegramBuffer methods. +- Consider using the `with` statement with unittest (e.g., `with self.subTest(...):`) to better manage and report multiple test cases within the same test method. + +### Good points +- Comprehensive test cases covering various scenarios. +- Use of `unittest.TestCase` for a structured testing approach. +- Use of descriptive method names that clearly state the intent of each test case. +- The tests seem well-organized and appear to cover a wide variety of buffer states. + +### Summary +The provided code is a unit test suite for the `TelegramBuffer` class, which handles the parsing and storage of telegrams in a buffer. The suite includes tests for normal operation, boundary conditions, and malformed telegrams, providing solid coverage. There's room for improvement with the flexibility and granularity of the tests using mocks and more assertive testing of edge cases. No significant bugs were identified, indicating a stable code base. + +### Open source alternatives +- `pytest` - A more feature-rich and flexible testing framework than unittest. +- `nose2` - Extends unittest for additional functionality and easier test organization. +- `hypothesis` - Provides property-based testing, useful for edge cases not explicitly defined in the test scenarios. \ No newline at end of file