Compare commits

...
126 Commits
Author SHA1 Message Date
Nigel 47d914edb7 Readded python 3.6. If it works, then there is no harm 2024-12-22 14:36:37 +01:00
Nigel 99ad21c896 added a bit of documentation on how to run tests (#165)
* added a bit of documentation on how to run tests

* formatting

* formatting

* formatting

* formatting

* formatting

* formatting
2024-08-25 14:55:22 +02:00
Nigel 94189b6b63 Preparing release 1.4.2 2024-07-14 14:21:13 +02:00
Dennis Siemensma 8829260b15 Bump Github Actions to latest versions in favor of Node deprecations (#159) 2024-06-15 11:33:15 +02:00
Ido Szargel 55ac551a2a Add socket timeout (#155)
* Add socket timeout
2024-06-10 16:19:01 +02:00
J. Nick Koston fc65326370 Swap pyserial-asyncio for pyserial-asyncio-fast (#158)
* Swap pyserial-asyncio for pyserial-asyncio-fast

fixes #154

* actually commit the import change
2024-06-07 16:08:35 +02:00
Nigel 059c0802ad Fix changelog 2024-06-04 16:06:38 +02:00
Nigel d7f30b5069 Release 1.4.1 2024-06-04 15:59:32 +02:00
Joakim Plate b42ceb6555 Avoid loading timezone at runtime (#157)
* Avoid loading timezone at runtime

Pytz will load timezone info from files in
a lazy fashion on first access. This triggers warnings in HA due to it blocking the event loop.

Pre-load the needed timezone info at module import instead, which will run in
executor in HA.

* Update value_types.py
2024-06-04 15:51:03 +02:00
gigatexel 3cf627eedc Change checksum log level (#151)
* Update filereader.py

* Update socket_.py

* Update serial_.py

* Update protocol.py

* Update socket_.py
2024-03-28 12:40:17 +01:00
Nigel d680d44683 Update CHANGELOG.rst 2024-03-12 13:26:04 +01:00
Nigel 6387cc4440 CHANGELOG formatting 2024-03-12 12:56:13 +01:00
Nigel 61985c5c89 Changelog formatting 2024-03-12 12:54:28 +01:00
Nigel 88aaa2d30f Release 1.4.0 2024-03-12 12:50:36 +01:00
dupondje 1ce0925b61 Copy head_parsers list on construct. (#150)
We need to create a copy of the head_parsers.
Otherwise the self.value_formats contains just a reference to
head_parsers.
But the self.value_formats is modified in the _parse_values, causing
invalid values in the head_parsers list.
2024-03-12 11:14:43 +01:00
dupondje bc35a051f2 Change DSMR 5 and 5B to use DSMR devices (#142)
At least the BE and the NL DSMR devices use MBUS.
Therefore we make the MBUS obis reference generic, and convert the V5
telegram to use MBUS values.

Finally set self.maxDiff = None to have a full test output on some
tests.
2024-03-07 11:21:23 +01:00
Gunnar Klauberg fcbcbb30e0 Q3D add CURRENT_ELECTRICITY_DELIVERY (#149)
add CURRENT_ELECTRICITY_DELIVERY sensor for Q3D
2024-02-19 14:31:09 +01:00
Nigel be61d4aaf9 Prepare release 1.3.2 2024-01-29 14:29:02 +01:00
Nigel 1680cacf49 fix protocol unit test for python3.12 (#148)
* fix protocol unit test for python3.12

* add workflow test for python 3.12
2024-01-29 14:25:39 +01:00
dupondje 739419e3ee Remove BELGIUM_MAX_POWER_PER_PHASE (#144)
This commit removes BELGIUM_MAX_POWER_PER_PHASE and uses
ACTUAL_TRESHOLD_ELECTRICITY as this has same regex. So useless to keep
the BELIUM variant.
2023-11-07 12:10:42 +01:00
Jean-Louis Dupond d2ff6f4bb9 Fix tests after FUSE_TRESHOLD rename 2023-11-06 15:10:36 +01:00
Jean-Louis Dupond a316d93082 Adjust the naming of the MAX_POWER and MAX_CURRENT obis values 2023-11-06 15:10:36 +01:00
Jean-Louis Dupond 01d7622c40 Release 1.3.1 2023-11-06 15:09:47 +01:00
dupondje e8a86272ea Merge pull request #143 from dupondje/fix_invalid_occured
Fix parsing peak usage when value is not set
2023-11-06 14:30:20 +01:00
Jean-Louis Dupond 389bcefb50 Fix parsing peak usage when value is not set
Sometimes peak usage is not yet visible in the 13 months history code.
It gives a 0.0kW value with timestamp (632525252525W).

Also the peak usage value can be invalid, handle this case also.
2023-11-06 09:50:34 +01:00
Nigel 88923622a9 Release 1.3.0 2023-08-01 09:26:40 +02:00
balazs92117 2f1e080df3 Refactor of name mapping, added E.ON Hungary (#137)
* Added EON HUNGARY specification

* refactoring obis name mapping
2023-08-01 09:23:17 +02:00
Nigel af2e6558de Revert "Added E.ON HUNGARY specification (#134)" (#135)
This reverts commit 0752deb58a.
2023-07-26 15:03:29 +02:00
balazs92117 0752deb58a Added E.ON HUNGARY specification (#134)
* Added EON HUNGARY specification
2023-07-26 13:31:59 +02:00
Nigel 3fd032f18f Release 1.2.4 2023-07-11 22:01:00 +02:00
Emilio Jose Palacios-Garcia 4602e8c99f EQUIPMENT IDENTIFIER is wrong for Fluvius meters when other mbus devices are present (#133)
* change obis of fluvius meter ID to BELGIUM_EQUIPMENT_IDENTIFIER

* update test case to use direct access to attributes

* fix test case for pipeline
2023-07-11 21:43:41 +02:00
Emilio Jose Palacios-Garcia f3444a515d EQUIPMENT IDENTIFIER is wrong for Fluvius meters when other mbus devices are present (#133)
* change obis of fluvius meter ID to BELGIUM_EQUIPMENT_IDENTIFIER
2023-07-11 21:43:25 +02:00
Nigel 99c7b0d9f0 Prepare release 1.2.3 2023-04-18 20:48:30 +02:00
dupondje 84973872af Fix parsing tests and line start matching (#132)
* Raise parsing exception and check it in tests

* use multiline regex and match everything at the line start
2023-04-14 16:51:01 +02:00
Nigel ae8a2baaab Merge branch 'master' of github.com:ndokter/dsmr_parser 2023-04-12 10:58:40 +02:00
Nigel d3fab4f105 Release 1.2.2 2023-04-12 10:57:38 +02:00
Nigel 5a59c36646 improve performance parsing speed by about 6% by caching compiled regexes (#131)
* improve performance parsing speed by about 6% by caching compiled regexes
2023-04-12 10:52:11 +02:00
Nigel 9847bdf66b Prepare releasse 1.2.1 2023-04-05 11:19:42 +02:00
Nigel 76da334234 Update release notes for 1.2.0 where not all additions where added 2023-04-05 11:14:43 +02:00
Emilio Jose Palacios-GarciaandEmilio Jose Palacios Garcia c03d2483d7 Bug/duplicate index BELGIUM_MAXIMUM_DEMAND_13_MONTHS (#129)
* fix index offset for month timestamp in BELGIUM_MAXIMUM_DEMAND_13_MONTHS

* add timestamps to test case

---------

Co-authored-by: Emilio Jose Palacios Garcia <epalacio@emilio.esat.kuleuven.be>
2023-04-05 11:06:21 +02:00
Nigel de167c89b6 issue-51-telegram refactored TelegramParser.parse to return Telegram … (#121)
* issue-51-telegram improved mbus device parsing; refactored TelegramParser.parse to return Telegram object which is backwards compatible with the dict result
2023-02-19 12:24:44 +01:00
Nigel 6fb2ad3495 Merge pull request #126 from jchevalier7/feat/iskra_ie
Add Iskra IE.x meters specification
2023-02-12 13:45:43 +01:00
Nigel f156995138 Merge pull request #125 from dupondje/ignore_invalid_time
Fix parsing with invalid timestamps
2023-02-12 13:43:43 +01:00
AA61SL fe175a9a69 feat: Fixing test file to run setup.py test 2023-02-09 11:07:21 +01:00
AA61SL 78880a1881 feat: add Iskra IE.x meters 2023-02-09 11:01:00 +01:00
Nigel ca8392504d Release 1.1.0 2023-02-08 12:02:14 +01:00
Jean-Louis Dupond edf535f987 Fix parsing with invalid timestamps
Sometimes the timestamp in the DSMR message is invalid (when no data
read read from the mbus meter?), and then parsing fails.
Fixing this by handling the exception and returning None for invalid
timestamps. Fixes: #120
2023-01-30 14:45:32 +01:00
Nigel 026b91cd49 Merge pull request #124 from yada75/reactive
add instantaneous reactive power + fixed swapped reactive total import export
2023-01-28 13:32:32 +01:00
yada75 02eb3a34dd Update obis_name_mapping.py 2023-01-18 20:02:54 +01:00
yada75 142b7359a1 Update telegram_specifications.py 2023-01-18 19:39:29 +01:00
yada75 21b8547655 Update obis_name_mapping.py 2023-01-18 19:34:35 +01:00
yada75 a2b9fe5e3b Update obis_references.py 2023-01-18 19:31:39 +01:00
Nigel 0a259a3c9d fix code style 2023-01-01 20:55:58 +01:00
Nigel f851f48225 Merge pull request #118 from dennissiemensma/bug/readme-shield
Fixed Shields badge in favor of badges/shields#8671
2023-01-01 20:37:24 +01:00
Nigel 3a3054324a Merge pull request #116 from lowdef/add_pylama_backin_post_py36
add pylama back in + fix uniform style issues reported by pylama
2023-01-01 20:36:39 +01:00
Nigel 041ac758f4 Merge pull request #115 from robcalon/patch-1
asynchronous read_as_object method
2023-01-01 20:35:48 +01:00
Dennis Siemensma d50d775e38 Fixed Shields badge in favor of badges/shields#8671 2022-12-30 21:05:17 +01:00
Nigel 10613acbd9 Merge pull request #114 from dupondje/fix_name_mapping
Fix missing value from name mapping
2022-12-29 19:50:26 +01:00
Hans Erik van Elburg 5f58bc8f72 add pylama back in + fix uniform style issues reported by pylama 2022-12-27 18:58:48 +01:00
Rob Calon 371e1f3c5e update docstring 2022-12-27 17:36:10 +01:00
Rob Calon 0eae28e183 correct rtype 2022-12-27 17:27:23 +01:00
Rob Calon 431ddadb5f asynchronous read_as_object method
fix inheritance of synchronous read_as_object method from parent
2022-12-27 17:20:24 +01:00
Jean-Louis Dupond 9e01dc32c0 Fix missing value from name mapping 2022-12-26 12:50:59 +01:00
Nigel 650d16862b Fix changelog 2022-12-22 10:18:47 +01:00
Nigel 301a00e226 Prepare release 1.0.0 2022-12-22 10:13:26 +01:00
Nigel d0cc429eda Merge pull request #113 from dupondje/add_peakusage
Add peakusage
2022-12-12 20:32:42 +01:00
Jean-Louis Dupond d05fe2692b Add support for Fluvius V1.7.1 DSMR messages
These include (since 1.6) the water mater messages.
And since 1.7.X also peak usage values.

https://maakjemeterslim.be/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBZ0lEIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--cdd9b48fd0838e89b177f03b745b23450fd8f53e/e-MUCS_P1_Ed_1_7_1.pdf?disposition=attachment
2022-12-12 20:27:53 +01:00
Dennis Siemensma e80ba9862b No longer run Python 3.6 in CI as it is end-of-life 2022-12-07 20:33:58 +01:00
Dennis Siemensma cffcac2e87 Bump versions of Actions used in CI 2022-12-07 20:32:01 +01:00
Dennis Siemensma 357bae1bce Run tests for Python 3.11 as well 2022-12-07 20:30:58 +01:00
Nigel 98a10998e9 Remove pylama because of incompatibility with python3.6, maybe find replacement 2022-10-19 14:20:29 +02:00
Nigel 00ac7c71be Prepare version 0.34 2022-10-19 13:52:08 +02:00
Nigel 594db890fe Merge pull request #110 from debug-richard/master
Adds support for the Sagemcom T210-D-r smart meter
2022-10-16 12:49:55 +02:00
Richard Schleich 09f4afcada Adds support for the Sagemcom T210-D-r smart meter installed by Austrian grid operators.
The smart meter uses the DSMR/P1 standard and embeds the data in an encrypted and authenticated "DLMS General Global Cipher" frame.
The encryption/decryption is handled by the "dlms_cosem" lib.
Test cases are included.
Adds OBIS codes for reactive energy.
2022-10-14 17:50:02 +02:00
Nigel 2aba206c6f Merge branch 'master' of github.com:ndokter/dsmr_parser 2022-04-20 22:10:27 +02:00
Nigel 247a7446f5 Prepare release 0.33 2022-04-20 22:10:06 +02:00
Nigel 5c378f3419 Update README.rst 2022-04-20 22:02:35 +02:00
Nigel b825faa719 Update README.rst 2022-04-20 22:00:58 +02:00
Nigel 7f35cd3c73 Merge pull request #63 from mjkl-gh/master
Improve documentation asyncio
2022-04-20 21:58:59 +02:00
Nigel 63338fbf06 Merge branch 'master' into master 2022-04-20 21:58:51 +02:00
Nigel dd9e264f5c Merge pull request #107 from dennissiemensma/feature/python-3.10
Test Python 3.10 in CI + legacy badge fix
2022-04-20 21:48:09 +02:00
Nigel 5c4d6ed98b Merge pull request #106 from gigatexel/master
Update telegram_specifications.py
2022-04-20 21:47:27 +02:00
Dennis Siemensma f238eb14a1 Converted markdown link to restructured link 2022-04-20 21:32:41 +02:00
Dennis Siemensma 179a75e58c Typo fix 2022-04-20 21:30:50 +02:00
Dennis Siemensma 32c20b61ac Refer to CI config for the currently supported/tested Python versions 2022-04-20 21:29:08 +02:00
Dennis Siemensma 602ed4928a YAML fix for CI config 2022-04-20 21:28:31 +02:00
Dennis Siemensma f5bedb1e6e CI now tests Python 3.10 as well 2022-04-20 21:26:07 +02:00
Dennis Siemensma bc6eab73de Replace legacy Travis status badge with GitHub Actions 2022-04-20 21:25:24 +02:00
gigatexel e5eddd006e Update telegram_specifications.py 2022-04-08 14:20:11 +02:00
gigatexel 0ed5fc0220 Update telegram_specifications.py 2022-04-08 14:09:56 +02:00
Nigel c0a509dab3 Merge pull request #105 from gigatexel/master
Improve compatiblity with Belgian standard
2022-04-08 10:06:02 +02:00
gigatexel 3d5599289e Update obis_name_mapping.py 2022-04-08 09:57:02 +02:00
gigatexel 99ab86fffb Update telegram_specifications.py 2022-04-08 09:56:36 +02:00
gigatexel 15b3653a02 Update telegram_specifications.py 2022-04-06 13:30:58 +02:00
gigatexel 527730781c Update obis_name_mapping.py 2022-04-06 13:30:12 +02:00
gigatexel 3f41a73b9d Update obis_references.py 2022-04-06 13:28:57 +02:00
Nigel ddbcb67088 Merge pull request #104 from ndokter/issue-101
Issue 101
2022-02-23 13:39:30 +01:00
Nigel 746fbc4fff Merge pull request #103 from vladimirvrabely/issue-101
Add FileReader test
2022-02-22 20:35:03 +01:00
Nigel 304593c246 Merge pull request #102 from ndokter/issue-101
fix FileReader from not completing
2022-02-22 20:33:31 +01:00
Vladimir 7610646e10 Add FileReader test 2022-02-22 09:50:11 +01:00
Nigel Dokter a6bc66667b fix FileReader from not completing 2022-02-21 20:05:23 +01:00
Nigel 0340b25733 Update README.rst 2022-02-21 13:38:06 +01:00
Nigel 5c838efbe1 Fix CHANGELOG formatting 2022-01-04 21:16:03 +01:00
Nigel 9f66c075d6 Prepare version 0.32 2022-01-04 21:14:53 +01:00
Nigel 2dafc7e7e4 Merge branch 'master' of github.com:ndokter/dsmr_parser 2022-01-04 21:10:44 +01:00
Nigel b74572238e Merge pull request #98 from rhpijnacker/feature/rfxtrx
Support DSMR data read via RFXtrx with integrated P1 reader
2022-01-04 09:59:54 +01:00
Ronald Pijnacker cedf71dbb5 Merge remote-tracking branch 'upstream/master' into feature/rfxtrx 2022-01-04 09:55:00 +01:00
Ronald Pijnacker eace91b591 Fix coding style issues 2022-01-04 09:53:49 +01:00
Ronald Pijnacker dd6d26670e Rewrite for compatibility with python 3.6 2022-01-04 09:49:11 +01:00
Ronald Pijnacker 7d28d0e370 Update according to coding style 2022-01-03 21:25:19 +01:00
Ronald Pijnacker c082cf4868 Add test case 2022-01-01 20:19:24 +01:00
Ronald Pijnacker 188cac5287 Small update 2021-12-30 20:32:44 +01:00
Ronald Pijnacker c7ed4acb03 Refactor into separate file 2021-12-30 20:23:58 +01:00
Ronald Pijnacker 8b64adb80c Small rename 2021-12-30 17:23:31 +01:00
Ronald Pijnacker 3eed3654d4 Wrap DSMR protocol in RFXtrx wrapper 2021-12-30 17:01:29 +01:00
Nigel b562510e40 Merge pull request #96 from lowdef/master
resolve name clash as reported in issue #95
2021-11-28 12:27:22 +01:00
Hans Erik van Elburg f0e035c8ed fix newer version pylama errors 2021-11-28 01:12:32 +01:00
Hans Erik van Elburg 83ef354c12 resolve name clash as reported in issue #95 2021-11-27 19:55:58 +01:00
Nigel dc2f493739 Merge pull request #70 from bremme/master
Add SocketReader to connect to remote serial port
2021-11-21 18:10:17 +01:00
Nigel 0a20dea7ed Merge pull request #93 from lowdef/master
fix pylama errors on PR92 + editorial fix
2021-11-21 17:56:01 +01:00
Hans Erik van Elburg 28e3c51f0d fix pylama errors on PR92 + editorial fix 2021-11-21 16:08:47 +01:00
bremme 97786576cf Add SocketReader documentation 2020-12-27 18:58:14 +01:00
bremme e12aff5c0d Add SocketReader for reading ipv4 tcp sockets 2020-12-27 18:57:21 +01:00
Maarten Kleijwegt bc7961f840 add asyncio read as object 2020-12-13 16:36:30 +01:00
Your Name 58851b5c5c Merge branch 'master' of https://github.com/mjkl-gh/dsmr_parser 2020-11-22 23:11:32 +01:00
Your Name ba9f3f3c25 First draft addin async documentation 2020-11-22 23:10:04 +01:00
36 changed files with 4157 additions and 746 deletions
+11 -8
View File
@@ -12,22 +12,25 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: python-version:
- 3.6 - '3.6'
- 3.7 - '3.7'
- 3.8 - '3.8'
- 3.9 - '3.9'
- '3.10'
- '3.11'
- '3.12'
name: Python ${{ matrix.python-version }} name: Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }} - name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Cached PIP dependencies - name: Cached PIP dependencies
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: | path: |
~/.cache/pip ~/.cache/pip
@@ -42,4 +45,4 @@ jobs:
run: tox run: tox
- name: Code coverage upload - name: Code coverage upload
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v4
+114 -30
View File
@@ -1,36 +1,120 @@
Change Log Change Log
---------- ----------
**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) **0.31** (2021-11-21)
- Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway (`pull request #92 <https://github.com/ndokter/dsmr_parser/pull/92>`_).
- 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) **0.30** (2021-08-18)
- Add support for Swedish smart meters (`pull request #86 <https://github.com/ndokter/dsmr_parser/pull/86>`_).
- Add support for Swedish smart meters (`PR #86 <https://github.com/ndokter/dsmr_parser/pull/86>`_).
**0.29** (2021-04-18) **0.29** (2021-04-18)
- Add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break (`pull request #71 <https://github.com/ndokter/dsmr_parser/pull/71>`_).
Remove deprecated asyncio coroutine decorator (`pull request #76 <https://github.com/ndokter/dsmr_parser/pull/76>`_). - 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) **0.28** (2021-02-21)
- Optional keep alive monitoring for TCP/IP connections (`pull request #73 <https://github.com/ndokter/dsmr_parser/pull/73>`_).
- Catch parse errors in TelegramParser, ignore lines that can not be parsed (`pull request #74 <https://github.com/ndokter/dsmr_parser/pull/74>`_). - 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) **0.27** (2020-12-24)
- fix for empty parentheses in ProfileGenericParser (redone) (`pull request #69 <https://github.com/ndokter/dsmr_parser/pull/69>`_).
- fix for empty parentheses in ProfileGenericParser (redone) (`PR #69 <https://github.com/ndokter/dsmr_parser/pull/69>`_).
**0.26** (2020-12-15) **0.26** (2020-12-15)
- reverted fix for empty parentheses in ProfileGenericParser (`pull request #68 <https://github.com/ndokter/dsmr_parser/pull/68>`_).
- reverted fix for empty parentheses in ProfileGenericParser (`PR #68 <https://github.com/ndokter/dsmr_parser/pull/68>`_).
**0.25** (2020-12-14) **0.25** (2020-12-14)
- fix for empty parentheses in ProfileGenericParser (`pull request #57 <https://github.com/ndokter/dsmr_parser/pull/57>`_).
- fix for empty parentheses in ProfileGenericParser (`PR #57 <https://github.com/ndokter/dsmr_parser/pull/57>`_).
**0.24** (2020-11-27) **0.24** (2020-11-27)
- Add Luxembourg equipment identifier (`pull request #62 <https://github.com/ndokter/dsmr_parser/pull/62>`_).
- Add Luxembourg equipment identifier (`PR #62 <https://github.com/ndokter/dsmr_parser/pull/62>`_).
**0.23** (2020-11-07) **0.23** (2020-11-07)
- Resolved issue with x-x:24.3.0 where it contains non-integer character (`pull request #61 <https://github.com/ndokter/dsmr_parser/pull/61>`_).
- Tests are not installed anymore (`pull request #59 <https://github.com/ndokter/dsmr_parser/pull/59>`_). - Resolved issue with x-x:24.3.0 where it contains non-integer character (`PR #61 <https://github.com/ndokter/dsmr_parser/pull/61>`_).
- Example telegram improvement (`pull request #58 <https://github.com/ndokter/dsmr_parser/pull/58>`_). - 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) **0.22** (2020-08-23)
@@ -58,40 +142,40 @@ Remove deprecated asyncio coroutine decorator (`pull request #76 <https://github
**0.18** (2020-01-28) **0.18** (2020-01-28)
- PyCRC replacement (`pull request #48 <https://github.com/ndokter/dsmr_parser/pull/48>`_). - PyCRC replacement (`PR #48 <https://github.com/ndokter/dsmr_parser/pull/48>`_).
**0.17** (2019-12-21) **0.17** (2019-12-21)
- Add a true telegram object (`pull request #40 <https://github.com/ndokter/dsmr_parser/pull/40>`_). - Add a true telegram object (`PR #40 <https://github.com/ndokter/dsmr_parser/pull/40>`_).
**0.16** (2019-12-21) **0.16** (2019-12-21)
- Add support for Belgian and Smarty meters (`pull request #44 <https://github.com/ndokter/dsmr_parser/pull/44>`_). - Add support for Belgian and Smarty meters (`PR #44 <https://github.com/ndokter/dsmr_parser/pull/44>`_).
**0.15** (2019-12-12) **0.15** (2019-12-12)
- Fixed asyncio loop issue (`pull request #43 <https://github.com/ndokter/dsmr_parser/pull/43>`_). - Fixed asyncio loop issue (`PR #43 <https://github.com/ndokter/dsmr_parser/pull/43>`_).
**0.14** (2019-10-08) **0.14** (2019-10-08)
- Changed serial reading to reduce CPU usage (`pull request #37 <https://github.com/ndokter/dsmr_parser/pull/37>`_). - Changed serial reading to reduce CPU usage (`PR #37 <https://github.com/ndokter/dsmr_parser/pull/37>`_).
**0.13** (2019-03-04) **0.13** (2019-03-04)
- Fix DSMR v5.0 serial settings which were not used (`pull request #33 <https://github.com/ndokter/dsmr_parser/pull/33>`_). - 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) **0.12** (2018-09-23)
- Add serial settings for DSMR v5.0 (`pull request #31 <https://github.com/ndokter/dsmr_parser/pull/31>`_). - Add serial settings for DSMR v5.0 (`PR #31 <https://github.com/ndokter/dsmr_parser/pull/31>`_).
- Lux-creos-obis-1.8.0 (`pull request #32 <https://github.com/ndokter/dsmr_parser/pull/32>`_). - Lux-creos-obis-1.8.0 (`PR #32 <https://github.com/ndokter/dsmr_parser/pull/32>`_).
**0.11** (2017-09-18) **0.11** (2017-09-18)
- NULL value fix in checksum (`pull request #26 <https://github.com/ndokter/dsmr_parser/pull/26>`_) - NULL value fix in checksum (`PR #26 <https://github.com/ndokter/dsmr_parser/pull/26>`_)
**0.10** (2017-06-05) **0.10** (2017-06-05)
- bugfix: don't force full telegram signatures (`pull request #25 <https://github.com/ndokter/dsmr_parser/pull/25>`_) - 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 - 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 - InvalidChecksumError's are logged as warning instead of error
@@ -111,7 +195,7 @@ Remove deprecated asyncio coroutine decorator (`pull request #76 <https://github
**0.7** (2017-01-14) **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:** **IMPORTANT: this release has the following backwards incompatible changes:**
@@ -121,8 +205,8 @@ Remove deprecated asyncio coroutine decorator (`pull request #76 <https://github
**0.6** (2017-01-04) **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>`_) - 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 (`pull request #12 <https://github.com/ndokter/dsmr_parser/pull/12/>`_) - Support added for TCP connections using the asyncio client (`PR #12 <https://github.com/ndokter/dsmr_parser/pull/12/>`_)
**0.5** (2016-12-29) **0.5** (2016-12-29)
@@ -130,16 +214,16 @@ Remove deprecated asyncio coroutine decorator (`pull request #76 <https://github
**0.4** (2016-11-21) **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>`_) - 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 (`pull request #8 <https://github.com/ndokter/dsmr_parser/pull/8>`_) - improved asyncio reader and improve it's error handling (`PR #8 <https://github.com/ndokter/dsmr_parser/pull/8>`_)
**0.3** (2016-11-12) **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) **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) **0.1** (2016-08-22)
+207 -144
View File
@@ -1,14 +1,11 @@
**Notice:** this repository is in need of a new maintainer. If you are interested or have ideas about this, please let me know.
DSMR Parser DSMR Parser
=========== ===========
.. image:: https://img.shields.io/pypi/v/dsmr-parser.svg .. image:: https://img.shields.io/pypi/v/dsmr-parser.svg
:target: https://pypi.python.org/pypi/dsmr-parser :target: https://pypi.python.org/pypi/dsmr-parser
.. image:: https://travis-ci.org/ndokter/dsmr_parser.svg?branch=master .. image:: https://img.shields.io/github/actions/workflow/status/ndokter/dsmr_parser/tests.yml?branch=master
:target: https://travis-ci.org/ndokter/dsmr_parser :target: https://github.com/ndokter/dsmr_parser/actions/workflows/tests.yml
A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It
also includes client implementation to directly read and parse smart meter data. also includes client implementation to directly read and parse smart meter data.
@@ -17,8 +14,7 @@ also includes client implementation to directly read and parse smart meter data.
Features Features
-------- --------
DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.5, 3.6, 3.7, 3.8 and 3.9. DSMR Parser supports DSMR versions 2, 3, 4 and 5. See for the `currently supported/tested Python versions here <https://github.com/ndokter/dsmr_parser/blob/master/.github/workflows/tests.yml#L14>`_.
Client module usage Client module usage
------------------- -------------------
@@ -42,178 +38,216 @@ process because the code is blocking (not asynchronous):
for telegram in serial_reader.read(): for telegram in serial_reader.read():
print(telegram) # see 'Telegram object' docs below print(telegram) # see 'Telegram object' docs below
**AsyncIO client** **Socket client**
To be documented. 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):
Parsing module usage
--------------------
The parsing module accepts complete unaltered telegram strings and parses these
into a dictionary.
.. code-block:: python .. code-block:: python
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications
from dsmr_parser.parsers import TelegramParser from dsmr_parser.clients import SocketReader
# String is formatted in separate lines for readability.
telegram_str = (
'/ISk5\\2MT382-1000\r\n'
'\r\n'
'0-0:96.1.1(4B384547303034303436333935353037)\r\n'
'1-0:1.8.1(12345.678*kWh)\r\n'
'1-0:1.8.2(12345.678*kWh)\r\n'
'1-0:2.8.1(12345.678*kWh)\r\n'
'1-0:2.8.2(12345.678*kWh)\r\n'
'0-0:96.14.0(0002)\r\n'
'1-0:1.7.0(001.19*kW)\r\n'
'1-0:2.7.0(000.00*kW)\r\n'
'0-0:17.0.0(016*A)\r\n'
'0-0:96.3.10(1)\r\n'
'0-0:96.13.1(303132333435363738)\r\n'
'0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E'
'3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F30313233'
'3435363738393A3B3C3D3E3F)\r\n'
'0-1:96.1.0(3232323241424344313233343536373839)\r\n'
'0-1:24.1.0(03)\r\n'
'0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n'
'(00001.001)\r\n'
'0-1:24.4.0(1)\r\n'
'!\r\n'
)
parser = TelegramParser(telegram_specifications.V3)
telegram = parser.parse(telegram_str) socket_reader = SocketReader(
print(telegram) # see 'Telegram object' docs below 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
Telegram dictionary **AsyncIO client**
-------------------
A dictionary of which the key indicates the field type. These regex values For a test run using a tcp server (lasting 20 seconds) use the following example:
correspond to one of dsmr_parser.obis_reference constants.
The value is either a CosemObject or MBusObject. These have a 'value' and 'unit'
property. MBusObject's additionally have a 'datetime' property. The 'value' can
contain any python type (int, str, Decimal) depending on the field. The 'unit'
contains 'kW', 'A', 'kWh' or 'm3'.
.. code-block:: python
# Contents of a parsed DSMR v3 telegram
{'\\d-\\d:17\\.0\\.0.+?\\r\\n': <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>}
Example to get some of the values:
.. code-block:: python .. code-block:: python
import asyncio
import logging
from dsmr_parser import obis_references from dsmr_parser import obis_references
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
# The telegram message timestamp. logging.basicConfig(level=logging.INFO, format='%(message)s')
message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP]
# Using the active tariff to determine the electricity being used and HOST = MY_HOST
# delivered for the right tariff. PORT = MY_PORT
active_tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF] DSMR_VERSION = MY_DSMR_VERSION
active_tariff = int(tariff.value)
electricity_used_total = telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[active_tariff - 1]] logger = logging.getLogger('tcpclient')
electricity_delivered_total = telegram[obis_references.ELECTRICITY_DELIVERED_TARIFF_ALL[active_tariff - 1]] logger.debug("Logger created")
gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING] def printTelegram(telegram):
logger.info(telegram)
# See dsmr_reader.obis_references for all readable telegram values.
# Note that the avilable values differ per DSMR version.
Telegram as an Object async def main():
--------------------- try:
An object version of the telegram is available as well. 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 .. code-block:: python
# DSMR v4.2 p1 using dsmr_parser and telegram objects 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 import telegram_specifications
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5
from dsmr_parser.objects import CosemObject, MBusObject, Telegram
from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import TelegramParser
import os
serial_reader = SerialReader( # String is formatted in separate lines for readability.
device='/dev/ttyUSB0', telegram_str = (
serial_settings=SERIAL_SETTINGS_V5, '/ISk5\\2MT382-1000\r\n'
telegram_specification=telegram_specifications.V4 '\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'
) )
# telegram = next(serial_reader.read_as_object()) parser = TelegramParser(telegram_specifications.V3)
# print(telegram)
for telegram in serial_reader.read_as_object(): # see 'Telegram object' docs below
os.system('clear') telegram = parser.parse(telegram_str)
print(telegram)
Example of output of print of the telegram object: Telegram object
---------------------
.. code-block:: console 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'.
P1_MESSAGE_HEADER: 42 [None] Note: Telegram extends dictionary, which done for backwards compatibility. The use of keys (e.g. `telegram[obis_references.CURRENT_ELECTRICITY_USAGE]`) is deprecated.
P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None]
EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None]
ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh]
ELECTRICITY_USED_TARIFF_2: 1435.706 [kWh]
ELECTRICITY_DELIVERED_TARIFF_1: 0.000 [kWh]
ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh]
ELECTRICITY_ACTIVE_TARIFF: 0002 [None]
CURRENT_ELECTRICITY_USAGE: 2.027 [kW]
CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW]
LONG_POWER_FAILURE_COUNT: 7 [None]
VOLTAGE_SAG_L1_COUNT: 0 [None]
VOLTAGE_SAG_L2_COUNT: 0 [None]
VOLTAGE_SAG_L3_COUNT: 0 [None]
VOLTAGE_SWELL_L1_COUNT: 0 [None]
VOLTAGE_SWELL_L2_COUNT: 0 [None]
VOLTAGE_SWELL_L3_COUNT: 0 [None]
TEXT_MESSAGE_CODE: None [None]
TEXT_MESSAGE: None [None]
DEVICE_TYPE: 3 [None]
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.170 [kW]
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 1.247 [kW]
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.209 [kW]
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW]
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW]
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW]
EQUIPMENT_IDENTIFIER_GAS: 4819243993373755377509728609491464 [None]
HOURLY_GAS_METER_READING: 981.443 [m3]
Accessing the telegrams information as attributes directly: 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 .. code-block:: python
telegram # Print contents of all available values
Out[3]: <dsmr_parser.objects.Telegram at 0x7f5e995d9898> # See dsmr_parser.obis_name_mapping for all readable telegram values.
telegram.CURRENT_ELECTRICITY_USAGE # The available values differ per DSMR version and meter.
Out[4]: <dsmr_parser.objects.CosemObject at 0x7f5e98ae5ac8> print(telegram)
telegram.CURRENT_ELECTRICITY_USAGE.value # P1_MESSAGE_HEADER: 42 [None]
Out[5]: Decimal('2.027') # P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None]
telegram.CURRENT_ELECTRICITY_USAGE.unit # EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None]
Out[6]: 'kW' # ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh]
# etc.
# 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: The telegram object has an iterator, can be used to find all the information elements in the current telegram:
@@ -250,7 +284,6 @@ The telegram object has an iterator, can be used to find all the information ele
'EQUIPMENT_IDENTIFIER_GAS', 'EQUIPMENT_IDENTIFIER_GAS',
'HOURLY_GAS_METER_READING'] 'HOURLY_GAS_METER_READING']
Installation Installation
------------ ------------
@@ -260,6 +293,36 @@ To install DSMR Parser:
$ pip 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 Known issues
------------ ------------
+1
View File
@@ -1,5 +1,6 @@
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5
from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader
from dsmr_parser.clients.socket_ import SocketReader
from dsmr_parser.clients.protocol import create_dsmr_protocol, \ from dsmr_parser.clients.protocol import create_dsmr_protocol, \
create_dsmr_reader, create_tcp_dsmr_reader create_dsmr_reader, create_tcp_dsmr_reader
+9 -7
View File
@@ -4,7 +4,6 @@ import tailer
from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.objects import Telegram
from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import TelegramParser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -64,14 +63,17 @@ class FileReader(object):
with open(self._file, "rb") as file_handle: with open(self._file, "rb") as file_handle:
while True: while True:
data = file_handle.readline() data = file_handle.readline()
str = data.decode()
self.telegram_buffer.append(str) if not data:
break
self.telegram_buffer.append(data.decode())
for telegram in self.telegram_buffer.get_all(): for telegram in self.telegram_buffer.get_all():
try: try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification) yield self.telegram_parser.parse(telegram)
except InvalidChecksumError as e: except InvalidChecksumError as e:
logger.warning(str(e)) logger.info(str(e))
except ParseError as e: except ParseError as e:
logger.error('Failed to parse telegram: %s', e) logger.error('Failed to parse telegram: %s', e)
@@ -118,7 +120,7 @@ class FileInputReader(object):
for telegram in self.telegram_buffer.get_all(): for telegram in self.telegram_buffer.get_all():
try: try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification) yield self.telegram_parser.parse(telegram)
except InvalidChecksumError as e: except InvalidChecksumError as e:
logger.warning(str(e)) logger.warning(str(e))
except ParseError as e: except ParseError as e:
@@ -164,7 +166,7 @@ class FileTailReader(object):
for telegram in self.telegram_buffer.get_all(): for telegram in self.telegram_buffer.get_all():
try: try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification) yield self.telegram_parser.parse(telegram)
except InvalidChecksumError as e: except InvalidChecksumError as e:
logger.warning(str(e)) logger.warning(str(e))
except ParseError as e: except ParseError as e:
+19 -4
View File
@@ -4,7 +4,7 @@ from functools import partial
import asyncio import asyncio
import logging 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 import telegram_specifications
from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.clients.telegram_buffer import TelegramBuffer
@@ -16,6 +16,15 @@ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs):
"""Creates a DSMR asyncio protocol.""" """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': if dsmr_version == '2.2':
specification = telegram_specifications.V2_2 specification = telegram_specifications.V2_2
@@ -41,11 +50,17 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs):
elif dsmr_version == "Q3D": elif dsmr_version == "Q3D":
specification = telegram_specifications.Q3D specification = telegram_specifications.Q3D
serial_settings = SERIAL_SETTINGS_V5 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: else:
raise NotImplementedError("No telegram parser found for version: %s", raise NotImplementedError("No telegram parser found for version: %s",
dsmr_version) dsmr_version)
protocol = partial(DSMRProtocol, loop, TelegramParser(specification), protocol = partial(protocol, loop, TelegramParser(specification),
telegram_callback=telegram_callback, **kwargs) telegram_callback=telegram_callback, **kwargs)
return protocol, serial_settings return protocol, serial_settings
@@ -113,7 +128,7 @@ class DSMRProtocol(asyncio.Protocol):
self.telegram_buffer.append(data) self.telegram_buffer.append(data)
for telegram in self.telegram_buffer.get_all(): for telegram in self.telegram_buffer.get_all():
# ensure actual telegram is ascii (7-bit) only (IEC 646 required in section 5.4 of IEC 62056-21) # ensure actual telegram is ascii (7-bit) only (ISO 646:1991 IRV required in section 5.5 of IEC 62056-21)
telegram = telegram.encode("latin1").decode("ascii") telegram = telegram.encode("latin1").decode("ascii")
self.handle_telegram(telegram) self.handle_telegram(telegram)
@@ -143,7 +158,7 @@ class DSMRProtocol(asyncio.Protocol):
try: try:
parsed_telegram = self.telegram_parser.parse(telegram) parsed_telegram = self.telegram_parser.parse(telegram)
except InvalidChecksumError as e: except InvalidChecksumError as e:
self.log.warning(str(e)) self.log.info(str(e))
except ParseError: except ParseError:
self.log.exception("failed to parse telegram") self.log.exception("failed to parse telegram")
else: else:
+62
View 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
+36 -5
View File
@@ -1,11 +1,10 @@
import logging import logging
import serial import serial
import serial_asyncio import serial_asyncio_fast
from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import TelegramParser
from dsmr_parser.objects import Telegram
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -38,7 +37,7 @@ class SerialReader(object):
try: try:
yield self.telegram_parser.parse(telegram) yield self.telegram_parser.parse(telegram)
except InvalidChecksumError as e: except InvalidChecksumError as e:
logger.warning(str(e)) logger.info(str(e))
except ParseError as e: except ParseError as e:
logger.error('Failed to parse telegram: %s', e) logger.error('Failed to parse telegram: %s', e)
@@ -55,7 +54,7 @@ class SerialReader(object):
for telegram in self.telegram_buffer.get_all(): for telegram in self.telegram_buffer.get_all():
try: try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification) yield self.telegram_parser.parse(telegram)
except InvalidChecksumError as e: except InvalidChecksumError as e:
logger.warning(str(e)) logger.warning(str(e))
except ParseError as e: except ParseError as e:
@@ -78,7 +77,7 @@ class AsyncSerialReader(SerialReader):
:rtype: None :rtype: None
""" """
# create Serial StreamReader # create Serial StreamReader
conn = serial_asyncio.open_serial_connection(**self.serial_settings) conn = serial_asyncio_fast.open_serial_connection(**self.serial_settings)
reader, _ = await conn reader, _ = await conn
while True: while True:
@@ -95,3 +94,35 @@ class AsyncSerialReader(SerialReader):
) )
except ParseError as e: except ParseError as e:
logger.warning('Failed to parse telegram: %s', 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)
+98
View 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""
+10 -20
View File
@@ -1,5 +1,13 @@
import re 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): class TelegramBuffer(object):
""" """
@@ -8,14 +16,14 @@ class TelegramBuffer(object):
""" """
def __init__(self): def __init__(self):
self._buffer = '' self._buffer = ""
def get_all(self): def get_all(self):
""" """
Remove complete telegrams from buffer and yield them. Remove complete telegrams from buffer and yield them.
:rtype generator: :rtype generator:
""" """
for telegram in self._find_telegrams(): for telegram in _FIND_TELEGRAMS_REGEX.findall(self._buffer):
self._remove(telegram) self._remove(telegram)
yield telegram yield telegram
@@ -37,21 +45,3 @@ class TelegramBuffer(object):
index = self._buffer.index(telegram) + len(telegram) index = self._buffer.index(telegram) + len(telegram)
self._buffer = self._buffer[index:] self._buffer = self._buffer[index:]
def _find_telegrams(self):
"""
Find complete telegrams in buffer from start ('/') till ending
checksum ('!AB12\r\n').
:rtype: list
"""
# - Match all characters after start of telegram except for the start
# itself again '^\/]+', which eliminates incomplete preceding telegrams.
# - Do non greedy match using '?' so start is matched up to the first
# checksum that's found.
# - The checksum is optional '{0,4}' because not all telegram versions
# support it.
return re.findall(
r'\/[^\/]+?\![A-F0-9]{0,4}\0?\r\n',
self._buffer,
re.DOTALL
)
-64
View File
@@ -1,64 +0,0 @@
from dsmr_parser import obis_references as obis
"""
dsmr_parser.obis_name_mapping
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains a mapping of obis references to names.
"""
EN = {
obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER',
obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP',
obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL',
obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1',
obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2',
obis.ELECTRICITY_EXPORTED_TOTAL: 'ELECTRICITY_EXPORTED_TOTAL',
obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1',
obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2',
obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF',
obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER',
obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE',
obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY',
obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT',
obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT',
obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG',
obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT',
obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT',
obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT',
obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT',
obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT',
obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT',
obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1',
obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2',
obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3',
obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1',
obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2',
obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3',
obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE',
obis.TEXT_MESSAGE: 'TEXT_MESSAGE',
obis.DEVICE_TYPE: 'DEVICE_TYPE',
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS',
obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING',
obis.GAS_METER_READING: 'GAS_METER_READING',
obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY',
obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION',
obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS',
obis.BELGIUM_HOURLY_GAS_METER_READING: 'BELGIUM_HOURLY_GAS_METER_READING',
obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER',
obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: 'LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL',
obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 'LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL',
obis.SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL: 'SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL',
obis.SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 'SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL',
obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER',
obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE',
obis.Q3D_EQUIPMENT_SERIALNUMBER: 'Q3D_EQUIPMENT_SERIALNUMBER',
}
REVERSE_EN = dict([(v, k) for k, v in EN.items()])
+111 -53
View File
@@ -6,52 +6,88 @@ refactored to full line signatures to maintain backwards compatibility.
Might be refactored in a backwards incompatible way as soon as proper telegram Might be refactored in a backwards incompatible way as soon as proper telegram
objects are introduced. objects are introduced.
""" """
P1_MESSAGE_HEADER = r'\d-\d:0\.2\.8.+?\r\n' P1_MESSAGE_HEADER = r'^\d-\d:0\.2\.8.+?\r\n'
P1_MESSAGE_TIMESTAMP = r'\d-\d:1\.0\.0.+?\r\n' P1_MESSAGE_TIMESTAMP = r'^\d-\d:1\.0\.0.+?\r\n'
ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1\.8\.0.+?\r\n' ELECTRICITY_USED_TARIFF_1 = r'^\d-\d:1\.8\.1.+?\r\n'
ELECTRICITY_USED_TARIFF_1 = r'\d-\d:1\.8\.1.+?\r\n' ELECTRICITY_USED_TARIFF_2 = r'^\d-\d:1\.8\.2.+?\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_EXPORTED_TOTAL = r'\d-\d:2\.8\.0.+?\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_1 = r'^\d-\d:2\.8\.1.+?\r\n'
ELECTRICITY_DELIVERED_TARIFF_2 = r'\d-\d:2\.8\.2.+?\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' ELECTRICITY_DELIVERED_TARIFF_3 = r'^\d-\d:2\.8\.3.+?\r\n'
EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n' ELECTRICITY_DELIVERED_TARIFF_4 = r'^\d-\d:2\.8\.4.+?\r\n'
CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' CURRENT_REACTIVE_IMPORTED = r'^\d-\d:3\.7\.0.+?\r\n'
CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n' ELECTRICITY_REACTIVE_IMPORTED_TOTAL = r'^\d-\d:3\.8\.0.+?\r\n'
LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n' ELECTRICITY_REACTIVE_IMPORTED_TARIFF_1 = r'^\d-\d:3\.8\.1.+?\r\n'
SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n' ELECTRICITY_REACTIVE_IMPORTED_TARIFF_2 = r'^\d-\d:3\.8\.2.+?\r\n'
POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n' CURRENT_REACTIVE_EXPORTED = r'^\d-\d:4\.7\.0.+?\r\n'
VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n' ELECTRICITY_REACTIVE_EXPORTED_TOTAL = r'^\d-\d:4\.8\.0.+?\r\n'
VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n' ELECTRICITY_REACTIVE_EXPORTED_TARIFF_1 = r'^\d-\d:4\.8\.1.+?\r\n'
VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n' ELECTRICITY_REACTIVE_EXPORTED_TARIFF_2 = r'^\d-\d:4\.8\.2.+?\r\n'
VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n' ELECTRICITY_ACTIVE_TARIFF = r'^\d-\d:96\.14\.0.+?\r\n'
VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n' EQUIPMENT_IDENTIFIER = r'^\d-\d:96\.1\.1.+?\r\n'
VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n' CURRENT_ELECTRICITY_USAGE = r'^\d-\d:1\.7\.0.+?\r\n'
INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n' CURRENT_ELECTRICITY_DELIVERY = r'^\d-\d:2\.7\.0.+?\r\n'
INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n' LONG_POWER_FAILURE_COUNT = r'^\d-\d:96\.7\.9.+?\r\n'
INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n' SHORT_POWER_FAILURE_COUNT = r'^\d-\d:96\.7\.21.+?\r\n'
INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n' POWER_EVENT_FAILURE_LOG = r'^\d-\d:99\.97\.0.+?\r\n'
INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n' VOLTAGE_SAG_L1_COUNT = r'^\d-\d:32\.32\.0.+?\r\n'
INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n' VOLTAGE_SAG_L2_COUNT = r'^\d-\d:52\.32\.0.+?\r\n'
TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n' VOLTAGE_SAG_L3_COUNT = r'^\d-\d:72\.32\.0.+?\r\n'
TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n' VOLTAGE_SWELL_L1_COUNT = r'^\d-\d:32\.36\.0.+?\r\n'
DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n' VOLTAGE_SWELL_L2_COUNT = r'^\d-\d:52\.36\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n' VOLTAGE_SWELL_L3_COUNT = r'^\d-\d:72\.36\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n' INSTANTANEOUS_VOLTAGE_L1 = r'^\d-\d:32\.7\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n' INSTANTANEOUS_VOLTAGE_L2 = r'^\d-\d:52\.7\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'\d-\d:22\.7\.0.+?\r\n' INSTANTANEOUS_VOLTAGE_L3 = r'^\d-\d:72\.7\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'\d-\d:42\.7\.0.+?\r\n' INSTANTANEOUS_CURRENT_L1 = r'^\d-\d:31\.7\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'\d-\d:62\.7\.0.+?\r\n' INSTANTANEOUS_CURRENT_L2 = r'^\d-\d:51\.7\.0.+?\r\n'
EQUIPMENT_IDENTIFIER_GAS = r'\d-\d:96\.1\.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 # 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' 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' 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_TRESHOLD_ELECTRICITY = r'^\d-\d:17\.0\.0.+?\r\n'
ACTUAL_SWITCH_POSITION = r'\d-\d:96\.3\.10.+?\r\n' ACTUAL_SWITCH_POSITION = r'^\d-\d:96\.3\.10.+?\r\n'
VALVE_POSITION_GAS = r'\d-\d:24\.4\.0.+?\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 17.0.0
# TODO 96.3.10
ELECTRICITY_USED_TARIFF_ALL = ( ELECTRICITY_USED_TARIFF_ALL = (
ELECTRICITY_USED_TARIFF_1, ELECTRICITY_USED_TARIFF_1,
@@ -62,13 +98,35 @@ ELECTRICITY_DELIVERED_TARIFF_ALL = (
ELECTRICITY_DELIVERED_TARIFF_2 ELECTRICITY_DELIVERED_TARIFF_2
) )
# Alternate codes for foreign countries. # International generalized additions
BELGIUM_HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n' # Different code, same format. ELECTRICITY_IMPORTED_TOTAL = r'^\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+)
LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'\d-\d:42\.0\.0.+?\r\n' # Logical device name ELECTRICITY_EXPORTED_TOTAL = r'^\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-)
LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+)
LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-) # International non generalized additions (country specific) / risk for necessary refactoring
SWEDEN_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+) BELGIUM_VERSION_INFORMATION = r'^\d-\d:96\.1\.4.+?\r\n'
SWEDEN_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-) BELGIUM_EQUIPMENT_IDENTIFIER = r'^\d-0:96\.1\.1.+?\r\n'
Q3D_EQUIPMENT_IDENTIFIER = r'\d-\d:0\.0\.0.+?\r\n' # Logical device name BELGIUM_CURRENT_AVERAGE_DEMAND = r'^\d-\d:1\.4\.0.+?\r\n'
Q3D_EQUIPMENT_STATE = r'\d-\d:96\.5\.5.+?\r\n' # Device state (hexadecimal) BELGIUM_MAXIMUM_DEMAND_MONTH = r'^\d-\d:1\.6\.0.+?\r\n'
Q3D_EQUIPMENT_SERIALNUMBER = r'\d-\d:96\.1\.255.+?\r\n' # Device Serialnumber 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'
+198 -47
View File
@@ -1,20 +1,14 @@
import dsmr_parser.obis_name_mapping
import datetime
import json
from decimal import Decimal from decimal import Decimal
import datetime
import json
class Telegram(object): import pytz
class Telegram(dict):
""" """
Container for raw and parsed telegram data. Container for parsed telegram data.
Initializing:
from dsmr_parser import telegram_specifications
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
from dsmr_parser.objects import CosemObject, MBusObject, Telegram
from dsmr_parser.parsers import TelegramParser
from test.example_telegrams import TELEGRAM_V4_2
parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
Attributes can be accessed on a telegram object by addressing by their english name, for example: Attributes can be accessed on a telegram object by addressing by their english name, for example:
telegram.ELECTRICITY_USED_TARIFF_1 telegram.ELECTRICITY_USED_TARIFF_1
@@ -23,25 +17,54 @@ class Telegram(object):
[k for k,v in telegram] [k for k,v in telegram]
yields: yields:
['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...] ['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...]
Note: Dict like usage is deprecated. The inheritance from dict is because of backwards compatibility.
""" """
def __init__(self, telegram_data, telegram_parser, telegram_specification): def __init__(self, *args, **kwargs):
self._telegram_data = telegram_data self._item_names = []
self._telegram_specification = telegram_specification self._mbus_devices = []
self._telegram_parser = telegram_parser super().__init__(*args, **kwargs)
self._obis_name_mapping = dsmr_parser.obis_name_mapping.EN
self._reverse_obis_name_mapping = dsmr_parser.obis_name_mapping.REVERSE_EN
self._dictionary = self._telegram_parser.parse(telegram_data)
self._item_names = self._get_item_names()
def __getattr__(self, name): def add(self, obis_reference, dsmr_object, obis_name):
''' will only get called for undefined attributes ''' # Update name mapping used to get value by attribute. Example: telegram.P1_MESSAGE_HEADER
obis_reference = self._reverse_obis_name_mapping[name] setattr(self, obis_name, dsmr_object)
value = self._dictionary[obis_reference]
setattr(self, name, value)
return value
def _get_item_names(self): # TODO isinstance check: MaxDemandParser (BELGIUM_MAXIMUM_DEMAND_13_MONTHS) returns a list
return [self._obis_name_mapping[k] for k, v in self._dictionary.items()] 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): def __iter__(self):
for attr in self._item_names: for attr in self._item_names:
@@ -51,21 +74,44 @@ class Telegram(object):
def __str__(self): def __str__(self):
output = "" output = ""
for attr, value in self: for attr, value in self:
output += "{}: \t {}\n".format(attr, str(value)) if isinstance(value, list):
output += ''.join(map(str, value))
else:
output += "{}: \t {}\n".format(attr, str(value))
return output return output
def to_json(self): def to_json(self):
return json.dumps(dict([[attr, json.loads(value.to_json())] for attr, value in self])) json_data = {}
for attr, value in self:
if isinstance(value, list):
json_data[attr] = [json.loads(item.to_json() if hasattr(item, 'to_json') else item)
for item in value]
elif hasattr(value, 'to_json'):
json_data[attr] = json.loads(value.to_json())
return json.dumps(json_data)
class DSMRObject(object): class DSMRObject(object):
""" """
Represents all data from a single telegram line. Represents all data from a single telegram line.
""" """
def __init__(self, obis_id_code, values):
def __init__(self, values): self.obis_id_code = obis_id_code
self.values = values 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): class MBusObject(DSMRObject):
@@ -94,18 +140,25 @@ class MBusObject(DSMRObject):
return self.values[1]['unit'] return self.values[1]['unit']
def __str__(self): def __str__(self):
output = "{}\t[{}] at {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat())) 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 return output
def to_json(self): def to_json(self):
timestamp = self.datetime timestamp = self.datetime
if isinstance(self.datetime, datetime.datetime): if isinstance(timestamp, datetime.datetime):
timestamp = self.datetime.astimezone().isoformat() timestamp = timestamp.astimezone().astimezone(pytz.utc).isoformat()
value = self.value value = self.value
if isinstance(self.value, datetime.datetime): if isinstance(value, datetime.datetime):
value = self.value.astimezone().isoformat() value = value.astimezone().astimezone(pytz.utc).isoformat()
if isinstance(self.value, Decimal): if isinstance(value, Decimal):
value = float(self.value) value = float(value)
output = { output = {
'datetime': timestamp, 'datetime': timestamp,
'value': value, 'value': value,
@@ -114,6 +167,61 @@ class MBusObject(DSMRObject):
return json.dumps(output) 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): class CosemObject(DSMRObject):
@property @property
@@ -127,14 +235,14 @@ class CosemObject(DSMRObject):
def __str__(self): def __str__(self):
print_value = self.value print_value = self.value
if isinstance(self.value, datetime.datetime): if isinstance(self.value, datetime.datetime):
print_value = self.value.astimezone().isoformat() print_value = self.value.astimezone().astimezone(pytz.utc).isoformat()
output = "{}\t[{}]".format(str(print_value), str(self.unit)) output = "{}\t[{}]".format(str(print_value), str(self.unit))
return output return output
def to_json(self): def to_json(self):
json_value = self.value json_value = self.value
if isinstance(self.value, datetime.datetime): if isinstance(self.value, datetime.datetime):
json_value = self.value.astimezone().isoformat() json_value = self.value.astimezone().astimezone(pytz.utc).isoformat()
if isinstance(self.value, Decimal): if isinstance(self.value, Decimal):
json_value = float(self.value) json_value = float(self.value)
output = { output = {
@@ -151,8 +259,8 @@ class ProfileGenericObject(DSMRObject):
containing the datetime (timestamp) and the value. containing the datetime (timestamp) and the value.
""" """
def __init__(self, values): def __init__(self, *args, **kwargs):
super().__init__(values) super().__init__(*args, **kwargs)
self._buffer_list = None self._buffer_list = None
@property @property
@@ -178,9 +286,16 @@ class ProfileGenericObject(DSMRObject):
if self._buffer_list is None: if self._buffer_list is None:
self._buffer_list = [] self._buffer_list = []
values_offset = 2 values_offset = 2
for i in range(self.buffer_length): for i in range(self.buffer_length):
offset = values_offset + i*2 offset = values_offset + i * 2
self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) self._buffer_list.append(
MBusObject(
obis_id_code=self.obis_id_code,
values=[self.values[offset], self.values[offset + 1]]
)
)
return self._buffer_list return self._buffer_list
def __str__(self): def __str__(self):
@@ -189,7 +304,7 @@ class ProfileGenericObject(DSMRObject):
for buffer_value in self.buffer: for buffer_value in self.buffer:
timestamp = buffer_value.datetime timestamp = buffer_value.datetime
if isinstance(timestamp, datetime.datetime): if isinstance(timestamp, datetime.datetime):
timestamp = str(timestamp.astimezone().isoformat()) timestamp = str(timestamp.astimezone().astimezone(pytz.utc).isoformat())
output += "\n\t event occured at: {}".format(timestamp) output += "\n\t event occured at: {}".format(timestamp)
output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit) output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit)
return output return output
@@ -215,3 +330,39 @@ class ProfileGenericObject(DSMRObject):
list.append(['buffer', buffer_repr]) list.append(['buffer', buffer_repr])
output = dict(list) output = dict(list)
return json.dumps(output) 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)
+142 -28
View File
@@ -1,10 +1,16 @@
import logging import logging
import re import re
from binascii import unhexlify
from ctypes import c_ushort from ctypes import c_ushort
from decimal import Decimal
from dsmr_parser.objects import MBusObject, CosemObject, ProfileGenericObject 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 from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.value_types import timestamp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -19,47 +25,90 @@ class TelegramParser(object):
telegram DSMR version (v4 and up). telegram DSMR version (v4 and up).
:type telegram_specification: dict :type telegram_specification: dict
""" """
self.telegram_specification = telegram_specification
self.apply_checksum_validation = apply_checksum_validation self.apply_checksum_validation = apply_checksum_validation
self.telegram_specification = telegram_specification
# 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']
}
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. Parse telegram from string to dict.
The telegram str type makes python 2.x integration easier. The telegram str type makes python 2.x integration easier.
:param str telegram_data: full telegram from start ('/') to checksum :param str telegram_data: full telegram from start ('/') to checksum
('!ABCD') including line endings in between the telegram's lines ('!ABCD') including line endings in between the telegram's lines
:rtype: dict :param str encryption_key: encryption key
:returns: Shortened example: :param str authentication_key: authentication key
{ :rtype: Telegram
..
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
..
}
:raises ParseError: :raises ParseError:
:raises InvalidChecksumError: :raises InvalidChecksumError:
""" """
if self.apply_checksum_validation \ if "general_global_cipher" in self.telegram_specification:
and self.telegram_specification['checksum_support']: 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) self.validate_checksum(telegram_data)
telegram = {} telegram = Telegram()
for signature, parser in self.telegram_specification['objects'].items(): for object in self.telegram_specification['objects']:
match = re.search(signature, telegram_data, re.DOTALL) pattern = self.telegram_specification_regexes[object["obis_reference"]]
matches = pattern.findall(telegram_data)
# Some signatures are optional and may not be present, # Some signatures are optional and may not be present,
# so only parse lines that match # so only parse lines that match
if match: for match in matches:
try: try:
telegram[signature] = parser.parse(match.group(0)) dsmr_object = object["value_parser"].parse(match)
except Exception: except ParseError:
logger.error("ignore line with signature {}, because parsing failed.".format(signature), logger.error(
exc_info=True) "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 return telegram
@@ -141,6 +190,20 @@ class DSMRObjectParser(object):
return [self.value_formats[i].parse(value) return [self.value_formats[i].parse(value)
for i, value in enumerate(values)] 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): def _parse(self, line):
# Match value groups, but exclude the parentheses # 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,}(?=\)))')
@@ -174,7 +237,52 @@ class MBusParser(DSMRObjectParser):
""" """
def parse(self, line): 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): class CosemParser(DSMRObjectParser):
@@ -198,7 +306,10 @@ class CosemParser(DSMRObjectParser):
""" """
def parse(self, line): 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): class ProfileGenericParser(DSMRObjectParser):
@@ -224,7 +335,7 @@ class ProfileGenericParser(DSMRObjectParser):
""" """
def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): def __init__(self, buffer_types, head_parsers, parsers_for_unidentified):
self.value_formats = head_parsers self.value_formats = head_parsers.copy()
self.buffer_types = buffer_types self.buffer_types = buffer_types
self.parsers_for_unidentified = parsers_for_unidentified self.parsers_for_unidentified = parsers_for_unidentified
@@ -257,7 +368,10 @@ class ProfileGenericParser(DSMRObjectParser):
return [self.value_formats[i].parse(value) for i, value in enumerate(values)] return [self.value_formats[i].parse(value) for i, value in enumerate(values)]
def parse(self, line): def parse(self, line):
return ProfileGenericObject(self._parse(line)) return ProfileGenericObject(
obis_id_code=self._parse_obis_id_code(line),
values=self._parse(line)
)
class ValueParser(object): class ValueParser(object):
@@ -265,7 +379,7 @@ class ValueParser(object):
Parses a single value from DSMRObject's. Parses a single value from DSMRObject's.
Example with coerce_type being int: 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: Example with coerce_type being str:
(42) becomes {'value': '42', 'unit': None} (42) becomes {'value': '42', 'unit': None}
@@ -7,4 +7,4 @@ PG_HEAD_PARSERS = [ValueParser(int), ValueParser(str)]
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)] PG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)]
BUFFER_TYPES = { BUFFER_TYPES = {
PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)] PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)]
} }
File diff suppressed because it is too large Load Diff
+14 -3
View File
@@ -2,17 +2,28 @@ import datetime
import pytz import pytz
# TODO : Use system timezone
# Preload timezone to avoid loading in event loop later
local_tz = pytz.timezone('Europe/Amsterdam')
def timestamp(value): 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: if len(value) == 13:
is_dst = value[12] == 'S' # assume format 160322150000W is_dst = value[12] == 'S' # assume format 160322150000W
else: else:
is_dst = False is_dst = False
local_tz = pytz.timezone('Europe/Amsterdam')
localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst) localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst)
return localized_datetime.astimezone(pytz.utc) return localized_datetime.astimezone(pytz.utc)
+5 -4
View File
@@ -4,16 +4,17 @@ setup(
name='dsmr-parser', name='dsmr-parser',
description='Library to parse Dutch Smart Meter Requirements (DSMR)', description='Library to parse Dutch Smart Meter Requirements (DSMR)',
author='Nigel Dokter and many others', author='Nigel Dokter and many others',
author_email='nigel@nldr.net', author_email='mail@nldr.net',
license='MIT', license='MIT',
url='https://github.com/ndokter/dsmr_parser', url='https://github.com/ndokter/dsmr_parser',
version='0.31', version='1.4.2',
packages=find_packages(exclude=('test', 'test.*')), packages=find_packages(exclude=('test', 'test.*')),
install_requires=[ install_requires=[
'pyserial>=3,<4', 'pyserial>=3,<4',
'pyserial-asyncio<1', 'pyserial-asyncio-fast>=0.11',
'pytz', 'pytz',
'Tailer==0.4.1' 'Tailer==0.4.1',
'dlms_cosem==21.3.2'
], ],
entry_points={ entry_points={
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
+240 -3
View File
@@ -129,6 +129,135 @@ TELEGRAM_V5 = (
'!6EEE\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 # EasyMeter via COM-1 Ethernet Gateway
# Q3D Manual (german) https://www.easymeter.com/downloads/products/zaehler/Q3D/Easymeter_Q3D_DE_2016-06-15.pdf # Q3D Manual (german) https://www.easymeter.com/downloads/products/zaehler/Q3D/Easymeter_Q3D_DE_2016-06-15.pdf
# - type code on page 8 # - type code on page 8
@@ -136,7 +265,7 @@ TELEGRAM_V5 = (
# #
# last two lines are added by the COM-1 Ethernet Gateway # last two lines are added by the COM-1 Ethernet Gateway
TELEGRAM_ESY5Q3DB1024_V304 = ( # Easymeter an Hauptstromzähler TELEGRAM_ESY5Q3DB1024_V304 = (
'/ESY5Q3DB1024 V3.04\r\n' '/ESY5Q3DB1024 V3.04\r\n'
'\r\n' '\r\n'
'1-0:0.0.0*255(0272031312565)\r\n' '1-0:0.0.0*255(0272031312565)\r\n'
@@ -150,10 +279,11 @@ TELEGRAM_ESY5Q3DB1024_V304 = ( # Easymeter an Hauptstromzähler
'0-0:96.1.255*255(1ESY1313002565)\r\n' '0-0:96.1.255*255(1ESY1313002565)\r\n'
'!\r\n' '!\r\n'
' 25803103\r\n' ' 25803103\r\n'
'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\r\n' '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
'\xff\xff\xff\xff\xff\r\n'
) )
TELEGRAM_ESY5Q3DA1004_V304 = ( # Easymeter an Wärmepumpe TELEGRAM_ESY5Q3DA1004_V304 = (
'/ESY5Q3DA1004 V3.04\r\n' '/ESY5Q3DA1004 V3.04\r\n'
'\r\n' '\r\n'
'1-0:0.0.0*255(1336001560)\r\n' '1-0:0.0.0*255(1336001560)\r\n'
@@ -168,3 +298,110 @@ TELEGRAM_ESY5Q3DA1004_V304 = ( # Easymeter an Wärmepumpe
' 25818685\r\n' ' 25818685\r\n'
'DE0000000000000000000000000000003\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'
)
+2 -3
View File
@@ -1,8 +1,7 @@
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications
from dsmr_parser.objects import Telegram
from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import TelegramParser
from example_telegrams import TELEGRAM_V4_2 from test.example_telegrams import TELEGRAM_V4_2
parser = TelegramParser(telegram_specifications.V4) parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) telegram = parser.parse(TELEGRAM_V4_2)
print(telegram) print(telegram)
View File
+73
View 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'
)
)
@@ -2,7 +2,6 @@ import unittest
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications
from dsmr_parser.objects import Telegram
from dsmr_parser.objects import ProfileGenericObject from dsmr_parser.objects import ProfileGenericObject
from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import TelegramParser
from dsmr_parser.parsers import ProfileGenericParser from dsmr_parser.parsers import ProfileGenericParser
@@ -18,7 +17,7 @@ class TestParserCornerCases(unittest.TestCase):
def test_power_event_log_empty_1(self): def test_power_event_log_empty_1(self):
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0) # POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
parser = TelegramParser(telegram_specifications.V5) parser = TelegramParser(telegram_specifications.V5)
telegram = Telegram(TELEGRAM_V5, parser, telegram_specifications.V5) telegram = parser.parse(TELEGRAM_V5)
object_type = ProfileGenericObject object_type = ProfileGenericObject
testitem = telegram.POWER_EVENT_FAILURE_LOG testitem = telegram.POWER_EVENT_FAILURE_LOG
@@ -1,15 +1,15 @@
import json
import unittest import unittest
import datetime import datetime
import pytz import pytz
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications, obis_references
from dsmr_parser import obis_name_mapping
from dsmr_parser.objects import CosemObject from dsmr_parser.objects import CosemObject
from dsmr_parser.objects import MBusObject from dsmr_parser.objects import MBusObject
from dsmr_parser.objects import Telegram
from dsmr_parser.objects import ProfileGenericObject from dsmr_parser.objects import ProfileGenericObject
from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import TelegramParser
from test.example_telegrams import TELEGRAM_V4_2 from test.example_telegrams import TELEGRAM_V4_2, TELEGRAM_V5_TWO_MBUS, TELEGRAM_V5
from decimal import Decimal from decimal import Decimal
@@ -30,7 +30,7 @@ class TelegramTest(unittest.TestCase):
def test_instantiate(self): def test_instantiate(self):
parser = TelegramParser(telegram_specifications.V4) parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) telegram = parser.parse(TELEGRAM_V4_2)
# P1_MESSAGE_HEADER (1-3:0.2.8) # P1_MESSAGE_HEADER (1-3:0.2.8)
self.verify_telegram_item(telegram, self.verify_telegram_item(telegram,
@@ -216,14 +216,6 @@ class TelegramTest(unittest.TestCase):
value_type=Decimal, value_type=Decimal,
value_val=Decimal('2')) value_val=Decimal('2'))
# DEVICE_TYPE (0-x:24.1.0)
self.verify_telegram_item(telegram,
'DEVICE_TYPE',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=3)
# INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)
self.verify_telegram_item(telegram, self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
@@ -272,7 +264,15 @@ class TelegramTest(unittest.TestCase):
value_type=Decimal, value_type=Decimal,
value_val=Decimal('0')) value_val=Decimal('0'))
# EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) # DEVICE_TYPE (0-1:24.1.0)
self.verify_telegram_item(telegram,
'DEVICE_TYPE',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=3)
# EQUIPMENT_IDENTIFIER_GAS (0-1:96.1.0)
self.verify_telegram_item(telegram, self.verify_telegram_item(telegram,
'EQUIPMENT_IDENTIFIER_GAS', 'EQUIPMENT_IDENTIFIER_GAS',
object_type=CosemObject, object_type=CosemObject,
@@ -314,9 +314,173 @@ class TelegramTest(unittest.TestCase):
self.item_names_tested.append(testitem_name) self.item_names_tested.append(testitem_name)
# check if all items in telegram V4 specification are covered # check if all items in telegram V4 specification are covered
V4_name_list = [obis_name_mapping.EN[signature] for signature, parser in V4_name_list = [object["value_name"] for object in
telegram_specifications.V4['objects'].items()] telegram_specifications.V4['objects']]
V4_name_set = set(V4_name_list) V4_name_set = set(V4_name_list)
item_names_tested_set = set(self.item_names_tested) item_names_tested_set = set(self.item_names_tested)
assert item_names_tested_set == V4_name_set 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
View 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
View 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
View 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
View 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)
+4 -1
View File
@@ -14,7 +14,10 @@ class TelegramParserV2_2Test(unittest.TestCase):
def test_parse(self): def test_parse(self):
parser = TelegramParser(telegram_specifications.V2_2) 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) # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)
+4 -1
View File
@@ -14,7 +14,10 @@ class TelegramParserV3Test(unittest.TestCase):
def test_parse(self): def test_parse(self):
parser = TelegramParser(telegram_specifications.V3) 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) # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)
+4 -1
View File
@@ -17,7 +17,10 @@ class TelegramParserV4_2Test(unittest.TestCase):
def test_parse(self): def test_parse(self):
parser = TelegramParser(telegram_specifications.V4) 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) # P1_MESSAGE_HEADER (1-3:0.2.8)
assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject) assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject)
+170 -142
View File
@@ -5,7 +5,6 @@ import unittest
import pytz import pytz
from dsmr_parser import obis_references as obis
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications
from dsmr_parser.exceptions import InvalidChecksumError, ParseError from dsmr_parser.exceptions import InvalidChecksumError, ParseError
from dsmr_parser.objects import CosemObject, MBusObject from dsmr_parser.objects import CosemObject, MBusObject
@@ -18,211 +17,219 @@ class TelegramParserV5Test(unittest.TestCase):
def test_parse(self): def test_parse(self):
parser = TelegramParser(telegram_specifications.V5) 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) # P1_MESSAGE_HEADER (1-3:0.2.8)
assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject) assert isinstance(telegram.P1_MESSAGE_HEADER, CosemObject)
assert result[obis.P1_MESSAGE_HEADER].unit is None assert telegram.P1_MESSAGE_HEADER.unit is None
assert isinstance(result[obis.P1_MESSAGE_HEADER].value, str) assert isinstance(telegram.P1_MESSAGE_HEADER.value, str)
assert result[obis.P1_MESSAGE_HEADER].value == '50' assert telegram.P1_MESSAGE_HEADER.value == '50'
# P1_MESSAGE_TIMESTAMP (0-0:1.0.0) # P1_MESSAGE_TIMESTAMP (0-0:1.0.0)
assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP], CosemObject) assert isinstance(telegram.P1_MESSAGE_TIMESTAMP, CosemObject)
assert result[obis.P1_MESSAGE_TIMESTAMP].unit is None assert telegram.P1_MESSAGE_TIMESTAMP.unit is None
assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP].value, datetime.datetime) assert isinstance(telegram.P1_MESSAGE_TIMESTAMP.value, datetime.datetime)
assert result[obis.P1_MESSAGE_TIMESTAMP].value == \ assert telegram.P1_MESSAGE_TIMESTAMP.value == \
datetime.datetime(2017, 1, 2, 18, 20, 2, tzinfo=pytz.UTC) datetime.datetime(2017, 1, 2, 18, 20, 2, tzinfo=pytz.UTC)
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1, CosemObject)
assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh' assert telegram.ELECTRICITY_USED_TARIFF_1.unit == 'kWh'
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal) assert isinstance(telegram.ELECTRICITY_USED_TARIFF_1.value, Decimal)
assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('4.426') assert telegram.ELECTRICITY_USED_TARIFF_1.value == Decimal('4.426')
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject) assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2, CosemObject)
assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh' assert telegram.ELECTRICITY_USED_TARIFF_2.unit == 'kWh'
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal) assert isinstance(telegram.ELECTRICITY_USED_TARIFF_2.value, Decimal)
assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('2.399') assert telegram.ELECTRICITY_USED_TARIFF_2.value == Decimal('2.399')
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject) assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1, CosemObject)
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh' assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.unit == 'kWh'
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal) assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_1.value, Decimal)
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('2.444') assert telegram.ELECTRICITY_DELIVERED_TARIFF_1.value == Decimal('2.444')
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject) assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2, CosemObject)
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh' assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.unit == 'kWh'
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal) assert isinstance(telegram.ELECTRICITY_DELIVERED_TARIFF_2.value, Decimal)
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('0') assert telegram.ELECTRICITY_DELIVERED_TARIFF_2.value == Decimal('0')
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject) assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF, CosemObject)
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None assert telegram.ELECTRICITY_ACTIVE_TARIFF.unit is None
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str) assert isinstance(telegram.ELECTRICITY_ACTIVE_TARIFF.value, str)
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0002' assert telegram.ELECTRICITY_ACTIVE_TARIFF.value == '0002'
# EQUIPMENT_IDENTIFIER (0-0:96.1.1) # EQUIPMENT_IDENTIFIER (0-0:96.1.1)
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject) assert isinstance(telegram.EQUIPMENT_IDENTIFIER, CosemObject)
assert result[obis.EQUIPMENT_IDENTIFIER].unit is None assert telegram.EQUIPMENT_IDENTIFIER.unit is None
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str) assert isinstance(telegram.EQUIPMENT_IDENTIFIER.value, str)
assert result[obis.EQUIPMENT_IDENTIFIER].value == '4B384547303034303436333935353037' assert telegram.EQUIPMENT_IDENTIFIER.value == '4B384547303034303436333935353037'
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject) assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE, CosemObject)
assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' assert telegram.CURRENT_ELECTRICITY_USAGE.unit == 'kW'
assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal) assert isinstance(telegram.CURRENT_ELECTRICITY_USAGE.value, Decimal)
assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.244') assert telegram.CURRENT_ELECTRICITY_USAGE.value == Decimal('0.244')
# CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject) assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY, CosemObject)
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW' assert telegram.CURRENT_ELECTRICITY_DELIVERY.unit == 'kW'
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal) assert isinstance(telegram.CURRENT_ELECTRICITY_DELIVERY.value, Decimal)
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0') assert telegram.CURRENT_ELECTRICITY_DELIVERY.value == Decimal('0')
# LONG_POWER_FAILURE_COUNT (96.7.9) # LONG_POWER_FAILURE_COUNT (96.7.9)
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject) assert isinstance(telegram.LONG_POWER_FAILURE_COUNT, CosemObject)
assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None assert telegram.LONG_POWER_FAILURE_COUNT.unit is None
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int) assert isinstance(telegram.LONG_POWER_FAILURE_COUNT.value, int)
assert result[obis.LONG_POWER_FAILURE_COUNT].value == 0 assert telegram.LONG_POWER_FAILURE_COUNT.value == 0
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21) # SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject) assert isinstance(telegram.SHORT_POWER_FAILURE_COUNT, CosemObject)
assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None assert telegram.SHORT_POWER_FAILURE_COUNT.unit is None
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int) assert isinstance(telegram.SHORT_POWER_FAILURE_COUNT.value, int)
assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 13 assert telegram.SHORT_POWER_FAILURE_COUNT.value == 13
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0) # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject) assert isinstance(telegram.VOLTAGE_SAG_L1_COUNT, CosemObject)
assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None assert telegram.VOLTAGE_SAG_L1_COUNT.unit is None
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT].value, int) assert isinstance(telegram.VOLTAGE_SAG_L1_COUNT.value, int)
assert result[obis.VOLTAGE_SAG_L1_COUNT].value == 0 assert telegram.VOLTAGE_SAG_L1_COUNT.value == 0
# VOLTAGE_SAG_L2_COUNT (1-0:52.32.0) # VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)
assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT], CosemObject) assert isinstance(telegram.VOLTAGE_SAG_L2_COUNT, CosemObject)
assert result[obis.VOLTAGE_SAG_L2_COUNT].unit is None assert telegram.VOLTAGE_SAG_L2_COUNT.unit is None
assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT].value, int) assert isinstance(telegram.VOLTAGE_SAG_L2_COUNT.value, int)
assert result[obis.VOLTAGE_SAG_L2_COUNT].value == 0 assert telegram.VOLTAGE_SAG_L2_COUNT.value == 0
# VOLTAGE_SAG_L3_COUNT (1-0:72.32.0) # VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)
assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT], CosemObject) assert isinstance(telegram.VOLTAGE_SAG_L3_COUNT, CosemObject)
assert result[obis.VOLTAGE_SAG_L3_COUNT].unit is None assert telegram.VOLTAGE_SAG_L3_COUNT.unit is None
assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT].value, int) assert isinstance(telegram.VOLTAGE_SAG_L3_COUNT.value, int)
assert result[obis.VOLTAGE_SAG_L3_COUNT].value == 0 assert telegram.VOLTAGE_SAG_L3_COUNT.value == 0
# VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0) # VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)
assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT], CosemObject) assert isinstance(telegram.VOLTAGE_SWELL_L1_COUNT, CosemObject)
assert result[obis.VOLTAGE_SWELL_L1_COUNT].unit is None assert telegram.VOLTAGE_SWELL_L1_COUNT.unit is None
assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT].value, int) assert isinstance(telegram.VOLTAGE_SWELL_L1_COUNT.value, int)
assert result[obis.VOLTAGE_SWELL_L1_COUNT].value == 0 assert telegram.VOLTAGE_SWELL_L1_COUNT.value == 0
# VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0) # VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)
assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT], CosemObject) assert isinstance(telegram.VOLTAGE_SWELL_L2_COUNT, CosemObject)
assert result[obis.VOLTAGE_SWELL_L2_COUNT].unit is None assert telegram.VOLTAGE_SWELL_L2_COUNT.unit is None
assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT].value, int) assert isinstance(telegram.VOLTAGE_SWELL_L2_COUNT.value, int)
assert result[obis.VOLTAGE_SWELL_L2_COUNT].value == 0 assert telegram.VOLTAGE_SWELL_L2_COUNT.value == 0
# VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0) # VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT], CosemObject) assert isinstance(telegram.VOLTAGE_SWELL_L3_COUNT, CosemObject)
assert result[obis.VOLTAGE_SWELL_L3_COUNT].unit is None assert telegram.VOLTAGE_SWELL_L3_COUNT.unit is None
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int) assert isinstance(telegram.VOLTAGE_SWELL_L3_COUNT.value, int)
assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0 assert telegram.VOLTAGE_SWELL_L3_COUNT.value == 0
# INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0) # INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject) assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1, CosemObject)
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V' assert telegram.INSTANTANEOUS_VOLTAGE_L1.unit == 'V'
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L1.value, Decimal)
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('230.0') assert telegram.INSTANTANEOUS_VOLTAGE_L1.value == Decimal('230.0')
# INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0) # INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject) assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2, CosemObject)
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V' assert telegram.INSTANTANEOUS_VOLTAGE_L2.unit == 'V'
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L2.value, Decimal)
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('230.0') assert telegram.INSTANTANEOUS_VOLTAGE_L2.value == Decimal('230.0')
# INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0) # INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject) assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3, CosemObject)
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V' assert telegram.INSTANTANEOUS_VOLTAGE_L3.unit == 'V'
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_VOLTAGE_L3.value, Decimal)
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('229.0') assert telegram.INSTANTANEOUS_VOLTAGE_L3.value == Decimal('229.0')
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject) assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1, CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A' assert telegram.INSTANTANEOUS_CURRENT_L1.unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_CURRENT_L1.value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.48') assert telegram.INSTANTANEOUS_CURRENT_L1.value == Decimal('0.48')
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject) assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2, CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A' assert telegram.INSTANTANEOUS_CURRENT_L2.unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_CURRENT_L2.value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.44') assert telegram.INSTANTANEOUS_CURRENT_L2.value == Decimal('0.44')
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject) assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3, CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A' assert telegram.INSTANTANEOUS_CURRENT_L3.unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_CURRENT_L3.value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.86') assert telegram.INSTANTANEOUS_CURRENT_L3.value == Decimal('0.86')
# TEXT_MESSAGE (0-0:96.13.0) # TEXT_MESSAGE (0-0:96.13.0)
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) assert isinstance(telegram.TEXT_MESSAGE, CosemObject)
assert result[obis.TEXT_MESSAGE].unit is None assert telegram.TEXT_MESSAGE.unit is None
assert result[obis.TEXT_MESSAGE].value is None assert telegram.TEXT_MESSAGE.value is None
# DEVICE_TYPE (0-x:24.1.0)
assert isinstance(result[obis.DEVICE_TYPE], CosemObject)
assert result[obis.DEVICE_TYPE].unit is None
assert isinstance(result[obis.DEVICE_TYPE].value, int)
assert result[obis.DEVICE_TYPE].value == 3
# INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, CosemObject)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW' assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.unit == 'kW'
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value, Decimal)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.070') assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE.value == Decimal('0.070')
# INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0) # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, CosemObject)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW' assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.unit == 'kW'
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value, Decimal)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.032') assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE.value == Decimal('0.032')
# INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0) # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, CosemObject)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW' assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.unit == 'kW'
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value, Decimal)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.142') assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE.value == Decimal('0.142')
# INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0) # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, CosemObject)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW' assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.unit == 'kW'
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value, Decimal)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0') assert telegram.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE.value == Decimal('0')
# INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0) # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, CosemObject)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW' assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.unit == 'kW'
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value, Decimal)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0') assert telegram.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE.value == Decimal('0')
# INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0) # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, CosemObject)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW' assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.unit == 'kW'
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal) assert isinstance(telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value, Decimal)
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0') assert telegram.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE.value == Decimal('0')
# EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) # There's only one Mbus device (gas meter) in this case. Alternatively
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject) # use get_mbus_device_by_channel
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None gas_meter_devices = telegram.MBUS_DEVICES
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str) gas_meter_device = gas_meter_devices[0]
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '3232323241424344313233343536373839'
# HOURLY_GAS_METER_READING (0-1:24.2.1) # MBUS_DEVICE_TYPE (0-1:96.1.0)
assert isinstance(result[obis.HOURLY_GAS_METER_READING], MBusObject) assert isinstance(gas_meter_device.MBUS_DEVICE_TYPE, CosemObject)
assert result[obis.HOURLY_GAS_METER_READING].unit == 'm3' assert gas_meter_device.MBUS_DEVICE_TYPE.unit is None
assert isinstance(result[obis.HOURLY_GAS_METER_READING].value, Decimal) assert isinstance(gas_meter_device.MBUS_DEVICE_TYPE.value, int)
assert result[obis.HOURLY_GAS_METER_READING].value == Decimal('0.107') 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): def test_checksum_valid(self):
# No exception is raised. # No exception is raised.
@@ -244,3 +251,24 @@ class TelegramParserV5Test(unittest.TestCase):
corrupted_telegram = TELEGRAM_V5.replace('!6EEE\r\n', '') corrupted_telegram = TELEGRAM_V5.replace('!6EEE\r\n', '')
with self.assertRaises(ParseError): with self.assertRaises(ParseError):
TelegramParser.validate_checksum(corrupted_telegram) 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
View 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 -3
View File
@@ -4,7 +4,7 @@ import unittest
from dsmr_parser import obis_references as obis from dsmr_parser import obis_references as obis
from dsmr_parser.clients.protocol import create_dsmr_protocol from dsmr_parser.clients.protocol import create_dsmr_protocol
from dsmr_parser.objects import Telegram
TELEGRAM_V2_2 = ( TELEGRAM_V2_2 = (
'/ISk5\2MT382-1004\r\n' '/ISk5\2MT382-1004\r\n'
@@ -44,7 +44,7 @@ class ProtocolTest(unittest.TestCase):
self.protocol.data_received(TELEGRAM_V2_2.encode('ascii')) self.protocol.data_received(TELEGRAM_V2_2.encode('ascii'))
telegram = self.protocol.telegram_callback.call_args_list[0][0][0] 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 float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01
assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
@@ -68,6 +68,6 @@ class ProtocolTest(unittest.TestCase):
# 2nd call of keep_alive should close the transport # 2nd call of keep_alive should close the transport
self.protocol.keep_alive() self.protocol.keep_alive()
assert mock_transport.close.called_once() mock_transport.close.assert_called_once()
self.protocol.connection_lost(None) self.protocol.connection_lost(None)
+77
View 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'
+8
View File
@@ -5,6 +5,8 @@ deps=
pylama pylama
pytest-asyncio pytest-asyncio
pytest-mock pytest-mock
dlms_cosem
setuptools
commands= commands=
py.test --cov=dsmr_parser test {posargs} py.test --cov=dsmr_parser test {posargs}
pylama dsmr_parser test pylama dsmr_parser test
@@ -12,9 +14,15 @@ commands=
[pylama:dsmr_parser/clients/__init__.py] [pylama:dsmr_parser/clients/__init__.py]
ignore = W0611 ignore = W0611
[pylama:dsmr_parser/clients/socket_.py]
ignore = C901
[pylama:dsmr_parser/parsers.py] [pylama:dsmr_parser/parsers.py]
ignore = W605 ignore = W605
[pylama:test/example_telegrams.py]
ignore = E501
[pylama:pylint] [pylama:pylint]
max_line_length = 120 max_line_length = 120