Merged master into autodetecttelegram
This commit is contained in:
commit
98652418cc
47
.github/workflows/tests.yml
vendored
Normal file
47
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push: ~
|
||||
pull_request: ~
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10 # Don't run forever when stale
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- '3.7'
|
||||
- '3.8'
|
||||
- '3.9'
|
||||
- '3.10'
|
||||
- '3.11'
|
||||
- '3.12'
|
||||
|
||||
name: Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cached PIP dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/.tox/python/.pytest_cache
|
||||
key: pip-${{ matrix.python-version }}-${{ hashFiles('setup.py', 'tox.ini') }}
|
||||
restore-keys: pip-${{ matrix.python-version }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install tox
|
||||
|
||||
- name: Run tests
|
||||
run: tox
|
||||
|
||||
- name: Code coverage upload
|
||||
uses: codecov/codecov-action@v4
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -2,7 +2,13 @@
|
||||
*.pyc
|
||||
.tox
|
||||
.cache
|
||||
.venv
|
||||
*.egg-info
|
||||
/.project
|
||||
/.pydevproject
|
||||
/.coverage
|
||||
build/
|
||||
dist/
|
||||
venv/
|
||||
*.*~
|
||||
*~
|
18
.travis.yml
18
.travis.yml
@ -1,18 +0,0 @@
|
||||
language: python
|
||||
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
|
||||
install: pip install tox-travis codecov
|
||||
|
||||
script: tox
|
||||
|
||||
after_success:
|
||||
- codecov
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: 2.7
|
199
CHANGELOG.rst
199
CHANGELOG.rst
@ -1,9 +1,190 @@
|
||||
Change Log
|
||||
----------
|
||||
|
||||
**0.9** (2017-03-02)
|
||||
**1.5.0** (2024-08-25)
|
||||
- Allow the telegram specification to optionally be autodetected (`PR #87 <https://github.com/ndokter/dsmr_parser/pull/87>`_ by `clonerswords <https://github.com/clonerswords>`_)
|
||||
|
||||
- allow the telegram specification to optionally be autodetected
|
||||
**1.4.2** (2024-07-14)
|
||||
|
||||
- Bump Github Actions to latest versions in favor of Node deprecations (`PR #159 <https://github.com/ndokter/dsmr_parser/pull/159>`_ by `dennissiemensma <https://github.com/dennissiemensma>`_)
|
||||
- Swap pyserial-asyncio for pyserial-asyncio-fast (`PR #158 <https://github.com/ndokter/dsmr_parser/pull/158>`_ by `bdraco <https://github.com/bdraco>`_)
|
||||
|
||||
**1.4.1** (2024-06-04)
|
||||
|
||||
- Avoid loading timezone at runtime (`PR #157 <https://github.com/ndokter/dsmr_parser/pull/157>`_ by `elupus <https://github.com/elupus>`_)
|
||||
|
||||
**1.4.0** (2024-03-12)
|
||||
|
||||
- Mbus alt (`PR #142 <https://github.com/ndokter/dsmr_parser/pull/142>`_ by `dupondje <https://github.com/dupondje>`_)
|
||||
- Q3D add CURRENT_ELECTRICITY_DELIVERY (`PR #149 <https://github.com/ndokter/dsmr_parser/pull/149>`_ by `Aeroid <https://github.com/Aeroid>`_)
|
||||
- Copy head_parsers list on construct. (`PR #150 <https://github.com/ndokter/dsmr_parser/pull/150>`_ by `dupondje <https://github.com/dupondje>`_)
|
||||
|
||||
**1.3.2** (2024-01-29)
|
||||
|
||||
- Fix unit test for pyton 3.12 (`PR #148 <https://github.com/ndokter/dsmr_parser/pull/148>`_ by `ndokter <https://github.com/ndokter>`_)
|
||||
|
||||
**1.3.1** (2023-11-06)
|
||||
|
||||
- Fix parsing peak usage with invalid timestamps (`PR #143 <https://github.com/ndokter/dsmr_parser/pull/143>`_ by `dupondje <https://github.com/dupondje>`_)
|
||||
|
||||
**1.3.0** (2023-08-01)
|
||||
|
||||
- added E.ON Hungary; refactored DSMR specifications to fix obis reference conflicts (`PR #137 <https://github.com/ndokter/dsmr_parser/pull/137>`_ by `balazs92117 <https://github.com/balazs92117>`_)
|
||||
|
||||
**1.2.4** (2023-07-11)
|
||||
|
||||
- EQUIPMENT IDENTIFIER is wrong for Fluvius meters when other mbus devices are present (`PR #133 <https://github.com/ndokter/dsmr_parser/pull/133>`_ by `ejpalacios <https://github.com/ejpalacios>`_)
|
||||
|
||||
|
||||
**1.2.3** (2023-04-18)
|
||||
|
||||
- Fix parsing tests and line start matching (`PR #132 <https://github.com/ndokter/dsmr_parser/pull/132>`_ by `dupondje <https://github.com/dupondje>`_)
|
||||
|
||||
**1.2.2** (2023-04-12)
|
||||
|
||||
- Improve performance. Thanks to `ejpalacios <https://github.com/bdraco>`_ (`PR #130 <https://github.com/ndokter/dsmr_parser/pull/130>`_ by `ndokter <https://github.com/ndokter>`_)
|
||||
|
||||
**1.2.1** (2023-04-05)
|
||||
|
||||
- Bug/duplicate index BELGIUM_MAXIMUM_DEMAND_13_MONTHS (`PR #129 <https://github.com/ndokter/dsmr_parser/pull/129>`_ by `ejpalacios <https://github.com/ejpalacios>`_)
|
||||
|
||||
**1.2.0** (2023-02-18)
|
||||
|
||||
- Improved gas meter (mbus devices) support and replaced Telegram dictionary with backwards compatible object (`PR #121 <https://github.com/ndokter/dsmr_parser/pull/121>`_ by `ndokter <https://github.com/ndokter>`_)
|
||||
- Fix parsing with invalid timestamps (`PR #125 <https://github.com/ndokter/dsmr_parser/pull/125>`_ by `dupondje <https://github.com/dupondje>`_)
|
||||
- Add Iskra IE.x meters specification (`PR #126 <https://github.com/ndokter/dsmr_parser/pull/126>`_ by `jchevalier7 <https://github.com/jchevalier7>`_)
|
||||
|
||||
**1.1.0** (2023-02-08)
|
||||
|
||||
- Add instantaneous reactive power + fixed swapped reactive total import export (`PR #124 <https://github.com/ndokter/dsmr_parser/pull/124>`_ by `yada75 <https://github.com/yada75>`_)
|
||||
|
||||
**1.0.0** (2022-12-22)
|
||||
|
||||
- switched to new numbering scheme https://semver.org/
|
||||
- Added support for Python 3.11 and dropped support for Python 3.6 (`PR #112 <https://github.com/ndokter/dsmr_parser/pull/112>`_ by `dennissiemensma <https://github.com/dennissiemensma>`_)
|
||||
- Add support for Fluvius V1.7.1 DSMR messages (`PR #110 <https://github.com/ndokter/dsmr_parser/pull/113>`_ by `dupondje <https://github.com/dupondje>`_)
|
||||
|
||||
**0.34** (2022-10-19)
|
||||
|
||||
- Adds support for the Sagemcom T210-D-r smart meter (`PR #110 <https://github.com/ndokter/dsmr_parser/pull/110>`_).
|
||||
|
||||
**0.33** (2022-04-20)
|
||||
|
||||
- Test Python 3.10 in CI + legacy badge fix (`PR #105 <https://github.com/ndokter/dsmr_parser/pull/105>`_).
|
||||
- Update telegram_specifications.py (`PR #106 <https://github.com/ndokter/dsmr_parser/pull/106>`_).
|
||||
- Improve compatiblity with Belgian standard (`PR #107 <https://github.com/ndokter/dsmr_parser/pull/107>`_).
|
||||
- Improve documentation asyncio (`PR #63 <https://github.com/ndokter/dsmr_parser/pull/63>`_).
|
||||
|
||||
**0.32** (2022-01-04)
|
||||
|
||||
- Support DSMR data read via RFXtrx with integrated P1 reader (`PR #98 <https://github.com/ndokter/dsmr_parser/pull/98>`_).
|
||||
|
||||
**0.31** (2021-11-21)
|
||||
|
||||
- Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway (`PR #92 <https://github.com/ndokter/dsmr_parser/pull/92>`_).
|
||||
|
||||
**0.30** (2021-08-18)
|
||||
|
||||
- Add support for Swedish smart meters (`PR #86 <https://github.com/ndokter/dsmr_parser/pull/86>`_).
|
||||
|
||||
**0.29** (2021-04-18)
|
||||
|
||||
- Add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break (`PR #71 <https://github.com/ndokter/dsmr_parser/pull/71>`_).
|
||||
Remove deprecated asyncio coroutine decorator (`PR #76 <https://github.com/ndokter/dsmr_parser/pull/76>`_).
|
||||
|
||||
**0.28** (2021-02-21)
|
||||
|
||||
- Optional keep alive monitoring for TCP/IP connections (`PR #73 <https://github.com/ndokter/dsmr_parser/pull/73>`_).
|
||||
- Catch parse errors in TelegramParser, ignore lines that can not be parsed (`PR #74 <https://github.com/ndokter/dsmr_parser/pull/74>`_).
|
||||
|
||||
**0.27** (2020-12-24)
|
||||
|
||||
- fix for empty parentheses in ProfileGenericParser (redone) (`PR #69 <https://github.com/ndokter/dsmr_parser/pull/69>`_).
|
||||
|
||||
**0.26** (2020-12-15)
|
||||
|
||||
- reverted fix for empty parentheses in ProfileGenericParser (`PR #68 <https://github.com/ndokter/dsmr_parser/pull/68>`_).
|
||||
|
||||
**0.25** (2020-12-14)
|
||||
|
||||
- fix for empty parentheses in ProfileGenericParser (`PR #57 <https://github.com/ndokter/dsmr_parser/pull/57>`_).
|
||||
|
||||
**0.24** (2020-11-27)
|
||||
|
||||
- Add Luxembourg equipment identifier (`PR #62 <https://github.com/ndokter/dsmr_parser/pull/62>`_).
|
||||
|
||||
**0.23** (2020-11-07)
|
||||
|
||||
- Resolved issue with x-x:24.3.0 where it contains non-integer character (`PR #61 <https://github.com/ndokter/dsmr_parser/pull/61>`_).
|
||||
- Tests are not installed anymore (`PR #59 <https://github.com/ndokter/dsmr_parser/pull/59>`_).
|
||||
- Example telegram improvement (`PR #58 <https://github.com/ndokter/dsmr_parser/pull/58>`_).
|
||||
|
||||
**0.22** (2020-08-23)
|
||||
|
||||
- CRC check speed is improved
|
||||
- Exception info improvement
|
||||
|
||||
**0.21** (2020-05-25)
|
||||
|
||||
- All objects can produce a json serialization of their state.
|
||||
|
||||
**0.20** (2020-05-12)
|
||||
|
||||
- All objects can now print their values
|
||||
- Add parser + object for generic profile
|
||||
|
||||
**0.19** (2020-05-03)
|
||||
|
||||
- Add following missing elements to telegram specification v4:
|
||||
- SHORT_POWER_FAILURE_COUNT,
|
||||
- INSTANTANEOUS_CURRENT_L1,
|
||||
- INSTANTANEOUS_CURRENT_L2,
|
||||
- INSTANTANEOUS_CURRENT_L3
|
||||
- Add missing tests + fix small test bugs
|
||||
- Complete telegram object v4 parse test
|
||||
|
||||
**0.18** (2020-01-28)
|
||||
|
||||
- PyCRC replacement (`PR #48 <https://github.com/ndokter/dsmr_parser/pull/48>`_).
|
||||
|
||||
**0.17** (2019-12-21)
|
||||
|
||||
- Add a true telegram object (`PR #40 <https://github.com/ndokter/dsmr_parser/pull/40>`_).
|
||||
|
||||
**0.16** (2019-12-21)
|
||||
|
||||
- Add support for Belgian and Smarty meters (`PR #44 <https://github.com/ndokter/dsmr_parser/pull/44>`_).
|
||||
|
||||
**0.15** (2019-12-12)
|
||||
|
||||
- Fixed asyncio loop issue (`PR #43 <https://github.com/ndokter/dsmr_parser/pull/43>`_).
|
||||
|
||||
**0.14** (2019-10-08)
|
||||
|
||||
- Changed serial reading to reduce CPU usage (`PR #37 <https://github.com/ndokter/dsmr_parser/pull/37>`_).
|
||||
|
||||
**0.13** (2019-03-04)
|
||||
|
||||
- Fix DSMR v5.0 serial settings which were not used (`PR #33 <https://github.com/ndokter/dsmr_parser/pull/33>`_).
|
||||
|
||||
**0.12** (2018-09-23)
|
||||
|
||||
- Add serial settings for DSMR v5.0 (`PR #31 <https://github.com/ndokter/dsmr_parser/pull/31>`_).
|
||||
- Lux-creos-obis-1.8.0 (`PR #32 <https://github.com/ndokter/dsmr_parser/pull/32>`_).
|
||||
|
||||
**0.11** (2017-09-18)
|
||||
|
||||
- NULL value fix in checksum (`PR #26 <https://github.com/ndokter/dsmr_parser/pull/26>`_)
|
||||
|
||||
**0.10** (2017-06-05)
|
||||
|
||||
- bugfix: don't force full telegram signatures (`PR #25 <https://github.com/ndokter/dsmr_parser/pull/25>`_)
|
||||
- removed unused code for automatic telegram detection as this needs reworking after the fix mentioned above
|
||||
- InvalidChecksumError's are logged as warning instead of error
|
||||
|
||||
**0.9** (2017-05-12)
|
||||
|
||||
- added DSMR v5 serial settings
|
||||
|
||||
**0.8** (2017-01-26)
|
||||
|
||||
@ -17,7 +198,7 @@ Change Log
|
||||
|
||||
**0.7** (2017-01-14)
|
||||
|
||||
- Internal refactoring related to the way clients feed their data into the parse module. Clients can now supply the telegram data in single characters, lines (which was common) or complete telegram strings. (`pull request #17 <https://github.com/ndokter/dsmr_parser/pull/17>`_)
|
||||
- Internal refactoring related to the way clients feed their data into the parse module. Clients can now supply the telegram data in single characters, lines (which was common) or complete telegram strings. (`PR #17 <https://github.com/ndokter/dsmr_parser/pull/17>`_)
|
||||
|
||||
**IMPORTANT: this release has the following backwards incompatible changes:**
|
||||
|
||||
@ -27,8 +208,8 @@ Change Log
|
||||
|
||||
**0.6** (2017-01-04)
|
||||
|
||||
- Fixed bug in CRC checksum verification for the asyncio client (`pull request #15 <https://github.com/ndokter/dsmr_parser/pull/15>`_)
|
||||
- Support added for TCP connections using the asyncio client (`pull request #12 <https://github.com/ndokter/dsmr_parser/pull/12/>`_)
|
||||
- Fixed bug in CRC checksum verification for the asyncio client (`PR #15 <https://github.com/ndokter/dsmr_parser/pull/15>`_)
|
||||
- Support added for TCP connections using the asyncio client (`PR #12 <https://github.com/ndokter/dsmr_parser/pull/12/>`_)
|
||||
|
||||
**0.5** (2016-12-29)
|
||||
|
||||
@ -36,16 +217,16 @@ Change Log
|
||||
|
||||
**0.4** (2016-11-21)
|
||||
|
||||
- DSMR v2.2 serial settings now uses parity serial.EVEN by default (`pull request #5 <https://github.com/ndokter/dsmr_parser/pull/5>`_)
|
||||
- improved asyncio reader and improve it's error handling (`pull request #8 <https://github.com/ndokter/dsmr_parser/pull/8>`_)
|
||||
- DSMR v2.2 serial settings now uses parity serial.EVEN by default (`PR #5 <https://github.com/ndokter/dsmr_parser/pull/5>`_)
|
||||
- improved asyncio reader and improve it's error handling (`PR #8 <https://github.com/ndokter/dsmr_parser/pull/8>`_)
|
||||
|
||||
**0.3** (2016-11-12)
|
||||
|
||||
- asyncio reader for non-blocking reads (`pull request #3 <https://github.com/ndokter/dsmr_parser/pull/3>`_)
|
||||
- asyncio reader for non-blocking reads (`PR #3 <https://github.com/ndokter/dsmr_parser/pull/3>`_)
|
||||
|
||||
**0.2** (2016-11-08)
|
||||
|
||||
- support for DMSR version 2.2 (`pull request #2 <https://github.com/ndokter/dsmr_parser/pull/2>`_)
|
||||
- support for DMSR version 2.2 (`PR #2 <https://github.com/ndokter/dsmr_parser/pull/2>`_)
|
||||
|
||||
**0.1** (2016-08-22)
|
||||
|
||||
|
346
README.rst
346
README.rst
@ -4,8 +4,8 @@ DSMR Parser
|
||||
.. image:: https://img.shields.io/pypi/v/dsmr-parser.svg
|
||||
:target: https://pypi.python.org/pypi/dsmr-parser
|
||||
|
||||
.. image:: https://travis-ci.org/ndokter/dsmr_parser.svg?branch=master
|
||||
:target: https://travis-ci.org/ndokter/dsmr_parser
|
||||
.. image:: https://img.shields.io/github/actions/workflow/status/ndokter/dsmr_parser/tests.yml?branch=master
|
||||
:target: https://github.com/ndokter/dsmr_parser/actions/workflows/tests.yml
|
||||
|
||||
A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It
|
||||
also includes client implementation to directly read and parse smart meter data.
|
||||
@ -14,8 +14,7 @@ also includes client implementation to directly read and parse smart meter data.
|
||||
Features
|
||||
--------
|
||||
|
||||
DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.4, 3.5 and 3.6.
|
||||
|
||||
DSMR Parser supports DSMR versions 2, 3, 4 and 5. See for the `currently supported/tested Python versions here <https://github.com/ndokter/dsmr_parser/blob/master/.github/workflows/tests.yml#L14>`_.
|
||||
|
||||
Client module usage
|
||||
-------------------
|
||||
@ -39,104 +38,251 @@ process because the code is blocking (not asynchronous):
|
||||
for telegram in serial_reader.read():
|
||||
print(telegram) # see 'Telegram object' docs below
|
||||
|
||||
**AsyncIO client**
|
||||
**Socket client**
|
||||
|
||||
To be documented.
|
||||
|
||||
|
||||
Parsing module usage
|
||||
--------------------
|
||||
The parsing module accepts complete unaltered telegram strings and parses these
|
||||
into a dictionary.
|
||||
Read a remote serial port (for example using ser2net) and work with the parsed telegrams.
|
||||
It should be run in a separate process because the code is blocking (not asynchronous):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
|
||||
telegram_str = (
|
||||
'/ISk5\2MT382-1000\r\n'
|
||||
'\r\n'
|
||||
'0-0:96.1.1(4B384547303034303436333935353037)\r\n'
|
||||
'1-0:1.8.1(12345.678*kWh)\r\n'
|
||||
'1-0:1.8.2(12345.678*kWh)\r\n'
|
||||
'1-0:2.8.1(12345.678*kWh)\r\n'
|
||||
'1-0:2.8.2(12345.678*kWh)\r\n'
|
||||
'0-0:96.14.0(0002)\r\n'
|
||||
'1-0:1.7.0(001.19*kW)\r\n'
|
||||
'1-0:2.7.0(000.00*kW)\r\n'
|
||||
'0-0:17.0.0(016*A)\r\n'
|
||||
'0-0:96.3.10(1)\r\n'
|
||||
'0-0:96.13.1(303132333435363738)\r\n'
|
||||
'0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E'
|
||||
'3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F30313233'
|
||||
'3435363738393A3B3C3D3E3F)\r\n'
|
||||
'0-1:96.1.0(3232323241424344313233343536373839)\r\n'
|
||||
'0-1:24.1.0(03)\r\n'
|
||||
'0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n'
|
||||
'(00001.001)\r\n'
|
||||
'0-1:24.4.0(1)\r\n'
|
||||
'!\r\n'
|
||||
)
|
||||
|
||||
parser = TelegramParser(telegram_specifications.V3)
|
||||
from dsmr_parser.clients import SocketReader
|
||||
|
||||
telegram = parser.parse(telegram_str)
|
||||
print(telegram) # see 'Telegram object' docs below
|
||||
socket_reader = SocketReader(
|
||||
host='127.0.0.1',
|
||||
port=2001,
|
||||
telegram_specification=telegram_specifications.V4
|
||||
)
|
||||
|
||||
for telegram in socket_reader.read():
|
||||
print(telegram) # see 'Telegram object' docs below
|
||||
|
||||
**AsyncIO client**
|
||||
|
||||
For a test run using a tcp server (lasting 20 seconds) use the following example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dsmr_parser import obis_references
|
||||
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||
|
||||
HOST = MY_HOST
|
||||
PORT = MY_PORT
|
||||
DSMR_VERSION = MY_DSMR_VERSION
|
||||
|
||||
logger = logging.getLogger('tcpclient')
|
||||
logger.debug("Logger created")
|
||||
|
||||
def printTelegram(telegram):
|
||||
logger.info(telegram)
|
||||
|
||||
|
||||
async def main():
|
||||
try:
|
||||
logger.debug("Getting loop")
|
||||
loop = asyncio.get_event_loop()
|
||||
logger.debug("Creating reader")
|
||||
await create_tcp_dsmr_reader(
|
||||
HOST,
|
||||
PORT,
|
||||
DSMR_VERSION,
|
||||
printTelegram,
|
||||
loop
|
||||
)
|
||||
logger.debug("Reader created going to sleep now")
|
||||
await asyncio.sleep(20)
|
||||
logger.info('Finished run')
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error: "+ e)
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
Note the creation of a callback function to call when a telegram is received. In this case `printTelegram`. Normally the used loop is the one running.
|
||||
|
||||
Currently the asyncio implementation does not support returning telegram objects directly as a `read_as_object()` for async tcp is currently not implemented.
|
||||
Moreover, the telegram passed to `telegram_callback(telegram)` is already parsed. Therefore we can't feed it into the telegram constructor directly as that expects unparsed telegrams
|
||||
|
||||
However, if we construct a mock TelegramParser that just returns the already parsed object we can work around this. An example is below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients.protocol import create_tcp_dsmr_reader
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||
|
||||
HOST = MY_HOST
|
||||
PORT = MY_PORT
|
||||
DSMR_VERSION = MY_DSMR_VERSION
|
||||
|
||||
logger = logging.getLogger('tcpclient')
|
||||
logger.debug("Logger created")
|
||||
|
||||
class mockTelegramParser(object):
|
||||
|
||||
def parse(self, telegram):
|
||||
return telegram
|
||||
|
||||
telegram_parser = mockTelegramParser()
|
||||
|
||||
def printTelegram(telegram):
|
||||
try:
|
||||
logger.info(Telegram(telegram, telegram_parser, telegram_specifications.V4))
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
async def main():
|
||||
try:
|
||||
logger.debug("Getting loop")
|
||||
loop = asyncio.get_event_loop()
|
||||
logger.debug("Creating reader")
|
||||
await create_tcp_dsmr_reader(
|
||||
HOST,
|
||||
PORT,
|
||||
DSMR_VERSION,
|
||||
printTelegram,
|
||||
loop
|
||||
)
|
||||
logger.debug("Reader created going to sleep now")
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error: "+ e)
|
||||
raise
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
logger.info('Closing down...')
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error: "+ e)
|
||||
|
||||
Parsing module usage
|
||||
--------------------
|
||||
The parsing module accepts complete unaltered telegram strings and parses these
|
||||
into a Telegram object.
|
||||
|
||||
Tip: getting full telegrams from a bytestream can be made easier by using the TelegramBuffer helper class.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
|
||||
# String is formatted in separate lines for readability.
|
||||
telegram_str = (
|
||||
'/ISk5\\2MT382-1000\r\n'
|
||||
'\r\n'
|
||||
'0-0:96.1.1(4B384547303034303436333935353037)\r\n'
|
||||
'1-0:1.8.1(12345.678*kWh)\r\n'
|
||||
'1-0:1.8.2(12345.678*kWh)\r\n'
|
||||
'1-0:2.8.1(12345.678*kWh)\r\n'
|
||||
'1-0:2.8.2(12345.678*kWh)\r\n'
|
||||
'0-0:96.14.0(0002)\r\n'
|
||||
'1-0:1.7.0(001.19*kW)\r\n'
|
||||
'1-0:2.7.0(000.00*kW)\r\n'
|
||||
'0-0:17.0.0(016*A)\r\n'
|
||||
'0-0:96.3.10(1)\r\n'
|
||||
'0-0:96.13.1(303132333435363738)\r\n'
|
||||
'0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E'
|
||||
'3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F30313233'
|
||||
'3435363738393A3B3C3D3E3F)\r\n'
|
||||
'0-1:96.1.0(3232323241424344313233343536373839)\r\n'
|
||||
'0-1:24.1.0(03)\r\n'
|
||||
'0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n'
|
||||
'(00001.001)\r\n'
|
||||
'0-1:24.4.0(1)\r\n'
|
||||
'!\r\n'
|
||||
)
|
||||
|
||||
parser = TelegramParser(telegram_specifications.V3)
|
||||
|
||||
# see 'Telegram object' docs below
|
||||
telegram = parser.parse(telegram_str)
|
||||
|
||||
Telegram object
|
||||
---------------
|
||||
---------------------
|
||||
|
||||
A dictionary of which the key indicates the field type. These regex values
|
||||
correspond to one of dsmr_parser.obis_reference constants.
|
||||
A Telegram has attributes for all the parsed values according to the given telegram specification. Each value is a DsmrObject which have a 'value' and 'unit' property. MBusObject's, which are DsmrObject's as well additionally have a 'datetime' property. The 'value' can contain any python type (int, str, Decimal) depending on the field. The 'unit' contains 'kW', 'A', 'kWh' or 'm3'.
|
||||
|
||||
The value is either a CosemObject or MBusObject. These have a 'value' and 'unit'
|
||||
property. MBusObject's additionally have a 'datetime' property. The 'value' can
|
||||
contain any python type (int, str, Decimal) depending on the field. The 'unit'
|
||||
contains 'kW', 'A', 'kWh' or 'm3'.
|
||||
Note: Telegram extends dictionary, which done for backwards compatibility. The use of keys (e.g. `telegram[obis_references.CURRENT_ELECTRICITY_USAGE]`) is deprecated.
|
||||
|
||||
Below are some examples on how to get the meter data. Alternatively check out the following unit test for a complete example: TelegramParserV5Test.test_parse
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Contents of a parsed DSMR v3 telegram
|
||||
{'\\d-\\d:17\\.0\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fc39eb8>,
|
||||
'\\d-\\d:1\\.7\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10f916390>,
|
||||
'\\d-\\d:1\\.8\\.1.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fc39e10>,
|
||||
'\\d-\\d:1\\.8\\.2.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fc39ef0>,
|
||||
'\\d-\\d:24\\.1\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fbaef28>,
|
||||
'\\d-\\d:24\\.3\\.0.+?\\r\\n.+?\\r\\n': <dsmr_parser.objects.MBusObject object at 0x10f9163c8>,
|
||||
'\\d-\\d:24\\.4\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fc39f60>,
|
||||
'\\d-\\d:2\\.7\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fc39fd0>,
|
||||
'\\d-\\d:2\\.8\\.1.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fbaee10>,
|
||||
'\\d-\\d:2\\.8\\.2.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fc39e80>,
|
||||
'\\d-\\d:96\\.13\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fc39d30>,
|
||||
'\\d-\\d:96\\.13\\.1.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fbaeeb8>,
|
||||
'\\d-\\d:96\\.14\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fbaef98>,
|
||||
'\\d-\\d:96\\.1\\.0.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fbaef60>,
|
||||
'\\d-\\d:96\\.1\\.1.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fc39f98>,
|
||||
'\\d-\\d:96\\.3\\.10.+?\\r\\n': <dsmr_parser.objects.CosemObject object at 0x10fc39dd8>}
|
||||
# Print contents of all available values
|
||||
# See dsmr_parser.obis_name_mapping for all readable telegram values.
|
||||
# The available values differ per DSMR version and meter.
|
||||
print(telegram)
|
||||
# P1_MESSAGE_HEADER: 42 [None]
|
||||
# P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None]
|
||||
# EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None]
|
||||
# ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh]
|
||||
# etc.
|
||||
|
||||
Example to get some of the values:
|
||||
# Example to get current electricity usage
|
||||
print(telegram.CURRENT_ELECTRICITY_USAGE) # <dsmr_parser.objects.CosemObject at 0x7f5e98ae5ac8>
|
||||
print(telegram.CURRENT_ELECTRICITY_USAGE.value) # Decimal('2.027')
|
||||
print(telegram.CURRENT_ELECTRICITY_USAGE.unit) # 'kW'
|
||||
|
||||
# All Mbus device readings like gas meters and water meters can be retrieved as follows. This
|
||||
# returns a list of MbusDevice objects:
|
||||
mbus_devices = telegram.MBUS_DEVICES
|
||||
|
||||
# A specific MbusDevice based on the channel it's connected to, can be retrieved as follows:
|
||||
mbus_device = telegram.get_mbus_device_by_channel(1)
|
||||
print(mbus_device.DEVICE_TYPE.value) # 3
|
||||
print(mbus_device.EQUIPMENT_IDENTIFIER_GAS.value) # '4730303339303031393336393930363139'
|
||||
print(mbus_device.HOURLY_GAS_METER_READING.value) # Decimal('246.138')
|
||||
|
||||
# DEPRECATED: the dictionary approach of getting the values by key or `.items()' or '.get() is deprecated
|
||||
telegram[obis_references.CURRENT_ELECTRICITY_USAGE]
|
||||
|
||||
|
||||
The telegram object has an iterator, can be used to find all the information elements in the current telegram:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dsmr_parser import obis_references
|
||||
|
||||
# The telegram message timestamp.
|
||||
message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP]
|
||||
|
||||
# Using the active tariff to determine the electricity being used and
|
||||
# delivered for the right tariff.
|
||||
active_tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF]
|
||||
active_tariff = int(tariff.value)
|
||||
|
||||
electricity_used_total = telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[active_tariff - 1]]
|
||||
electricity_delivered_total = telegram[obis_references.ELECTRICITY_DELIVERED_TARIFF_ALL[active_tariff - 1]]
|
||||
|
||||
gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING]
|
||||
|
||||
# See dsmr_reader.obis_references for all readable telegram values.
|
||||
# Note that the avilable values differ per DSMR version.
|
||||
|
||||
[attr for attr, value in telegram]
|
||||
Out[11]:
|
||||
['P1_MESSAGE_HEADER',
|
||||
'P1_MESSAGE_TIMESTAMP',
|
||||
'EQUIPMENT_IDENTIFIER',
|
||||
'ELECTRICITY_USED_TARIFF_1',
|
||||
'ELECTRICITY_USED_TARIFF_2',
|
||||
'ELECTRICITY_DELIVERED_TARIFF_1',
|
||||
'ELECTRICITY_DELIVERED_TARIFF_2',
|
||||
'ELECTRICITY_ACTIVE_TARIFF',
|
||||
'CURRENT_ELECTRICITY_USAGE',
|
||||
'CURRENT_ELECTRICITY_DELIVERY',
|
||||
'LONG_POWER_FAILURE_COUNT',
|
||||
'VOLTAGE_SAG_L1_COUNT',
|
||||
'VOLTAGE_SAG_L2_COUNT',
|
||||
'VOLTAGE_SAG_L3_COUNT',
|
||||
'VOLTAGE_SWELL_L1_COUNT',
|
||||
'VOLTAGE_SWELL_L2_COUNT',
|
||||
'VOLTAGE_SWELL_L3_COUNT',
|
||||
'TEXT_MESSAGE_CODE',
|
||||
'TEXT_MESSAGE',
|
||||
'DEVICE_TYPE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
|
||||
'EQUIPMENT_IDENTIFIER_GAS',
|
||||
'HOURLY_GAS_METER_READING']
|
||||
|
||||
Installation
|
||||
------------
|
||||
@ -147,6 +293,36 @@ To install DSMR Parser:
|
||||
|
||||
$ pip install dsmr-parser
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
Create a virtualenv and activate it followed by the installation of the dsmr-parser:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -e .
|
||||
|
||||
Install tox and run it:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install tox
|
||||
tox
|
||||
|
||||
You should see that the tests have succeeded:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
======================================================================================================== 59 passed in 0.91s ========================================================================================================
|
||||
py: commands[1]> pylama dsmr_parser test
|
||||
py: OK (11.55=setup[9.73]+cmd[1.29,0.53] seconds)
|
||||
congratulations :) (11.69 seconds)
|
||||
|
||||
|
||||
Now you can make changes by editing the code and rerunning tox to verify your changes.
|
||||
|
||||
Known issues
|
||||
------------
|
||||
|
||||
|
@ -16,8 +16,8 @@ def console():
|
||||
help='alternatively connect using TCP host.')
|
||||
parser.add_argument('--port', default=None,
|
||||
help='TCP port to use for connection')
|
||||
parser.add_argument('--version', default='2.2', choices=['2.2', '4'],
|
||||
help='DSMR version (2.2, 4)')
|
||||
parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L', '5S', 'Q3D'],
|
||||
help='DSMR version (2.2, 4, 5, 5B, 5L, 5S, Q3D)')
|
||||
parser.add_argument('--verbose', '-v', action='count')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
@ -1,5 +1,6 @@
|
||||
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
|
||||
SERIAL_SETTINGS_V4
|
||||
SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5
|
||||
from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader
|
||||
from dsmr_parser.clients.socket_ import SocketReader
|
||||
from dsmr_parser.clients.protocol import create_dsmr_protocol, \
|
||||
create_dsmr_reader, create_tcp_dsmr_reader
|
||||
|
173
dsmr_parser/clients/filereader.py
Normal file
173
dsmr_parser/clients/filereader.py
Normal file
@ -0,0 +1,173 @@
|
||||
import logging
|
||||
import fileinput
|
||||
import tailer
|
||||
|
||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileReader(object):
|
||||
"""
|
||||
Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects
|
||||
for each read telegram.
|
||||
Usage:
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients.filereader import FileReader
|
||||
|
||||
if __name__== "__main__":
|
||||
|
||||
infile = '/data/smartmeter/readings.txt'
|
||||
|
||||
file_reader = FileReader(
|
||||
file = infile,
|
||||
telegram_specification = telegram_specifications.V4
|
||||
)
|
||||
|
||||
for telegram in file_reader.read_as_object():
|
||||
print(telegram)
|
||||
|
||||
The file can be created like:
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5
|
||||
|
||||
if __name__== "__main__":
|
||||
|
||||
outfile = '/data/smartmeter/readings.txt'
|
||||
|
||||
serial_reader = SerialReader(
|
||||
device='/dev/ttyUSB0',
|
||||
serial_settings=SERIAL_SETTINGS_V5,
|
||||
telegram_specification=telegram_specifications.V4
|
||||
)
|
||||
|
||||
for telegram in serial_reader.read_as_object():
|
||||
f=open(outfile,"ab+")
|
||||
f.write(telegram._telegram_data.encode())
|
||||
f.close()
|
||||
"""
|
||||
|
||||
def __init__(self, file, telegram_specification):
|
||||
self._file = file
|
||||
self.telegram_parser = TelegramParser(telegram_specification)
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
self.telegram_specification = telegram_specification
|
||||
|
||||
def read_as_object(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from a file and return a Telegram object.
|
||||
:rtype: generator
|
||||
"""
|
||||
with open(self._file, "rb") as file_handle:
|
||||
while True:
|
||||
data = file_handle.readline()
|
||||
|
||||
if not data:
|
||||
break
|
||||
|
||||
self.telegram_buffer.append(data.decode())
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
logger.info(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
|
||||
class FileInputReader(object):
|
||||
"""
|
||||
Filereader to read and parse raw telegram strings from stdin or files specified at the commandline
|
||||
and instantiate Telegram objects for each read telegram.
|
||||
Usage python script "syphon_smartmeter_readings_stdin.py":
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients.filereader import FileInputReader
|
||||
|
||||
if __name__== "__main__":
|
||||
|
||||
fileinput_reader = FileReader(
|
||||
file = infile,
|
||||
telegram_specification = telegram_specifications.V4
|
||||
)
|
||||
|
||||
for telegram in fileinput_reader.read_as_object():
|
||||
print(telegram)
|
||||
|
||||
Command line:
|
||||
tail -f /data/smartmeter/readings.txt | python3 syphon_smartmeter_readings_stdin.py
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, telegram_specification):
|
||||
self.telegram_parser = TelegramParser(telegram_specification)
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
self.telegram_specification = telegram_specification
|
||||
|
||||
def read_as_object(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from stdin of filearguments specified on teh command line
|
||||
and return a Telegram object.
|
||||
:rtype: generator
|
||||
"""
|
||||
with fileinput.input(mode='rb') as file_handle:
|
||||
while True:
|
||||
data = file_handle.readline()
|
||||
str = data.decode()
|
||||
self.telegram_buffer.append(str)
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
|
||||
class FileTailReader(object):
|
||||
"""
|
||||
Filereader to read and parse raw telegram strings from the tail of a
|
||||
given file and instantiate Telegram objects for each read telegram.
|
||||
Usage python script "syphon_smartmeter_readings_stdin.py":
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients.filereader import FileTailReader
|
||||
|
||||
if __name__== "__main__":
|
||||
|
||||
infile = '/data/smartmeter/readings.txt'
|
||||
|
||||
filetail_reader = FileTailReader(
|
||||
file = infile,
|
||||
telegram_specification = telegram_specifications.V5
|
||||
)
|
||||
|
||||
for telegram in filetail_reader.read_as_object():
|
||||
print(telegram)
|
||||
"""
|
||||
|
||||
def __init__(self, file, telegram_specification):
|
||||
self._file = file
|
||||
self.telegram_parser = TelegramParser(telegram_specification)
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
self.telegram_specification = telegram_specification
|
||||
|
||||
def read_as_object(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from a files tail and return a Telegram object.
|
||||
:rtype: generator
|
||||
"""
|
||||
with open(self._file, "rb") as file_handle:
|
||||
for data in tailer.follow(file_handle):
|
||||
str = data.decode()
|
||||
self.telegram_buffer.append(str)
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
@ -4,17 +4,26 @@ from functools import partial
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from serial_asyncio import create_serial_connection
|
||||
from serial_asyncio_fast import create_serial_connection
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||
from dsmr_parser.exceptions import ParseError
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
|
||||
SERIAL_SETTINGS_V4
|
||||
SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5
|
||||
|
||||
|
||||
def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None):
|
||||
def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs):
|
||||
"""Creates a DSMR asyncio protocol."""
|
||||
protocol = _create_dsmr_protocol(dsmr_version, telegram_callback,
|
||||
DSMRProtocol, loop, **kwargs)
|
||||
return protocol
|
||||
|
||||
|
||||
# pylama noqa - because of "complex" (too long) if-elif-else.
|
||||
# Match - case might be a solution but it is not available in <3.10
|
||||
def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, **kwargs): #noqa
|
||||
"""Creates a DSMR asyncio protocol."""
|
||||
|
||||
if dsmr_version == '2.2':
|
||||
@ -23,12 +32,36 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None):
|
||||
elif dsmr_version == '4':
|
||||
specification = telegram_specifications.V4
|
||||
serial_settings = SERIAL_SETTINGS_V4
|
||||
elif dsmr_version == '4+':
|
||||
specification = telegram_specifications.V5
|
||||
serial_settings = SERIAL_SETTINGS_V4
|
||||
elif dsmr_version == '5':
|
||||
specification = telegram_specifications.V5
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
elif dsmr_version == '5B':
|
||||
specification = telegram_specifications.BELGIUM_FLUVIUS
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
elif dsmr_version == "5L":
|
||||
specification = telegram_specifications.LUXEMBOURG_SMARTY
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
elif dsmr_version == "5S":
|
||||
specification = telegram_specifications.SWEDEN
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
elif dsmr_version == "Q3D":
|
||||
specification = telegram_specifications.Q3D
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
elif dsmr_version == 'ISKRA_IE':
|
||||
specification = telegram_specifications.ISKRA_IE
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
elif dsmr_version == '5EONHU':
|
||||
specification = telegram_specifications.EON_HUNGARY
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
else:
|
||||
raise NotImplementedError("No telegram parser found for version: %s",
|
||||
dsmr_version)
|
||||
|
||||
protocol = partial(DSMRProtocol, loop, TelegramParser(specification),
|
||||
telegram_callback=telegram_callback)
|
||||
protocol = partial(protocol, loop, TelegramParser(specification),
|
||||
telegram_callback=telegram_callback, **kwargs)
|
||||
|
||||
return protocol, serial_settings
|
||||
|
||||
@ -44,10 +77,14 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):
|
||||
|
||||
|
||||
def create_tcp_dsmr_reader(host, port, dsmr_version,
|
||||
telegram_callback, loop=None):
|
||||
telegram_callback, loop=None,
|
||||
keep_alive_interval=None):
|
||||
"""Creates a DSMR asyncio protocol coroutine using TCP connection."""
|
||||
if not loop:
|
||||
loop = asyncio.get_event_loop()
|
||||
protocol, _ = create_dsmr_protocol(
|
||||
dsmr_version, telegram_callback, loop=None)
|
||||
dsmr_version, telegram_callback, loop=loop,
|
||||
keep_alive_interval=keep_alive_interval)
|
||||
conn = loop.create_connection(protocol, host, port)
|
||||
return conn
|
||||
|
||||
@ -58,7 +95,8 @@ class DSMRProtocol(asyncio.Protocol):
|
||||
transport = None
|
||||
telegram_callback = None
|
||||
|
||||
def __init__(self, loop, telegram_parser, telegram_callback=None):
|
||||
def __init__(self, loop, telegram_parser,
|
||||
telegram_callback=None, keep_alive_interval=None):
|
||||
"""Initialize class."""
|
||||
self.loop = loop
|
||||
self.log = logging.getLogger(__name__)
|
||||
@ -69,25 +107,46 @@ class DSMRProtocol(asyncio.Protocol):
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
# keep a lock until the connection is closed
|
||||
self._closed = asyncio.Event()
|
||||
self._keep_alive_interval = keep_alive_interval
|
||||
self._active = True
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Just logging for now."""
|
||||
self.transport = transport
|
||||
self.log.debug('connected')
|
||||
self._active = False
|
||||
if self.loop and self._keep_alive_interval:
|
||||
self.loop.call_later(self._keep_alive_interval, self.keep_alive)
|
||||
|
||||
def data_received(self, data):
|
||||
"""Add incoming data to buffer."""
|
||||
data = data.decode('ascii')
|
||||
|
||||
# accept latin-1 (8-bit) on the line, to allow for non-ascii transport or padding
|
||||
data = data.decode("latin1")
|
||||
self._active = True
|
||||
self.log.debug('received data: %s', data)
|
||||
self.telegram_buffer.append(data)
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
# ensure actual telegram is ascii (7-bit) only (ISO 646:1991 IRV required in section 5.5 of IEC 62056-21)
|
||||
telegram = telegram.encode("latin1").decode("ascii")
|
||||
self.handle_telegram(telegram)
|
||||
|
||||
def keep_alive(self):
|
||||
if self._active:
|
||||
self.log.debug('keep-alive checked')
|
||||
self._active = False
|
||||
if self.loop:
|
||||
self.loop.call_later(self._keep_alive_interval, self.keep_alive)
|
||||
else:
|
||||
self.log.warning('keep-alive check failed')
|
||||
if self.transport:
|
||||
self.transport.close()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""Stop when connection is lost."""
|
||||
if exc:
|
||||
self.log.exception('disconnected due to exception')
|
||||
self.log.exception('disconnected due to exception', exc_info=exc)
|
||||
else:
|
||||
self.log.info('disconnected because of close/abort.')
|
||||
self._closed.set()
|
||||
@ -98,12 +157,13 @@ class DSMRProtocol(asyncio.Protocol):
|
||||
|
||||
try:
|
||||
parsed_telegram = self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
self.log.info(str(e))
|
||||
except ParseError:
|
||||
self.log.exception("failed to parse telegram")
|
||||
else:
|
||||
self.telegram_callback(parsed_telegram)
|
||||
|
||||
@asyncio.coroutine
|
||||
def wait_closed(self):
|
||||
async def wait_closed(self):
|
||||
"""Wait until connection is closed."""
|
||||
yield from self._closed.wait()
|
||||
await self._closed.wait()
|
||||
|
62
dsmr_parser/clients/rfxtrx_protocol.py
Normal file
62
dsmr_parser/clients/rfxtrx_protocol.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""Asyncio protocol implementation for handling telegrams over a RFXtrx connection ."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from serial_asyncio_fast import create_serial_connection
|
||||
from .protocol import DSMRProtocol, _create_dsmr_protocol
|
||||
|
||||
|
||||
def create_rfxtrx_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs):
|
||||
"""Creates a RFXtrxDSMR asyncio protocol."""
|
||||
protocol = _create_dsmr_protocol(dsmr_version, telegram_callback,
|
||||
RFXtrxDSMRProtocol, loop, **kwargs)
|
||||
return protocol
|
||||
|
||||
|
||||
def create_rfxtrx_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):
|
||||
"""Creates a DSMR asyncio protocol coroutine using a RFXtrx serial port."""
|
||||
protocol, serial_settings = create_rfxtrx_dsmr_protocol(
|
||||
dsmr_version, telegram_callback, loop=None)
|
||||
serial_settings['url'] = port
|
||||
|
||||
conn = create_serial_connection(loop, protocol, **serial_settings)
|
||||
return conn
|
||||
|
||||
|
||||
def create_rfxtrx_tcp_dsmr_reader(host, port, dsmr_version,
|
||||
telegram_callback, loop=None,
|
||||
keep_alive_interval=None):
|
||||
"""Creates a DSMR asyncio protocol coroutine using a RFXtrx TCP connection."""
|
||||
if not loop:
|
||||
loop = asyncio.get_event_loop()
|
||||
protocol, _ = create_rfxtrx_dsmr_protocol(
|
||||
dsmr_version, telegram_callback, loop=loop,
|
||||
keep_alive_interval=keep_alive_interval)
|
||||
conn = loop.create_connection(protocol, host, port)
|
||||
return conn
|
||||
|
||||
|
||||
PACKETTYPE_DSMR = 0x62
|
||||
SUBTYPE_P1 = 0x01
|
||||
|
||||
|
||||
class RFXtrxDSMRProtocol(DSMRProtocol):
|
||||
|
||||
remaining_data = b''
|
||||
|
||||
def data_received(self, data):
|
||||
"""Add incoming data to buffer."""
|
||||
|
||||
data = self.remaining_data + data
|
||||
|
||||
packetlength = data[0] + 1 if len(data) > 0 else 1
|
||||
while packetlength <= len(data):
|
||||
packettype = data[1]
|
||||
subtype = data[2]
|
||||
if (packettype == PACKETTYPE_DSMR and subtype == SUBTYPE_P1):
|
||||
dsmr_data = data[4:packetlength]
|
||||
super().data_received(dsmr_data)
|
||||
data = data[packetlength:]
|
||||
packetlength = data[0] + 1 if len(data) > 0 else 1
|
||||
|
||||
self.remaining_data = data
|
@ -1,10 +1,9 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import serial
|
||||
import serial_asyncio
|
||||
import serial_asyncio_fast
|
||||
|
||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||
from dsmr_parser.exceptions import ParseError
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
|
||||
|
||||
@ -20,12 +19,32 @@ class SerialReader(object):
|
||||
|
||||
self.telegram_parser = TelegramParser(telegram_specification)
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
self.telegram_specification = telegram_specification
|
||||
|
||||
def read(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from the serial interface and parse it
|
||||
into CosemObject's and MbusObject's
|
||||
|
||||
:rtype: generator
|
||||
"""
|
||||
with serial.Serial(**self.serial_settings) as serial_handle:
|
||||
while True:
|
||||
data = serial_handle.read(max(1, min(1024, serial_handle.in_waiting)))
|
||||
self.telegram_buffer.append(data.decode('ascii'))
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
logger.info(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
def read_as_object(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from the serial interface and return a Telegram object.
|
||||
|
||||
:rtype: generator
|
||||
"""
|
||||
with serial.Serial(**self.serial_settings) as serial_handle:
|
||||
@ -36,6 +55,8 @@ class SerialReader(object):
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
@ -45,8 +66,7 @@ class AsyncSerialReader(SerialReader):
|
||||
|
||||
PORT_KEY = 'url'
|
||||
|
||||
@asyncio.coroutine
|
||||
def read(self, queue):
|
||||
async def read(self, queue):
|
||||
"""
|
||||
Read complete DSMR telegram's from the serial interface and parse it
|
||||
into CosemObject's and MbusObject's.
|
||||
@ -57,13 +77,13 @@ class AsyncSerialReader(SerialReader):
|
||||
:rtype: None
|
||||
"""
|
||||
# create Serial StreamReader
|
||||
conn = serial_asyncio.open_serial_connection(**self.serial_settings)
|
||||
reader, _ = yield from conn
|
||||
conn = serial_asyncio_fast.open_serial_connection(**self.serial_settings)
|
||||
reader, _ = await conn
|
||||
|
||||
while True:
|
||||
# Read line if available or give control back to loop until new
|
||||
# data has arrived.
|
||||
data = yield from reader.readline()
|
||||
data = await reader.readline()
|
||||
self.telegram_buffer.append(data.decode('ascii'))
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
@ -74,3 +94,35 @@ class AsyncSerialReader(SerialReader):
|
||||
)
|
||||
except ParseError as e:
|
||||
logger.warning('Failed to parse telegram: %s', e)
|
||||
|
||||
async def read_as_object(self, queue):
|
||||
"""
|
||||
Read complete DSMR telegram's from the serial interface
|
||||
and return a Telegram object.
|
||||
|
||||
Instead of being a generator, Telegram objects are pushed
|
||||
to provided queue for asynchronous processing.
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
# create Serial StreamReader
|
||||
conn = serial_asyncio_fast.open_serial_connection(**self.serial_settings)
|
||||
reader, _ = await conn
|
||||
|
||||
while True:
|
||||
|
||||
# Read line if available or give control back to loop until new
|
||||
# data has arrived.
|
||||
data = await reader.readline()
|
||||
self.telegram_buffer.append(data.decode('ascii'))
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
queue.put_nowait(
|
||||
self.telegram_parser.parse(telegram)
|
||||
)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
@ -20,3 +20,13 @@ SERIAL_SETTINGS_V4 = {
|
||||
'rtscts': 0,
|
||||
'timeout': 20
|
||||
}
|
||||
|
||||
SERIAL_SETTINGS_V5 = {
|
||||
'baudrate': 115200,
|
||||
'bytesize': serial.EIGHTBITS,
|
||||
'parity': serial.PARITY_NONE,
|
||||
'stopbits': serial.STOPBITS_ONE,
|
||||
'xonxoff': 0,
|
||||
'rtscts': 0,
|
||||
'timeout': 20
|
||||
}
|
||||
|
98
dsmr_parser/clients/socket_.py
Normal file
98
dsmr_parser/clients/socket_.py
Normal file
@ -0,0 +1,98 @@
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SocketReader(object):
|
||||
|
||||
BUFFER_SIZE = 256
|
||||
|
||||
def __init__(self, host, port, telegram_specification):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self.telegram_parser = TelegramParser(telegram_specification)
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
self.telegram_specification = telegram_specification
|
||||
|
||||
def read(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from remote interface and parse it
|
||||
into CosemObject's and MbusObject's
|
||||
|
||||
:rtype: generator
|
||||
"""
|
||||
buffer = b""
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:
|
||||
socket_handle.settimeout(60)
|
||||
socket_handle.connect((self.host, self.port))
|
||||
|
||||
while True:
|
||||
try:
|
||||
buffer += socket_handle.recv(self.BUFFER_SIZE)
|
||||
except socket.timeout:
|
||||
logger.error("Socket timeout occurred, exiting")
|
||||
break
|
||||
|
||||
lines = buffer.splitlines(keepends=True)
|
||||
|
||||
if len(lines) == 0:
|
||||
continue
|
||||
|
||||
for data in lines:
|
||||
try:
|
||||
self.telegram_buffer.append(data.decode('ascii'))
|
||||
except UnicodeDecodeError:
|
||||
# Some garbage came through the channel
|
||||
# E.g.: Happens at EON_HUNGARY, but only once at the start of the socket.
|
||||
logger.error('Failed to parse telegram due to unicode decode error')
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
logger.info(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
buffer = b""
|
||||
|
||||
def read_as_object(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from remote and return a Telegram object.
|
||||
|
||||
:rtype: generator
|
||||
"""
|
||||
buffer = b""
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:
|
||||
|
||||
socket_handle.connect((self.host, self.port))
|
||||
|
||||
while True:
|
||||
buffer += socket_handle.recv(self.BUFFER_SIZE)
|
||||
|
||||
lines = buffer.splitlines(keepends=True)
|
||||
|
||||
if len(lines) == 0:
|
||||
continue
|
||||
|
||||
for data in lines:
|
||||
self.telegram_buffer.append(data.decode('ascii'))
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
buffer = b""
|
@ -1,5 +1,13 @@
|
||||
import re
|
||||
|
||||
# - Match all characters after start of telegram except for the start
|
||||
# itself again '^\/]+', which eliminates incomplete preceding telegrams.
|
||||
# - Do non greedy match using '?' so start is matched up to the first
|
||||
# checksum that's found.
|
||||
# - The checksum is optional '{0,4}' because not all telegram versions
|
||||
# support it.
|
||||
_FIND_TELEGRAMS_REGEX = re.compile(r"\/[^\/]+?\![A-F0-9]{0,4}\0?\r\n", re.DOTALL)
|
||||
|
||||
|
||||
class TelegramBuffer(object):
|
||||
"""
|
||||
@ -8,14 +16,14 @@ class TelegramBuffer(object):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._buffer = ''
|
||||
self._buffer = ""
|
||||
|
||||
def get_all(self):
|
||||
"""
|
||||
Remove complete telegrams from buffer and yield them.
|
||||
:rtype generator:
|
||||
"""
|
||||
for telegram in self._find_telegrams():
|
||||
for telegram in _FIND_TELEGRAMS_REGEX.findall(self._buffer):
|
||||
self._remove(telegram)
|
||||
yield telegram
|
||||
|
||||
@ -37,21 +45,3 @@ class TelegramBuffer(object):
|
||||
index = self._buffer.index(telegram) + len(telegram)
|
||||
|
||||
self._buffer = self._buffer[index:]
|
||||
|
||||
def _find_telegrams(self):
|
||||
"""
|
||||
Find complete telegrams in buffer from start ('/') till ending
|
||||
checksum ('!AB12\r\n').
|
||||
:rtype: list
|
||||
"""
|
||||
# - Match all characters after start of telegram except for the start
|
||||
# itself again '^\/]+', which eliminates incomplete preceding telegrams.
|
||||
# - Do non greedy match using '?' so start is matched up to the first
|
||||
# checksum that's found.
|
||||
# - The checksum is optional '{0,4}' because not all telegram versions
|
||||
# support it.
|
||||
return re.findall(
|
||||
r'\/[^\/]+?\![A-F0-9]{0,4}\r\n',
|
||||
self._buffer,
|
||||
re.DOTALL
|
||||
)
|
||||
|
@ -6,43 +6,88 @@ refactored to full line signatures to maintain backwards compatibility.
|
||||
Might be refactored in a backwards incompatible way as soon as proper telegram
|
||||
objects are introduced.
|
||||
"""
|
||||
P1_MESSAGE_HEADER = r'\d-\d:0\.2\.8.+?\r\n'
|
||||
P1_MESSAGE_TIMESTAMP = r'\d-\d:1\.0\.0.+?\r\n'
|
||||
ELECTRICITY_USED_TARIFF_1 = r'\d-\d:1\.8\.1.+?\r\n'
|
||||
ELECTRICITY_USED_TARIFF_2 = r'\d-\d:1\.8\.2.+?\r\n'
|
||||
ELECTRICITY_DELIVERED_TARIFF_1 = r'\d-\d:2\.8\.1.+?\r\n'
|
||||
ELECTRICITY_DELIVERED_TARIFF_2 = r'\d-\d:2\.8\.2.+?\r\n'
|
||||
ELECTRICITY_ACTIVE_TARIFF = r'\d-\d:96\.14\.0.+?\r\n'
|
||||
EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n'
|
||||
CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n'
|
||||
CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n'
|
||||
LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n'
|
||||
POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n'
|
||||
VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n'
|
||||
VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n'
|
||||
VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n'
|
||||
VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n'
|
||||
VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n'
|
||||
VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n'
|
||||
TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n'
|
||||
TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n'
|
||||
DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'\d-\d:22\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'\d-\d:42\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'\d-\d:62\.7\.0.+?\r\n'
|
||||
EQUIPMENT_IDENTIFIER_GAS = r'\d-\d:96\.1\.0.+?\r\n'
|
||||
P1_MESSAGE_HEADER = r'^\d-\d:0\.2\.8.+?\r\n'
|
||||
P1_MESSAGE_TIMESTAMP = r'^\d-\d:1\.0\.0.+?\r\n'
|
||||
ELECTRICITY_USED_TARIFF_1 = r'^\d-\d:1\.8\.1.+?\r\n'
|
||||
ELECTRICITY_USED_TARIFF_2 = r'^\d-\d:1\.8\.2.+?\r\n'
|
||||
ELECTRICITY_USED_TARIFF_3 = r'^\d-\d:1\.8\.3.+?\r\n'
|
||||
ELECTRICITY_USED_TARIFF_4 = r'^\d-\d:1\.8\.4.+?\r\n'
|
||||
ELECTRICITY_DELIVERED_TARIFF_1 = r'^\d-\d:2\.8\.1.+?\r\n'
|
||||
ELECTRICITY_DELIVERED_TARIFF_2 = r'^\d-\d:2\.8\.2.+?\r\n'
|
||||
ELECTRICITY_DELIVERED_TARIFF_3 = r'^\d-\d:2\.8\.3.+?\r\n'
|
||||
ELECTRICITY_DELIVERED_TARIFF_4 = r'^\d-\d:2\.8\.4.+?\r\n'
|
||||
CURRENT_REACTIVE_IMPORTED = r'^\d-\d:3\.7\.0.+?\r\n'
|
||||
ELECTRICITY_REACTIVE_IMPORTED_TOTAL = r'^\d-\d:3\.8\.0.+?\r\n'
|
||||
ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1 = r'^\d-\d:3\.8\.1.+?\r\n'
|
||||
ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2 = r'^\d-\d:3\.8\.2.+?\r\n'
|
||||
CURRENT_REACTIVE_EXPORTED = r'^\d-\d:4\.7\.0.+?\r\n'
|
||||
ELECTRICITY_REACTIVE_EXPORTED_TOTAL = r'^\d-\d:4\.8\.0.+?\r\n'
|
||||
ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1 = r'^\d-\d:4\.8\.1.+?\r\n'
|
||||
ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2 = r'^\d-\d:4\.8\.2.+?\r\n'
|
||||
ELECTRICITY_ACTIVE_TARIFF = r'^\d-\d:96\.14\.0.+?\r\n'
|
||||
EQUIPMENT_IDENTIFIER = r'^\d-\d:96\.1\.1.+?\r\n'
|
||||
CURRENT_ELECTRICITY_USAGE = r'^\d-\d:1\.7\.0.+?\r\n'
|
||||
CURRENT_ELECTRICITY_DELIVERY = r'^\d-\d:2\.7\.0.+?\r\n'
|
||||
LONG_POWER_FAILURE_COUNT = r'^\d-\d:96\.7\.9.+?\r\n'
|
||||
SHORT_POWER_FAILURE_COUNT = r'^\d-\d:96\.7\.21.+?\r\n'
|
||||
POWER_EVENT_FAILURE_LOG = r'^\d-\d:99\.97\.0.+?\r\n'
|
||||
VOLTAGE_SAG_L1_COUNT = r'^\d-\d:32\.32\.0.+?\r\n'
|
||||
VOLTAGE_SAG_L2_COUNT = r'^\d-\d:52\.32\.0.+?\r\n'
|
||||
VOLTAGE_SAG_L3_COUNT = r'^\d-\d:72\.32\.0.+?\r\n'
|
||||
VOLTAGE_SWELL_L1_COUNT = r'^\d-\d:32\.36\.0.+?\r\n'
|
||||
VOLTAGE_SWELL_L2_COUNT = r'^\d-\d:52\.36\.0.+?\r\n'
|
||||
VOLTAGE_SWELL_L3_COUNT = r'^\d-\d:72\.36\.0.+?\r\n'
|
||||
INSTANTANEOUS_VOLTAGE_L1 = r'^\d-\d:32\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_VOLTAGE_L2 = r'^\d-\d:52\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_VOLTAGE_L3 = r'^\d-\d:72\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_CURRENT_L1 = r'^\d-\d:31\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_CURRENT_L2 = r'^\d-\d:51\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_CURRENT_L3 = r'^\d-\d:71\.7\.0.+?\r\n'
|
||||
FUSE_THRESHOLD_L1 = r'^\d-\d:31\.4\.0.+?\r\n' # Applicable when current limitation is active
|
||||
FUSE_THRESHOLD_L2 = r'^\d-\d:51\.4\.0.+?\r\n' # Applicable when current limitation is active
|
||||
FUSE_THRESHOLD_L3 = r'^\d-\d:71\.4\.0.+?\r\n' # Applicable when current limitation is active
|
||||
TEXT_MESSAGE_CODE = r'^\d-\d:96\.13\.1.+?\r\n'
|
||||
TEXT_MESSAGE = r'^\d-\d:96\.13\.0.+?\r\n'
|
||||
DEVICE_TYPE = r'^\d-\d:24\.1\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'^\d-\d:21\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'^\d-\d:41\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'^\d-\d:61\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'^\d-\d:22\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'^\d-\d:42\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'^\d-\d:62\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_REACTIVE_POWER_L1_POSITIVE = r'^\d-\d:23\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_REACTIVE_POWER_L1_NEGATIVE = r'^\d-\d:24\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_REACTIVE_POWER_L2_POSITIVE = r'^\d-\d:43\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_REACTIVE_POWER_L2_NEGATIVE = r'^\d-\d:44\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_REACTIVE_POWER_L3_POSITIVE = r'^\d-\d:63\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_REACTIVE_POWER_L3_NEGATIVE = r'^\d-\d:64\.7\.0.+?\r\n'
|
||||
EQUIPMENT_IDENTIFIER_GAS = r'^\d-\d:96\.1\.0.+?\r\n'
|
||||
# TODO differences between gas meter readings in v3 and lower and v4 and up
|
||||
HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.1.+?\r\n'
|
||||
GAS_METER_READING = r'\d-\d:24\.3\.0.+?\r\n.+?\r\n'
|
||||
ACTUAL_TRESHOLD_ELECTRICITY = r'\d-\d:17\.0\.0.+?\r\n'
|
||||
ACTUAL_SWITCH_POSITION = r'\d-\d:96\.3\.10.+?\r\n'
|
||||
VALVE_POSITION_GAS = r'\d-\d:24\.4\.0.+?\r\n'
|
||||
HOURLY_GAS_METER_READING = r'^\d-\d:24\.2\.1.+?\r\n'
|
||||
GAS_METER_READING = r'^\d-\d:24\.3\.0.+?\r\n.+?\r\n'
|
||||
ACTUAL_TRESHOLD_ELECTRICITY = r'^\d-\d:17\.0\.0.+?\r\n'
|
||||
ACTUAL_SWITCH_POSITION = r'^\d-\d:96\.3\.10.+?\r\n'
|
||||
VALVE_POSITION_GAS = r'^\d-\d:24\.4\.0.+?\r\n'
|
||||
|
||||
# Multiple 'slaves' can be linked to the main device.
|
||||
# The type is reported on 24.1.0
|
||||
# Specifications are in EN 13757-3
|
||||
# For example: Water mater = 7, Gas meter = 3
|
||||
# Identifier is on 96.1.0 (in NL for ex) or
|
||||
# on 96.1.1 (in BE for ex)
|
||||
# The values are reported on 24.2.1
|
||||
# With an exception in Belgium for the GAS meter
|
||||
# Be aware that for the gas volume, another OBIS-code is published
|
||||
# than the one listed in section 7 of DSMR P1.
|
||||
# This is due to the fact that in Belgium the not-temperature
|
||||
# corrected gas volume is used while in the Netherlands,
|
||||
# the temperature corrected gas volume is used.
|
||||
MBUS_DEVICE_TYPE = r'^\d-[1-9]:24\.1\.0.+?\r\n'
|
||||
MBUS_EQUIPMENT_IDENTIFIER = r'^\d-[1-9]:96\.1\.[01].+?\r\n'
|
||||
MBUS_VALVE_POSITION = r'^\d-[1-9]:24\.4\.0.+?\r\n'
|
||||
MBUS_METER_READING = r'^\d-[1-9]:24\.2\.[13].+?\r\n'
|
||||
|
||||
# TODO 17.0.0
|
||||
# TODO 96.3.10
|
||||
|
||||
ELECTRICITY_USED_TARIFF_ALL = (
|
||||
ELECTRICITY_USED_TARIFF_1,
|
||||
@ -52,3 +97,36 @@ ELECTRICITY_DELIVERED_TARIFF_ALL = (
|
||||
ELECTRICITY_DELIVERED_TARIFF_1,
|
||||
ELECTRICITY_DELIVERED_TARIFF_2
|
||||
)
|
||||
|
||||
# International generalized additions
|
||||
ELECTRICITY_IMPORTED_TOTAL = r'^\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+)
|
||||
ELECTRICITY_EXPORTED_TOTAL = r'^\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-)
|
||||
|
||||
# International non generalized additions (country specific) / risk for necessary refactoring
|
||||
BELGIUM_VERSION_INFORMATION = r'^\d-\d:96\.1\.4.+?\r\n'
|
||||
BELGIUM_EQUIPMENT_IDENTIFIER = r'^\d-0:96\.1\.1.+?\r\n'
|
||||
BELGIUM_CURRENT_AVERAGE_DEMAND = r'^\d-\d:1\.4\.0.+?\r\n'
|
||||
BELGIUM_MAXIMUM_DEMAND_MONTH = r'^\d-\d:1\.6\.0.+?\r\n'
|
||||
BELGIUM_MAXIMUM_DEMAND_13_MONTHS = r'^\d-\d:98\.1\.0.+?\r\n'
|
||||
|
||||
LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'^\d-\d:42\.0\.0.+?\r\n' # Logical device name
|
||||
|
||||
Q3D_EQUIPMENT_IDENTIFIER = r'^\d-\d:0\.0\.0.+?\r\n' # Logical device name
|
||||
Q3D_EQUIPMENT_STATE = r'^\d-\d:96\.5\.5.+?\r\n' # Device state (hexadecimal)
|
||||
Q3D_EQUIPMENT_SERIALNUMBER = r'^\d-\d:96\.1\.255.+?\r\n' # Device Serialnumber
|
||||
|
||||
# EON Hungary
|
||||
EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 = r'^\d-\d:5\.8\.0.+?\r\n'
|
||||
EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 = r'^\d-\d:6\.8\.0.+?\r\n'
|
||||
EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 = r'^\d-\d:7\.8\.0.+?\r\n'
|
||||
EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 = r'^\d-\d:8\.8\.0.+?\r\n'
|
||||
EON_HU_ELECTRICITY_COMBINED = r'^\d-\d:15\.8\.0.+?\r\n'
|
||||
EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL = r'^\d-\d:13\.7\.0.+?\r\n'
|
||||
EON_HU_INSTANTANEOUS_POWER_FACTOR_L1 = r'^\d-\d:33\.7\.0.+?\r\n'
|
||||
EON_HU_INSTANTANEOUS_POWER_FACTOR_L2 = r'^\d-\d:53\.7\.0.+?\r\n'
|
||||
EON_HU_INSTANTANEOUS_POWER_FACTOR_L3 = r'^\d-\d:73\.7\.0.+?\r\n'
|
||||
EON_HU_FREQUENCY = r'^\d-\d:14\.7\.0.+?\r\n'
|
||||
EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 = r'^\d-\d:5\.7\.0.+?\r\n'
|
||||
EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 = r'^\d-\d:6\.7\.0.+?\r\n'
|
||||
EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 = r'^\d-\d:7\.7\.0.+?\r\n'
|
||||
EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 = r'^\d-\d:8\.7\.0.+?\r\n'
|
||||
|
@ -1,11 +1,117 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import pytz
|
||||
|
||||
|
||||
class Telegram(dict):
|
||||
"""
|
||||
Container for parsed telegram data.
|
||||
|
||||
Attributes can be accessed on a telegram object by addressing by their english name, for example:
|
||||
telegram.ELECTRICITY_USED_TARIFF_1
|
||||
|
||||
All attributes in a telegram can be iterated over, for example:
|
||||
[k for k,v in telegram]
|
||||
yields:
|
||||
['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...]
|
||||
|
||||
Note: Dict like usage is deprecated. The inheritance from dict is because of backwards compatibility.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._item_names = []
|
||||
self._mbus_devices = []
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def add(self, obis_reference, dsmr_object, obis_name):
|
||||
# Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER
|
||||
setattr(self, obis_name, dsmr_object)
|
||||
|
||||
# TODO isinstance check: MaxDemandParser (BELGIUM_MAXIMUM_DEMAND_13_MONTHS) returns a list
|
||||
if isinstance(dsmr_object, DSMRObject) and dsmr_object.is_mbus_reading:
|
||||
self._add_mbus(obis_reference, dsmr_object, obis_name)
|
||||
elif obis_name not in self._item_names: # TODO repeating obis references
|
||||
self._item_names.append(obis_name)
|
||||
|
||||
# Fill dict which is only used for backwards compatibility
|
||||
if obis_reference not in self:
|
||||
self[obis_reference] = dsmr_object
|
||||
|
||||
def _add_mbus(self, obis_reference, dsmr_object, obis_name):
|
||||
"""
|
||||
The given DsmrObject is assumed to be Mbus related and will be grouped into a MbusDevice.
|
||||
Grouping is done by the DsmrObject channel ID.
|
||||
"""
|
||||
channel_id = dsmr_object.obis_id_code[1]
|
||||
|
||||
# Create new MbusDevice or update existing one as it's records are being added one by one.
|
||||
mbus_device = self.get_mbus_device_by_channel(channel_id)
|
||||
if not mbus_device:
|
||||
mbus_device = MbusDevice(channel_id=channel_id)
|
||||
self._mbus_devices.append(mbus_device)
|
||||
|
||||
mbus_device.add(obis_reference, dsmr_object, obis_name)
|
||||
|
||||
if not hasattr(self, 'MBUS_DEVICES'):
|
||||
setattr(self, 'MBUS_DEVICES', self._mbus_devices)
|
||||
self._item_names.append('MBUS_DEVICES')
|
||||
|
||||
def get_mbus_device_by_channel(self, channel_id):
|
||||
"""
|
||||
:rtype: MbusDevice|None
|
||||
"""
|
||||
for mbus_device in self._mbus_devices:
|
||||
if mbus_device.channel_id == channel_id:
|
||||
return mbus_device
|
||||
|
||||
def __iter__(self):
|
||||
for attr in self._item_names:
|
||||
value = getattr(self, attr)
|
||||
yield attr, value
|
||||
|
||||
def __str__(self):
|
||||
output = ""
|
||||
for attr, value in self:
|
||||
if isinstance(value, list):
|
||||
output += ''.join(map(str, value))
|
||||
else:
|
||||
output += "{}: \t {}\n".format(attr, str(value))
|
||||
|
||||
return output
|
||||
|
||||
def to_json(self):
|
||||
json_data = {}
|
||||
|
||||
for attr, value in self:
|
||||
if isinstance(value, list):
|
||||
json_data[attr] = [json.loads(item.to_json() if hasattr(item, 'to_json') else item)
|
||||
for item in value]
|
||||
elif hasattr(value, 'to_json'):
|
||||
json_data[attr] = json.loads(value.to_json())
|
||||
|
||||
return json.dumps(json_data)
|
||||
|
||||
|
||||
class DSMRObject(object):
|
||||
"""
|
||||
Represents all data from a single telegram line.
|
||||
"""
|
||||
|
||||
def __init__(self, values):
|
||||
def __init__(self, obis_id_code, values):
|
||||
self.obis_id_code = obis_id_code
|
||||
self.values = values
|
||||
|
||||
@property
|
||||
def is_mbus_reading(self):
|
||||
""" Detect Mbus related readings using obis id + channel. """
|
||||
obis_id, channel_id = self.obis_id_code
|
||||
|
||||
return obis_id == 0 and channel_id != 0
|
||||
|
||||
def to_json(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MBusObject(DSMRObject):
|
||||
|
||||
@ -19,7 +125,7 @@ class MBusObject(DSMRObject):
|
||||
# TODO object, but let the parse set them differently? So don't use
|
||||
# TODO hardcoded indexes here.
|
||||
if len(self.values) != 2: # v2
|
||||
return self.values[5]['value']
|
||||
return self.values[6]['value']
|
||||
else:
|
||||
return self.values[1]['value']
|
||||
|
||||
@ -29,10 +135,92 @@ class MBusObject(DSMRObject):
|
||||
# TODO object, but let the parse set them differently? So don't use
|
||||
# TODO hardcoded indexes here.
|
||||
if len(self.values) != 2: # v2
|
||||
return self.values[4]['value']
|
||||
return self.values[5]['value']
|
||||
else:
|
||||
return self.values[1]['unit']
|
||||
|
||||
def __str__(self):
|
||||
timestamp = self.datetime
|
||||
if isinstance(timestamp, datetime.datetime):
|
||||
timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat()
|
||||
output = "{}\t[{}] at {}".format(
|
||||
str(self.value),
|
||||
str(self.unit),
|
||||
str(timestamp)
|
||||
)
|
||||
return output
|
||||
|
||||
def to_json(self):
|
||||
timestamp = self.datetime
|
||||
if isinstance(timestamp, datetime.datetime):
|
||||
timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat()
|
||||
value = self.value
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = value.astimezone().astimezone(pytz.utc).isoformat()
|
||||
if isinstance(value, Decimal):
|
||||
value = float(value)
|
||||
output = {
|
||||
'datetime': timestamp,
|
||||
'value': value,
|
||||
'unit': self.unit
|
||||
}
|
||||
return json.dumps(output)
|
||||
|
||||
|
||||
class MBusObjectPeak(DSMRObject):
|
||||
|
||||
@property
|
||||
def datetime(self):
|
||||
return self.values[0]['value']
|
||||
|
||||
@property
|
||||
def occurred(self):
|
||||
return self.values[1]['value']
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.values[2]['value']
|
||||
|
||||
@property
|
||||
def unit(self):
|
||||
return self.values[2]['unit']
|
||||
|
||||
def __str__(self):
|
||||
timestamp = self.datetime
|
||||
if isinstance(timestamp, datetime.datetime):
|
||||
timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat()
|
||||
timestamp_occurred = self.occurred
|
||||
if isinstance(timestamp_occurred, datetime.datetime):
|
||||
timestamp_occurred = timestamp_occurred.astimezone().astimezone(pytz.utc).isoformat()
|
||||
value = self.value
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = value.astimezone().astimezone(pytz.utc).isoformat()
|
||||
if isinstance(value, Decimal):
|
||||
value = float(value)
|
||||
output = "{}\t[{}] at {} occurred {}"\
|
||||
.format(str(value), str(self.unit), str(timestamp), str(timestamp_occurred))
|
||||
return output
|
||||
|
||||
def to_json(self):
|
||||
timestamp = self.datetime
|
||||
if isinstance(timestamp, datetime.datetime):
|
||||
timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat()
|
||||
timestamp_occurred = self.occurred
|
||||
if isinstance(timestamp_occurred, datetime.datetime):
|
||||
timestamp_occurred = timestamp_occurred.astimezone().astimezone(pytz.utc).isoformat()
|
||||
value = self.value
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = value.astimezone().astimezone(pytz.utc).isoformat()
|
||||
if isinstance(value, Decimal):
|
||||
value = float(value)
|
||||
output = {
|
||||
'datetime': timestamp,
|
||||
'occurred': timestamp_occurred,
|
||||
'value': value,
|
||||
'unit': self.unit
|
||||
}
|
||||
return json.dumps(output)
|
||||
|
||||
|
||||
class CosemObject(DSMRObject):
|
||||
|
||||
@ -44,6 +232,137 @@ class CosemObject(DSMRObject):
|
||||
def unit(self):
|
||||
return self.values[0]['unit']
|
||||
|
||||
def __str__(self):
|
||||
print_value = self.value
|
||||
if isinstance(self.value, datetime.datetime):
|
||||
print_value = self.value.astimezone().astimezone(pytz.utc).isoformat()
|
||||
output = "{}\t[{}]".format(str(print_value), str(self.unit))
|
||||
return output
|
||||
|
||||
class ProfileGeneric(DSMRObject):
|
||||
pass # TODO implement
|
||||
def to_json(self):
|
||||
json_value = self.value
|
||||
if isinstance(self.value, datetime.datetime):
|
||||
json_value = self.value.astimezone().astimezone(pytz.utc).isoformat()
|
||||
if isinstance(self.value, Decimal):
|
||||
json_value = float(self.value)
|
||||
output = {
|
||||
'value': json_value,
|
||||
'unit': self.unit
|
||||
}
|
||||
return json.dumps(output)
|
||||
|
||||
|
||||
class ProfileGenericObject(DSMRObject):
|
||||
"""
|
||||
Represents all data in a GenericProfile value.
|
||||
All buffer values are returned as a list of MBusObjects,
|
||||
containing the datetime (timestamp) and the value.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._buffer_list = None
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
# value is added to make sure the telegram iterator does not break
|
||||
return self.values
|
||||
|
||||
@property
|
||||
def unit(self):
|
||||
# value is added to make sure all items have a unit so code that relies on that does not break
|
||||
return None
|
||||
|
||||
@property
|
||||
def buffer_length(self):
|
||||
return self.values[0]['value']
|
||||
|
||||
@property
|
||||
def buffer_type(self):
|
||||
return self.values[1]['value']
|
||||
|
||||
@property
|
||||
def buffer(self):
|
||||
if self._buffer_list is None:
|
||||
self._buffer_list = []
|
||||
values_offset = 2
|
||||
|
||||
for i in range(self.buffer_length):
|
||||
offset = values_offset + i * 2
|
||||
self._buffer_list.append(
|
||||
MBusObject(
|
||||
obis_id_code=self.obis_id_code,
|
||||
values=[self.values[offset], self.values[offset + 1]]
|
||||
)
|
||||
)
|
||||
|
||||
return self._buffer_list
|
||||
|
||||
def __str__(self):
|
||||
output = "\t buffer length: {}\n".format(self.buffer_length)
|
||||
output += "\t buffer type: {}".format(self.buffer_type)
|
||||
for buffer_value in self.buffer:
|
||||
timestamp = buffer_value.datetime
|
||||
if isinstance(timestamp, datetime.datetime):
|
||||
timestamp = str(timestamp.astimezone().astimezone(pytz.utc).isoformat())
|
||||
output += "\n\t event occured at: {}".format(timestamp)
|
||||
output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit)
|
||||
return output
|
||||
|
||||
def to_json(self):
|
||||
"""
|
||||
:return: A json of all values in the GenericProfileObject , with the following structure
|
||||
{'buffer_length': n,
|
||||
'buffer_type': obis_ref,
|
||||
'buffer': [{'datetime': d1,
|
||||
'value': v1,
|
||||
'unit': u1},
|
||||
...
|
||||
{'datetime': dn,
|
||||
'value': vn,
|
||||
'unit': un}
|
||||
]
|
||||
}
|
||||
"""
|
||||
list = [['buffer_length', self.buffer_length]]
|
||||
list.append(['buffer_type', self.buffer_type])
|
||||
buffer_repr = [json.loads(buffer_item.to_json()) for buffer_item in self.buffer]
|
||||
list.append(['buffer', buffer_repr])
|
||||
output = dict(list)
|
||||
return json.dumps(output)
|
||||
|
||||
|
||||
class MbusDevice:
|
||||
"""
|
||||
This object is similar to the Telegram except that it only contains readings related to the same mbus device.
|
||||
"""
|
||||
|
||||
def __init__(self, channel_id):
|
||||
self.channel_id = channel_id
|
||||
self._item_names = []
|
||||
|
||||
def add(self, obis_reference, dsmr_object, obis_name):
|
||||
# Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER
|
||||
# Also keep track of the added names internally
|
||||
setattr(self, obis_name, dsmr_object)
|
||||
self._item_names.append(obis_name)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._item_names)
|
||||
|
||||
def __iter__(self):
|
||||
for attr in self._item_names:
|
||||
value = getattr(self, attr)
|
||||
yield attr, value
|
||||
|
||||
def __str__(self):
|
||||
output = "MBUS DEVICE (channel {})\n".format(self.channel_id)
|
||||
for attr, value in self:
|
||||
output += "\t{}: \t {}\n".format(attr, str(value))
|
||||
return output
|
||||
|
||||
def to_json(self):
|
||||
data = {obis_name: json.loads(value.to_json()) for obis_name, value in self}
|
||||
data['CHANNEL_ID'] = self.channel_id
|
||||
|
||||
return json.dumps(data)
|
||||
|
@ -1,16 +1,22 @@
|
||||
import logging
|
||||
import re
|
||||
from binascii import unhexlify
|
||||
|
||||
from PyCRC.CRC16 import CRC16
|
||||
from ctypes import c_ushort
|
||||
from decimal import Decimal
|
||||
|
||||
from dsmr_parser.objects import MBusObject, CosemObject
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError, \
|
||||
TelegramSpecificationMatchError
|
||||
from dlms_cosem.connection import XDlmsApduFactory
|
||||
from dlms_cosem.protocol.xdlms import GeneralGlobalCipher
|
||||
|
||||
from dsmr_parser.objects import MBusObject, MBusObjectPeak, CosemObject, ProfileGenericObject, Telegram
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError, TelegramSpecificationMatchError
|
||||
from dsmr_parser.value_types import timestamp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TelegramParser(object):
|
||||
crc16_tab = []
|
||||
|
||||
def __init__(self, telegram_specification=None, apply_checksum_validation=True):
|
||||
"""
|
||||
@ -20,47 +26,95 @@ class TelegramParser(object):
|
||||
telegram DSMR version (v4 and up).
|
||||
:type telegram_specification: dict
|
||||
"""
|
||||
self.telegram_specification = telegram_specification
|
||||
self.apply_checksum_validation = apply_checksum_validation
|
||||
self.telegram_specification = telegram_specification
|
||||
self._telegram_specification_regex = None
|
||||
|
||||
def parse(self, telegram_data):
|
||||
def parse(self, telegram_data, encryption_key="", authentication_key="", throw_ex=False): # noqa: C901
|
||||
"""
|
||||
Parse telegram from string to dict.
|
||||
|
||||
The telegram str type makes python 2.x integration easier.
|
||||
|
||||
:param str telegram_data: full telegram from start ('/') to checksum
|
||||
('!ABCD') including line endings in between the telegram's lines
|
||||
:rtype: dict
|
||||
:returns: Shortened example:
|
||||
{
|
||||
..
|
||||
r'\d-\d:96\.1\.1.+?\r\n': <CosemObject>, # EQUIPMENT_IDENTIFIER
|
||||
r'\d-\d:1\.8\.1.+?\r\n': <CosemObject>, # ELECTRICITY_USED_TARIFF_1
|
||||
r'\d-\d:24\.3\.0.+?\r\n.+?\r\n': <MBusObject>, # GAS_METER_READING
|
||||
..
|
||||
}
|
||||
:param str encryption_key: encryption key
|
||||
:param str authentication_key: authentication key
|
||||
:rtype: Telegram
|
||||
:raises ParseError:
|
||||
:raises InvalidChecksumError:
|
||||
"""
|
||||
if not self.telegram_specification:
|
||||
self.telegram_specification = \
|
||||
match_telegram_specification(telegram_data)
|
||||
if not self._telegram_specification_regex:
|
||||
# Regexes are compiled once to improve performance
|
||||
self._telegram_specification_regexes = {
|
||||
object["obis_reference"]: re.compile(object["obis_reference"], re.DOTALL | re.MULTILINE)
|
||||
for object in self.telegram_specification['objects']
|
||||
}
|
||||
|
||||
if self.apply_checksum_validation \
|
||||
and self.telegram_specification['checksum_support']:
|
||||
if "general_global_cipher" in self.telegram_specification:
|
||||
if self.telegram_specification["general_global_cipher"]:
|
||||
enc_key = unhexlify(encryption_key)
|
||||
auth_key = unhexlify(authentication_key)
|
||||
telegram_data = unhexlify(telegram_data)
|
||||
apdu = XDlmsApduFactory.apdu_from_bytes(apdu_bytes=telegram_data)
|
||||
if apdu.security_control.security_suite != 0:
|
||||
logger.warning("Untested security suite")
|
||||
if apdu.security_control.authenticated and not apdu.security_control.encrypted:
|
||||
logger.warning("Untested authentication only")
|
||||
if not apdu.security_control.authenticated and not apdu.security_control.encrypted:
|
||||
logger.warning("Untested not encrypted or authenticated")
|
||||
if apdu.security_control.compressed:
|
||||
logger.warning("Untested compression")
|
||||
if apdu.security_control.broadcast_key:
|
||||
logger.warning("Untested broadcast key")
|
||||
telegram_data = apdu.to_plain_apdu(enc_key, auth_key).decode("ascii")
|
||||
else:
|
||||
try:
|
||||
if unhexlify(telegram_data[0:2])[0] == GeneralGlobalCipher.TAG:
|
||||
raise RuntimeError("Looks like a general_global_cipher frame "
|
||||
"but telegram specification is not matching!")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
if unhexlify(telegram_data[0:2])[0] == GeneralGlobalCipher.TAG:
|
||||
raise RuntimeError(
|
||||
"Looks like a general_global_cipher frame but telegram specification is not matching!")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.apply_checksum_validation and self.telegram_specification['checksum_support']:
|
||||
self.validate_checksum(telegram_data)
|
||||
|
||||
telegram = {}
|
||||
telegram = Telegram()
|
||||
|
||||
for signature, parser in self.telegram_specification['objects'].items():
|
||||
match = re.search(signature, telegram_data, re.DOTALL)
|
||||
for object in self.telegram_specification['objects']:
|
||||
pattern = self._telegram_specification_regexes[object["obis_reference"]]
|
||||
matches = pattern.findall(telegram_data)
|
||||
|
||||
# All telegram specification lines/signatures are expected to be
|
||||
# present.
|
||||
if not match:
|
||||
raise ParseError('Telegram specification does not match '
|
||||
'telegram data')
|
||||
telegram[signature] = parser.parse(match.group(0))
|
||||
# Some signatures are optional and may not be present,
|
||||
# so only parse lines that match
|
||||
for match in matches:
|
||||
try:
|
||||
dsmr_object = object["value_parser"].parse(match)
|
||||
except ParseError:
|
||||
logger.error(
|
||||
"ignore line with signature {}, because parsing failed.".format(object["obis_reference"]),
|
||||
exc_info=True
|
||||
)
|
||||
if throw_ex:
|
||||
raise
|
||||
except Exception as err:
|
||||
logger.error("Unexpected {}: {}".format(type(err), err))
|
||||
raise
|
||||
else:
|
||||
telegram.add(
|
||||
obis_reference=object["obis_reference"],
|
||||
dsmr_object=dsmr_object,
|
||||
obis_name=object["value_name"]
|
||||
)
|
||||
|
||||
return telegram
|
||||
|
||||
@ -85,7 +139,7 @@ class TelegramParser(object):
|
||||
'incomplete. The checksum and/or content values are missing.'
|
||||
)
|
||||
|
||||
calculated_crc = CRC16().calculate(checksum_contents.group(0))
|
||||
calculated_crc = TelegramParser.crc16(checksum_contents.group(0))
|
||||
expected_crc = int(checksum_hex.group(0), base=16)
|
||||
|
||||
if calculated_crc != expected_crc:
|
||||
@ -97,6 +151,33 @@ class TelegramParser(object):
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def crc16(telegram):
|
||||
"""
|
||||
Calculate the CRC16 value for the given telegram
|
||||
|
||||
:param str telegram:
|
||||
"""
|
||||
crcValue = 0x0000
|
||||
|
||||
if len(TelegramParser.crc16_tab) == 0:
|
||||
for i in range(0, 256):
|
||||
crc = c_ushort(i).value
|
||||
for j in range(0, 8):
|
||||
if (crc & 0x0001):
|
||||
crc = c_ushort(crc >> 1).value ^ 0xA001
|
||||
else:
|
||||
crc = c_ushort(crc >> 1).value
|
||||
TelegramParser.crc16_tab.append(hex(crc))
|
||||
|
||||
for c in telegram:
|
||||
d = ord(c)
|
||||
tmp = crcValue ^ d
|
||||
rotated = c_ushort(crcValue >> 8).value
|
||||
crcValue = rotated ^ int(TelegramParser.crc16_tab[(tmp & 0x00ff)], 0)
|
||||
|
||||
return crcValue
|
||||
|
||||
|
||||
def match_telegram_specification(telegram_data):
|
||||
"""
|
||||
@ -128,6 +209,7 @@ def match_telegram_specification(telegram_data):
|
||||
)
|
||||
|
||||
|
||||
|
||||
class DSMRObjectParser(object):
|
||||
"""
|
||||
Parses an object (can also be see as a 'line') from a telegram.
|
||||
@ -136,19 +218,42 @@ class DSMRObjectParser(object):
|
||||
def __init__(self, *value_formats):
|
||||
self.value_formats = value_formats
|
||||
|
||||
def _is_line_wellformed(self, line, values):
|
||||
# allows overriding by child class
|
||||
return (values and (len(values) == len(self.value_formats)))
|
||||
|
||||
def _parse_values(self, values):
|
||||
# allows overriding by child class
|
||||
return [self.value_formats[i].parse(value)
|
||||
for i, value in enumerate(values)]
|
||||
|
||||
def _parse_obis_id_code(self, line):
|
||||
"""
|
||||
Get the OBIS ID code
|
||||
|
||||
Example line:
|
||||
'0-2:24.2.1(200426223001S)(00246.138*m3)'
|
||||
|
||||
OBIS ID code = 0-2 returned as tuple
|
||||
"""
|
||||
try:
|
||||
return int(line[0]), int(line[2])
|
||||
except ValueError:
|
||||
raise ParseError("Invalid OBIS ID code for line '%s' in '%s'", line, self)
|
||||
|
||||
def _parse(self, line):
|
||||
# Match value groups, but exclude the parentheses
|
||||
pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*]{0,}(?=\)))+')
|
||||
pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))')
|
||||
|
||||
values = re.findall(pattern, line)
|
||||
|
||||
if not self._is_line_wellformed(line, values):
|
||||
raise ParseError("Invalid '%s' line for '%s'", line, self)
|
||||
|
||||
# Convert empty value groups to None for clarity.
|
||||
values = [None if value == '' else value for value in values]
|
||||
|
||||
if not values or len(values) != len(self.value_formats):
|
||||
raise ParseError("Invalid '%s' line for '%s'", line, self)
|
||||
|
||||
return [self.value_formats[i].parse(value)
|
||||
for i, value in enumerate(values)]
|
||||
return self._parse_values(values)
|
||||
|
||||
|
||||
class MBusParser(DSMRObjectParser):
|
||||
@ -169,7 +274,52 @@ class MBusParser(DSMRObjectParser):
|
||||
"""
|
||||
|
||||
def parse(self, line):
|
||||
return MBusObject(self._parse(line))
|
||||
return MBusObject(
|
||||
obis_id_code=self._parse_obis_id_code(line),
|
||||
values=self._parse(line)
|
||||
)
|
||||
|
||||
|
||||
class MaxDemandParser(DSMRObjectParser):
|
||||
"""
|
||||
Max demand history parser.
|
||||
|
||||
These are lines with multiple values. Each containing 2 timestamps and a value
|
||||
|
||||
Line format:
|
||||
'ID (Count) (ID) (ID) (TST) (TST) (Mv1*U1)'
|
||||
|
||||
1 2 3 4 5 6 7
|
||||
|
||||
1) OBIS Reduced ID-code
|
||||
2) Amount of values in the response
|
||||
3) ID of the source
|
||||
4) ^^
|
||||
5) Time Stamp (TST) of the month
|
||||
6) Time Stamp (TST) when the max demand occured
|
||||
6) Measurement value 1 (most recent entry of buffer attribute without unit)
|
||||
7) Unit of measurement values (Unit of capture objects attribute)
|
||||
"""
|
||||
|
||||
def parse(self, line):
|
||||
pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))')
|
||||
values = re.findall(pattern, line)
|
||||
|
||||
obis_id_code = self._parse_obis_id_code(line)
|
||||
|
||||
objects = []
|
||||
|
||||
count = int(values[0])
|
||||
for i in range(1, count + 1):
|
||||
timestamp_month = ValueParser(timestamp).parse(values[i * 3 + 0])
|
||||
timestamp_occurred = ValueParser(timestamp).parse(values[i * 3 + 1])
|
||||
value = ValueParser(Decimal).parse(values[i * 3 + 2])
|
||||
objects.append(MBusObjectPeak(
|
||||
obis_id_code=obis_id_code,
|
||||
values=[timestamp_month, timestamp_occurred, value]
|
||||
))
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
class CosemParser(DSMRObjectParser):
|
||||
@ -185,14 +335,18 @@ class CosemParser(DSMRObjectParser):
|
||||
1 23 45
|
||||
|
||||
1) OBIS Reduced ID-code
|
||||
2) Separator “(“, ASCII 28h
|
||||
2) Separator "(", ASCII 28h
|
||||
3) COSEM object attribute value
|
||||
4) Unit of measurement values (Unit of capture objects attribute) – only if applicable
|
||||
5) Separator “)”, ASCII 29h
|
||||
4) Unit of measurement values (Unit of capture objects attribute) - only if
|
||||
applicable
|
||||
5) Separator ")", ASCII 29h
|
||||
"""
|
||||
|
||||
def parse(self, line):
|
||||
return CosemObject(self._parse(line))
|
||||
return CosemObject(
|
||||
obis_id_code=self._parse_obis_id_code(line),
|
||||
values=self._parse(line)
|
||||
)
|
||||
|
||||
|
||||
class ProfileGenericParser(DSMRObjectParser):
|
||||
@ -217,8 +371,44 @@ class ProfileGenericParser(DSMRObjectParser):
|
||||
9) Unit of buffer values (Unit of capture objects attribute)
|
||||
"""
|
||||
|
||||
def __init__(self, buffer_types, head_parsers, parsers_for_unidentified):
|
||||
self.value_formats = head_parsers.copy()
|
||||
self.buffer_types = buffer_types
|
||||
self.parsers_for_unidentified = parsers_for_unidentified
|
||||
|
||||
def _is_line_wellformed(self, line, values):
|
||||
if values and (len(values) == 1) and (values[0] == ''):
|
||||
# special case: single empty parentheses (indicated by empty string)
|
||||
return True
|
||||
|
||||
if values and (len(values) >= 2) and (values[0].isdigit()):
|
||||
buffer_length = int(values[0])
|
||||
return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2))
|
||||
else:
|
||||
return False
|
||||
|
||||
def _parse_values(self, values):
|
||||
if values and (len(values) == 1) and (values[0] is None):
|
||||
# special case: single empty parentheses; make sure empty ProfileGenericObject is created
|
||||
values = [0, None] # buffer_length=0, buffer_value_obis_ID=None
|
||||
buffer_length = int(values[0])
|
||||
buffer_value_obis_ID = values[1]
|
||||
if (buffer_length > 0):
|
||||
if buffer_value_obis_ID in self.buffer_types:
|
||||
bufferValueParsers = self.buffer_types[buffer_value_obis_ID]
|
||||
else:
|
||||
bufferValueParsers = self.parsers_for_unidentified
|
||||
# add the parsers for the encountered value type z times
|
||||
for _ in range(buffer_length):
|
||||
self.value_formats.extend(bufferValueParsers)
|
||||
|
||||
return [self.value_formats[i].parse(value) for i, value in enumerate(values)]
|
||||
|
||||
def parse(self, line):
|
||||
raise NotImplementedError()
|
||||
return ProfileGenericObject(
|
||||
obis_id_code=self._parse_obis_id_code(line),
|
||||
values=self._parse(line)
|
||||
)
|
||||
|
||||
|
||||
class ValueParser(object):
|
||||
@ -226,7 +416,7 @@ class ValueParser(object):
|
||||
Parses a single value from DSMRObject's.
|
||||
|
||||
Example with coerce_type being int:
|
||||
(002*A) becomes {'value': 1, 'unit': 'A'}
|
||||
(002*A) becomes {'value': 2, 'unit': 'A'}
|
||||
|
||||
Example with coerce_type being str:
|
||||
(42) becomes {'value': '42', 'unit': None}
|
||||
@ -236,7 +426,6 @@ class ValueParser(object):
|
||||
self.coerce_type = coerce_type
|
||||
|
||||
def parse(self, value):
|
||||
|
||||
unit_of_measurement = None
|
||||
|
||||
if value and '*' in value:
|
||||
|
10
dsmr_parser/profile_generic_specifications.py
Normal file
10
dsmr_parser/profile_generic_specifications.py
Normal file
@ -0,0 +1,10 @@
|
||||
from dsmr_parser.parsers import ValueParser
|
||||
from dsmr_parser.value_types import timestamp
|
||||
|
||||
PG_FAILURE_EVENT = r'0-0:96.7.19'
|
||||
|
||||
PG_HEAD_PARSERS = [ValueParser(int), ValueParser(str)]
|
||||
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)]
|
||||
BUFFER_TYPES = {
|
||||
PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -2,17 +2,28 @@ import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
# TODO : Use system timezone
|
||||
# Preload timezone to avoid loading in event loop later
|
||||
local_tz = pytz.timezone('Europe/Amsterdam')
|
||||
|
||||
|
||||
def timestamp(value):
|
||||
naive_datetime = datetime.datetime.strptime(value[:-1], '%y%m%d%H%M%S')
|
||||
try:
|
||||
naive_datetime = datetime.datetime.strptime(value[:-1], '%y%m%d%H%M%S')
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
# TODO comment on this exception
|
||||
# Timestamp has the following format:
|
||||
# YYMMDDhhmmssX
|
||||
# ASCII presentation of Time stamp with
|
||||
# Year, Month, Day, Hour, Minute, Second,
|
||||
# and an indication whether DST is active
|
||||
# (X=S) or DST is not active (X=W)
|
||||
if len(value) == 13:
|
||||
is_dst = value[12] == 'S' # assume format 160322150000W
|
||||
else:
|
||||
is_dst = False
|
||||
|
||||
local_tz = pytz.timezone('Europe/Amsterdam')
|
||||
localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst)
|
||||
|
||||
return localized_datetime.astimezone(pytz.utc)
|
||||
|
14
setup.py
14
setup.py
@ -3,16 +3,18 @@ from setuptools import setup, find_packages
|
||||
setup(
|
||||
name='dsmr-parser',
|
||||
description='Library to parse Dutch Smart Meter Requirements (DSMR)',
|
||||
author='Nigel Dokter',
|
||||
author_email='nigeldokter@gmail.com',
|
||||
author='Nigel Dokter and many others',
|
||||
author_email='mail@nldr.net',
|
||||
license='MIT',
|
||||
url='https://github.com/ndokter/dsmr_parser',
|
||||
version='0.9',
|
||||
packages=find_packages(),
|
||||
version='1.5.0',
|
||||
packages=find_packages(exclude=('test', 'test.*')),
|
||||
install_requires=[
|
||||
'pyserial>=3,<4',
|
||||
'pyserial-asyncio<1',
|
||||
'pyserial-asyncio-fast>=0.11',
|
||||
'pytz',
|
||||
'PyCRC>=1.2,<2'
|
||||
'Tailer==0.4.1',
|
||||
'dlms_cosem==21.3.2'
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
|
||||
|
@ -1,5 +1,5 @@
|
||||
TELEGRAM_V2_2 = (
|
||||
'/ISk5\2MT382-1004\r\n'
|
||||
'/ISk5\\2MT382-1004\r\n'
|
||||
'\r\n'
|
||||
'0-0:96.1.1(00000000000000)\r\n'
|
||||
'1-0:1.8.1(00001.001*kWh)\r\n'
|
||||
@ -22,7 +22,7 @@ TELEGRAM_V2_2 = (
|
||||
)
|
||||
|
||||
TELEGRAM_V3 = (
|
||||
'/ISk5\2MT382-1000\r\n'
|
||||
'/ISk5\\2MT382-1000\r\n'
|
||||
'\r\n'
|
||||
'0-0:96.1.1(4B384547303034303436333935353037)\r\n'
|
||||
'1-0:1.8.1(12345.678*kWh)\r\n'
|
||||
@ -87,7 +87,7 @@ TELEGRAM_V4_2 = (
|
||||
)
|
||||
|
||||
TELEGRAM_V5 = (
|
||||
'/ISk5\2MT382-1000\r\n'
|
||||
'/ISk5\\2MT382-1000\r\n'
|
||||
'\r\n'
|
||||
'1-3:0.2.8(50)\r\n'
|
||||
'0-0:1.0.0(170102192002W)\r\n'
|
||||
@ -126,5 +126,282 @@ TELEGRAM_V5 = (
|
||||
'0-1:24.2.1(170102161005W)(00000.107*m3)\r\n'
|
||||
'0-2:24.1.0(003)\r\n'
|
||||
'0-2:96.1.0()\r\n'
|
||||
'!87B3\r\n'
|
||||
'!6EEE\r\n'
|
||||
)
|
||||
|
||||
# V5 telegram with 2 MBUS devices
|
||||
TELEGRAM_V5_TWO_MBUS = (
|
||||
'/ISK5\\2M550T-1012\r\n'
|
||||
'\r\n'
|
||||
'1-3:0.2.8(50)\r\n'
|
||||
'0-0:1.0.0(200426223325S)\r\n'
|
||||
'0-0:96.1.1(4530303434303037333832323436303139)\r\n'
|
||||
'1-0:1.8.1(002130.115*kWh)\r\n'
|
||||
'1-0:1.8.2(000245.467*kWh)\r\n'
|
||||
'1-0:2.8.1(000000.000*kWh)\r\n'
|
||||
'1-0:2.8.2(000000.000*kWh)\r\n'
|
||||
'0-0:96.14.0(0001)\r\n'
|
||||
'1-0:1.7.0(00.111*kW)\r\n'
|
||||
'1-0:2.7.0(00.000*kW)\r\n'
|
||||
'0-0:96.7.21(00005)\r\n'
|
||||
'0-0:96.7.9(00003)\r\n'
|
||||
'1-0:99.97.0(1)(0-0:96.7.19)(190326095015W)(0000002014*s)\r\n'
|
||||
'1-0:32.32.0(00001)\r\n'
|
||||
'1-0:52.32.0(00001)\r\n'
|
||||
'1-0:72.32.0(00192)\r\n'
|
||||
'1-0:32.36.0(00001)\r\n'
|
||||
'1-0:52.36.0(00001)\r\n'
|
||||
'1-0:72.36.0(00001)\r\n'
|
||||
'0-0:96.13.0()\r\n'
|
||||
'1-0:32.7.0(229.9*V)\r\n'
|
||||
'1-0:52.7.0(229.2*V)\r\n'
|
||||
'1-0:72.7.0(222.9*V)\r\n'
|
||||
'1-0:31.7.0(000*A)\r\n'
|
||||
'1-0:51.7.0(000*A)\r\n'
|
||||
'1-0:71.7.0(001*A)\r\n'
|
||||
'1-0:21.7.0(00.056*kW)\r\n'
|
||||
'1-0:41.7.0(00.000*kW)\r\n'
|
||||
'1-0:61.7.0(00.055*kW)\r\n'
|
||||
'1-0:22.7.0(00.000*kW)\r\n'
|
||||
'1-0:42.7.0(00.000*kW)\r\n'
|
||||
'1-0:62.7.0(00.000*kW)\r\n'
|
||||
'0-1:24.1.0(003)\r\n'
|
||||
'0-1:96.1.0()\r\n'
|
||||
'0-1:24.2.1(700101010000W)(00000000)\r\n'
|
||||
'0-2:24.1.0(003)\r\n'
|
||||
'0-2:96.1.0(4730303339303031393336393930363139)\r\n'
|
||||
'0-2:24.2.1(200426223001S)(00246.138*m3)\r\n'
|
||||
'!56DD\r\n'
|
||||
)
|
||||
|
||||
TELEGRAM_FLUVIUS_V171 = (
|
||||
'/FLU5\\253769484_A\r\n'
|
||||
'\r\n'
|
||||
'0-0:96.1.4(50217)\r\n'
|
||||
'0-0:96.1.1(3153414733313031303231363035)\r\n'
|
||||
'0-0:1.0.0(200512135409S)\r\n'
|
||||
'1-0:1.8.1(000000.034*kWh)\r\n'
|
||||
'1-0:1.8.2(000015.758*kWh)\r\n'
|
||||
'1-0:2.8.1(000000.000*kWh)\r\n'
|
||||
'1-0:2.8.2(000000.011*kWh)\r\n'
|
||||
'1-0:1.4.0(02.351*kW)\r\n'
|
||||
'1-0:1.6.0(200509134558S)(02.589*kW)\r\n'
|
||||
'0-0:98.1.0(3)(1-0:1.6.0)(1-0:1.6.0)(200501000000S)(200423192538S)(03.695*kW)(200401000000S)(200305122139S)(05.980*kW)(200301000000S)(200210035421W)(04.318*kW)\r\n'
|
||||
'0-0:96.14.0(0001)\r\n'
|
||||
'1-0:1.7.0(00.000*kW)\r\n'
|
||||
'1-0:2.7.0(00.000*kW)\r\n'
|
||||
'1-0:21.7.0(00.000*kW)\r\n'
|
||||
'1-0:41.7.0(00.000*kW)\r\n'
|
||||
'1-0:61.7.0(00.000*kW)\r\n'
|
||||
'1-0:22.7.0(00.000*kW)\r\n'
|
||||
'1-0:42.7.0(00.000*kW)\r\n'
|
||||
'1-0:62.7.0(00.000*kW)\r\n'
|
||||
'1-0:32.7.0(234.7*V)\r\n'
|
||||
'1-0:52.7.0(234.7*V)\r\n'
|
||||
'1-0:72.7.0(234.7*V)\r\n'
|
||||
'1-0:31.7.0(000.00*A)\r\n'
|
||||
'1-0:51.7.0(000.00*A)\r\n'
|
||||
'1-0:71.7.0(000.00*A)\r\n'
|
||||
'0-0:96.3.10(1)\r\n'
|
||||
'0-0:17.0.0(999.9*kW)\r\n'
|
||||
'1-0:31.4.0(999*A)\r\n'
|
||||
'0-0:96.13.0()\r\n'
|
||||
'0-1:24.1.0(003)\r\n'
|
||||
'0-1:96.1.1(37464C4F32313139303333373333)\r\n'
|
||||
'0-1:24.4.0(1)\r\n'
|
||||
'0-1:24.2.3(200512134558S)(00112.384*m3)\r\n'
|
||||
'0-2:24.1.0(007)\r\n'
|
||||
'0-2:96.1.1(3853414731323334353637383930)\r\n'
|
||||
'0-2:24.2.1(200512134558S)(00872.234*m3)\r\n'
|
||||
'!3AD7\r\n'
|
||||
)
|
||||
|
||||
TELEGRAM_FLUVIUS_V171_ALT = (
|
||||
'/FLU5\\253769484_A\r\n'
|
||||
'\r\n'
|
||||
'0-0:96.1.4(50217)\r\n'
|
||||
'0-0:96.1.1(3153414733313030373231333236)\r\n'
|
||||
'0-0:1.0.0(231102121548W)\r\n'
|
||||
'1-0:1.8.1(000301.548*kWh)\r\n'
|
||||
'1-0:1.8.2(000270.014*kWh)\r\n'
|
||||
'1-0:2.8.1(000000.005*kWh)\r\n'
|
||||
'1-0:2.8.2(000000.000*kWh)\r\n'
|
||||
'0-0:96.14.0(0001)\r\n'
|
||||
'1-0:1.4.0(00.052*kW)\r\n'
|
||||
'1-0:1.6.0(231102114500W)(03.064*kW)\r\n'
|
||||
'0-0:98.1.0(4)(1-0:1.6.0)(1-0:1.6.0)(230801000000S)(632525252525W)(00.000*kW)(230901000000S)(230831181500S)(01.862*kW)(231001000000S)(230910183000S)(04.229*kW)(231101000000W)(231016130000S)(04.927*kW)\r\n'
|
||||
'1-0:1.7.0(00.338*kW)\r\n'
|
||||
'1-0:2.7.0(00.000*kW)\r\n'
|
||||
'1-0:21.7.0(00.047*kW)\r\n'
|
||||
'1-0:41.7.0(00.179*kW)\r\n'
|
||||
'1-0:61.7.0(00.111*kW)\r\n'
|
||||
'1-0:22.7.0(00.000*kW)\r\n'
|
||||
'1-0:42.7.0(00.000*kW)\r\n'
|
||||
'1-0:62.7.0(00.000*kW)\r\n'
|
||||
'1-0:32.7.0(232.9*V)\r\n'
|
||||
'1-0:52.7.0(228.1*V)\r\n'
|
||||
'1-0:72.7.0(228.1*V)\r\n'
|
||||
'1-0:31.7.0(000.27*A)\r\n'
|
||||
'1-0:51.7.0(000.88*A)\r\n'
|
||||
'1-0:71.7.0(000.52*A)\r\n'
|
||||
'0-0:96.3.10(1)\r\n'
|
||||
'0-0:17.0.0(999.9*kW)\r\n'
|
||||
'1-0:31.4.0(999*A)\r\n'
|
||||
'0-0:96.13.0()\r\n'
|
||||
'0-1:24.1.0(003)\r\n'
|
||||
'0-1:96.1.1(37464C4F32313233303838303237)\r\n'
|
||||
'0-1:24.4.0(1)\r\n'
|
||||
'0-1:24.2.3(231102121002W)(00092.287*m3)\r\n'
|
||||
'0-2:24.1.0(007)\r\n'
|
||||
'0-2:96.1.1(3853455430303030393631313733)\r\n'
|
||||
'0-2:24.2.1(231102121532W)(00008.579*m3)\r\n'
|
||||
'!C4B0\r\n'
|
||||
)
|
||||
|
||||
# EasyMeter via COM-1 Ethernet Gateway
|
||||
# Q3D Manual (german) https://www.easymeter.com/downloads/products/zaehler/Q3D/Easymeter_Q3D_DE_2016-06-15.pdf
|
||||
# - type code on page 8
|
||||
# - D0-Specs on page 20
|
||||
#
|
||||
# last two lines are added by the COM-1 Ethernet Gateway
|
||||
|
||||
TELEGRAM_ESY5Q3DB1024_V304 = (
|
||||
'/ESY5Q3DB1024 V3.04\r\n'
|
||||
'\r\n'
|
||||
'1-0:0.0.0*255(0272031312565)\r\n'
|
||||
'1-0:1.8.0*255(00052185.7825309*kWh)\r\n'
|
||||
'1-0:2.8.0*255(00019949.3221493*kWh)\r\n'
|
||||
'1-0:21.7.0*255(000747.85*W)\r\n'
|
||||
'1-0:41.7.0*255(000737.28*W)\r\n'
|
||||
'1-0:61.7.0*255(000639.73*W)\r\n'
|
||||
'1-0:1.7.0*255(002124.86*W)\r\n'
|
||||
'1-0:96.5.5*255(80)\r\n'
|
||||
'0-0:96.1.255*255(1ESY1313002565)\r\n'
|
||||
'!\r\n'
|
||||
' 25803103\r\n'
|
||||
'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
|
||||
'\xff\xff\xff\xff\xff\r\n'
|
||||
)
|
||||
|
||||
TELEGRAM_ESY5Q3DA1004_V304 = (
|
||||
'/ESY5Q3DA1004 V3.04\r\n'
|
||||
'\r\n'
|
||||
'1-0:0.0.0*255(1336001560)\r\n'
|
||||
'1-0:1.8.0*255(00032549.5061662*kWh)\r\n'
|
||||
'1-0:21.7.0*255(000557.29*W)\r\n'
|
||||
'1-0:41.7.0*255(000521.62*W)\r\n'
|
||||
'1-0:61.7.0*255(000609.30*W)\r\n'
|
||||
'1-0:1.7.0*255(001688.21*W)\r\n'
|
||||
'1-0:96.5.5*255(80)\r\n'
|
||||
'0-0:96.1.255*255(1ESY1336001560)\r\n'
|
||||
'!\r\n'
|
||||
' 25818685\r\n'
|
||||
'DE0000000000000000000000000000003\r\n'
|
||||
)
|
||||
|
||||
TELEGRAM_SAGEMCOM_T210_D_R = (
|
||||
'/EST5\\253710000_A\r\n'
|
||||
'\r\n'
|
||||
'1-3:0.2.8(50)\r\n'
|
||||
'0-0:1.0.0(221006155014S)\r\n'
|
||||
'1-0:1.8.0(006545766*Wh)\r\n'
|
||||
'1-0:1.8.1(005017120*Wh)\r\n'
|
||||
'1-0:1.8.2(001528646*Wh)\r\n'
|
||||
'1-0:1.7.0(000000286*W)\r\n'
|
||||
'1-0:2.8.0(000000058*Wh)\r\n'
|
||||
'1-0:2.8.1(000000000*Wh)\r\n'
|
||||
'1-0:2.8.2(000000058*Wh)\r\n'
|
||||
'1-0:2.7.0(000000000*W)\r\n'
|
||||
'1-0:3.8.0(000000747*varh)\r\n'
|
||||
'1-0:3.8.1(000000000*varh)\r\n'
|
||||
'1-0:3.8.2(000000747*varh)\r\n'
|
||||
'1-0:3.7.0(000000000*var)\r\n'
|
||||
'1-0:4.8.0(003897726*varh)\r\n'
|
||||
'1-0:4.8.1(002692848*varh)\r\n'
|
||||
'1-0:4.8.2(001204878*varh)\r\n'
|
||||
'1-0:4.7.0(000000166*var)\r\n'
|
||||
'!7EF9\r\n'
|
||||
)
|
||||
|
||||
TELEGRAM_ISKRA_IE = (
|
||||
'/ISk5\2MIE5T-200\r\n'
|
||||
'\r\n'
|
||||
'1-0:0.0.0(00000000)\r\n'
|
||||
'0-0:96.1.0(09610)\r\n'
|
||||
'0-0:1.0.0(230202132747S)\r\n'
|
||||
'1-0:1.8.1(000010.181*kWh)\r\n'
|
||||
'1-0:1.8.2(000010.182*kWh)\r\n'
|
||||
'1-0:2.8.1(000010.281*kWh)\r\n'
|
||||
'1-0:2.8.2(000010.282*kWh)\r\n'
|
||||
'0-0:96.14.0(0001)\r\n'
|
||||
'1-0:1.7.0(00.170*kW)\r\n'
|
||||
'1-0:2.7.0(00.270*kW)\r\n'
|
||||
'1-0:21.7.0(00.217*kW)\r\n'
|
||||
'1-0:41.7.0(00.417*kW)\r\n'
|
||||
'1-0:61.7.0(00.617*kW)\r\n'
|
||||
'1-0:22.7.0(00.227*kW)\r\n'
|
||||
'1-0:42.7.0(00.427*kW)\r\n'
|
||||
'1-0:62.7.0(00.627*kW)\r\n'
|
||||
'1-0:32.7.0(242.5*V)\r\n'
|
||||
'1-0:52.7.0(241.7*V)\r\n'
|
||||
'1-0:72.7.0(243.3*V)\r\n'
|
||||
'1-0:31.7.0(000*A)\r\n'
|
||||
'1-0:51.7.0(000*A)\r\n'
|
||||
'1-0:71.7.0(000*A)\r\n'
|
||||
'0-0:96.3.10(1)\r\n'
|
||||
'0-0:96.13.0()\r\n'
|
||||
'0-1:96.1.1()\r\n'
|
||||
'!AD3B\r\n'
|
||||
)
|
||||
|
||||
# V5 telegram of EON in Hungary
|
||||
TELEGRAM_V5_EON_HU = (
|
||||
'/SAG5SAG-METER\r\n'
|
||||
'\r\n'
|
||||
'0-0:1.0.0(230724150730S)\r\n'
|
||||
'0-0:42.0.0(53414733303832323030303032313630)\r\n'
|
||||
'0-0:96.1.0(383930303832323030303032313630)\r\n'
|
||||
'0-0:96.14.0(0001)\r\n'
|
||||
'0-0:96.3.10(1)\r\n'
|
||||
'0-0:17.0.0(90.000*kW)\r\n'
|
||||
'1-0:1.8.0(000173.640*kWh)\r\n'
|
||||
'1-0:1.8.1(000047.719*kWh)\r\n'
|
||||
'1-0:1.8.2(000125.921*kWh)\r\n'
|
||||
'1-0:1.8.3(000000.000*kWh)\r\n'
|
||||
'1-0:1.8.4(000000.000*kWh)\r\n'
|
||||
'1-0:2.8.0(000627.177*kWh)\r\n'
|
||||
'1-0:2.8.1(000401.829*kWh)\r\n'
|
||||
'1-0:2.8.2(000225.348*kWh)\r\n'
|
||||
'1-0:2.8.3(000000.000*kWh)\r\n'
|
||||
'1-0:2.8.4(000000.000*kWh)\r\n'
|
||||
'1-0:3.8.0(000000.123*kvarh)\r\n'
|
||||
'1-0:4.8.0(000303.131*kvarh)\r\n'
|
||||
'1-0:5.8.0(000000.668*kvarh)\r\n'
|
||||
'1-0:6.8.0(000000.071*kvarh)\r\n'
|
||||
'1-0:7.8.0(000160.487*kvarh)\r\n'
|
||||
'1-0:8.8.0(000143.346*kvarh)\r\n'
|
||||
'1-0:15.8.0(000800.817*kWh)\r\n'
|
||||
'1-0:32.7.0(240.4*V)\r\n'
|
||||
'1-0:52.7.0(239.1*V)\r\n'
|
||||
'1-0:72.7.0(241.2*V)\r\n'
|
||||
'1-0:31.7.0(003*A)\r\n'
|
||||
'1-0:51.7.0(004*A)\r\n'
|
||||
'1-0:71.7.0(003*A)\r\n'
|
||||
'1-0:13.7.0(4.556)\r\n'
|
||||
'1-0:33.7.0(4.591)\r\n'
|
||||
'1-0:53.7.0(4.542)\r\n'
|
||||
'1-0:73.7.0(4.552)\r\n'
|
||||
'1-0:14.7.0(50.00*Hz)\r\n'
|
||||
'1-0:1.7.0(00.000*kW)\r\n'
|
||||
'1-0:2.7.0(02.601*kW)\r\n'
|
||||
'1-0:5.7.0(00.000*kvar)\r\n'
|
||||
'1-0:6.7.0(00.000*kvar)\r\n'
|
||||
'1-0:7.7.0(00.504*kvar)\r\n'
|
||||
'1-0:8.7.0(00.000*kvar)\r\n'
|
||||
'1-0:31.4.0(200.00*A)\r\n'
|
||||
'1-0:51.4.0(200.00*A)\r\n'
|
||||
'1-0:71.4.0(200.00*A)\r\n'
|
||||
'0-0:98.1.0(230701000000S)(000040.777*kWh)(000008.950*kWh)(000031.827*kWh)(000142.250*kWh)(000111.164*kWh)(000031.086*kWh)(000000.030*kvarh)(000073.988*kvarh)(000000.205*kvarh)(000000.048*kvarh)(000039.199*kvarh)(000035.020*kvarh)(000183.027*kWh)(03.564*kW)(02.156*kW)(03.564*kW)(04.104*kW)(04.104*kW)(03.400*kW)\r\n'
|
||||
'0-0:96.13.0()\r\n'
|
||||
'!99DA\r\n'
|
||||
)
|
||||
|
7
test/experiment_telegram.py
Normal file
7
test/experiment_telegram.py
Normal file
@ -0,0 +1,7 @@
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from test.example_telegrams import TELEGRAM_V4_2
|
||||
parser = TelegramParser(telegram_specifications.V4)
|
||||
telegram = parser.parse(TELEGRAM_V4_2)
|
||||
|
||||
print(telegram)
|
0
test/objects/__init__.py
Normal file
0
test/objects/__init__.py
Normal file
73
test/objects/test_mbusdevice.py
Normal file
73
test/objects/test_mbusdevice.py
Normal file
@ -0,0 +1,73 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from dsmr_parser import telegram_specifications, obis_references
|
||||
from dsmr_parser.objects import MbusDevice
|
||||
|
||||
|
||||
class MbusDeviceTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
v5_objects = telegram_specifications.V5['objects']
|
||||
|
||||
device_type_parser = [
|
||||
object["value_parser"]
|
||||
for object in v5_objects
|
||||
if object["obis_reference"] == obis_references.MBUS_DEVICE_TYPE
|
||||
][0]
|
||||
device_type = device_type_parser.parse('0-2:24.1.0(003)\r\n')
|
||||
|
||||
equipment_parser = [
|
||||
object["value_parser"]
|
||||
for object in v5_objects
|
||||
if object["obis_reference"] == obis_references.MBUS_EQUIPMENT_IDENTIFIER
|
||||
][0]
|
||||
equipment = equipment_parser.parse('0-2:96.1.0(4730303339303031393336393930363139)\r\n')
|
||||
|
||||
gas_reading_parser = [
|
||||
object["value_parser"]
|
||||
for object in v5_objects
|
||||
if object["obis_reference"] == obis_references.MBUS_METER_READING
|
||||
][0]
|
||||
gas_reading = gas_reading_parser.parse('0-2:24.2.1(200426223001S)(00246.138*m3)\r\n')
|
||||
|
||||
mbus_device = MbusDevice(channel_id=2)
|
||||
mbus_device.add(obis_references.MBUS_DEVICE_TYPE, device_type, "MBUS_DEVICE_TYPE")
|
||||
mbus_device.add(obis_references.MBUS_EQUIPMENT_IDENTIFIER, equipment, "MBUS_EQUIPMENT_IDENTIFIER")
|
||||
mbus_device.add(obis_references.MBUS_METER_READING, gas_reading, "MBUS_METER_READING")
|
||||
|
||||
self.mbus_device = mbus_device
|
||||
|
||||
def test_attributes(self):
|
||||
self.assertEqual(self.mbus_device.MBUS_DEVICE_TYPE.value, 3)
|
||||
self.assertEqual(self.mbus_device.MBUS_DEVICE_TYPE.unit, None)
|
||||
|
||||
self.assertEqual(self.mbus_device.MBUS_EQUIPMENT_IDENTIFIER.value,
|
||||
'4730303339303031393336393930363139')
|
||||
self.assertEqual(self.mbus_device.MBUS_EQUIPMENT_IDENTIFIER.unit, None)
|
||||
|
||||
self.assertEqual(self.mbus_device.MBUS_METER_READING.value, Decimal('246.138'))
|
||||
self.assertEqual(self.mbus_device.MBUS_METER_READING.unit, 'm3')
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(
|
||||
json.loads(self.mbus_device.to_json()),
|
||||
{
|
||||
'CHANNEL_ID': 2,
|
||||
'MBUS_DEVICE_TYPE': {'value': 3, 'unit': None},
|
||||
'MBUS_EQUIPMENT_IDENTIFIER': {'value': '4730303339303031393336393930363139', 'unit': None},
|
||||
'MBUS_METER_READING': {'datetime': '2020-04-26T20:30:01+00:00', 'value': 246.138, 'unit': 'm3'}}
|
||||
)
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(
|
||||
str(self.mbus_device),
|
||||
(
|
||||
'MBUS DEVICE (channel 2)\n'
|
||||
'\tMBUS_DEVICE_TYPE: 3 [None]\n'
|
||||
'\tMBUS_EQUIPMENT_IDENTIFIER: 4730303339303031393336393930363139 [None]\n'
|
||||
'\tMBUS_METER_READING: 246.138 [m3] at 2020-04-26T20:30:01+00:00\n'
|
||||
)
|
||||
)
|
88
test/objects/test_parser_corner_cases.py
Normal file
88
test/objects/test_parser_corner_cases.py
Normal file
@ -0,0 +1,88 @@
|
||||
import unittest
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
|
||||
from dsmr_parser.objects import ProfileGenericObject
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from dsmr_parser.parsers import ProfileGenericParser
|
||||
from dsmr_parser.profile_generic_specifications import BUFFER_TYPES
|
||||
from dsmr_parser.profile_generic_specifications import PG_HEAD_PARSERS
|
||||
from dsmr_parser.profile_generic_specifications import PG_UNIDENTIFIED_BUFFERTYPE_PARSERS
|
||||
from test.example_telegrams import TELEGRAM_V5
|
||||
|
||||
|
||||
class TestParserCornerCases(unittest.TestCase):
|
||||
""" Test instantiation of Telegram object """
|
||||
|
||||
def test_power_event_log_empty_1(self):
|
||||
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
telegram = parser.parse(TELEGRAM_V5)
|
||||
|
||||
object_type = ProfileGenericObject
|
||||
testitem = telegram.POWER_EVENT_FAILURE_LOG
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 0
|
||||
assert testitem.buffer_type == '0-0:96.7.19'
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 0
|
||||
|
||||
def test_power_event_log_empty_2(self):
|
||||
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
|
||||
object_type = ProfileGenericObject
|
||||
|
||||
# Power Event Log with 0 items and no object type
|
||||
pefl_line = r'1-0:99.97.0(0)()\r\n'
|
||||
testitem = pef_parser.parse(pefl_line)
|
||||
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 0
|
||||
assert testitem.buffer_type is None
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 0
|
||||
assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]
|
||||
json = testitem.to_json()
|
||||
assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}'
|
||||
|
||||
def test_power_event_log_null_values(self):
|
||||
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
|
||||
object_type = ProfileGenericObject
|
||||
|
||||
# Power Event Log with 1 item and no object type and nno values for the item
|
||||
pefl_line = r'1-0:99.97.0(1)()()()\r\n'
|
||||
testitem = pef_parser.parse(pefl_line)
|
||||
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 1
|
||||
assert testitem.buffer_type is None
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 1
|
||||
assert testitem.values == [{'value': 1, 'unit': None}, {'value': None, 'unit': None},
|
||||
{'value': None, 'unit': None}, {'value': None, 'unit': None}]
|
||||
json = testitem.to_json()
|
||||
assert json == \
|
||||
'{"buffer_length": 1, "buffer_type": null, "buffer": [{"datetime": null, "value": null, "unit": null}]}'
|
||||
|
||||
def test_power_event_log_brackets_only(self):
|
||||
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
|
||||
# Issue 57
|
||||
# Test of an ill formatted empty POWER_EVENT_FAILURE_LOG, observed on some smartmeters
|
||||
# The idea is that instead of failing, the parser converts it to an empty POWER_EVENT_FAILURE_LOG
|
||||
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
|
||||
object_type = ProfileGenericObject
|
||||
|
||||
pefl_line = r'1-0:99.97.0()\r\n'
|
||||
testitem = pef_parser.parse(pefl_line)
|
||||
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 0
|
||||
assert testitem.buffer_type is None
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 0
|
||||
assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]
|
||||
json = testitem.to_json()
|
||||
assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}'
|
486
test/objects/test_telegram.py
Normal file
486
test/objects/test_telegram.py
Normal file
@ -0,0 +1,486 @@
|
||||
import json
|
||||
import unittest
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
from dsmr_parser import telegram_specifications, obis_references
|
||||
|
||||
from dsmr_parser.objects import CosemObject
|
||||
from dsmr_parser.objects import MBusObject
|
||||
from dsmr_parser.objects import ProfileGenericObject
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from test.example_telegrams import TELEGRAM_V4_2, TELEGRAM_V5_TWO_MBUS, TELEGRAM_V5
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class TelegramTest(unittest.TestCase):
|
||||
""" Test instantiation of Telegram object """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.item_names_tested = []
|
||||
super(TelegramTest, self).__init__(*args, **kwargs)
|
||||
|
||||
def verify_telegram_item(self, telegram, testitem_name, object_type, unit_val, value_type, value_val):
|
||||
testitem = eval("telegram.{}".format(testitem_name))
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.unit == unit_val
|
||||
assert isinstance(testitem.value, value_type)
|
||||
assert testitem.value == value_val
|
||||
self.item_names_tested.append(testitem_name)
|
||||
|
||||
def test_instantiate(self):
|
||||
parser = TelegramParser(telegram_specifications.V4)
|
||||
telegram = parser.parse(TELEGRAM_V4_2)
|
||||
|
||||
# P1_MESSAGE_HEADER (1-3:0.2.8)
|
||||
self.verify_telegram_item(telegram,
|
||||
'P1_MESSAGE_HEADER',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=str,
|
||||
value_val='42')
|
||||
|
||||
# P1_MESSAGE_TIMESTAMP (0-0:1.0.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'P1_MESSAGE_TIMESTAMP',
|
||||
CosemObject,
|
||||
unit_val=None,
|
||||
value_type=datetime.datetime,
|
||||
value_val=datetime.datetime(2016, 11, 13, 19, 57, 57, tzinfo=pytz.UTC))
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_USED_TARIFF_1',
|
||||
object_type=CosemObject,
|
||||
unit_val='kWh',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('1581.123'))
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_USED_TARIFF_2',
|
||||
object_type=CosemObject,
|
||||
unit_val='kWh',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('1435.706'))
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_DELIVERED_TARIFF_1',
|
||||
object_type=CosemObject,
|
||||
unit_val='kWh',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_DELIVERED_TARIFF_2',
|
||||
object_type=CosemObject,
|
||||
unit_val='kWh',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_ACTIVE_TARIFF',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=str,
|
||||
value_val='0002')
|
||||
|
||||
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'EQUIPMENT_IDENTIFIER',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=str,
|
||||
value_val='3960221976967177082151037881335713')
|
||||
|
||||
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'CURRENT_ELECTRICITY_USAGE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('2.027'))
|
||||
|
||||
# CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'CURRENT_ELECTRICITY_DELIVERY',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
|
||||
self.verify_telegram_item(telegram,
|
||||
'SHORT_POWER_FAILURE_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=15)
|
||||
|
||||
# LONG_POWER_FAILURE_COUNT (96.7.9)
|
||||
self.verify_telegram_item(telegram,
|
||||
'LONG_POWER_FAILURE_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=7)
|
||||
|
||||
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SAG_L1_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SAG_L2_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SAG_L3_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SWELL_L1_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SWELL_L2_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SWELL_L3_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# TEXT_MESSAGE_CODE (0-0:96.13.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'TEXT_MESSAGE_CODE',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=type(None),
|
||||
value_val=None)
|
||||
|
||||
# TEXT_MESSAGE (0-0:96.13.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'TEXT_MESSAGE',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=type(None),
|
||||
value_val=None)
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_CURRENT_L1',
|
||||
object_type=CosemObject,
|
||||
unit_val='A',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_CURRENT_L2',
|
||||
object_type=CosemObject,
|
||||
unit_val='A',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('6'))
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_CURRENT_L3',
|
||||
object_type=CosemObject,
|
||||
unit_val='A',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('2'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0.170'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('1.247'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0.209'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# DEVICE_TYPE (0-1:24.1.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'DEVICE_TYPE',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=3)
|
||||
|
||||
# EQUIPMENT_IDENTIFIER_GAS (0-1:96.1.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'EQUIPMENT_IDENTIFIER_GAS',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=str,
|
||||
value_val='4819243993373755377509728609491464')
|
||||
|
||||
# HOURLY_GAS_METER_READING (0-1:24.2.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'HOURLY_GAS_METER_READING',
|
||||
object_type=MBusObject,
|
||||
unit_val='m3',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('981.443'))
|
||||
|
||||
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
|
||||
testitem_name = 'POWER_EVENT_FAILURE_LOG'
|
||||
object_type = ProfileGenericObject
|
||||
testitem = eval("telegram.{}".format(testitem_name))
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 3
|
||||
assert testitem.buffer_type == '0-0:96.7.19'
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 3
|
||||
assert all([isinstance(item, MBusObject) for item in buffer])
|
||||
date0 = datetime.datetime(2000, 1, 4, 17, 3, 20, tzinfo=datetime.timezone.utc)
|
||||
date1 = datetime.datetime(1999, 12, 31, 23, 0, 1, tzinfo=datetime.timezone.utc)
|
||||
date2 = datetime.datetime(2000, 1, 1, 23, 0, 3, tzinfo=datetime.timezone.utc)
|
||||
assert buffer[0].datetime == date0
|
||||
assert buffer[1].datetime == date1
|
||||
assert buffer[2].datetime == date2
|
||||
assert buffer[0].value == 237126
|
||||
assert buffer[1].value == 2147583646
|
||||
assert buffer[2].value == 2317482647
|
||||
assert all([isinstance(item.value, int) for item in buffer])
|
||||
assert all([isinstance(item.unit, str) for item in buffer])
|
||||
assert all([(item.unit == 's') for item in buffer])
|
||||
self.item_names_tested.append(testitem_name)
|
||||
|
||||
# check if all items in telegram V4 specification are covered
|
||||
V4_name_list = [object["value_name"] for object in
|
||||
telegram_specifications.V4['objects']]
|
||||
V4_name_set = set(V4_name_list)
|
||||
item_names_tested_set = set(self.item_names_tested)
|
||||
|
||||
assert item_names_tested_set == V4_name_set
|
||||
|
||||
def test_iter(self):
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
telegram = parser.parse(TELEGRAM_V5)
|
||||
|
||||
for obis_name, dsmr_object in telegram:
|
||||
break
|
||||
|
||||
# Verify that the iterator works for at least one value
|
||||
self.assertEqual(obis_name, "P1_MESSAGE_HEADER")
|
||||
self.assertEqual(dsmr_object.value, '50')
|
||||
|
||||
def test_mbus_devices(self):
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
telegram = parser.parse(TELEGRAM_V5_TWO_MBUS)
|
||||
mbus_devices = telegram.MBUS_DEVICES
|
||||
|
||||
self.assertEqual(len(mbus_devices), 2)
|
||||
|
||||
mbus_device_1 = mbus_devices[0]
|
||||
self.assertEqual(mbus_device_1.MBUS_DEVICE_TYPE.value, 3)
|
||||
self.assertEqual(mbus_device_1.MBUS_EQUIPMENT_IDENTIFIER.value, None)
|
||||
self.assertEqual(mbus_device_1.MBUS_METER_READING.value, Decimal('0'))
|
||||
|
||||
mbus_device_2 = mbus_devices[1]
|
||||
self.assertEqual(mbus_device_2.MBUS_DEVICE_TYPE.value, 3)
|
||||
self.assertEqual(mbus_device_2.MBUS_EQUIPMENT_IDENTIFIER.value, '4730303339303031393336393930363139')
|
||||
self.assertEqual(mbus_device_2.MBUS_METER_READING.value, Decimal('246.138'))
|
||||
|
||||
def test_get_mbus_device_by_channel(self):
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
telegram = parser.parse(TELEGRAM_V5_TWO_MBUS)
|
||||
|
||||
mbus_device_1 = telegram.get_mbus_device_by_channel(1)
|
||||
self.assertEqual(mbus_device_1.MBUS_DEVICE_TYPE.value, 3)
|
||||
self.assertEqual(mbus_device_1.MBUS_EQUIPMENT_IDENTIFIER.value, None)
|
||||
self.assertEqual(mbus_device_1.MBUS_METER_READING.value, Decimal('0'))
|
||||
|
||||
mbus_device_2 = telegram.get_mbus_device_by_channel(2)
|
||||
self.assertEqual(mbus_device_2.MBUS_DEVICE_TYPE.value, 3)
|
||||
self.assertEqual(mbus_device_2.MBUS_EQUIPMENT_IDENTIFIER.value, '4730303339303031393336393930363139')
|
||||
self.assertEqual(mbus_device_2.MBUS_METER_READING.value, Decimal('246.138'))
|
||||
|
||||
def test_without_mbus_devices(self):
|
||||
parser = TelegramParser(telegram_specifications.V5, apply_checksum_validation=False)
|
||||
telegram = parser.parse('')
|
||||
|
||||
self.assertFalse(hasattr(telegram, 'MBUS_DEVICES'))
|
||||
self.assertIsNone(telegram.get_mbus_device_by_channel(1))
|
||||
|
||||
def test_to_json(self):
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
telegram = parser.parse(TELEGRAM_V5)
|
||||
json_data = json.loads(telegram.to_json())
|
||||
|
||||
self.maxDiff = None
|
||||
|
||||
self.assertEqual(
|
||||
json_data,
|
||||
{'CURRENT_ELECTRICITY_DELIVERY': {'unit': 'kW', 'value': 0.0},
|
||||
'CURRENT_ELECTRICITY_USAGE': {'unit': 'kW', 'value': 0.244},
|
||||
'ELECTRICITY_ACTIVE_TARIFF': {'unit': None, 'value': '0002'},
|
||||
'ELECTRICITY_DELIVERED_TARIFF_1': {'unit': 'kWh', 'value': 2.444},
|
||||
'ELECTRICITY_DELIVERED_TARIFF_2': {'unit': 'kWh', 'value': 0.0},
|
||||
'ELECTRICITY_USED_TARIFF_1': {'unit': 'kWh', 'value': 4.426},
|
||||
'ELECTRICITY_USED_TARIFF_2': {'unit': 'kWh', 'value': 2.399},
|
||||
'EQUIPMENT_IDENTIFIER': {'unit': None,
|
||||
'value': '4B384547303034303436333935353037'},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE': {'unit': 'kW', 'value': 0.0},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE': {'unit': 'kW', 'value': 0.07},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE': {'unit': 'kW', 'value': 0.0},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE': {'unit': 'kW', 'value': 0.032},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE': {'unit': 'kW', 'value': 0.0},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE': {'unit': 'kW', 'value': 0.142},
|
||||
'INSTANTANEOUS_CURRENT_L1': {'unit': 'A', 'value': 0.48},
|
||||
'INSTANTANEOUS_CURRENT_L2': {'unit': 'A', 'value': 0.44},
|
||||
'INSTANTANEOUS_CURRENT_L3': {'unit': 'A', 'value': 0.86},
|
||||
'INSTANTANEOUS_VOLTAGE_L1': {'unit': 'V', 'value': 230.0},
|
||||
'INSTANTANEOUS_VOLTAGE_L2': {'unit': 'V', 'value': 230.0},
|
||||
'INSTANTANEOUS_VOLTAGE_L3': {'unit': 'V', 'value': 229.0},
|
||||
'LONG_POWER_FAILURE_COUNT': {'unit': None, 'value': 0},
|
||||
'MBUS_DEVICES': [{'CHANNEL_ID': 1,
|
||||
'MBUS_DEVICE_TYPE': {'unit': None, 'value': 3},
|
||||
'MBUS_EQUIPMENT_IDENTIFIER': {'unit': None,
|
||||
'value': '3232323241424344313233343536373839'},
|
||||
'MBUS_METER_READING': {'datetime': '2017-01-02T15:10:05+00:00',
|
||||
'unit': 'm3',
|
||||
'value': 0.107}},
|
||||
{'CHANNEL_ID': 2,
|
||||
'MBUS_DEVICE_TYPE': {'unit': None, 'value': 3},
|
||||
'MBUS_EQUIPMENT_IDENTIFIER': {'unit': None,
|
||||
'value': None}}],
|
||||
'P1_MESSAGE_HEADER': {'unit': None, 'value': '50'},
|
||||
'P1_MESSAGE_TIMESTAMP': {'unit': None, 'value': '2017-01-02T18:20:02+00:00'},
|
||||
'POWER_EVENT_FAILURE_LOG': {'buffer': [],
|
||||
'buffer_length': 0,
|
||||
'buffer_type': '0-0:96.7.19'},
|
||||
'SHORT_POWER_FAILURE_COUNT': {'unit': None, 'value': 13},
|
||||
'TEXT_MESSAGE': {'unit': None, 'value': None},
|
||||
'VOLTAGE_SAG_L1_COUNT': {'unit': None, 'value': 0},
|
||||
'VOLTAGE_SAG_L2_COUNT': {'unit': None, 'value': 0},
|
||||
'VOLTAGE_SAG_L3_COUNT': {'unit': None, 'value': 0},
|
||||
'VOLTAGE_SWELL_L1_COUNT': {'unit': None, 'value': 0},
|
||||
'VOLTAGE_SWELL_L2_COUNT': {'unit': None, 'value': 0},
|
||||
'VOLTAGE_SWELL_L3_COUNT': {'unit': None, 'value': 0}}
|
||||
)
|
||||
|
||||
def test_to_str(self):
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
telegram = parser.parse(TELEGRAM_V5)
|
||||
|
||||
self.maxDiff = None
|
||||
|
||||
self.assertEqual(
|
||||
str(telegram),
|
||||
(
|
||||
'P1_MESSAGE_HEADER: 50 [None]\n'
|
||||
'P1_MESSAGE_TIMESTAMP: 2017-01-02T18:20:02+00:00 [None]\n'
|
||||
'EQUIPMENT_IDENTIFIER: 4B384547303034303436333935353037 [None]\n'
|
||||
'ELECTRICITY_USED_TARIFF_1: 4.426 [kWh]\n'
|
||||
'ELECTRICITY_USED_TARIFF_2: 2.399 [kWh]\n'
|
||||
'ELECTRICITY_DELIVERED_TARIFF_1: 2.444 [kWh]\n'
|
||||
'ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh]\n'
|
||||
'ELECTRICITY_ACTIVE_TARIFF: 0002 [None]\n'
|
||||
'CURRENT_ELECTRICITY_USAGE: 0.244 [kW]\n'
|
||||
'CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW]\n'
|
||||
'LONG_POWER_FAILURE_COUNT: 0 [None]\n'
|
||||
'SHORT_POWER_FAILURE_COUNT: 13 [None]\n'
|
||||
'POWER_EVENT_FAILURE_LOG: buffer length: 0\n'
|
||||
' buffer type: 0-0:96.7.19\n'
|
||||
'VOLTAGE_SAG_L1_COUNT: 0 [None]\n'
|
||||
'VOLTAGE_SAG_L2_COUNT: 0 [None]\n'
|
||||
'VOLTAGE_SAG_L3_COUNT: 0 [None]\n'
|
||||
'VOLTAGE_SWELL_L1_COUNT: 0 [None]\n'
|
||||
'VOLTAGE_SWELL_L2_COUNT: 0 [None]\n'
|
||||
'VOLTAGE_SWELL_L3_COUNT: 0 [None]\n'
|
||||
'INSTANTANEOUS_VOLTAGE_L1: 230.0 [V]\n'
|
||||
'INSTANTANEOUS_VOLTAGE_L2: 230.0 [V]\n'
|
||||
'INSTANTANEOUS_VOLTAGE_L3: 229.0 [V]\n'
|
||||
'INSTANTANEOUS_CURRENT_L1: 0.48 [A]\n'
|
||||
'INSTANTANEOUS_CURRENT_L2: 0.44 [A]\n'
|
||||
'INSTANTANEOUS_CURRENT_L3: 0.86 [A]\n'
|
||||
'TEXT_MESSAGE: None [None]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.070 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 0.032 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.142 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW]\n'
|
||||
'MBUS DEVICE (channel 1)\n'
|
||||
' MBUS_DEVICE_TYPE: 3 [None]\n'
|
||||
' MBUS_EQUIPMENT_IDENTIFIER: 3232323241424344313233343536373839 [None]\n'
|
||||
' MBUS_METER_READING: 0.107 [m3] at 2017-01-02T15:10:05+00:00\n'
|
||||
'MBUS DEVICE (channel 2)\n'
|
||||
' MBUS_DEVICE_TYPE: 3 [None]\n'
|
||||
' MBUS_EQUIPMENT_IDENTIFIER: None [None]\n'
|
||||
)
|
||||
)
|
||||
|
||||
def test_getitem(self):
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
telegram = parser.parse(TELEGRAM_V5)
|
||||
|
||||
self.assertEqual(telegram[obis_references.P1_MESSAGE_HEADER].value, '50')
|
21
test/test_filereader.py
Normal file
21
test/test_filereader.py
Normal file
@ -0,0 +1,21 @@
|
||||
import unittest
|
||||
import tempfile
|
||||
|
||||
from dsmr_parser.clients.filereader import FileReader
|
||||
from dsmr_parser.telegram_specifications import V5
|
||||
from test.example_telegrams import TELEGRAM_V5
|
||||
|
||||
|
||||
class FileReaderTest(unittest.TestCase):
|
||||
def test_read_as_object(self):
|
||||
with tempfile.NamedTemporaryFile() as file:
|
||||
with open(file.name, "w") as f:
|
||||
f.write(TELEGRAM_V5)
|
||||
|
||||
telegrams = []
|
||||
reader = FileReader(file=file.name, telegram_specification=V5)
|
||||
# Call
|
||||
for telegram in reader.read_as_object():
|
||||
telegrams.append(telegram)
|
||||
|
||||
self.assertEqual(len(telegrams), 1)
|
413
test/test_parse_fluvius.py
Normal file
413
test/test_parse_fluvius.py
Normal file
@ -0,0 +1,413 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import pytz
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
|
||||
from dsmr_parser.objects import CosemObject, MBusObject, MBusObjectPeak
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from test.example_telegrams import TELEGRAM_FLUVIUS_V171, TELEGRAM_FLUVIUS_V171_ALT
|
||||
|
||||
|
||||
class TelegramParserFluviusTest(unittest.TestCase):
|
||||
""" Test parsing of a DSMR Fluvius telegram. """
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS)
|
||||
try:
|
||||
result = parser.parse(TELEGRAM_FLUVIUS_V171, throw_ex=True)
|
||||
except Exception as ex:
|
||||
assert False, f"parse trigged an exception {ex}"
|
||||
|
||||
# BELGIUM_VERSION_INFORMATION (0-0:96.1.4)
|
||||
assert isinstance(result.BELGIUM_VERSION_INFORMATION, CosemObject)
|
||||
assert result.BELGIUM_VERSION_INFORMATION.unit is None
|
||||
assert isinstance(result.BELGIUM_VERSION_INFORMATION.value, str)
|
||||
assert result.BELGIUM_VERSION_INFORMATION.value == '50217'
|
||||
|
||||
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
|
||||
assert isinstance(result.BELGIUM_EQUIPMENT_IDENTIFIER, CosemObject)
|
||||
assert result.BELGIUM_EQUIPMENT_IDENTIFIER.unit is None
|
||||
assert isinstance(result.BELGIUM_EQUIPMENT_IDENTIFIER.value, str)
|
||||
assert result.BELGIUM_EQUIPMENT_IDENTIFIER.value == '3153414733313031303231363035'
|
||||
|
||||
# P1_MESSAGE_TIMESTAMP (0-0:1.0.0)
|
||||
assert isinstance(result.P1_MESSAGE_TIMESTAMP, CosemObject)
|
||||
assert result.P1_MESSAGE_TIMESTAMP.unit is None
|
||||
assert isinstance(result.P1_MESSAGE_TIMESTAMP.value, datetime.datetime)
|
||||
assert result.P1_MESSAGE_TIMESTAMP.value == \
|
||||
pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 5, 12, 13, 54, 9))
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||
assert isinstance(result.ELECTRICITY_USED_TARIFF_1, CosemObject)
|
||||
assert result.ELECTRICITY_USED_TARIFF_1.unit == 'kWh'
|
||||
assert isinstance(result.ELECTRICITY_USED_TARIFF_1.value, Decimal)
|
||||
assert result.ELECTRICITY_USED_TARIFF_1.value == Decimal('0.034')
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
|
||||
assert isinstance(result.ELECTRICITY_USED_TARIFF_2, CosemObject)
|
||||
assert result.ELECTRICITY_USED_TARIFF_2.unit == 'kWh'
|
||||
assert isinstance(result.ELECTRICITY_USED_TARIFF_2.value, Decimal)
|
||||
assert result.ELECTRICITY_USED_TARIFF_2.value == Decimal('15.758')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
|
||||
assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject)
|
||||
assert result.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh'
|
||||
assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal)
|
||||
assert result.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('0.000')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
|
||||
assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject)
|
||||
assert result.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh'
|
||||
assert isinstance(result.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal)
|
||||
assert result.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('0.011')
|
||||
|
||||
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
|
||||
assert isinstance(result.ELECTRICITY_ACTIVE_TARIFF, CosemObject)
|
||||
assert result.ELECTRICITY_ACTIVE_TARIFF.unit is None
|
||||
assert isinstance(result.ELECTRICITY_ACTIVE_TARIFF.value, str)
|
||||
assert result.ELECTRICITY_ACTIVE_TARIFF.value == '0001'
|
||||
|
||||
# BELGIUM_CURRENT_AVERAGE_DEMAND (1-0:1.4.0)
|
||||
assert isinstance(result.BELGIUM_CURRENT_AVERAGE_DEMAND, CosemObject)
|
||||
assert result.BELGIUM_CURRENT_AVERAGE_DEMAND.unit == 'kW'
|
||||
assert isinstance(result.BELGIUM_CURRENT_AVERAGE_DEMAND.value, Decimal)
|
||||
assert result.BELGIUM_CURRENT_AVERAGE_DEMAND.value == Decimal('2.351')
|
||||
|
||||
# BELGIUM_MAXIMUM_DEMAND_MONTH (1-0:1.6.0)
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_MONTH, MBusObject)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_MONTH.unit == 'kW'
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_MONTH.value, Decimal)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_MONTH.value == Decimal('2.589')
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_MONTH.datetime, datetime.datetime)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_MONTH.datetime == \
|
||||
pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 5, 9, 13, 45, 58))
|
||||
|
||||
# BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 0
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0], MBusObjectPeak)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].unit == 'kW'
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].value, Decimal)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].value == Decimal('3.695')
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].datetime, datetime.datetime)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].datetime == \
|
||||
pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 5, 1, 0, 0, 0))
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].occurred, datetime.datetime)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[0].occurred == \
|
||||
pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 4, 23, 19, 25, 38))
|
||||
# BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 1
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1], MBusObjectPeak)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].unit == 'kW'
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].value, Decimal)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].value == Decimal('5.980')
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].datetime, datetime.datetime)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].datetime == \
|
||||
pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 4, 1, 0, 0, 0))
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].occurred, datetime.datetime)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[1].occurred == \
|
||||
pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 3, 5, 12, 21, 39))
|
||||
# BELGIUM_MAXIMUM_DEMAND_13_MONTHS (0-0:98.1.0) Value 2
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2], MBusObjectPeak)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].unit == 'kW'
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].value, Decimal)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].value == Decimal('4.318')
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].datetime, datetime.datetime)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].datetime == \
|
||||
pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 3, 1, 0, 0, 0))
|
||||
assert isinstance(result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].occurred, datetime.datetime)
|
||||
assert result.BELGIUM_MAXIMUM_DEMAND_13_MONTHS[2].occurred == \
|
||||
pytz.timezone("Europe/Brussels").localize(datetime.datetime(2020, 2, 10, 3, 54, 21))
|
||||
|
||||
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
|
||||
assert isinstance(result.CURRENT_ELECTRICITY_USAGE, CosemObject)
|
||||
assert result.CURRENT_ELECTRICITY_USAGE.unit == 'kW'
|
||||
assert isinstance(result.CURRENT_ELECTRICITY_USAGE.value, Decimal)
|
||||
assert result.CURRENT_ELECTRICITY_USAGE.value == Decimal('0.000')
|
||||
|
||||
# CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)
|
||||
assert isinstance(result.CURRENT_ELECTRICITY_DELIVERY, CosemObject)
|
||||
assert result.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW'
|
||||
assert isinstance(result.CURRENT_ELECTRICITY_DELIVERY.value, Decimal)
|
||||
assert result.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, CosemObject)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.unit == 'kW'
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value, Decimal)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, CosemObject)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.unit == 'kW'
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value, Decimal)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, CosemObject)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.unit == 'kW'
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value, Decimal)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, CosemObject)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.unit == 'kW'
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value, Decimal)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, CosemObject)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.unit == 'kW'
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value, Decimal)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, CosemObject)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.unit == 'kW'
|
||||
assert isinstance(result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value, Decimal)
|
||||
assert result.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_VOLTAGE_L1, CosemObject)
|
||||
assert result.INSTANTANEOUS_VOLTAGE_L1.unit == 'V'
|
||||
assert isinstance(result.INSTANTANEOUS_VOLTAGE_L1.value, Decimal)
|
||||
assert result.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('234.7')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_VOLTAGE_L2, CosemObject)
|
||||
assert result.INSTANTANEOUS_VOLTAGE_L2.unit == 'V'
|
||||
assert isinstance(result.INSTANTANEOUS_VOLTAGE_L2.value, Decimal)
|
||||
assert result.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('234.7')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_VOLTAGE_L3, CosemObject)
|
||||
assert result.INSTANTANEOUS_VOLTAGE_L3.unit == 'V'
|
||||
assert isinstance(result.INSTANTANEOUS_VOLTAGE_L3.value, Decimal)
|
||||
assert result.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('234.7')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_CURRENT_L1, CosemObject)
|
||||
assert result.INSTANTANEOUS_CURRENT_L1.unit == 'A'
|
||||
assert isinstance(result.INSTANTANEOUS_CURRENT_L1.value, Decimal)
|
||||
assert result.INSTANTANEOUS_CURRENT_L1.value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_CURRENT_L2, CosemObject)
|
||||
assert result.INSTANTANEOUS_CURRENT_L2.unit == 'A'
|
||||
assert isinstance(result.INSTANTANEOUS_CURRENT_L2.value, Decimal)
|
||||
assert result.INSTANTANEOUS_CURRENT_L2.value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
|
||||
assert isinstance(result.INSTANTANEOUS_CURRENT_L3, CosemObject)
|
||||
assert result.INSTANTANEOUS_CURRENT_L3.unit == 'A'
|
||||
assert isinstance(result.INSTANTANEOUS_CURRENT_L3.value, Decimal)
|
||||
assert result.INSTANTANEOUS_CURRENT_L3.value == Decimal('0.000')
|
||||
|
||||
# ACTUAL_SWITCH_POSITION (0-0:96.3.10)
|
||||
assert isinstance(result.ACTUAL_SWITCH_POSITION, CosemObject)
|
||||
assert result.ACTUAL_SWITCH_POSITION.unit is None
|
||||
assert isinstance(result.ACTUAL_SWITCH_POSITION.value, int)
|
||||
assert result.ACTUAL_SWITCH_POSITION.value == 1
|
||||
|
||||
# ACTUAL_TRESHOLD_ELECTRICITY (0-0:17.0.0)
|
||||
assert isinstance(result.ACTUAL_TRESHOLD_ELECTRICITY, CosemObject)
|
||||
assert result.ACTUAL_TRESHOLD_ELECTRICITY.unit == 'kW'
|
||||
assert isinstance(result.ACTUAL_TRESHOLD_ELECTRICITY.value, Decimal)
|
||||
assert result.ACTUAL_TRESHOLD_ELECTRICITY.value == Decimal('999.9')
|
||||
|
||||
# FUSE_THRESHOLD_L1 (1-0:31.4.0)
|
||||
assert isinstance(result.FUSE_THRESHOLD_L1, CosemObject)
|
||||
assert result.FUSE_THRESHOLD_L1.unit == 'A'
|
||||
assert isinstance(result.FUSE_THRESHOLD_L1.value, Decimal)
|
||||
assert result.FUSE_THRESHOLD_L1.value == Decimal('999')
|
||||
|
||||
# TEXT_MESSAGE (0-0:96.13.0)
|
||||
assert isinstance(result.TEXT_MESSAGE, CosemObject)
|
||||
assert result.TEXT_MESSAGE.unit is None
|
||||
assert result.TEXT_MESSAGE.value is None
|
||||
|
||||
# MBUS DEVICE 1
|
||||
mbus1 = result.get_mbus_device_by_channel(1)
|
||||
|
||||
# MBUS_DEVICE_TYPE (0-1:24.1.0)
|
||||
assert isinstance(mbus1.MBUS_DEVICE_TYPE, CosemObject)
|
||||
assert mbus1.MBUS_DEVICE_TYPE.unit is None
|
||||
assert isinstance(mbus1.MBUS_DEVICE_TYPE.value, int)
|
||||
assert mbus1.MBUS_DEVICE_TYPE.value == 3
|
||||
|
||||
# MBUS_EQUIPMENT_IDENTIFIER (0-1:96.1.1)
|
||||
assert isinstance(mbus1.MBUS_EQUIPMENT_IDENTIFIER, CosemObject)
|
||||
assert mbus1.MBUS_EQUIPMENT_IDENTIFIER.unit is None
|
||||
assert isinstance(mbus1.MBUS_EQUIPMENT_IDENTIFIER.value, str)
|
||||
assert mbus1.MBUS_EQUIPMENT_IDENTIFIER.value == '37464C4F32313139303333373333'
|
||||
|
||||
# MBUS_VALVE_POSITION (0-1:24.4.0)
|
||||
assert isinstance(result.MBUS_VALVE_POSITION, CosemObject)
|
||||
assert result.MBUS_VALVE_POSITION.unit is None
|
||||
assert isinstance(result.MBUS_VALVE_POSITION.value, int)
|
||||
assert result.MBUS_VALVE_POSITION.value == 1
|
||||
|
||||
# MBUS_METER_READING (0-1:24.2.3)
|
||||
assert isinstance(mbus1.MBUS_METER_READING, MBusObject)
|
||||
assert mbus1.MBUS_METER_READING.unit == 'm3'
|
||||
assert isinstance(mbus1.MBUS_METER_READING.value, Decimal)
|
||||
assert mbus1.MBUS_METER_READING.value == Decimal('112.384')
|
||||
|
||||
# MBUS DEVICE 2
|
||||
mbus2 = result.get_mbus_device_by_channel(2)
|
||||
|
||||
# MBUS_DEVICE_TYPE (0-2:24.1.0)
|
||||
assert isinstance(mbus2.MBUS_DEVICE_TYPE, CosemObject)
|
||||
assert mbus2.MBUS_DEVICE_TYPE.unit is None
|
||||
assert isinstance(mbus2.MBUS_DEVICE_TYPE.value, int)
|
||||
assert mbus2.MBUS_DEVICE_TYPE.value == 7
|
||||
|
||||
# MBUS_EQUIPMENT_IDENTIFIER (0-2:96.1.1)
|
||||
assert isinstance(mbus2.MBUS_EQUIPMENT_IDENTIFIER, CosemObject)
|
||||
assert mbus2.MBUS_EQUIPMENT_IDENTIFIER.unit is None
|
||||
assert isinstance(mbus2.MBUS_EQUIPMENT_IDENTIFIER.value, str)
|
||||
assert mbus2.MBUS_EQUIPMENT_IDENTIFIER.value == '3853414731323334353637383930'
|
||||
|
||||
# MBUS_METER_READING (0-1:24.2.1)
|
||||
assert isinstance(mbus2.MBUS_METER_READING, MBusObject)
|
||||
assert mbus2.MBUS_METER_READING.unit == 'm3'
|
||||
assert isinstance(mbus2.MBUS_METER_READING.value, Decimal)
|
||||
assert mbus2.MBUS_METER_READING.value == Decimal('872.234')
|
||||
|
||||
def test_checksum_valid(self):
|
||||
# No exception is raised.
|
||||
TelegramParser.validate_checksum(TELEGRAM_FLUVIUS_V171)
|
||||
|
||||
def test_checksum_invalid(self):
|
||||
# Remove the electricty used data value. This causes the checksum to
|
||||
# not match anymore.
|
||||
corrupted_telegram = TELEGRAM_FLUVIUS_V171.replace(
|
||||
'1-0:1.8.1(000000.034*kWh)\r\n',
|
||||
''
|
||||
)
|
||||
|
||||
with self.assertRaises(InvalidChecksumError):
|
||||
TelegramParser.validate_checksum(corrupted_telegram)
|
||||
|
||||
def test_checksum_missing(self):
|
||||
# Remove the checksum value causing a ParseError.
|
||||
corrupted_telegram = TELEGRAM_FLUVIUS_V171.replace('!3AD7\r\n', '')
|
||||
with self.assertRaises(ParseError):
|
||||
TelegramParser.validate_checksum(corrupted_telegram)
|
||||
|
||||
def test_to_json(self):
|
||||
parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS)
|
||||
telegram = parser.parse(TELEGRAM_FLUVIUS_V171_ALT)
|
||||
json_data = json.loads(telegram.to_json())
|
||||
|
||||
self.maxDiff = None
|
||||
|
||||
self.assertEqual(
|
||||
json_data,
|
||||
{'BELGIUM_VERSION_INFORMATION': {'value': '50217', 'unit': None},
|
||||
'BELGIUM_EQUIPMENT_IDENTIFIER': {'value': '3153414733313030373231333236', 'unit': None},
|
||||
'P1_MESSAGE_TIMESTAMP': {'value': '2023-11-02T11:15:48+00:00', 'unit': None},
|
||||
'ELECTRICITY_USED_TARIFF_1': {'value': 301.548, 'unit': 'kWh'},
|
||||
'ELECTRICITY_USED_TARIFF_2': {'value': 270.014, 'unit': 'kWh'},
|
||||
'ELECTRICITY_DELIVERED_TARIFF_1': {'value': 0.005, 'unit': 'kWh'},
|
||||
'ELECTRICITY_DELIVERED_TARIFF_2': {'value': 0.0, 'unit': 'kWh'},
|
||||
'ELECTRICITY_ACTIVE_TARIFF': {'value': '0001', 'unit': None},
|
||||
'BELGIUM_CURRENT_AVERAGE_DEMAND': {'value': 0.052, 'unit': 'kW'},
|
||||
'BELGIUM_MAXIMUM_DEMAND_MONTH': {'datetime': '2023-11-02T10:45:00+00:00',
|
||||
'value': 3.064, 'unit': 'kW'},
|
||||
'BELGIUM_MAXIMUM_DEMAND_13_MONTHS': [{'datetime': '2023-07-31T22:00:00+00:00',
|
||||
'occurred': None, 'value': 0.0, 'unit': 'kW'},
|
||||
{'datetime': '2023-08-31T22:00:00+00:00',
|
||||
'occurred': '2023-08-31T16:15:00+00:00',
|
||||
'value': 1.862, 'unit': 'kW'},
|
||||
{'datetime': '2023-09-30T22:00:00+00:00',
|
||||
'occurred': '2023-09-10T16:30:00+00:00',
|
||||
'value': 4.229, 'unit': 'kW'},
|
||||
{'datetime': '2023-10-31T23:00:00+00:00',
|
||||
'occurred': '2023-10-16T11:00:00+00:00',
|
||||
'value': 4.927, 'unit': 'kW'}],
|
||||
'CURRENT_ELECTRICITY_USAGE': {'value': 0.338, 'unit': 'kW'},
|
||||
'CURRENT_ELECTRICITY_DELIVERY': {'value': 0.0, 'unit': 'kW'},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE': {'value': 0.047, 'unit': 'kW'},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE': {'value': 0.179, 'unit': 'kW'},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE': {'value': 0.111, 'unit': 'kW'},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE': {'value': 0.0, 'unit': 'kW'},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE': {'value': 0.0, 'unit': 'kW'},
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE': {'value': 0.0, 'unit': 'kW'},
|
||||
'INSTANTANEOUS_VOLTAGE_L1': {'value': 232.9, 'unit': 'V'},
|
||||
'INSTANTANEOUS_VOLTAGE_L2': {'value': 228.1, 'unit': 'V'},
|
||||
'INSTANTANEOUS_VOLTAGE_L3': {'value': 228.1, 'unit': 'V'},
|
||||
'INSTANTANEOUS_CURRENT_L1': {'value': 0.27, 'unit': 'A'},
|
||||
'INSTANTANEOUS_CURRENT_L2': {'value': 0.88, 'unit': 'A'},
|
||||
'INSTANTANEOUS_CURRENT_L3': {'value': 0.52, 'unit': 'A'},
|
||||
'ACTUAL_SWITCH_POSITION': {'value': 1, 'unit': None},
|
||||
'ACTUAL_TRESHOLD_ELECTRICITY': {'value': 999.9, 'unit': 'kW'},
|
||||
'FUSE_THRESHOLD_L1': {'value': 999.0, 'unit': 'A'},
|
||||
'TEXT_MESSAGE': {'value': None, 'unit': None},
|
||||
'MBUS_DEVICES': [{'MBUS_DEVICE_TYPE': {'value': 3, 'unit': None},
|
||||
'MBUS_EQUIPMENT_IDENTIFIER': {'value': '37464C4F32313233303838303237',
|
||||
'unit': None},
|
||||
'MBUS_VALVE_POSITION': {'value': 1, 'unit': None},
|
||||
'MBUS_METER_READING': {'datetime': '2023-11-02T11:10:02+00:00',
|
||||
'value': 92.287, 'unit': 'm3'},
|
||||
'CHANNEL_ID': 1},
|
||||
{'MBUS_DEVICE_TYPE': {'value': 7, 'unit': None},
|
||||
'MBUS_EQUIPMENT_IDENTIFIER': {'value': '3853455430303030393631313733',
|
||||
'unit': None},
|
||||
'MBUS_METER_READING': {'datetime': '2023-11-02T11:15:32+00:00',
|
||||
'value': 8.579, 'unit': 'm3'},
|
||||
'CHANNEL_ID': 2}]}
|
||||
)
|
||||
|
||||
def test_to_str(self):
|
||||
parser = TelegramParser(telegram_specifications.BELGIUM_FLUVIUS)
|
||||
telegram = parser.parse(TELEGRAM_FLUVIUS_V171_ALT)
|
||||
|
||||
self.assertEqual(
|
||||
str(telegram),
|
||||
(
|
||||
'BELGIUM_VERSION_INFORMATION: 50217 [None]\n'
|
||||
'BELGIUM_EQUIPMENT_IDENTIFIER: 3153414733313030373231333236 [None]\n'
|
||||
'P1_MESSAGE_TIMESTAMP: 2023-11-02T11:15:48+00:00 [None]\n'
|
||||
'ELECTRICITY_USED_TARIFF_1: 301.548 [kWh]\n'
|
||||
'ELECTRICITY_USED_TARIFF_2: 270.014 [kWh]\n'
|
||||
'ELECTRICITY_DELIVERED_TARIFF_1: 0.005 [kWh]\n'
|
||||
'ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh]\n'
|
||||
'ELECTRICITY_ACTIVE_TARIFF: 0001 [None]\n'
|
||||
'BELGIUM_CURRENT_AVERAGE_DEMAND: 0.052 [kW]\n'
|
||||
'BELGIUM_MAXIMUM_DEMAND_MONTH: 3.064 [kW] at 2023-11-02T10:45:00+00:00\n'
|
||||
'0.0 [kW] at 2023-07-31T22:00:00+00:00 occurred None'
|
||||
'1.862 [kW] at 2023-08-31T22:00:00+00:00 occurred 2023-08-31T16:15:00+00:00'
|
||||
'4.229 [kW] at 2023-09-30T22:00:00+00:00 occurred 2023-09-10T16:30:00+00:00'
|
||||
'4.927 [kW] at 2023-10-31T23:00:00+00:00 occurred 2023-10-16T11:00:00+00:00'
|
||||
'CURRENT_ELECTRICITY_USAGE: 0.338 [kW]\n'
|
||||
'CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.047 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 0.179 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.111 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW]\n'
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW]\n'
|
||||
'INSTANTANEOUS_VOLTAGE_L1: 232.9 [V]\n'
|
||||
'INSTANTANEOUS_VOLTAGE_L2: 228.1 [V]\n'
|
||||
'INSTANTANEOUS_VOLTAGE_L3: 228.1 [V]\n'
|
||||
'INSTANTANEOUS_CURRENT_L1: 0.27 [A]\n'
|
||||
'INSTANTANEOUS_CURRENT_L2: 0.88 [A]\n'
|
||||
'INSTANTANEOUS_CURRENT_L3: 0.52 [A]\n'
|
||||
'ACTUAL_SWITCH_POSITION: 1 [None]\n'
|
||||
'ACTUAL_TRESHOLD_ELECTRICITY: 999.9 [kW]\n'
|
||||
'FUSE_THRESHOLD_L1: 999 [A]\n'
|
||||
'TEXT_MESSAGE: None [None]\n'
|
||||
'MBUS DEVICE (channel 1)\n'
|
||||
' MBUS_DEVICE_TYPE: 3 [None]\n'
|
||||
' MBUS_EQUIPMENT_IDENTIFIER: 37464C4F32313233303838303237 [None]\n'
|
||||
' MBUS_VALVE_POSITION: 1 [None]\n'
|
||||
' MBUS_METER_READING: 92.287 [m3] at 2023-11-02T11:10:02+00:00\n'
|
||||
'MBUS DEVICE (channel 2)\n'
|
||||
' MBUS_DEVICE_TYPE: 7 [None]\n'
|
||||
' MBUS_EQUIPMENT_IDENTIFIER: 3853455430303030393631313733 [None]\n'
|
||||
' MBUS_METER_READING: 8.579 [m3] at 2023-11-02T11:15:32+00:00\n'
|
||||
)
|
||||
)
|
177
test/test_parse_iskra_ie.py
Normal file
177
test/test_parse_iskra_ie.py
Normal file
@ -0,0 +1,177 @@
|
||||
import unittest
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
|
||||
from dsmr_parser.objects import CosemObject
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser import obis_references as obis
|
||||
from test.example_telegrams import TELEGRAM_ISKRA_IE
|
||||
|
||||
|
||||
class TelegramParserIskraIETest(unittest.TestCase):
|
||||
""" Test parsing of a Iskra IE5 telegram. """
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.ISKRA_IE)
|
||||
try:
|
||||
result = parser.parse(TELEGRAM_ISKRA_IE, throw_ex=True)
|
||||
except Exception as ex:
|
||||
assert False, f"parse trigged an exception {ex}"
|
||||
|
||||
# EQUIPMENT_IDENTIFIER_GAS (0-0:96.1.0)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '09610'
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('10.181')
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('10.182')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('10.281')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('10.282')
|
||||
|
||||
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
|
||||
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject)
|
||||
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None
|
||||
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str)
|
||||
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0001'
|
||||
|
||||
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject)
|
||||
assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal)
|
||||
assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.170')
|
||||
|
||||
# CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject)
|
||||
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW'
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)
|
||||
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0.270')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.217')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.417')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.617')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0.227')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0.427')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0.627')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('242.5')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('241.7')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('243.3')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.000')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.000')
|
||||
|
||||
# ACTUAL_SWITCH_POSITION (0-0:96.3.10)
|
||||
assert isinstance(result[obis.ACTUAL_SWITCH_POSITION], CosemObject)
|
||||
assert result[obis.ACTUAL_SWITCH_POSITION].unit is None
|
||||
assert isinstance(result[obis.ACTUAL_SWITCH_POSITION].value, str)
|
||||
assert result[obis.ACTUAL_SWITCH_POSITION].value == '1'
|
||||
|
||||
# TEXT_MESSAGE (0-0:96.13.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert result[obis.TEXT_MESSAGE].unit is None
|
||||
assert result[obis.TEXT_MESSAGE].value is None
|
||||
|
||||
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].unit is None
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].value is None
|
||||
|
||||
def test_checksum_valid(self):
|
||||
# No exception is raised.
|
||||
TelegramParser.validate_checksum(TELEGRAM_ISKRA_IE)
|
||||
|
||||
def test_checksum_invalid(self):
|
||||
# Remove the electricty used data value. This causes the checksum to not match anymore.
|
||||
corrupted_telegram = TELEGRAM_ISKRA_IE.replace(
|
||||
'1-0:1.8.1(000010.181*kWh)\r\n',
|
||||
''
|
||||
)
|
||||
|
||||
with self.assertRaises(InvalidChecksumError):
|
||||
TelegramParser.validate_checksum(corrupted_telegram)
|
||||
|
||||
def test_checksum_missing(self):
|
||||
# Remove the checksum value causing a ParseError.
|
||||
corrupted_telegram = TELEGRAM_ISKRA_IE.replace('!AD3B\r\n', '')
|
||||
with self.assertRaises(ParseError):
|
||||
TelegramParser.validate_checksum(corrupted_telegram)
|
107
test/test_parse_sagemcom_t210_d_r.py
Normal file
107
test/test_parse_sagemcom_t210_d_r.py
Normal file
@ -0,0 +1,107 @@
|
||||
from binascii import unhexlify
|
||||
from copy import deepcopy
|
||||
|
||||
import unittest
|
||||
|
||||
from dlms_cosem.exceptions import DecryptionError
|
||||
from dlms_cosem.protocol.xdlms import GeneralGlobalCipher
|
||||
from dlms_cosem.security import SecurityControlField, encrypt
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.exceptions import ParseError
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from test.example_telegrams import TELEGRAM_SAGEMCOM_T210_D_R
|
||||
|
||||
|
||||
class TelegramParserEncryptedTest(unittest.TestCase):
|
||||
""" Test parsing of a DSML encypted DSMR v5.x telegram. """
|
||||
DUMMY_ENCRYPTION_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
DUMMY_AUTHENTICATION_KEY = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
|
||||
|
||||
def __generate_encrypted(self, security_suite=0, authenticated=True, encrypted=True):
|
||||
security_control = SecurityControlField(
|
||||
security_suite=security_suite, authenticated=authenticated, encrypted=encrypted
|
||||
)
|
||||
encryption_key = unhexlify(self.DUMMY_ENCRYPTION_KEY)
|
||||
authentication_key = unhexlify(self.DUMMY_AUTHENTICATION_KEY)
|
||||
system_title = "SYSTEMID".encode("ascii")
|
||||
invocation_counter = int.from_bytes(bytes.fromhex("10000001"), "big")
|
||||
plain_data = TELEGRAM_SAGEMCOM_T210_D_R.encode("ascii")
|
||||
|
||||
encrypted = encrypt(
|
||||
security_control=security_control,
|
||||
key=encryption_key,
|
||||
auth_key=authentication_key,
|
||||
system_title=system_title,
|
||||
invocation_counter=invocation_counter,
|
||||
plain_text=plain_data,
|
||||
)
|
||||
|
||||
full_frame = bytearray(GeneralGlobalCipher.TAG.to_bytes(1, "big", signed=False))
|
||||
full_frame.extend(len(system_title).to_bytes(1, "big", signed=False))
|
||||
full_frame.extend(system_title)
|
||||
full_frame.extend([0x82]) # Length of the following length bytes
|
||||
# https://github.com/pwitab/dlms-cosem/blob/739f81a58e5f07663a512d4a128851333a0ed5e6/dlms_cosem/a_xdr.py#L33
|
||||
|
||||
security_control = security_control.to_bytes()
|
||||
invocation_counter = invocation_counter.to_bytes(4, "big", signed=False)
|
||||
full_frame.extend((len(encrypted)
|
||||
+ len(invocation_counter)
|
||||
+ len(security_control)).to_bytes(2, "big", signed=False))
|
||||
full_frame.extend(security_control)
|
||||
full_frame.extend(invocation_counter)
|
||||
full_frame.extend(encrypted)
|
||||
|
||||
return full_frame
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)
|
||||
result = parser.parse(self.__generate_encrypted().hex(),
|
||||
self.DUMMY_ENCRYPTION_KEY,
|
||||
self.DUMMY_AUTHENTICATION_KEY)
|
||||
self.assertEqual(len(result), 18)
|
||||
|
||||
def test_damaged_frame(self):
|
||||
# If the frame is damaged decrypting fails (crc is technically not needed)
|
||||
parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)
|
||||
|
||||
generated = self.__generate_encrypted()
|
||||
generated[150] = 0x00
|
||||
generated = generated.hex()
|
||||
|
||||
with self.assertRaises(DecryptionError):
|
||||
parser.parse(generated, self.DUMMY_ENCRYPTION_KEY, self.DUMMY_AUTHENTICATION_KEY)
|
||||
|
||||
def test_plain(self):
|
||||
# If a plain request is parsed with "general_global_cipher": True it fails
|
||||
parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
parser.parse(TELEGRAM_SAGEMCOM_T210_D_R, self.DUMMY_ENCRYPTION_KEY, self.DUMMY_AUTHENTICATION_KEY)
|
||||
|
||||
def test_general_global_cipher_not_specified(self):
|
||||
# If a GGC frame is detected but general_global_cipher is not set it fails
|
||||
parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)
|
||||
parser = deepcopy(parser) # We do not want to change the module value
|
||||
parser.telegram_specification['general_global_cipher'] = False
|
||||
|
||||
with self.assertRaises(ParseError):
|
||||
parser.parse(self.__generate_encrypted().hex(), self.DUMMY_ENCRYPTION_KEY, self.DUMMY_AUTHENTICATION_KEY)
|
||||
|
||||
def test_only_encrypted(self):
|
||||
# Not implemented by dlms_cosem
|
||||
parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)
|
||||
|
||||
only_auth = self.__generate_encrypted(0, authenticated=False, encrypted=True).hex()
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
parser.parse(only_auth, self.DUMMY_ENCRYPTION_KEY)
|
||||
|
||||
def test_only_auth(self):
|
||||
# Not implemented by dlms_cosem
|
||||
parser = TelegramParser(telegram_specifications.SAGEMCOM_T210_D_R)
|
||||
|
||||
only_auth = self.__generate_encrypted(0, authenticated=True, encrypted=False).hex()
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
parser.parse(only_auth, authentication_key=self.DUMMY_AUTHENTICATION_KEY)
|
@ -23,7 +23,10 @@ class TelegramParserV2_2Test(unittest.TestCase):
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.V2_2)
|
||||
result = parser.parse(TELEGRAM_V2_2)
|
||||
try:
|
||||
result = parser.parse(TELEGRAM_V2_2, throw_ex=True)
|
||||
except Exception as ex:
|
||||
assert False, f"parse trigged an exception {ex}"
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)
|
||||
|
@ -23,7 +23,10 @@ class TelegramParserV3Test(unittest.TestCase):
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.V3)
|
||||
result = parser.parse(TELEGRAM_V3)
|
||||
try:
|
||||
result = parser.parse(TELEGRAM_V3, throw_ex=True)
|
||||
except Exception as ex:
|
||||
assert False, f"parse trigged an exception {ex}"
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)
|
||||
|
@ -26,7 +26,10 @@ class TelegramParserV4_2Test(unittest.TestCase):
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.V4)
|
||||
result = parser.parse(TELEGRAM_V4_2)
|
||||
try:
|
||||
result = parser.parse(TELEGRAM_V4_2, throw_ex=True)
|
||||
except Exception as ex:
|
||||
assert False, f"parse trigged an exception {ex}"
|
||||
|
||||
# P1_MESSAGE_HEADER (1-3:0.2.8)
|
||||
assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject)
|
||||
@ -89,6 +92,12 @@ class TelegramParserV4_2Test(unittest.TestCase):
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)
|
||||
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0')
|
||||
|
||||
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
|
||||
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject)
|
||||
assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None
|
||||
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int)
|
||||
assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 15
|
||||
|
||||
# LONG_POWER_FAILURE_COUNT (96.7.9)
|
||||
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject)
|
||||
assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None
|
||||
@ -141,8 +150,26 @@ class TelegramParserV4_2Test(unittest.TestCase):
|
||||
assert result[obis.TEXT_MESSAGE].unit is None
|
||||
assert result[obis.TEXT_MESSAGE].value is None
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('6')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('2')
|
||||
|
||||
# DEVICE_TYPE (0-x:24.1.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert isinstance(result[obis.DEVICE_TYPE], CosemObject)
|
||||
assert result[obis.DEVICE_TYPE].unit is None
|
||||
assert isinstance(result[obis.DEVICE_TYPE].value, int)
|
||||
assert result[obis.DEVICE_TYPE].value == 3
|
||||
|
@ -5,7 +5,6 @@ import unittest
|
||||
|
||||
import pytz
|
||||
|
||||
from dsmr_parser import obis_references as obis
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
|
||||
from dsmr_parser.objects import CosemObject, MBusObject
|
||||
@ -27,169 +26,219 @@ class TelegramParserV5Test(unittest.TestCase):
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
result = parser.parse(TELEGRAM_V5)
|
||||
|
||||
try:
|
||||
telegram = parser.parse(TELEGRAM_V5, throw_ex=True)
|
||||
except Exception as ex:
|
||||
assert False, f"parse trigged an exception {ex}"
|
||||
print('test: ', type(telegram.P1_MESSAGE_HEADER), telegram.P1_MESSAGE_HEADER.__dict__)
|
||||
# P1_MESSAGE_HEADER (1-3:0.2.8)
|
||||
assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject)
|
||||
assert result[obis.P1_MESSAGE_HEADER].unit is None
|
||||
assert isinstance(result[obis.P1_MESSAGE_HEADER].value, str)
|
||||
assert result[obis.P1_MESSAGE_HEADER].value == '50'
|
||||
assert isinstance(telegram.P1_MESSAGE_HEADER, CosemObject)
|
||||
assert telegram.P1_MESSAGE_HEADER.unit is None
|
||||
assert isinstance(telegram.P1_MESSAGE_HEADER.value, str)
|
||||
assert telegram.P1_MESSAGE_HEADER.value == '50'
|
||||
|
||||
# P1_MESSAGE_TIMESTAMP (0-0:1.0.0)
|
||||
assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP], CosemObject)
|
||||
assert result[obis.P1_MESSAGE_TIMESTAMP].unit is None
|
||||
assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP].value, datetime.datetime)
|
||||
assert result[obis.P1_MESSAGE_TIMESTAMP].value == \
|
||||
assert isinstance(telegram.P1_MESSAGE_TIMESTAMP, CosemObject)
|
||||
assert telegram.P1_MESSAGE_TIMESTAMP.unit is None
|
||||
assert isinstance(telegram.P1_MESSAGE_TIMESTAMP.value, datetime.datetime)
|
||||
assert telegram.P1_MESSAGE_TIMESTAMP.value == \
|
||||
datetime.datetime(2017, 1, 2, 18, 20, 2, tzinfo=pytz.UTC)
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('4.426')
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1, CosemObject)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_1.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1.value, Decimal)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_1.value == Decimal('4.426')
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('2.399')
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2, CosemObject)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_2.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2.value, Decimal)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_2.value == Decimal('2.399')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('2.444')
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('2.444')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('0')
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('0')
|
||||
|
||||
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
|
||||
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject)
|
||||
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None
|
||||
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str)
|
||||
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0002'
|
||||
assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF, CosemObject)
|
||||
assert telegram.ELECTRICITY_ACTIVE_TARIFF.unit is None
|
||||
assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF.value, str)
|
||||
assert telegram.ELECTRICITY_ACTIVE_TARIFF.value == '0002'
|
||||
|
||||
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].unit is None
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].value == '4B384547303034303436333935353037'
|
||||
assert isinstance(telegram.EQUIPMENT_IDENTIFIER, CosemObject)
|
||||
assert telegram.EQUIPMENT_IDENTIFIER.unit is None
|
||||
assert isinstance(telegram.EQUIPMENT_IDENTIFIER.value, str)
|
||||
assert telegram.EQUIPMENT_IDENTIFIER.value == '4B384547303034303436333935353037'
|
||||
|
||||
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject)
|
||||
assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal)
|
||||
assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.244')
|
||||
assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE, CosemObject)
|
||||
assert telegram.CURRENT_ELECTRICITY_USAGE.unit == 'kW'
|
||||
assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE.value, Decimal)
|
||||
assert telegram.CURRENT_ELECTRICITY_USAGE.value == Decimal('0.244')
|
||||
|
||||
# CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject)
|
||||
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW'
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)
|
||||
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0')
|
||||
assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY, CosemObject)
|
||||
assert telegram.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW'
|
||||
assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY.value, Decimal)
|
||||
assert telegram.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('0')
|
||||
|
||||
# LONG_POWER_FAILURE_COUNT (96.7.9)
|
||||
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject)
|
||||
assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None
|
||||
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int)
|
||||
assert result[obis.LONG_POWER_FAILURE_COUNT].value == 0
|
||||
assert isinstance(telegram.LONG_POWER_FAILURE_COUNT, CosemObject)
|
||||
assert telegram.LONG_POWER_FAILURE_COUNT.unit is None
|
||||
assert isinstance(telegram.LONG_POWER_FAILURE_COUNT.value, int)
|
||||
assert telegram.LONG_POWER_FAILURE_COUNT.value == 0
|
||||
|
||||
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
|
||||
assert isinstance(telegram.SHORT_POWER_FAILURE_COUNT, CosemObject)
|
||||
assert telegram.SHORT_POWER_FAILURE_COUNT.unit is None
|
||||
assert isinstance(telegram.SHORT_POWER_FAILURE_COUNT.value, int)
|
||||
assert telegram.SHORT_POWER_FAILURE_COUNT.value == 13
|
||||
|
||||
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SAG_L1_COUNT].value == 0
|
||||
assert isinstance(telegram.VOLTAGE_SAG_L1_COUNT, CosemObject)
|
||||
assert telegram.VOLTAGE_SAG_L1_COUNT.unit is None
|
||||
assert isinstance(telegram.VOLTAGE_SAG_L1_COUNT.value, int)
|
||||
assert telegram.VOLTAGE_SAG_L1_COUNT.value == 0
|
||||
|
||||
# VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SAG_L2_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SAG_L2_COUNT].value == 0
|
||||
assert isinstance(telegram.VOLTAGE_SAG_L2_COUNT, CosemObject)
|
||||
assert telegram.VOLTAGE_SAG_L2_COUNT.unit is None
|
||||
assert isinstance(telegram.VOLTAGE_SAG_L2_COUNT.value, int)
|
||||
assert telegram.VOLTAGE_SAG_L2_COUNT.value == 0
|
||||
|
||||
# VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SAG_L3_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SAG_L3_COUNT].value == 0
|
||||
assert isinstance(telegram.VOLTAGE_SAG_L3_COUNT, CosemObject)
|
||||
assert telegram.VOLTAGE_SAG_L3_COUNT.unit is None
|
||||
assert isinstance(telegram.VOLTAGE_SAG_L3_COUNT.value, int)
|
||||
assert telegram.VOLTAGE_SAG_L3_COUNT.value == 0
|
||||
|
||||
# VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SWELL_L1_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SWELL_L1_COUNT].value == 0
|
||||
assert isinstance(telegram.VOLTAGE_SWELL_L1_COUNT, CosemObject)
|
||||
assert telegram.VOLTAGE_SWELL_L1_COUNT.unit is None
|
||||
assert isinstance(telegram.VOLTAGE_SWELL_L1_COUNT.value, int)
|
||||
assert telegram.VOLTAGE_SWELL_L1_COUNT.value == 0
|
||||
|
||||
# VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SWELL_L2_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SWELL_L2_COUNT].value == 0
|
||||
assert isinstance(telegram.VOLTAGE_SWELL_L2_COUNT, CosemObject)
|
||||
assert telegram.VOLTAGE_SWELL_L2_COUNT.unit is None
|
||||
assert isinstance(telegram.VOLTAGE_SWELL_L2_COUNT.value, int)
|
||||
assert telegram.VOLTAGE_SWELL_L2_COUNT.value == 0
|
||||
|
||||
# VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SWELL_L3_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0
|
||||
assert isinstance(telegram.VOLTAGE_SWELL_L3_COUNT, CosemObject)
|
||||
assert telegram.VOLTAGE_SWELL_L3_COUNT.unit is None
|
||||
assert isinstance(telegram.VOLTAGE_SWELL_L3_COUNT.value, int)
|
||||
assert telegram.VOLTAGE_SWELL_L3_COUNT.value == 0
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L1.unit == 'V'
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('230.0')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L2.unit == 'V'
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('230.0')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L3.unit == 'V'
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('229.0')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L1.unit == 'A'
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L1.value == Decimal('0.48')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L2.unit == 'A'
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L2.value == Decimal('0.44')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L3.unit == 'A'
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L3.value == Decimal('0.86')
|
||||
|
||||
# TEXT_MESSAGE (0-0:96.13.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert result[obis.TEXT_MESSAGE].unit is None
|
||||
assert result[obis.TEXT_MESSAGE].value is None
|
||||
|
||||
# DEVICE_TYPE (0-x:24.1.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert result[obis.DEVICE_TYPE].unit is None
|
||||
assert isinstance(result[obis.DEVICE_TYPE].value, int)
|
||||
assert result[obis.DEVICE_TYPE].value == 3
|
||||
assert isinstance(telegram.TEXT_MESSAGE, CosemObject)
|
||||
assert telegram.TEXT_MESSAGE.unit is None
|
||||
assert telegram.TEXT_MESSAGE.value is None
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.070')
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.unit == 'kW'
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value == Decimal('0.070')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.032')
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.unit == 'kW'
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value == Decimal('0.032')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.142')
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.unit == 'kW'
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value == Decimal('0.142')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0')
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.unit == 'kW'
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value == Decimal('0')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0')
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.unit == 'kW'
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value == Decimal('0')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0')
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.unit == 'kW'
|
||||
assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value == Decimal('0')
|
||||
|
||||
# EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '3232323241424344313233343536373839'
|
||||
# There's only one Mbus device (gas meter) in this case. Alternatively
|
||||
# use get_mbus_device_by_channel
|
||||
gas_meter_devices = telegram.MBUS_DEVICES
|
||||
gas_meter_device = gas_meter_devices[0]
|
||||
|
||||
# HOURLY_GAS_METER_READING (0-1:24.2.1)
|
||||
assert isinstance(result[obis.HOURLY_GAS_METER_READING], MBusObject)
|
||||
assert result[obis.HOURLY_GAS_METER_READING].unit == 'm3'
|
||||
assert isinstance(result[obis.HOURLY_GAS_METER_READING].value, Decimal)
|
||||
assert result[obis.HOURLY_GAS_METER_READING].value == Decimal('0.107')
|
||||
# MBUS_DEVICE_TYPE (0-1:96.1.0)
|
||||
assert isinstance(gas_meter_device.MBUS_DEVICE_TYPE, CosemObject)
|
||||
assert gas_meter_device.MBUS_DEVICE_TYPE.unit is None
|
||||
assert isinstance(gas_meter_device.MBUS_DEVICE_TYPE.value, int)
|
||||
assert gas_meter_device.MBUS_DEVICE_TYPE.value == 3
|
||||
|
||||
# MBUS_EQUIPMENT_IDENTIFIER (0-1:96.1.0)
|
||||
assert isinstance(gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER, CosemObject)
|
||||
assert gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER.unit is None
|
||||
assert isinstance(gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER.value, str)
|
||||
assert gas_meter_device.MBUS_EQUIPMENT_IDENTIFIER.value == '3232323241424344313233343536373839'
|
||||
|
||||
# MBUS_METER_READING (0-1:24.2.1)
|
||||
assert isinstance(gas_meter_device.MBUS_METER_READING, MBusObject)
|
||||
assert gas_meter_device.MBUS_METER_READING.unit == 'm3'
|
||||
assert isinstance(telegram.MBUS_METER_READING.value, Decimal)
|
||||
assert gas_meter_device.MBUS_METER_READING.value == Decimal('0.107')
|
||||
|
||||
def test_checksum_valid(self):
|
||||
# No exception is raised.
|
||||
@ -208,7 +257,27 @@ class TelegramParserV5Test(unittest.TestCase):
|
||||
|
||||
def test_checksum_missing(self):
|
||||
# Remove the checksum value causing a ParseError.
|
||||
corrupted_telegram = TELEGRAM_V5.replace('!87B3\r\n', '')
|
||||
|
||||
corrupted_telegram = TELEGRAM_V5.replace('!6EEE\r\n', '')
|
||||
with self.assertRaises(ParseError):
|
||||
TelegramParser.validate_checksum(corrupted_telegram)
|
||||
|
||||
def test_gas_timestamp_invalid(self):
|
||||
# Issue 120
|
||||
# Sometimes a MBUS device (For ex a Gas Meter) returns an invalid timestamp
|
||||
# Instead of failing, we should just ignore the timestamp
|
||||
invalid_date_telegram = TELEGRAM_V5.replace(
|
||||
'0-1:24.2.1(170102161005W)(00000.107*m3)\r\n',
|
||||
'0-1:24.2.1(632525252525S)(00000.000)\r\n'
|
||||
)
|
||||
invalid_date_telegram = invalid_date_telegram.replace('!6EEE\r\n', '!90C2\r\n')
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
telegram = parser.parse(invalid_date_telegram)
|
||||
|
||||
# MBUS DEVICE 1
|
||||
mbus1 = telegram.get_mbus_device_by_channel(1)
|
||||
|
||||
# MBUS_METER_READING (0-1:24.2.1)
|
||||
assert isinstance(mbus1.MBUS_METER_READING, MBusObject)
|
||||
assert mbus1.MBUS_METER_READING.unit is None
|
||||
assert isinstance(mbus1.MBUS_METER_READING.value, Decimal)
|
||||
assert mbus1.MBUS_METER_READING.value == Decimal('0.000')
|
||||
|
308
test/test_parse_v5_eon_hungary.py
Normal file
308
test/test_parse_v5_eon_hungary.py
Normal file
@ -0,0 +1,308 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import pytz
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
|
||||
from dsmr_parser.objects import CosemObject
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from test.example_telegrams import TELEGRAM_V5_EON_HU
|
||||
|
||||
|
||||
class TelegramParserV5EONHUTest(unittest.TestCase):
|
||||
""" Test parsing of a DSMR v5 EON Hungary telegram. """
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.EON_HUNGARY)
|
||||
try:
|
||||
telegram = parser.parse(TELEGRAM_V5_EON_HU, throw_ex=True)
|
||||
except Exception as ex:
|
||||
assert False, f"parse trigged an exception {ex}"
|
||||
|
||||
# P1_MESSAGE_TIMESTAMP (0-0:1.0.0)
|
||||
assert isinstance(telegram.P1_MESSAGE_TIMESTAMP, CosemObject)
|
||||
assert telegram.P1_MESSAGE_TIMESTAMP.unit is None
|
||||
assert isinstance(telegram.P1_MESSAGE_TIMESTAMP.value, datetime.datetime)
|
||||
assert telegram.P1_MESSAGE_TIMESTAMP.value == \
|
||||
pytz.timezone("Europe/Budapest").localize(datetime.datetime(2023, 7, 24, 15, 7, 30))
|
||||
|
||||
# EON_HU_COSEM_LOGICAL_DEVICE_NAME (0-0:42.0.0)
|
||||
assert isinstance(telegram.COSEM_LOGICAL_DEVICE_NAME, CosemObject)
|
||||
assert telegram.COSEM_LOGICAL_DEVICE_NAME.unit is None
|
||||
assert isinstance(telegram.COSEM_LOGICAL_DEVICE_NAME.value, str)
|
||||
assert telegram.COSEM_LOGICAL_DEVICE_NAME.value == '53414733303832323030303032313630'
|
||||
|
||||
# EON_HU_EQUIPMENT_SERIAL_NUMBER (0-0:96.1.0)
|
||||
assert isinstance(telegram.EQUIPMENT_SERIAL_NUMBER, CosemObject)
|
||||
assert telegram.EQUIPMENT_SERIAL_NUMBER.unit is None
|
||||
assert isinstance(telegram.EQUIPMENT_SERIAL_NUMBER.value, str)
|
||||
assert telegram.EQUIPMENT_SERIAL_NUMBER.value == '383930303832323030303032313630'
|
||||
|
||||
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
|
||||
assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF, CosemObject)
|
||||
assert telegram.ELECTRICITY_ACTIVE_TARIFF.unit is None
|
||||
assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF.value, str)
|
||||
assert telegram.ELECTRICITY_ACTIVE_TARIFF.value == '0001'
|
||||
|
||||
# ACTUAL_SWITCH_POSITION (0-0:96.3.10)
|
||||
assert isinstance(telegram.ACTUAL_SWITCH_POSITION, CosemObject)
|
||||
assert telegram.ACTUAL_SWITCH_POSITION.unit is None
|
||||
assert isinstance(telegram.ACTUAL_SWITCH_POSITION.value, str)
|
||||
assert telegram.ACTUAL_SWITCH_POSITION.value == '1'
|
||||
|
||||
# ACTUAL_TRESHOLD_ELECTRICITY (0-0:17.0.0)
|
||||
assert isinstance(telegram.ACTUAL_TRESHOLD_ELECTRICITY, CosemObject)
|
||||
assert telegram.ACTUAL_TRESHOLD_ELECTRICITY.unit == 'kW'
|
||||
assert isinstance(telegram.ACTUAL_TRESHOLD_ELECTRICITY.value, Decimal)
|
||||
assert telegram.ACTUAL_TRESHOLD_ELECTRICITY.value == Decimal('90.000')
|
||||
|
||||
# ELECTRICITY_IMPORTED_TOTAL (1-0:1.8.0)
|
||||
assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL, CosemObject)
|
||||
assert telegram.ELECTRICITY_IMPORTED_TOTAL.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_IMPORTED_TOTAL.value, Decimal)
|
||||
assert telegram.ELECTRICITY_IMPORTED_TOTAL.value == Decimal('000173.640')
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1, CosemObject)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_1.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1.value, Decimal)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_1.value == Decimal('000047.719')
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2, CosemObject)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_2.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2.value, Decimal)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_2.value == Decimal('000125.921')
|
||||
|
||||
# EON_HU_ELECTRICITY_USED_TARIFF_3 (1-0:1.8.3)
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_3, CosemObject)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_3.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_3.value, Decimal)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_3.value == Decimal('000000.000')
|
||||
|
||||
# EON_HU_ELECTRICITY_USED_TARIFF_4 (1-0:1.8.4)
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_4, CosemObject)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_4.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_USED_TARIFF_4.value, Decimal)
|
||||
assert telegram.ELECTRICITY_USED_TARIFF_4.value == Decimal('000000.000')
|
||||
|
||||
# ELECTRICITY_EXPORTED_TOTAL (1-0:2.8.0)
|
||||
assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL, CosemObject)
|
||||
assert telegram.ELECTRICITY_EXPORTED_TOTAL.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_EXPORTED_TOTAL.value, Decimal)
|
||||
assert telegram.ELECTRICITY_EXPORTED_TOTAL.value == Decimal('000627.177')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('000401.829')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('000225.348')
|
||||
|
||||
# EON_HU_ELECTRICITY_DELIVERED_TARIFF_3 (1-0:2.8.3)
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_3, CosemObject)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_3.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_3.value, Decimal)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_3.value == Decimal('000000.000')
|
||||
|
||||
# EON_HU_ELECTRICITY_DELIVERED_TARIFF_4 (1-0:2.8.4)
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_4, CosemObject)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_4.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_4.value, Decimal)
|
||||
assert telegram.ELECTRICITY_DELIVERED_TARIFF_4.value == Decimal('000000.000')
|
||||
|
||||
# ELECTRICITY_REACTIVE_IMPORTED_TOTAL (1-0:3.8.0)
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL, CosemObject)
|
||||
assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.unit == 'kvarh'
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value, Decimal)
|
||||
assert telegram.ELECTRICITY_REACTIVE_IMPORTED_TOTAL.value == Decimal('000000.123')
|
||||
|
||||
# ELECTRICITY_REACTIVE_EXPORTED_TOTAL (1-0:4.8.0)
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL, CosemObject)
|
||||
assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.unit == 'kvarh'
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value, Decimal)
|
||||
assert telegram.ELECTRICITY_REACTIVE_EXPORTED_TOTAL.value == Decimal('000303.131')
|
||||
|
||||
# EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q1 (1-0:5.8.0)
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q1, CosemObject)
|
||||
assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q1.unit == 'kvarh'
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q1.value, Decimal)
|
||||
assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q1.value == Decimal('000000.668')
|
||||
|
||||
# EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q2 (1-0:6.8.0)
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q2, CosemObject)
|
||||
assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q2.unit == 'kvarh'
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q2.value, Decimal)
|
||||
assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q2.value == Decimal('000000.071')
|
||||
|
||||
# EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q3 (1-0:7.8.0)
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q3, CosemObject)
|
||||
assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q3.unit == 'kvarh'
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q3.value, Decimal)
|
||||
assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q3.value == Decimal('000160.487')
|
||||
|
||||
# EON_HU_ELECTRICITY_REACTIVE_TOTAL_Q4 (1-0:8.8.0)
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q4, CosemObject)
|
||||
assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q4.unit == 'kvarh'
|
||||
assert isinstance(telegram.ELECTRICITY_REACTIVE_TOTAL_Q4.value, Decimal)
|
||||
assert telegram.ELECTRICITY_REACTIVE_TOTAL_Q4.value == Decimal('000143.346')
|
||||
|
||||
# EON_HU_ELECTRICITY_COMBINED (1-0:15.8.0)
|
||||
assert isinstance(telegram.ELECTRICITY_COMBINED, CosemObject)
|
||||
assert telegram.ELECTRICITY_COMBINED.unit == 'kWh'
|
||||
assert isinstance(telegram.ELECTRICITY_COMBINED.value, Decimal)
|
||||
assert telegram.ELECTRICITY_COMBINED.value == Decimal('000800.817')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L2 (1-0:32.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L1.unit == 'V'
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('240.4')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L2.unit == 'V'
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('239.1')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L3.unit == 'V'
|
||||
assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('241.2')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L1.unit == 'A'
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L1.value == Decimal('003')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L2.unit == 'A'
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L2.value == Decimal('004')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L3.unit == 'A'
|
||||
assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_CURRENT_L3.value == Decimal('003')
|
||||
|
||||
# EON_HU_INSTANTANEOUS_POWER_FACTOR_TOTAL (1-0:13.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL.unit is None
|
||||
assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_POWER_FACTOR_TOTAL.value == Decimal('4.556')
|
||||
|
||||
# EON_HU_INSTANTANEOUS_POWER_FACTOR_L1 (1-0:33.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L1, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_POWER_FACTOR_L1.unit is None
|
||||
assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L1.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_POWER_FACTOR_L1.value == Decimal('4.591')
|
||||
|
||||
# EON_HU_INSTANTANEOUS_POWER_FACTOR_L2 (1-0:53.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L2, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_POWER_FACTOR_L2.unit is None
|
||||
assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L2.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_POWER_FACTOR_L2.value == Decimal('4.542')
|
||||
|
||||
# EON_HU_INSTANTANEOUS_POWER_FACTOR_L3 (1-0:73.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L3, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_POWER_FACTOR_L3.unit is None
|
||||
assert isinstance(telegram.INSTANTANEOUS_POWER_FACTOR_L3.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_POWER_FACTOR_L3.value == Decimal('4.552')
|
||||
|
||||
# EON_HU_FREQUENCY (1-0:14.7.0)
|
||||
assert isinstance(telegram.FREQUENCY, CosemObject)
|
||||
assert telegram.FREQUENCY.unit == "Hz"
|
||||
assert isinstance(telegram.FREQUENCY.value, Decimal)
|
||||
assert telegram.FREQUENCY.value == Decimal('50.00')
|
||||
|
||||
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
|
||||
assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE, CosemObject)
|
||||
assert telegram.CURRENT_ELECTRICITY_USAGE.unit == 'kW'
|
||||
assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE.value, Decimal)
|
||||
assert telegram.CURRENT_ELECTRICITY_USAGE.value == Decimal('00.000')
|
||||
|
||||
# CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)
|
||||
assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY, CosemObject)
|
||||
assert telegram.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW'
|
||||
assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY.value, Decimal)
|
||||
assert telegram.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('02.601')
|
||||
|
||||
# EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q1 (1-0:5.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q1, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q1.unit == 'kvar'
|
||||
assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q1.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q1.value == Decimal('00.000')
|
||||
|
||||
# EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q2 (1-0:6.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q2, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q2.unit == 'kvar'
|
||||
assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q2.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q2.value == Decimal('00.000')
|
||||
|
||||
# EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q3 (1-0:7.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q3, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q3.unit == 'kvar'
|
||||
assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q3.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q3.value == Decimal('00.504')
|
||||
|
||||
# EON_HU_INSTANTANEOUS_REACTIVE_POWER_Q4 (1-0:8.7.0)
|
||||
assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q4, CosemObject)
|
||||
assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.unit == 'kvar'
|
||||
assert isinstance(telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.value, Decimal)
|
||||
assert telegram.INSTANTANEOUS_REACTIVE_POWER_Q4.value == Decimal('00.000')
|
||||
|
||||
# FUSE_THRESHOLD_L1 (1-0:31.4.0)
|
||||
assert isinstance(telegram.FUSE_THRESHOLD_L1, CosemObject)
|
||||
assert telegram.FUSE_THRESHOLD_L1.unit == 'A'
|
||||
assert isinstance(telegram.FUSE_THRESHOLD_L1.value, Decimal)
|
||||
assert telegram.FUSE_THRESHOLD_L1.value == Decimal('200.00')
|
||||
|
||||
# FUSE_THRESHOLD_L2 (1-0:31.4.0)
|
||||
assert isinstance(telegram.FUSE_THRESHOLD_L2, CosemObject)
|
||||
assert telegram.FUSE_THRESHOLD_L2.unit == 'A'
|
||||
assert isinstance(telegram.FUSE_THRESHOLD_L2.value, Decimal)
|
||||
assert telegram.FUSE_THRESHOLD_L2.value == Decimal('200.00')
|
||||
|
||||
# FUSE_THRESHOLD_L3 (1-0:31.4.0)
|
||||
assert isinstance(telegram.FUSE_THRESHOLD_L3, CosemObject)
|
||||
assert telegram.FUSE_THRESHOLD_L3.unit == 'A'
|
||||
assert isinstance(telegram.FUSE_THRESHOLD_L3.value, Decimal)
|
||||
assert telegram.FUSE_THRESHOLD_L3.value == Decimal('200.00')
|
||||
|
||||
# TEXT_MESSAGE (0-0:96.13.0)
|
||||
assert isinstance(telegram.TEXT_MESSAGE, CosemObject)
|
||||
assert telegram.TEXT_MESSAGE.unit is None
|
||||
assert telegram.TEXT_MESSAGE.value is None
|
||||
|
||||
def test_checksum_valid(self):
|
||||
# No exception is raised.
|
||||
TelegramParser.validate_checksum(TELEGRAM_V5_EON_HU)
|
||||
|
||||
def test_checksum_invalid(self):
|
||||
# Remove the electricty used data value. This causes the checksum to
|
||||
# not match anymore.
|
||||
corrupted_telegram = TELEGRAM_V5_EON_HU.replace(
|
||||
'1-0:1.8.1(000047.719*kWh)\r\n',
|
||||
''
|
||||
)
|
||||
|
||||
with self.assertRaises(InvalidChecksumError):
|
||||
TelegramParser.validate_checksum(corrupted_telegram)
|
||||
|
||||
def test_checksum_missing(self):
|
||||
# Remove the checksum value causing a ParseError.
|
||||
corrupted_telegram = TELEGRAM_V5_EON_HU.replace('!99DA\r\n', '')
|
||||
with self.assertRaises(ParseError):
|
||||
TelegramParser.validate_checksum(corrupted_telegram)
|
@ -3,10 +3,8 @@ from unittest.mock import Mock
|
||||
import unittest
|
||||
|
||||
from dsmr_parser import obis_references as obis
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from dsmr_parser.clients.protocol import DSMRProtocol
|
||||
|
||||
from dsmr_parser.clients.protocol import create_dsmr_protocol
|
||||
from dsmr_parser.objects import Telegram
|
||||
|
||||
TELEGRAM_V2_2 = (
|
||||
'/ISk5\2MT382-1004\r\n'
|
||||
@ -35,9 +33,10 @@ TELEGRAM_V2_2 = (
|
||||
class ProtocolTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
telegram_parser = TelegramParser(telegram_specifications.V2_2)
|
||||
self.protocol = DSMRProtocol(None, telegram_parser,
|
||||
telegram_callback=Mock())
|
||||
new_protocol, _ = create_dsmr_protocol('2.2',
|
||||
telegram_callback=Mock(),
|
||||
keep_alive_interval=1)
|
||||
self.protocol = new_protocol()
|
||||
|
||||
def test_complete_packet(self):
|
||||
"""Protocol should assemble incoming lines into complete packet."""
|
||||
@ -45,10 +44,30 @@ class ProtocolTest(unittest.TestCase):
|
||||
self.protocol.data_received(TELEGRAM_V2_2.encode('ascii'))
|
||||
|
||||
telegram = self.protocol.telegram_callback.call_args_list[0][0][0]
|
||||
assert isinstance(telegram, dict)
|
||||
assert isinstance(telegram, Telegram)
|
||||
|
||||
assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01
|
||||
assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
|
||||
|
||||
assert float(telegram[obis.GAS_METER_READING].value) == 1.001
|
||||
assert telegram[obis.GAS_METER_READING].unit == 'm3'
|
||||
|
||||
def test_receive_packet(self):
|
||||
"""Protocol packet reception."""
|
||||
|
||||
mock_transport = Mock()
|
||||
self.protocol.connection_made(mock_transport)
|
||||
assert not self.protocol._active
|
||||
|
||||
self.protocol.data_received(TELEGRAM_V2_2.encode('ascii'))
|
||||
assert self.protocol._active
|
||||
|
||||
# 1st call of keep_alive resets 'active' flag
|
||||
self.protocol.keep_alive()
|
||||
assert not self.protocol._active
|
||||
|
||||
# 2nd call of keep_alive should close the transport
|
||||
self.protocol.keep_alive()
|
||||
mock_transport.close.assert_called_once()
|
||||
|
||||
self.protocol.connection_lost(None)
|
||||
|
77
test/test_rfxtrx_protocol.py
Normal file
77
test/test_rfxtrx_protocol.py
Normal file
@ -0,0 +1,77 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import unittest
|
||||
|
||||
from dsmr_parser import obis_references as obis
|
||||
from dsmr_parser.clients.rfxtrx_protocol import create_rfxtrx_dsmr_protocol, PACKETTYPE_DSMR, SUBTYPE_P1
|
||||
from dsmr_parser.objects import Telegram
|
||||
|
||||
TELEGRAM_V2_2 = (
|
||||
'/ISk5\2MT382-1004\r\n'
|
||||
'\r\n'
|
||||
'0-0:96.1.1(00000000000000)\r\n'
|
||||
'1-0:1.8.1(00001.001*kWh)\r\n'
|
||||
'1-0:1.8.2(00001.001*kWh)\r\n'
|
||||
'1-0:2.8.1(00001.001*kWh)\r\n'
|
||||
'1-0:2.8.2(00001.001*kWh)\r\n'
|
||||
'0-0:96.14.0(0001)\r\n'
|
||||
'1-0:1.7.0(0001.01*kW)\r\n'
|
||||
'1-0:2.7.0(0000.00*kW)\r\n'
|
||||
'0-0:17.0.0(0999.00*kW)\r\n'
|
||||
'0-0:96.3.10(1)\r\n'
|
||||
'0-0:96.13.1()\r\n'
|
||||
'0-0:96.13.0()\r\n'
|
||||
'0-1:24.1.0(3)\r\n'
|
||||
'0-1:96.1.0(000000000000)\r\n'
|
||||
'0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n'
|
||||
'(00001.001)\r\n'
|
||||
'0-1:24.4.0(1)\r\n'
|
||||
'!\r\n'
|
||||
)
|
||||
|
||||
OTHER_RF_PACKET = b'\x03\x01\x02\x03'
|
||||
|
||||
|
||||
def encode_telegram_as_RF_packets(telegram):
|
||||
data = b''
|
||||
|
||||
for line in telegram.split('\n'):
|
||||
packet_data = (line + '\n').encode('ascii')
|
||||
packet_header = bytes(bytearray([
|
||||
len(packet_data) + 3, # excluding length byte
|
||||
PACKETTYPE_DSMR,
|
||||
SUBTYPE_P1,
|
||||
0 # seq num (ignored)
|
||||
]))
|
||||
|
||||
data += packet_header + packet_data
|
||||
# other RF packets can pass by on the line
|
||||
data += OTHER_RF_PACKET
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class RFXtrxProtocolTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
new_protocol, _ = create_rfxtrx_dsmr_protocol('2.2',
|
||||
telegram_callback=Mock(),
|
||||
keep_alive_interval=1)
|
||||
self.protocol = new_protocol()
|
||||
|
||||
def test_complete_packet(self):
|
||||
"""Protocol should assemble incoming lines into complete packet."""
|
||||
|
||||
data = encode_telegram_as_RF_packets(TELEGRAM_V2_2)
|
||||
# send data broken up in two parts
|
||||
self.protocol.data_received(data[0:200])
|
||||
self.protocol.data_received(data[200:])
|
||||
|
||||
telegram = self.protocol.telegram_callback.call_args_list[0][0][0]
|
||||
assert isinstance(telegram, Telegram)
|
||||
|
||||
assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01
|
||||
assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
|
||||
|
||||
assert float(telegram[obis.GAS_METER_READING].value) == 1.001
|
||||
assert telegram[obis.GAS_METER_READING].unit == 'm3'
|
20
tox.ini
20
tox.ini
@ -1,15 +1,12 @@
|
||||
[tox]
|
||||
envlist = py34,py35,p36
|
||||
|
||||
[testenv]
|
||||
deps=
|
||||
pytest
|
||||
pytest-cov
|
||||
pylama
|
||||
pytest-asyncio
|
||||
pytest-catchlog
|
||||
pytest-mock
|
||||
PyCRC
|
||||
dlms_cosem
|
||||
setuptools
|
||||
commands=
|
||||
py.test --cov=dsmr_parser test {posargs}
|
||||
pylama dsmr_parser test
|
||||
@ -17,8 +14,17 @@ commands=
|
||||
[pylama:dsmr_parser/clients/__init__.py]
|
||||
ignore = W0611
|
||||
|
||||
[pylama:dsmr_parser/clients/socket_.py]
|
||||
ignore = C901
|
||||
|
||||
[pylama:dsmr_parser/parsers.py]
|
||||
ignore = W605
|
||||
|
||||
[pylama:test/example_telegrams.py]
|
||||
ignore = E501
|
||||
|
||||
[pylama:pylint]
|
||||
max_line_length = 100
|
||||
max_line_length = 120
|
||||
|
||||
[pylama:pycodestyle]
|
||||
max_line_length = 100
|
||||
max_line_length = 120
|
||||
|
Loading…
Reference in New Issue
Block a user