Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dc77a8231 | ||
|
|
d61f2229c8 | ||
|
|
3ddf0366e6 | ||
|
|
629767590b | ||
|
|
81cccbd228 | ||
|
|
5b1e830018 | ||
|
|
2d712b506d | ||
|
|
1318204d0c | ||
|
|
feb0f88ddc | ||
|
|
602129a665 | ||
|
|
af9a99d995 | ||
|
|
a7b4929eab | ||
|
|
8999b1c924 | ||
|
|
2391f38c4a | ||
|
|
d1a6e15c68 | ||
|
|
b7c8626d0c | ||
|
|
684023d0b6 | ||
|
|
1a886aa03b | ||
|
|
1c7535448e | ||
|
|
2a95d33c02 | ||
|
|
de6725dff8 | ||
|
|
1b1a3f505e | ||
|
|
ebea385753 | ||
|
|
700cf6a2b3 | ||
|
|
7a687a99a6 | ||
|
|
86d665df39 | ||
|
|
ec1d37ea08 | ||
|
|
dc902a83e9 | ||
|
|
be4386bc59 | ||
|
|
0427ace079 | ||
|
|
addf9b590e | ||
|
|
51f821a7fc | ||
|
|
5630520771 | ||
|
|
f9e188812f | ||
|
|
837ba3b6f7 | ||
|
|
94447c3571 | ||
|
|
c2dea29c83 | ||
|
|
bb486fc76f | ||
|
|
789871899c | ||
|
|
b6278a8991 | ||
|
|
a0ce89054a | ||
|
|
9610fbc3c1 | ||
|
|
d1ad4fa585 | ||
|
|
a44afb1a59 | ||
|
|
d98c93a57f | ||
|
|
fc4a96ebab | ||
|
|
c4331f6cd6 | ||
|
|
88c9ccd83d | ||
|
|
78419f6cc7 | ||
|
|
d423070853 | ||
|
|
987d35332a | ||
|
|
fe710e9272 | ||
|
|
a7b0b03391 | ||
|
|
5d88284d8d | ||
|
|
b6537678a7 | ||
|
|
fee3f696c4 | ||
|
|
dc6c35a0b6 | ||
|
|
47c20e763f | ||
|
|
e7164d9af7 | ||
|
|
d714528c5a | ||
|
|
0675a6e2e7 | ||
|
|
e6625df4a7 | ||
|
|
8c2485d70f | ||
|
|
eac9681e07 | ||
|
|
659560222a | ||
|
|
74fe1f2d1e | ||
|
|
a01e673646 | ||
|
|
09614860a6 | ||
|
|
3bfb555d0e | ||
|
|
2d4b0d8e72 | ||
|
|
43500e6bc2 | ||
|
|
50cef2646b | ||
|
|
3a8b4d2458 | ||
|
|
12aa003799 | ||
|
|
86690ef44b | ||
|
|
7c9c59308e | ||
|
|
c1d7ba151d | ||
|
|
a65949823d | ||
|
|
3c78b0b6c4 | ||
|
|
5313edd6cb | ||
|
|
aa4f9fb967 | ||
|
|
ea804d485e | ||
|
|
1b522fc7f0 | ||
|
|
a137ef0e02 | ||
|
|
d712d468ac | ||
|
|
95bab6a51a | ||
|
|
49999618ea | ||
|
|
8ba400800b | ||
|
|
f7ba363b93 | ||
|
|
b31bcd61fc | ||
|
|
f9e9e70771 | ||
|
|
50a5cb9203 | ||
|
|
8388624721 | ||
|
|
c36f68a884 | ||
|
|
8bdf77c78d | ||
|
|
c04b0a5add | ||
|
|
ed2290a597 | ||
|
|
85c67464a1 | ||
|
|
48783acc00 | ||
|
|
41f6a7ac16 | ||
|
|
59811dbf9f | ||
|
|
9b09969763 | ||
|
|
64f5dc2e87 | ||
|
|
887dd3a2aa | ||
|
|
ad6ab304f5 | ||
|
|
8582395017 | ||
|
|
3327c78c0e | ||
|
|
472c54968e | ||
|
|
d534b1d8b0 | ||
|
|
d94bc8de03 | ||
|
|
41381f1705 | ||
|
|
e3203a5334 | ||
|
|
c78ebe3e2d | ||
|
|
d6e28db116 | ||
|
|
5f1afeb1f8 | ||
|
|
dcb59fddb1 | ||
|
|
9b0a85e84b | ||
|
|
1dca6ab396 | ||
|
|
efc09df71f | ||
|
|
de5c884d8d | ||
|
|
d2f57a8926 | ||
|
|
3d64fea247 | ||
|
|
46860e04c1 | ||
|
|
148bdabc12 | ||
|
|
9d20bb8ad5 | ||
|
|
24ab9aa712 | ||
|
|
0c40070752 | ||
|
|
6c8a9dcbdb | ||
|
|
c1a6b930c8 | ||
|
|
9e74c4c23c | ||
|
|
b4a520c8b4 | ||
|
|
a88dfe1a41 | ||
|
|
c4dcc73191 | ||
|
|
c4caf54576 | ||
|
|
8a868ce826 | ||
|
|
9623f3b092 | ||
|
|
adcfdfe2ae | ||
|
|
45ee8dbb32 | ||
|
|
7a4c204850 | ||
|
|
45f5fe2c36 | ||
|
|
07634abed1 | ||
|
|
e2e4bb36a2 | ||
|
|
4eeefec426 | ||
|
|
f3d8311ac2 | ||
|
|
9b488e74f8 | ||
|
|
fadf206715 | ||
|
|
e97ab7c7ea | ||
|
|
759e0a0d92 | ||
|
|
21334e5a0a | ||
|
|
11672d0512 | ||
|
|
87a5a2d2fa | ||
|
|
663024239f | ||
|
|
0e7819b535 | ||
|
|
60317a0dc5 | ||
|
|
d990a316ad | ||
|
|
f10032f701 | ||
|
|
1373d570d2 | ||
|
|
29fc97a65c | ||
|
|
3b43cbf841 | ||
|
|
9d8cad8b46 | ||
|
|
0d6c763e86 | ||
|
|
8b60d48edd | ||
|
|
03b761e15b | ||
|
|
061334f702 | ||
|
|
920c9aedc2 | ||
|
|
991dd09e2a | ||
|
|
ce4d5b0e62 | ||
|
|
e512456cc2 | ||
|
|
3c9db523fa | ||
|
|
763237ef1d | ||
|
|
cdc9e395aa |
@@ -2,7 +2,13 @@
|
||||
*.pyc
|
||||
.tox
|
||||
.cache
|
||||
.venv
|
||||
*.egg-info
|
||||
/.project
|
||||
/.pydevproject
|
||||
/.coverage
|
||||
build/
|
||||
dist/
|
||||
venv/
|
||||
*.*~
|
||||
*~
|
||||
+2
-1
@@ -2,8 +2,9 @@ language: python
|
||||
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.8
|
||||
|
||||
install: pip install tox-travis codecov
|
||||
|
||||
|
||||
+109
@@ -1,6 +1,115 @@
|
||||
Change Log
|
||||
----------
|
||||
|
||||
**0.27** (2020-12-24)
|
||||
- fix for empty parentheses in ProfileGenericParser (redone) (`pull request #69 <https://github.com/ndokter/dsmr_parser/pull/69>`_).
|
||||
|
||||
**0.26** (2020-12-15)
|
||||
- reverted fix for empty parentheses in ProfileGenericParser (`pull request #68 <https://github.com/ndokter/dsmr_parser/pull/68>`_).
|
||||
|
||||
**0.25** (2020-12-14)
|
||||
- fix for empty parentheses in ProfileGenericParser (`pull request #57 <https://github.com/ndokter/dsmr_parser/pull/57>`_).
|
||||
|
||||
**0.24** (2020-11-27)
|
||||
- Add Luxembourg equipment identifier (`pull request #62 <https://github.com/ndokter/dsmr_parser/pull/62>`_).
|
||||
|
||||
**0.23** (2020-11-07)
|
||||
- Resolved issue with x-x:24.3.0 where it contains non-integer character (`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>`_).
|
||||
- Example telegram improvement (`pull request #58 <https://github.com/ndokter/dsmr_parser/pull/58>`_).
|
||||
|
||||
**0.22** (2020-08-23)
|
||||
|
||||
- CRC check speed is improved
|
||||
- Exception info improvement
|
||||
|
||||
**0.21** (2020-05-25)
|
||||
|
||||
- All objects can produce a json serialization of their state.
|
||||
|
||||
**0.20** (2020-05-12)
|
||||
|
||||
- All objects can now print their values
|
||||
- Add parser + object for generic profile
|
||||
|
||||
**0.19** (2020-05-03)
|
||||
|
||||
- Add following missing elements to telegram specification v4:
|
||||
- SHORT_POWER_FAILURE_COUNT,
|
||||
- INSTANTANEOUS_CURRENT_L1,
|
||||
- INSTANTANEOUS_CURRENT_L2,
|
||||
- INSTANTANEOUS_CURRENT_L3
|
||||
- Add missing tests + fix small test bugs
|
||||
- Complete telegram object v4 parse test
|
||||
|
||||
**0.18** (2020-01-28)
|
||||
|
||||
- PyCRC replacement (`pull request #48 <https://github.com/ndokter/dsmr_parser/pull/48>`_).
|
||||
|
||||
**0.17** (2019-12-21)
|
||||
|
||||
- Add a true telegram object (`pull request #40 <https://github.com/ndokter/dsmr_parser/pull/40>`_).
|
||||
|
||||
**0.16** (2019-12-21)
|
||||
|
||||
- Add support for Belgian and Smarty meters (`pull request #44 <https://github.com/ndokter/dsmr_parser/pull/44>`_).
|
||||
|
||||
**0.15** (2019-12-12)
|
||||
|
||||
- Fixed asyncio loop issue (`pull request #43 <https://github.com/ndokter/dsmr_parser/pull/43>`_).
|
||||
|
||||
**0.14** (2019-10-08)
|
||||
|
||||
- Changed serial reading to reduce CPU usage (`pull request #37 <https://github.com/ndokter/dsmr_parser/pull/37>`_).
|
||||
|
||||
**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>`_).
|
||||
|
||||
**0.12** (2018-09-23)
|
||||
|
||||
- Add serial settings for DSMR v5.0 (`pull request #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>`_).
|
||||
|
||||
**0.11** (2017-09-18)
|
||||
|
||||
- NULL value fix in checksum (`pull request #26 <https://github.com/ndokter/dsmr_parser/pull/26>`_)
|
||||
|
||||
**0.10** (2017-06-05)
|
||||
|
||||
- bugfix: don't force full telegram signatures (`pull request #25 <https://github.com/ndokter/dsmr_parser/pull/25>`_)
|
||||
- removed unused code for automatic telegram detection as this needs reworking after the fix mentioned above
|
||||
- InvalidChecksumError's are logged as warning instead of error
|
||||
|
||||
**0.9** (2017-05-12)
|
||||
|
||||
- added DSMR v5 serial settings
|
||||
|
||||
**0.8** (2017-01-26)
|
||||
|
||||
- added support for DSMR v3
|
||||
- added support for DSMR v5
|
||||
|
||||
**IMPORTANT: this release has the following backwards incompatible changes:**
|
||||
|
||||
- Removed TelegramParserV2_2 in favor of TelegramParser
|
||||
- Removed TelegramParserV4 in favor of TelegramParser
|
||||
|
||||
**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>`_)
|
||||
|
||||
**IMPORTANT: this release has the following backwards incompatible changes:**
|
||||
|
||||
- Client related imports from dsmr_parser.serial and dsmr_parser.protocol have been moved to dsmr_parser.clients (import these from the clients/__init__.py module)
|
||||
- The .parse() method of TelegramParser, TelegramParserV2_2, TelegramParserV4 now accepts a string containing the entire telegram (including \r\n characters) and not a list
|
||||
|
||||
|
||||
**0.6** (2017-01-04)
|
||||
|
||||
- Fixed bug in CRC checksum verification for the asyncio client (`pull request #15 <https://github.com/ndokter/dsmr_parser/pull/15>`_)
|
||||
- Support added for TCP connections using the asyncio client (`pull request #12 <https://github.com/ndokter/dsmr_parser/pull/12/>`_)
|
||||
|
||||
**0.5** (2016-12-29)
|
||||
|
||||
- CRC checksum verification for DSMR v4 telegrams (`issue #10 <https://github.com/ndokter/dsmr_parser/issues/10>`_)
|
||||
|
||||
+215
-21
@@ -8,50 +8,244 @@ DSMR Parser
|
||||
:target: https://travis-ci.org/ndokter/dsmr_parser
|
||||
|
||||
A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It
|
||||
also includes a serial client to directly read and parse smart meter data.
|
||||
also includes client implementation to directly read and parse smart meter data.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
DSMR Parser currently supports DSMR versions 2.2 and 4.x. It has been tested with Python 3.4 and 3.5.
|
||||
DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.4, 3.5 and 3.6.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
Client module usage
|
||||
-------------------
|
||||
|
||||
Using the serial reader to connect to your smart meter and parse it's telegrams:
|
||||
**Serial client**
|
||||
|
||||
Read the serial port and work with the parsed telegrams. It should be run in a separate
|
||||
process because the code is blocking (not asynchronous):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V4
|
||||
|
||||
serial_reader = SerialReader(
|
||||
device='/dev/ttyUSB0',
|
||||
serial_settings=SERIAL_SETTINGS_V4,
|
||||
telegram_specification=telegram_specifications.V4
|
||||
)
|
||||
|
||||
for telegram in serial_reader.read():
|
||||
print(telegram) # see 'Telegram object' docs below
|
||||
|
||||
**AsyncIO client**
|
||||
|
||||
To be documented.
|
||||
|
||||
|
||||
Parsing module usage
|
||||
--------------------
|
||||
The parsing module accepts complete unaltered telegram strings and parses these
|
||||
into a dictionary.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
|
||||
# String is formatted in separate lines for readability.
|
||||
telegram_str = (
|
||||
'/ISk5\\2MT382-1000\r\n'
|
||||
'\r\n'
|
||||
'0-0:96.1.1(4B384547303034303436333935353037)\r\n'
|
||||
'1-0:1.8.1(12345.678*kWh)\r\n'
|
||||
'1-0:1.8.2(12345.678*kWh)\r\n'
|
||||
'1-0:2.8.1(12345.678*kWh)\r\n'
|
||||
'1-0:2.8.2(12345.678*kWh)\r\n'
|
||||
'0-0:96.14.0(0002)\r\n'
|
||||
'1-0:1.7.0(001.19*kW)\r\n'
|
||||
'1-0:2.7.0(000.00*kW)\r\n'
|
||||
'0-0:17.0.0(016*A)\r\n'
|
||||
'0-0:96.3.10(1)\r\n'
|
||||
'0-0:96.13.1(303132333435363738)\r\n'
|
||||
'0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E'
|
||||
'3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F30313233'
|
||||
'3435363738393A3B3C3D3E3F)\r\n'
|
||||
'0-1:96.1.0(3232323241424344313233343536373839)\r\n'
|
||||
'0-1:24.1.0(03)\r\n'
|
||||
'0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n'
|
||||
'(00001.001)\r\n'
|
||||
'0-1:24.4.0(1)\r\n'
|
||||
'!\r\n'
|
||||
)
|
||||
|
||||
parser = TelegramParser(telegram_specifications.V3)
|
||||
|
||||
telegram = parser.parse(telegram_str)
|
||||
print(telegram) # see 'Telegram object' docs below
|
||||
|
||||
Telegram dictionary
|
||||
-------------------
|
||||
|
||||
A dictionary of which the key indicates the field type. These regex values
|
||||
correspond to one of dsmr_parser.obis_reference constants.
|
||||
|
||||
The value is either a CosemObject or MBusObject. These have a 'value' and 'unit'
|
||||
property. MBusObject's additionally have a 'datetime' property. The 'value' can
|
||||
contain any python type (int, str, Decimal) depending on the field. The 'unit'
|
||||
contains 'kW', 'A', 'kWh' or 'm3'.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Contents of a parsed DSMR v3 telegram
|
||||
{'\\d-\\d:17\\.0\\.0.+?\\r\\n': <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
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser import obis_references
|
||||
from dsmr_parser.serial import SerialReader, SERIAL_SETTINGS_V4
|
||||
|
||||
# The telegram message timestamp.
|
||||
message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP]
|
||||
|
||||
# Using the active tariff to determine the electricity being used and
|
||||
# delivered for the right tariff.
|
||||
active_tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF]
|
||||
active_tariff = int(tariff.value)
|
||||
|
||||
electricity_used_total = telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[active_tariff - 1]]
|
||||
electricity_delivered_total = telegram[obis_references.ELECTRICITY_DELIVERED_TARIFF_ALL[active_tariff - 1]]
|
||||
|
||||
gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING]
|
||||
|
||||
# See dsmr_reader.obis_references for all readable telegram values.
|
||||
# Note that the avilable values differ per DSMR version.
|
||||
|
||||
Telegram as an Object
|
||||
---------------------
|
||||
An object version of the telegram is available as well.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# DSMR v4.2 p1 using dsmr_parser and telegram objects
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5
|
||||
from dsmr_parser.objects import CosemObject, MBusObject, Telegram
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
import os
|
||||
|
||||
serial_reader = SerialReader(
|
||||
device='/dev/ttyUSB0',
|
||||
serial_settings=SERIAL_SETTINGS_V4,
|
||||
serial_settings=SERIAL_SETTINGS_V5,
|
||||
telegram_specification=telegram_specifications.V4
|
||||
)
|
||||
|
||||
for telegram in serial_reader.read():
|
||||
# telegram = next(serial_reader.read_as_object())
|
||||
# print(telegram)
|
||||
|
||||
# The telegram message timestamp.
|
||||
message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP]
|
||||
for telegram in serial_reader.read_as_object():
|
||||
os.system('clear')
|
||||
print(telegram)
|
||||
|
||||
# Using the active tariff to determine the electricity being used and
|
||||
# delivered for the right tariff.
|
||||
tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF]
|
||||
tariff = int(tariff.value)
|
||||
Example of output of print of the telegram object:
|
||||
|
||||
electricity_used_total \
|
||||
= telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[tariff - 1]]
|
||||
electricity_delivered_total = \
|
||||
telegram[obis_referencesELECTRICITY_DELIVERED_TARIFF_ALL[tariff - 1]]
|
||||
.. code-block:: console
|
||||
|
||||
gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING]
|
||||
P1_MESSAGE_HEADER: 42 [None]
|
||||
P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None]
|
||||
EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None]
|
||||
ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh]
|
||||
ELECTRICITY_USED_TARIFF_2: 1435.706 [kWh]
|
||||
ELECTRICITY_DELIVERED_TARIFF_1: 0.000 [kWh]
|
||||
ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh]
|
||||
ELECTRICITY_ACTIVE_TARIFF: 0002 [None]
|
||||
CURRENT_ELECTRICITY_USAGE: 2.027 [kW]
|
||||
CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW]
|
||||
LONG_POWER_FAILURE_COUNT: 7 [None]
|
||||
VOLTAGE_SAG_L1_COUNT: 0 [None]
|
||||
VOLTAGE_SAG_L2_COUNT: 0 [None]
|
||||
VOLTAGE_SAG_L3_COUNT: 0 [None]
|
||||
VOLTAGE_SWELL_L1_COUNT: 0 [None]
|
||||
VOLTAGE_SWELL_L2_COUNT: 0 [None]
|
||||
VOLTAGE_SWELL_L3_COUNT: 0 [None]
|
||||
TEXT_MESSAGE_CODE: None [None]
|
||||
TEXT_MESSAGE: None [None]
|
||||
DEVICE_TYPE: 3 [None]
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.170 [kW]
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 1.247 [kW]
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.209 [kW]
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW]
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW]
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW]
|
||||
EQUIPMENT_IDENTIFIER_GAS: 4819243993373755377509728609491464 [None]
|
||||
HOURLY_GAS_METER_READING: 981.443 [m3]
|
||||
|
||||
# See dsmr_reader.obis_references for all readable telegram values.
|
||||
Accessing the telegrams information as attributes directly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
telegram
|
||||
Out[3]: <dsmr_parser.objects.Telegram at 0x7f5e995d9898>
|
||||
telegram.CURRENT_ELECTRICITY_USAGE
|
||||
Out[4]: <dsmr_parser.objects.CosemObject at 0x7f5e98ae5ac8>
|
||||
telegram.CURRENT_ELECTRICITY_USAGE.value
|
||||
Out[5]: Decimal('2.027')
|
||||
telegram.CURRENT_ELECTRICITY_USAGE.unit
|
||||
Out[6]: 'kW'
|
||||
|
||||
The telegram object has an iterator, can be used to find all the information elements in the current telegram:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
[attr for attr, value in telegram]
|
||||
Out[11]:
|
||||
['P1_MESSAGE_HEADER',
|
||||
'P1_MESSAGE_TIMESTAMP',
|
||||
'EQUIPMENT_IDENTIFIER',
|
||||
'ELECTRICITY_USED_TARIFF_1',
|
||||
'ELECTRICITY_USED_TARIFF_2',
|
||||
'ELECTRICITY_DELIVERED_TARIFF_1',
|
||||
'ELECTRICITY_DELIVERED_TARIFF_2',
|
||||
'ELECTRICITY_ACTIVE_TARIFF',
|
||||
'CURRENT_ELECTRICITY_USAGE',
|
||||
'CURRENT_ELECTRICITY_DELIVERY',
|
||||
'LONG_POWER_FAILURE_COUNT',
|
||||
'VOLTAGE_SAG_L1_COUNT',
|
||||
'VOLTAGE_SAG_L2_COUNT',
|
||||
'VOLTAGE_SAG_L3_COUNT',
|
||||
'VOLTAGE_SWELL_L1_COUNT',
|
||||
'VOLTAGE_SWELL_L2_COUNT',
|
||||
'VOLTAGE_SWELL_L3_COUNT',
|
||||
'TEXT_MESSAGE_CODE',
|
||||
'TEXT_MESSAGE',
|
||||
'DEVICE_TYPE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
|
||||
'EQUIPMENT_IDENTIFIER_GAS',
|
||||
'HOURLY_GAS_METER_READING']
|
||||
|
||||
|
||||
Installation
|
||||
|
||||
+31
-4
@@ -1,8 +1,9 @@
|
||||
from functools import partial
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .protocol import create_dsmr_reader
|
||||
from dsmr_parser.clients import create_dsmr_reader, create_tcp_dsmr_reader
|
||||
|
||||
|
||||
def console():
|
||||
@@ -11,6 +12,10 @@ def console():
|
||||
parser = argparse.ArgumentParser(description=console.__doc__)
|
||||
parser.add_argument('--device', default='/dev/ttyUSB0',
|
||||
help='port to read DSMR data from')
|
||||
parser.add_argument('--host', default=None,
|
||||
help='alternatively connect using TCP host.')
|
||||
parser.add_argument('--port', default=None,
|
||||
help='TCP port to use for connection')
|
||||
parser.add_argument('--version', default='2.2', choices=['2.2', '4'],
|
||||
help='DSMR version (2.2, 4)')
|
||||
parser.add_argument('--verbose', '-v', action='count')
|
||||
@@ -32,7 +37,29 @@ def console():
|
||||
print(obj.value, obj.unit)
|
||||
print()
|
||||
|
||||
conn = create_dsmr_reader(args.device, args.version, print_callback, loop=loop)
|
||||
# create tcp or serial connection depending on args
|
||||
if args.host and args.port:
|
||||
create_connection = partial(create_tcp_dsmr_reader,
|
||||
args.host, args.port, args.version,
|
||||
print_callback, loop=loop)
|
||||
else:
|
||||
create_connection = partial(create_dsmr_reader,
|
||||
args.device, args.version,
|
||||
print_callback, loop=loop)
|
||||
|
||||
loop.create_task(conn)
|
||||
loop.run_forever()
|
||||
try:
|
||||
# connect and keep connected until interrupted by ctrl-c
|
||||
while True:
|
||||
# create serial or tcp connection
|
||||
conn = create_connection()
|
||||
transport, protocol = loop.run_until_complete(conn)
|
||||
# wait until connection it closed
|
||||
loop.run_until_complete(protocol.wait_closed())
|
||||
# wait 5 seconds before attempting reconnect
|
||||
loop.run_until_complete(asyncio.sleep(5))
|
||||
except KeyboardInterrupt:
|
||||
# cleanup connection after user initiated shutdown
|
||||
transport.close()
|
||||
loop.run_until_complete(asyncio.sleep(0))
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
|
||||
SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5
|
||||
from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader
|
||||
from dsmr_parser.clients.protocol import create_dsmr_protocol, \
|
||||
create_dsmr_reader, create_tcp_dsmr_reader
|
||||
@@ -0,0 +1,171 @@
|
||||
import logging
|
||||
import fileinput
|
||||
import tailer
|
||||
|
||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
|
||||
from dsmr_parser.objects import Telegram
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileReader(object):
|
||||
"""
|
||||
Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects
|
||||
for each read telegram.
|
||||
Usage:
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients.filereader import FileReader
|
||||
|
||||
if __name__== "__main__":
|
||||
|
||||
infile = '/data/smartmeter/readings.txt'
|
||||
|
||||
file_reader = FileReader(
|
||||
file = infile,
|
||||
telegram_specification = telegram_specifications.V4
|
||||
)
|
||||
|
||||
for telegram in file_reader.read_as_object():
|
||||
print(telegram)
|
||||
|
||||
The file can be created like:
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5
|
||||
|
||||
if __name__== "__main__":
|
||||
|
||||
outfile = '/data/smartmeter/readings.txt'
|
||||
|
||||
serial_reader = SerialReader(
|
||||
device='/dev/ttyUSB0',
|
||||
serial_settings=SERIAL_SETTINGS_V5,
|
||||
telegram_specification=telegram_specifications.V4
|
||||
)
|
||||
|
||||
for telegram in serial_reader.read_as_object():
|
||||
f=open(outfile,"ab+")
|
||||
f.write(telegram._telegram_data.encode())
|
||||
f.close()
|
||||
"""
|
||||
|
||||
def __init__(self, file, telegram_specification):
|
||||
self._file = file
|
||||
self.telegram_parser = TelegramParser(telegram_specification)
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
self.telegram_specification = telegram_specification
|
||||
|
||||
def read_as_object(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from a file and return a Telegram object.
|
||||
:rtype: generator
|
||||
"""
|
||||
with open(self._file, "rb") as file_handle:
|
||||
while True:
|
||||
data = file_handle.readline()
|
||||
str = data.decode()
|
||||
self.telegram_buffer.append(str)
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
|
||||
class FileInputReader(object):
|
||||
"""
|
||||
Filereader to read and parse raw telegram strings from stdin or files specified at the commandline
|
||||
and instantiate Telegram objects for each read telegram.
|
||||
Usage python script "syphon_smartmeter_readings_stdin.py":
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients.filereader import FileInputReader
|
||||
|
||||
if __name__== "__main__":
|
||||
|
||||
fileinput_reader = FileReader(
|
||||
file = infile,
|
||||
telegram_specification = telegram_specifications.V4
|
||||
)
|
||||
|
||||
for telegram in fileinput_reader.read_as_object():
|
||||
print(telegram)
|
||||
|
||||
Command line:
|
||||
tail -f /data/smartmeter/readings.txt | python3 syphon_smartmeter_readings_stdin.py
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, telegram_specification):
|
||||
self.telegram_parser = TelegramParser(telegram_specification)
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
self.telegram_specification = telegram_specification
|
||||
|
||||
def read_as_object(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from stdin of filearguments specified on teh command line
|
||||
and return a Telegram object.
|
||||
:rtype: generator
|
||||
"""
|
||||
with fileinput.input(mode='rb') as file_handle:
|
||||
while True:
|
||||
data = file_handle.readline()
|
||||
str = data.decode()
|
||||
self.telegram_buffer.append(str)
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
|
||||
class FileTailReader(object):
|
||||
"""
|
||||
Filereader to read and parse raw telegram strings from the tail of a
|
||||
given file and instantiate Telegram objects for each read telegram.
|
||||
Usage python script "syphon_smartmeter_readings_stdin.py":
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients.filereader import FileTailReader
|
||||
|
||||
if __name__== "__main__":
|
||||
|
||||
infile = '/data/smartmeter/readings.txt'
|
||||
|
||||
filetail_reader = FileTailReader(
|
||||
file = infile,
|
||||
telegram_specification = telegram_specifications.V5
|
||||
)
|
||||
|
||||
for telegram in filetail_reader.read_as_object():
|
||||
print(telegram)
|
||||
"""
|
||||
|
||||
def __init__(self, file, telegram_specification):
|
||||
self._file = file
|
||||
self.telegram_parser = TelegramParser(telegram_specification)
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
self.telegram_specification = telegram_specification
|
||||
|
||||
def read_as_object(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from a files tail and return a Telegram object.
|
||||
:rtype: generator
|
||||
"""
|
||||
with open(self._file, "rb") as file_handle:
|
||||
for data in tailer.follow(file_handle):
|
||||
str = data.decode()
|
||||
self.telegram_buffer.append(str)
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Asyncio protocol implementation for handling telegrams."""
|
||||
|
||||
from functools import partial
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from serial_asyncio import create_serial_connection
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
|
||||
SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5
|
||||
|
||||
|
||||
def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None):
|
||||
"""Creates a DSMR asyncio protocol."""
|
||||
|
||||
if dsmr_version == '2.2':
|
||||
specification = telegram_specifications.V2_2
|
||||
serial_settings = SERIAL_SETTINGS_V2_2
|
||||
elif dsmr_version == '4':
|
||||
specification = telegram_specifications.V4
|
||||
serial_settings = SERIAL_SETTINGS_V4
|
||||
elif dsmr_version == '5':
|
||||
specification = telegram_specifications.V5
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
elif dsmr_version == '5B':
|
||||
specification = telegram_specifications.BELGIUM_FLUVIUS
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
elif dsmr_version == "5L":
|
||||
specification = telegram_specifications.LUXEMBOURG_SMARTY
|
||||
serial_settings = SERIAL_SETTINGS_V5
|
||||
else:
|
||||
raise NotImplementedError("No telegram parser found for version: %s",
|
||||
dsmr_version)
|
||||
|
||||
protocol = partial(DSMRProtocol, loop, TelegramParser(specification),
|
||||
telegram_callback=telegram_callback)
|
||||
|
||||
return protocol, serial_settings
|
||||
|
||||
|
||||
def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):
|
||||
"""Creates a DSMR asyncio protocol coroutine using serial port."""
|
||||
protocol, serial_settings = create_dsmr_protocol(
|
||||
dsmr_version, telegram_callback, loop=None)
|
||||
serial_settings['url'] = port
|
||||
|
||||
conn = create_serial_connection(loop, protocol, **serial_settings)
|
||||
return conn
|
||||
|
||||
|
||||
def create_tcp_dsmr_reader(host, port, dsmr_version,
|
||||
telegram_callback, loop=None):
|
||||
"""Creates a DSMR asyncio protocol coroutine using TCP connection."""
|
||||
if not loop:
|
||||
loop = asyncio.get_event_loop()
|
||||
protocol, _ = create_dsmr_protocol(
|
||||
dsmr_version, telegram_callback, loop=loop)
|
||||
conn = loop.create_connection(protocol, host, port)
|
||||
return conn
|
||||
|
||||
|
||||
class DSMRProtocol(asyncio.Protocol):
|
||||
"""Assemble and handle incoming data into complete DSM telegrams."""
|
||||
|
||||
transport = None
|
||||
telegram_callback = None
|
||||
|
||||
def __init__(self, loop, telegram_parser, telegram_callback=None):
|
||||
"""Initialize class."""
|
||||
self.loop = loop
|
||||
self.log = logging.getLogger(__name__)
|
||||
self.telegram_parser = telegram_parser
|
||||
# callback to call on complete telegram
|
||||
self.telegram_callback = telegram_callback
|
||||
# buffer to keep incomplete incoming data
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
# keep a lock until the connection is closed
|
||||
self._closed = asyncio.Event()
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Just logging for now."""
|
||||
self.transport = transport
|
||||
self.log.debug('connected')
|
||||
|
||||
def data_received(self, data):
|
||||
"""Add incoming data to buffer."""
|
||||
data = data.decode('ascii')
|
||||
self.log.debug('received data: %s', data)
|
||||
self.telegram_buffer.append(data)
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
self.handle_telegram(telegram)
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""Stop when connection is lost."""
|
||||
if exc:
|
||||
self.log.exception('disconnected due to exception', exc_info=exc)
|
||||
else:
|
||||
self.log.info('disconnected because of close/abort.')
|
||||
self._closed.set()
|
||||
|
||||
def handle_telegram(self, telegram):
|
||||
"""Send off parsed telegram to handling callback."""
|
||||
self.log.debug('got telegram: %s', telegram)
|
||||
|
||||
try:
|
||||
parsed_telegram = self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
self.log.warning(str(e))
|
||||
except ParseError:
|
||||
self.log.exception("failed to parse telegram")
|
||||
else:
|
||||
self.telegram_callback(parsed_telegram)
|
||||
|
||||
@asyncio.coroutine
|
||||
def wait_closed(self):
|
||||
"""Wait until connection is closed."""
|
||||
yield from self._closed.wait()
|
||||
@@ -0,0 +1,99 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import serial
|
||||
import serial_asyncio
|
||||
|
||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from dsmr_parser.objects import Telegram
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SerialReader(object):
|
||||
PORT_KEY = 'port'
|
||||
|
||||
def __init__(self, device, serial_settings, telegram_specification):
|
||||
self.serial_settings = serial_settings
|
||||
self.serial_settings[self.PORT_KEY] = device
|
||||
|
||||
self.telegram_parser = TelegramParser(telegram_specification)
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
self.telegram_specification = telegram_specification
|
||||
|
||||
def read(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from the serial interface and parse it
|
||||
into CosemObject's and MbusObject's
|
||||
|
||||
:rtype: generator
|
||||
"""
|
||||
with serial.Serial(**self.serial_settings) as serial_handle:
|
||||
while True:
|
||||
data = serial_handle.read(max(1, min(1024, serial_handle.in_waiting)))
|
||||
self.telegram_buffer.append(data.decode('ascii'))
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield self.telegram_parser.parse(telegram)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
def read_as_object(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from the serial interface and return a Telegram object.
|
||||
|
||||
:rtype: generator
|
||||
"""
|
||||
with serial.Serial(**self.serial_settings) as serial_handle:
|
||||
while True:
|
||||
data = serial_handle.readline()
|
||||
self.telegram_buffer.append(data.decode('ascii'))
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
|
||||
except InvalidChecksumError as e:
|
||||
logger.warning(str(e))
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
|
||||
class AsyncSerialReader(SerialReader):
|
||||
"""Serial reader using asyncio pyserial."""
|
||||
|
||||
PORT_KEY = 'url'
|
||||
|
||||
@asyncio.coroutine
|
||||
def read(self, queue):
|
||||
"""
|
||||
Read complete DSMR telegram's from the serial interface and parse it
|
||||
into CosemObject's and MbusObject's.
|
||||
|
||||
Instead of being a generator, values are pushed to provided queue for
|
||||
asynchronous processing.
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
# create Serial StreamReader
|
||||
conn = serial_asyncio.open_serial_connection(**self.serial_settings)
|
||||
reader, _ = yield from conn
|
||||
|
||||
while True:
|
||||
# Read line if available or give control back to loop until new
|
||||
# data has arrived.
|
||||
data = yield from reader.readline()
|
||||
self.telegram_buffer.append(data.decode('ascii'))
|
||||
|
||||
for telegram in self.telegram_buffer.get_all():
|
||||
try:
|
||||
# Push new parsed telegram onto queue.
|
||||
queue.put_nowait(
|
||||
self.telegram_parser.parse(telegram)
|
||||
)
|
||||
except ParseError as e:
|
||||
logger.warning('Failed to parse telegram: %s', e)
|
||||
@@ -0,0 +1,32 @@
|
||||
import serial
|
||||
|
||||
|
||||
SERIAL_SETTINGS_V2_2 = {
|
||||
'baudrate': 9600,
|
||||
'bytesize': serial.SEVENBITS,
|
||||
'parity': serial.PARITY_EVEN,
|
||||
'stopbits': serial.STOPBITS_ONE,
|
||||
'xonxoff': 0,
|
||||
'rtscts': 0,
|
||||
'timeout': 20
|
||||
}
|
||||
|
||||
SERIAL_SETTINGS_V4 = {
|
||||
'baudrate': 115200,
|
||||
'bytesize': serial.SEVENBITS,
|
||||
'parity': serial.PARITY_EVEN,
|
||||
'stopbits': serial.STOPBITS_ONE,
|
||||
'xonxoff': 0,
|
||||
'rtscts': 0,
|
||||
'timeout': 20
|
||||
}
|
||||
|
||||
SERIAL_SETTINGS_V5 = {
|
||||
'baudrate': 115200,
|
||||
'bytesize': serial.EIGHTBITS,
|
||||
'parity': serial.PARITY_NONE,
|
||||
'stopbits': serial.STOPBITS_ONE,
|
||||
'xonxoff': 0,
|
||||
'rtscts': 0,
|
||||
'timeout': 20
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import re
|
||||
|
||||
|
||||
class TelegramBuffer(object):
|
||||
"""
|
||||
Used as a buffer for a stream of telegram data. Constructs full telegram
|
||||
strings from the buffered data and returns it.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._buffer = ''
|
||||
|
||||
def get_all(self):
|
||||
"""
|
||||
Remove complete telegrams from buffer and yield them.
|
||||
:rtype generator:
|
||||
"""
|
||||
for telegram in self._find_telegrams():
|
||||
self._remove(telegram)
|
||||
yield telegram
|
||||
|
||||
def append(self, data):
|
||||
"""
|
||||
Add telegram data to buffer.
|
||||
:param str data: chars, lines or full telegram strings of telegram data
|
||||
"""
|
||||
self._buffer += data
|
||||
|
||||
def _remove(self, telegram):
|
||||
"""
|
||||
Remove telegram from buffer and incomplete data preceding it. This
|
||||
is easier than validating the data before adding it to the buffer.
|
||||
:param str telegram:
|
||||
:return:
|
||||
"""
|
||||
# Remove data leading up to the telegram and the telegram itself.
|
||||
index = self._buffer.index(telegram) + len(telegram)
|
||||
|
||||
self._buffer = self._buffer[index:]
|
||||
|
||||
def _find_telegrams(self):
|
||||
"""
|
||||
Find complete telegrams in buffer from start ('/') till ending
|
||||
checksum ('!AB12\r\n').
|
||||
:rtype: list
|
||||
"""
|
||||
# - Match all characters after start of telegram except for the start
|
||||
# itself again '^\/]+', which eliminates incomplete preceding telegrams.
|
||||
# - Do non greedy match using '?' so start is matched up to the first
|
||||
# checksum that's found.
|
||||
# - The checksum is optional '{0,4}' because not all telegram versions
|
||||
# support it.
|
||||
return re.findall(
|
||||
r'\/[^\/]+?\![A-F0-9]{0,4}\0?\r\n',
|
||||
self._buffer,
|
||||
re.DOTALL
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
from dsmr_parser import obis_references as obis
|
||||
|
||||
"""
|
||||
dsmr_parser.obis_name_mapping
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains a mapping of obis references to names.
|
||||
"""
|
||||
|
||||
EN = {
|
||||
obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER',
|
||||
obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP',
|
||||
obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL',
|
||||
obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1',
|
||||
obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2',
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1',
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2',
|
||||
obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF',
|
||||
obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER',
|
||||
obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE',
|
||||
obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY',
|
||||
obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT',
|
||||
obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT',
|
||||
obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG',
|
||||
obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT',
|
||||
obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT',
|
||||
obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT',
|
||||
obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT',
|
||||
obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT',
|
||||
obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT',
|
||||
obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1',
|
||||
obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2',
|
||||
obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3',
|
||||
obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1',
|
||||
obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2',
|
||||
obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3',
|
||||
obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE',
|
||||
obis.TEXT_MESSAGE: 'TEXT_MESSAGE',
|
||||
obis.DEVICE_TYPE: 'DEVICE_TYPE',
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
|
||||
obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS',
|
||||
obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING',
|
||||
obis.GAS_METER_READING: 'GAS_METER_READING',
|
||||
obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY',
|
||||
obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION',
|
||||
obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS',
|
||||
obis.BELGIUM_HOURLY_GAS_METER_READING: 'BELGIUM_HOURLY_GAS_METER_READING',
|
||||
obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER',
|
||||
obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: 'LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL',
|
||||
obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: 'LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL'
|
||||
}
|
||||
|
||||
REVERSE_EN = dict([(v, k) for k, v in EN.items()])
|
||||
@@ -1,36 +1,56 @@
|
||||
P1_MESSAGE_HEADER = r'1-3:0\.2\.8'
|
||||
P1_MESSAGE_TIMESTAMP = r'0-0:1\.0\.0'
|
||||
ELECTRICITY_USED_TARIFF_1 = r'1-0:1\.8\.1'
|
||||
ELECTRICITY_USED_TARIFF_2 = r'1-0:1\.8\.2'
|
||||
ELECTRICITY_DELIVERED_TARIFF_1 = r'1-0:2\.8\.1'
|
||||
ELECTRICITY_DELIVERED_TARIFF_2 = r'1-0:2\.8\.2'
|
||||
ELECTRICITY_ACTIVE_TARIFF = r'0-0:96\.14\.0'
|
||||
EQUIPMENT_IDENTIFIER = r'0-0:96\.1\.1'
|
||||
CURRENT_ELECTRICITY_USAGE = r'1-0:1\.7\.0'
|
||||
CURRENT_ELECTRICITY_DELIVERY = r'1-0:2\.7\.0'
|
||||
LONG_POWER_FAILURE_COUNT = r'96\.7\.9'
|
||||
POWER_EVENT_FAILURE_LOG = r'99\.97\.0'
|
||||
VOLTAGE_SAG_L1_COUNT = r'1-0:32\.32\.0'
|
||||
VOLTAGE_SAG_L2_COUNT = r'1-0:52\.32\.0'
|
||||
VOLTAGE_SAG_L3_COUNT = r'1-0:72\.32\.0'
|
||||
VOLTAGE_SWELL_L1_COUNT = r'1-0:32\.36\.0'
|
||||
VOLTAGE_SWELL_L2_COUNT = r'1-0:52\.36\.0'
|
||||
VOLTAGE_SWELL_L3_COUNT = r'1-0:72\.36\.0'
|
||||
TEXT_MESSAGE_CODE = r'0-0:96\.13\.1'
|
||||
TEXT_MESSAGE = r'0-0:96\.13\.0'
|
||||
DEVICE_TYPE = r'0-\d:24\.1\.0'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'1-0:21\.7\.0'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'1-0:41\.7\.0'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'1-0:61\.7\.0'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'1-0:22\.7\.0'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'1-0:42\.7\.0'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'1-0:62\.7\.0'
|
||||
EQUIPMENT_IDENTIFIER_GAS = r'0-\d:96\.1\.0'
|
||||
HOURLY_GAS_METER_READING = r'0-1:24\.2\.1'
|
||||
GAS_METER_READING = r'0-\d:24\.3\.0'
|
||||
ACTUAL_TRESHOLD_ELECTRICITY = r'0-0:17\.0\.0'
|
||||
ACTUAL_SWITCH_POSITION = r'0-0:96\.3\.10'
|
||||
VALVE_POSITION_GAS = r'0-\d:24\.4\.0'
|
||||
"""
|
||||
Contains the signatures of each telegram line.
|
||||
|
||||
Previously contained the channel + obis reference signatures, but has been
|
||||
refactored to full line signatures to maintain backwards compatibility.
|
||||
Might be refactored in a backwards incompatible way as soon as proper telegram
|
||||
objects are introduced.
|
||||
"""
|
||||
P1_MESSAGE_HEADER = r'\d-\d:0\.2\.8.+?\r\n'
|
||||
P1_MESSAGE_TIMESTAMP = r'\d-\d:1\.0\.0.+?\r\n'
|
||||
ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1\.8\.0.+?\r\n'
|
||||
ELECTRICITY_USED_TARIFF_1 = r'\d-\d:1\.8\.1.+?\r\n'
|
||||
ELECTRICITY_USED_TARIFF_2 = r'\d-\d:1\.8\.2.+?\r\n'
|
||||
ELECTRICITY_DELIVERED_TARIFF_1 = r'\d-\d:2\.8\.1.+?\r\n'
|
||||
ELECTRICITY_DELIVERED_TARIFF_2 = r'\d-\d:2\.8\.2.+?\r\n'
|
||||
ELECTRICITY_ACTIVE_TARIFF = r'\d-\d:96\.14\.0.+?\r\n'
|
||||
EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n'
|
||||
CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n'
|
||||
CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n'
|
||||
LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n'
|
||||
SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n'
|
||||
POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n'
|
||||
VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n'
|
||||
VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n'
|
||||
VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n'
|
||||
VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n'
|
||||
VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n'
|
||||
VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n'
|
||||
INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n'
|
||||
TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n'
|
||||
TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n'
|
||||
DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'\d-\d:22\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'\d-\d:42\.7\.0.+?\r\n'
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'\d-\d:62\.7\.0.+?\r\n'
|
||||
EQUIPMENT_IDENTIFIER_GAS = r'\d-\d:96\.1\.0.+?\r\n'
|
||||
# TODO differences between gas meter readings in v3 and lower and v4 and up
|
||||
HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.1.+?\r\n'
|
||||
GAS_METER_READING = r'\d-\d:24\.3\.0.+?\r\n.+?\r\n'
|
||||
ACTUAL_TRESHOLD_ELECTRICITY = r'\d-\d:17\.0\.0.+?\r\n'
|
||||
ACTUAL_SWITCH_POSITION = r'\d-\d:96\.3\.10.+?\r\n'
|
||||
VALVE_POSITION_GAS = r'\d-\d:24\.4\.0.+?\r\n'
|
||||
|
||||
# TODO 17.0.0
|
||||
# TODO 96.3.10
|
||||
|
||||
ELECTRICITY_USED_TARIFF_ALL = (
|
||||
ELECTRICITY_USED_TARIFF_1,
|
||||
@@ -40,3 +60,9 @@ ELECTRICITY_DELIVERED_TARIFF_ALL = (
|
||||
ELECTRICITY_DELIVERED_TARIFF_1,
|
||||
ELECTRICITY_DELIVERED_TARIFF_2
|
||||
)
|
||||
|
||||
# Alternate codes for foreign countries.
|
||||
BELGIUM_HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n' # Different code, same format.
|
||||
LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'\d-\d:42\.0\.0.+?\r\n' # Logical device name
|
||||
LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+)
|
||||
LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-)
|
||||
|
||||
+175
-18
@@ -1,4 +1,67 @@
|
||||
import dsmr_parser.obis_name_mapping
|
||||
import datetime
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class Telegram(object):
|
||||
"""
|
||||
Container for raw and parsed telegram data.
|
||||
Initializing:
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
|
||||
from dsmr_parser.objects import CosemObject, MBusObject, Telegram
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from test.example_telegrams import TELEGRAM_V4_2
|
||||
parser = TelegramParser(telegram_specifications.V4)
|
||||
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
|
||||
|
||||
Attributes can be accessed on a telegram object by addressing by their english name, for example:
|
||||
telegram.ELECTRICITY_USED_TARIFF_1
|
||||
|
||||
All attributes in a telegram can be iterated over, for example:
|
||||
[k for k,v in telegram]
|
||||
yields:
|
||||
['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...]
|
||||
"""
|
||||
def __init__(self, telegram_data, telegram_parser, telegram_specification):
|
||||
self._telegram_data = telegram_data
|
||||
self._telegram_specification = telegram_specification
|
||||
self._telegram_parser = telegram_parser
|
||||
self._obis_name_mapping = dsmr_parser.obis_name_mapping.EN
|
||||
self._reverse_obis_name_mapping = dsmr_parser.obis_name_mapping.REVERSE_EN
|
||||
self._dictionary = self._telegram_parser.parse(telegram_data)
|
||||
self._item_names = self._get_item_names()
|
||||
|
||||
def __getattr__(self, name):
|
||||
''' will only get called for undefined attributes '''
|
||||
obis_reference = self._reverse_obis_name_mapping[name]
|
||||
value = self._dictionary[obis_reference]
|
||||
setattr(self, name, value)
|
||||
return value
|
||||
|
||||
def _get_item_names(self):
|
||||
return [self._obis_name_mapping[k] for k, v in self._dictionary.items()]
|
||||
|
||||
def __iter__(self):
|
||||
for attr in self._item_names:
|
||||
value = getattr(self, attr)
|
||||
yield attr, value
|
||||
|
||||
def __str__(self):
|
||||
output = ""
|
||||
for attr, value in self:
|
||||
output += "{}: \t {}\n".format(attr, str(value))
|
||||
return output
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(dict([[attr, json.loads(value.to_json())] for attr, value in self]))
|
||||
|
||||
|
||||
class DSMRObject(object):
|
||||
"""
|
||||
Represents all data from a single telegram line.
|
||||
"""
|
||||
|
||||
def __init__(self, values):
|
||||
self.values = values
|
||||
@@ -12,26 +75,43 @@ class MBusObject(DSMRObject):
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.values[1]['value']
|
||||
# TODO temporary workaround for DSMR v2.2. Maybe use the same type of
|
||||
# TODO object, but let the parse set them differently? So don't use
|
||||
# TODO hardcoded indexes here.
|
||||
if len(self.values) != 2: # v2
|
||||
return self.values[6]['value']
|
||||
else:
|
||||
return self.values[1]['value']
|
||||
|
||||
@property
|
||||
def unit(self):
|
||||
return self.values[1]['unit']
|
||||
# TODO temporary workaround for DSMR v2.2. Maybe use the same type of
|
||||
# TODO object, but let the parse set them differently? So don't use
|
||||
# TODO hardcoded indexes here.
|
||||
if len(self.values) != 2: # v2
|
||||
return self.values[5]['value']
|
||||
else:
|
||||
return self.values[1]['unit']
|
||||
|
||||
def __str__(self):
|
||||
output = "{}\t[{}] at {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat()))
|
||||
return output
|
||||
|
||||
class MBusObjectV2_2(DSMRObject):
|
||||
|
||||
@property
|
||||
def datetime(self):
|
||||
return self.values[0]['value']
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.values[5]['value']
|
||||
|
||||
@property
|
||||
def unit(self):
|
||||
return self.values[4]['value']
|
||||
def to_json(self):
|
||||
timestamp = self.datetime
|
||||
if isinstance(self.datetime, datetime.datetime):
|
||||
timestamp = self.datetime.astimezone().isoformat()
|
||||
value = self.value
|
||||
if isinstance(self.value, datetime.datetime):
|
||||
value = self.value.astimezone().isoformat()
|
||||
if isinstance(self.value, Decimal):
|
||||
value = float(self.value)
|
||||
output = {
|
||||
'datetime': timestamp,
|
||||
'value': value,
|
||||
'unit': self.unit
|
||||
}
|
||||
return json.dumps(output)
|
||||
|
||||
|
||||
class CosemObject(DSMRObject):
|
||||
@@ -44,7 +124,84 @@ class CosemObject(DSMRObject):
|
||||
def unit(self):
|
||||
return self.values[0]['unit']
|
||||
|
||||
def __str__(self):
|
||||
print_value = self.value
|
||||
if isinstance(self.value, datetime.datetime):
|
||||
print_value = self.value.astimezone().isoformat()
|
||||
output = "{}\t[{}]".format(str(print_value), str(self.unit))
|
||||
return output
|
||||
|
||||
class ProfileGeneric(DSMRObject):
|
||||
pass
|
||||
# TODO implement
|
||||
def to_json(self):
|
||||
json_value = self.value
|
||||
if isinstance(self.value, datetime.datetime):
|
||||
json_value = self.value.astimezone().isoformat()
|
||||
if isinstance(self.value, Decimal):
|
||||
json_value = float(self.value)
|
||||
output = {
|
||||
'value': json_value,
|
||||
'unit': self.unit
|
||||
}
|
||||
return json.dumps(output)
|
||||
|
||||
|
||||
class ProfileGenericObject(DSMRObject):
|
||||
"""
|
||||
Represents all data in a GenericProfile value.
|
||||
All buffer values are returned as a list of MBusObjects,
|
||||
containing the datetime (timestamp) and the value.
|
||||
"""
|
||||
|
||||
def __init__(self, values):
|
||||
super().__init__(values)
|
||||
self._buffer_list = None
|
||||
|
||||
@property
|
||||
def buffer_length(self):
|
||||
return self.values[0]['value']
|
||||
|
||||
@property
|
||||
def buffer_type(self):
|
||||
return self.values[1]['value']
|
||||
|
||||
@property
|
||||
def buffer(self):
|
||||
if self._buffer_list is None:
|
||||
self._buffer_list = []
|
||||
values_offset = 2
|
||||
for i in range(self.buffer_length):
|
||||
offset = values_offset + i*2
|
||||
self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]]))
|
||||
return self._buffer_list
|
||||
|
||||
def __str__(self):
|
||||
output = "\t buffer length: {}\n".format(self.buffer_length)
|
||||
output += "\t buffer type: {}".format(self.buffer_type)
|
||||
for buffer_value in self.buffer:
|
||||
timestamp = buffer_value.datetime
|
||||
if isinstance(timestamp, datetime.datetime):
|
||||
timestamp = str(timestamp.astimezone().isoformat())
|
||||
output += "\n\t event occured at: {}".format(timestamp)
|
||||
output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit)
|
||||
return output
|
||||
|
||||
def to_json(self):
|
||||
"""
|
||||
:return: A json of all values in the GenericProfileObject , with the following structure
|
||||
{'buffer_length': n,
|
||||
'buffer_type': obis_ref,
|
||||
'buffer': [{'datetime': d1,
|
||||
'value': v1,
|
||||
'unit': u1},
|
||||
...
|
||||
{'datetime': dn,
|
||||
'value': vn,
|
||||
'unit': un}
|
||||
]
|
||||
}
|
||||
"""
|
||||
list = [['buffer_length', self.buffer_length]]
|
||||
list.append(['buffer_type', self.buffer_type])
|
||||
buffer_repr = [json.loads(buffer_item.to_json()) for buffer_item in self.buffer]
|
||||
list.append(['buffer', buffer_repr])
|
||||
output = dict(list)
|
||||
return json.dumps(output)
|
||||
|
||||
+141
-85
@@ -1,73 +1,79 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from PyCRC.CRC16 import CRC16
|
||||
from ctypes import c_ushort
|
||||
|
||||
from .objects import MBusObject, MBusObjectV2_2, CosemObject
|
||||
from .exceptions import ParseError, InvalidChecksumError
|
||||
from .obis_references import GAS_METER_READING
|
||||
from dsmr_parser.objects import MBusObject, CosemObject, ProfileGenericObject
|
||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TelegramParser(object):
|
||||
|
||||
def __init__(self, telegram_specification):
|
||||
crc16_tab = []
|
||||
|
||||
def __init__(self, telegram_specification, apply_checksum_validation=True):
|
||||
"""
|
||||
:param telegram_specification: determines how the telegram is parsed
|
||||
:param apply_checksum_validation: validate checksum if applicable for
|
||||
telegram DSMR version (v4 and up).
|
||||
:type telegram_specification: dict
|
||||
"""
|
||||
self.telegram_specification = telegram_specification
|
||||
self.apply_checksum_validation = apply_checksum_validation
|
||||
|
||||
def _find_line_parser(self, line_value):
|
||||
for obis_reference, parser in self.telegram_specification.items():
|
||||
if re.search(obis_reference, line_value):
|
||||
return obis_reference, parser
|
||||
|
||||
return None, None
|
||||
|
||||
def parse(self, line_values):
|
||||
telegram = {}
|
||||
|
||||
for line_value in line_values:
|
||||
# TODO temporarily strip newline characters.
|
||||
line_value = line_value.strip()
|
||||
|
||||
obis_reference, dsmr_object = self.parse_line(line_value)
|
||||
|
||||
telegram[obis_reference] = dsmr_object
|
||||
|
||||
return telegram
|
||||
|
||||
def parse_line(self, line_value):
|
||||
logger.debug('Parsing line \'%s\'', line_value)
|
||||
|
||||
obis_reference, parser = self._find_line_parser(line_value)
|
||||
|
||||
if not parser:
|
||||
logger.warning("No line class found for: '%s'", line_value)
|
||||
return None, None
|
||||
|
||||
return obis_reference, parser.parse(line_value)
|
||||
|
||||
|
||||
class TelegramParserV4(TelegramParser):
|
||||
|
||||
@staticmethod
|
||||
def validate_telegram_checksum(line_values):
|
||||
def parse(self, telegram_data):
|
||||
"""
|
||||
:type line_values: list
|
||||
Parse telegram from string to dict.
|
||||
|
||||
The telegram str type makes python 2.x integration easier.
|
||||
|
||||
:param str telegram_data: full telegram from start ('/') to checksum
|
||||
('!ABCD') including line endings in between the telegram's lines
|
||||
:rtype: dict
|
||||
:returns: Shortened example:
|
||||
{
|
||||
..
|
||||
r'\d-\d:96\.1\.1.+?\r\n': <CosemObject>, # EQUIPMENT_IDENTIFIER
|
||||
r'\d-\d:1\.8\.1.+?\r\n': <CosemObject>, # ELECTRICITY_USED_TARIFF_1
|
||||
r'\d-\d:24\.3\.0.+?\r\n.+?\r\n': <MBusObject>, # GAS_METER_READING
|
||||
..
|
||||
}
|
||||
:raises ParseError:
|
||||
:raises InvalidChecksumError:
|
||||
"""
|
||||
|
||||
full_telegram = ''.join(line_values)
|
||||
if self.apply_checksum_validation \
|
||||
and self.telegram_specification['checksum_support']:
|
||||
self.validate_checksum(telegram_data)
|
||||
|
||||
# Extract the bytes that count towards the checksum.
|
||||
checksum_contents = re.search(r'\/.+\!', full_telegram, re.DOTALL)
|
||||
telegram = {}
|
||||
|
||||
for signature, parser in self.telegram_specification['objects'].items():
|
||||
match = re.search(signature, telegram_data, re.DOTALL)
|
||||
|
||||
# Some signatures are optional and may not be present,
|
||||
# so only parse lines that match
|
||||
if match:
|
||||
telegram[signature] = parser.parse(match.group(0))
|
||||
|
||||
return telegram
|
||||
|
||||
@staticmethod
|
||||
def validate_checksum(telegram):
|
||||
"""
|
||||
:param str telegram:
|
||||
:raises ParseError:
|
||||
:raises InvalidChecksumError:
|
||||
"""
|
||||
|
||||
# Extract the part for which the checksum applies.
|
||||
checksum_contents = re.search(r'\/.+\!', telegram, re.DOTALL)
|
||||
|
||||
# Extract the hexadecimal checksum value itself.
|
||||
checksum_hex = re.search(r'((?<=\!)[0-9A-Z]{4}(?=\r\n))+', full_telegram)
|
||||
# The line ending '\r\n' for the checksum line can be ignored.
|
||||
checksum_hex = re.search(r'((?<=\!)[0-9A-Z]{4})+', telegram)
|
||||
|
||||
if not checksum_contents or not checksum_hex:
|
||||
raise ParseError(
|
||||
@@ -75,9 +81,8 @@ class TelegramParserV4(TelegramParser):
|
||||
'incomplete. The checksum and/or content values are missing.'
|
||||
)
|
||||
|
||||
calculated_crc = CRC16().calculate(checksum_contents.group(0))
|
||||
expected_crc = checksum_hex.group(0)
|
||||
expected_crc = int(expected_crc, base=16)
|
||||
calculated_crc = TelegramParser.crc16(checksum_contents.group(0))
|
||||
expected_crc = int(checksum_hex.group(0), base=16)
|
||||
|
||||
if calculated_crc != expected_crc:
|
||||
raise InvalidChecksumError(
|
||||
@@ -88,51 +93,64 @@ class TelegramParserV4(TelegramParser):
|
||||
)
|
||||
)
|
||||
|
||||
def parse(self, line_values):
|
||||
self.validate_telegram_checksum(line_values)
|
||||
@staticmethod
|
||||
def crc16(telegram):
|
||||
"""
|
||||
Calculate the CRC16 value for the given telegram
|
||||
|
||||
return super().parse(line_values)
|
||||
:param str telegram:
|
||||
"""
|
||||
crcValue = 0x0000
|
||||
|
||||
if len(TelegramParser.crc16_tab) == 0:
|
||||
for i in range(0, 256):
|
||||
crc = c_ushort(i).value
|
||||
for j in range(0, 8):
|
||||
if (crc & 0x0001):
|
||||
crc = c_ushort(crc >> 1).value ^ 0xA001
|
||||
else:
|
||||
crc = c_ushort(crc >> 1).value
|
||||
TelegramParser.crc16_tab.append(hex(crc))
|
||||
|
||||
class TelegramParserV2_2(TelegramParser):
|
||||
for c in telegram:
|
||||
d = ord(c)
|
||||
tmp = crcValue ^ d
|
||||
rotated = c_ushort(crcValue >> 8).value
|
||||
crcValue = rotated ^ int(TelegramParser.crc16_tab[(tmp & 0x00ff)], 0)
|
||||
|
||||
def parse(self, line_values):
|
||||
"""Join lines for gas meter."""
|
||||
|
||||
def join_lines(line_values):
|
||||
join_next = re.compile(GAS_METER_READING)
|
||||
|
||||
join = None
|
||||
for line_value in line_values:
|
||||
if join:
|
||||
yield join.strip() + line_value
|
||||
join = None
|
||||
elif join_next.match(line_value):
|
||||
join = line_value
|
||||
else:
|
||||
yield line_value
|
||||
|
||||
return super().parse(join_lines(line_values))
|
||||
return crcValue
|
||||
|
||||
|
||||
class DSMRObjectParser(object):
|
||||
"""
|
||||
Parses an object (can also be see as a 'line') from a telegram.
|
||||
"""
|
||||
|
||||
def __init__(self, *value_formats):
|
||||
self.value_formats = value_formats
|
||||
|
||||
def _is_line_wellformed(self, line, values):
|
||||
# allows overriding by child class
|
||||
return (values and (len(values) == len(self.value_formats)))
|
||||
|
||||
def _parse_values(self, values):
|
||||
# allows overriding by child class
|
||||
return [self.value_formats[i].parse(value)
|
||||
for i, value in enumerate(values)]
|
||||
|
||||
def _parse(self, line):
|
||||
# Match value groups, but exclude the parentheses
|
||||
pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*]{0,}(?=\)))+')
|
||||
pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))')
|
||||
|
||||
values = re.findall(pattern, line)
|
||||
|
||||
if not self._is_line_wellformed(line, values):
|
||||
raise ParseError("Invalid '%s' line for '%s'", line, self)
|
||||
|
||||
# Convert empty value groups to None for clarity.
|
||||
values = [None if value == '' else value for value in values]
|
||||
|
||||
if not values or len(values) != len(self.value_formats):
|
||||
raise ParseError("Invalid '%s' line for '%s'", line, self)
|
||||
|
||||
return [self.value_formats[i].parse(value)
|
||||
for i, value in enumerate(values)]
|
||||
return self._parse_values(values)
|
||||
|
||||
|
||||
class MBusParser(DSMRObjectParser):
|
||||
@@ -153,11 +171,7 @@ class MBusParser(DSMRObjectParser):
|
||||
"""
|
||||
|
||||
def parse(self, line):
|
||||
values = self._parse(line)
|
||||
if len(values) == 2:
|
||||
return MBusObject(values)
|
||||
else:
|
||||
return MBusObjectV2_2(values)
|
||||
return MBusObject(self._parse(line))
|
||||
|
||||
|
||||
class CosemParser(DSMRObjectParser):
|
||||
@@ -173,10 +187,11 @@ class CosemParser(DSMRObjectParser):
|
||||
1 23 45
|
||||
|
||||
1) OBIS Reduced ID-code
|
||||
2) Separator “(“, ASCII 28h
|
||||
2) Separator "(", ASCII 28h
|
||||
3) COSEM object attribute value
|
||||
4) Unit of measurement values (Unit of capture objects attribute) – only if applicable
|
||||
5) Separator “)”, ASCII 29h
|
||||
4) Unit of measurement values (Unit of capture objects attribute) - only if
|
||||
applicable
|
||||
5) Separator ")", ASCII 29h
|
||||
"""
|
||||
|
||||
def parse(self, line):
|
||||
@@ -204,12 +219,53 @@ class ProfileGenericParser(DSMRObjectParser):
|
||||
8) Buffer value 2 (oldest entry of buffer attribute without unit)
|
||||
9) Unit of buffer values (Unit of capture objects attribute)
|
||||
"""
|
||||
def __init__(self, buffer_types, head_parsers, parsers_for_unidentified):
|
||||
self.value_formats = head_parsers
|
||||
self.buffer_types = buffer_types
|
||||
self.parsers_for_unidentified = parsers_for_unidentified
|
||||
|
||||
def _is_line_wellformed(self, line, values):
|
||||
if values and (len(values) == 1) and (values[0] == ''):
|
||||
# special case: single empty parentheses (indicated by empty string)
|
||||
return True
|
||||
|
||||
if values and (len(values) >= 2) and (values[0].isdigit()):
|
||||
buffer_length = int(values[0])
|
||||
return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2))
|
||||
else:
|
||||
return False
|
||||
|
||||
def _parse_values(self, values):
|
||||
if values and (len(values) == 1) and (values[0] is None):
|
||||
# special case: single empty parentheses; make sure empty ProfileGenericObject is created
|
||||
values = [0, None] # buffer_length=0, buffer_value_obis_ID=None
|
||||
buffer_length = int(values[0])
|
||||
buffer_value_obis_ID = values[1]
|
||||
if (buffer_length > 0):
|
||||
if buffer_value_obis_ID in self.buffer_types:
|
||||
bufferValueParsers = self.buffer_types[buffer_value_obis_ID]
|
||||
else:
|
||||
bufferValueParsers = self.parsers_for_unidentified
|
||||
# add the parsers for the encountered value type z times
|
||||
for _ in range(buffer_length):
|
||||
self.value_formats.extend(bufferValueParsers)
|
||||
|
||||
return [self.value_formats[i].parse(value) for i, value in enumerate(values)]
|
||||
|
||||
def parse(self, line):
|
||||
raise NotImplementedError()
|
||||
return ProfileGenericObject(self._parse(line))
|
||||
|
||||
|
||||
class ValueParser(object):
|
||||
"""
|
||||
Parses a single value from DSMRObject's.
|
||||
|
||||
Example with coerce_type being int:
|
||||
(002*A) becomes {'value': 1, 'unit': 'A'}
|
||||
|
||||
Example with coerce_type being str:
|
||||
(42) becomes {'value': '42', 'unit': None}
|
||||
"""
|
||||
|
||||
def __init__(self, coerce_type):
|
||||
self.coerce_type = coerce_type
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from dsmr_parser.parsers import ValueParser
|
||||
from dsmr_parser.value_types import timestamp
|
||||
|
||||
PG_FAILURE_EVENT = r'0-0:96.7.19'
|
||||
|
||||
PG_HEAD_PARSERS = [ValueParser(int), ValueParser(str)]
|
||||
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)]
|
||||
BUFFER_TYPES = {
|
||||
PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)]
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
"""Asyncio protocol implementation for handling telegrams."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from serial_asyncio import create_serial_connection
|
||||
|
||||
from . import telegram_specifications
|
||||
from .exceptions import ParseError
|
||||
from .parsers import (
|
||||
TelegramParserV2_2,
|
||||
TelegramParserV4
|
||||
)
|
||||
from .serial import (
|
||||
SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4,
|
||||
is_end_of_telegram,
|
||||
is_start_of_telegram
|
||||
)
|
||||
|
||||
|
||||
def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):
|
||||
"""Creates a DSMR asyncio protocol coroutine."""
|
||||
|
||||
if dsmr_version == '2.2':
|
||||
specifications = telegram_specifications.V2_2
|
||||
telegram_parser = TelegramParserV2_2
|
||||
serial_settings = SERIAL_SETTINGS_V2_2
|
||||
elif dsmr_version == '4':
|
||||
specifications = telegram_specifications.V4
|
||||
telegram_parser = TelegramParserV4
|
||||
serial_settings = SERIAL_SETTINGS_V4
|
||||
|
||||
serial_settings['url'] = port
|
||||
|
||||
protocol = partial(DSMRProtocol, loop, telegram_parser(specifications),
|
||||
telegram_callback=telegram_callback)
|
||||
|
||||
conn = create_serial_connection(loop, protocol, **serial_settings)
|
||||
|
||||
return conn
|
||||
|
||||
|
||||
class DSMRProtocol(asyncio.Protocol):
|
||||
"""Assemble and handle incoming data into complete DSM telegrams."""
|
||||
|
||||
transport = None
|
||||
telegram_callback = None
|
||||
|
||||
def __init__(self, loop, telegram_parser, telegram_callback=None):
|
||||
"""Initialize class."""
|
||||
self.loop = loop
|
||||
self.log = logging.getLogger(__name__)
|
||||
self.telegram_parser = telegram_parser
|
||||
# callback to call on complete telegram
|
||||
self.telegram_callback = telegram_callback
|
||||
# buffer to keep incoming telegram lines
|
||||
self.telegram = []
|
||||
# buffer to keep incomplete incoming data
|
||||
self.buffer = ''
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Just logging for now."""
|
||||
self.transport = transport
|
||||
self.log.debug('connected')
|
||||
|
||||
def data_received(self, data):
|
||||
"""Add incoming data to buffer."""
|
||||
data = data.decode()
|
||||
self.log.debug('received data: %s', data.strip())
|
||||
self.buffer += data
|
||||
self.handle_lines()
|
||||
|
||||
def handle_lines(self):
|
||||
"""Assemble incoming data into single lines."""
|
||||
while "\r\n" in self.buffer:
|
||||
line, self.buffer = self.buffer.split("\r\n", 1)
|
||||
self.log.debug('got line: %s', line)
|
||||
|
||||
# Telegrams need to be complete because the values belong to a
|
||||
# particular reading and can also be related to eachother.
|
||||
if not self.telegram and not is_start_of_telegram(line):
|
||||
continue
|
||||
|
||||
self.telegram.append(line)
|
||||
if is_end_of_telegram(line):
|
||||
try:
|
||||
parsed_telegram = self.telegram_parser.parse(self.telegram)
|
||||
self.handle_telegram(parsed_telegram)
|
||||
except ParseError:
|
||||
self.log.exception("failed to parse telegram")
|
||||
self.telegram = []
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""Stop when connection is lost."""
|
||||
self.log.error('disconnected')
|
||||
|
||||
def handle_telegram(self, telegram):
|
||||
"""Send off parsed telegram to handling callback."""
|
||||
self.log.debug('got telegram: %s', telegram)
|
||||
|
||||
if self.telegram_callback:
|
||||
self.telegram_callback(telegram)
|
||||
@@ -1,138 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import serial
|
||||
import serial_asyncio
|
||||
|
||||
from dsmr_parser.exceptions import ParseError
|
||||
from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2, \
|
||||
TelegramParserV4
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SERIAL_SETTINGS_V2_2 = {
|
||||
'baudrate': 9600,
|
||||
'bytesize': serial.SEVENBITS,
|
||||
'parity': serial.PARITY_EVEN,
|
||||
'stopbits': serial.STOPBITS_ONE,
|
||||
'xonxoff': 0,
|
||||
'rtscts': 0,
|
||||
'timeout': 20
|
||||
}
|
||||
|
||||
SERIAL_SETTINGS_V4 = {
|
||||
'baudrate': 115200,
|
||||
'bytesize': serial.SEVENBITS,
|
||||
'parity': serial.PARITY_EVEN,
|
||||
'stopbits': serial.STOPBITS_ONE,
|
||||
'xonxoff': 0,
|
||||
'rtscts': 0,
|
||||
'timeout': 20
|
||||
}
|
||||
|
||||
|
||||
def is_start_of_telegram(line):
|
||||
"""
|
||||
:type line: line
|
||||
"""
|
||||
return line.startswith('/')
|
||||
|
||||
|
||||
def is_end_of_telegram(line):
|
||||
"""
|
||||
:type line: line
|
||||
"""
|
||||
return line.startswith('!')
|
||||
|
||||
|
||||
class SerialReader(object):
|
||||
PORT_KEY = 'port'
|
||||
|
||||
def __init__(self, device, serial_settings, telegram_specification):
|
||||
self.serial_settings = serial_settings
|
||||
self.serial_settings[self.PORT_KEY] = device
|
||||
|
||||
if serial_settings is SERIAL_SETTINGS_V2_2:
|
||||
telegram_parser = TelegramParserV2_2
|
||||
elif serial_settings is SERIAL_SETTINGS_V4:
|
||||
telegram_parser = TelegramParserV4
|
||||
else:
|
||||
telegram_parser = TelegramParser
|
||||
|
||||
self.telegram_parser = telegram_parser(telegram_specification)
|
||||
|
||||
def read(self):
|
||||
"""
|
||||
Read complete DSMR telegram's from the serial interface and parse it
|
||||
into CosemObject's and MbusObject's
|
||||
|
||||
:rtype dict
|
||||
"""
|
||||
with serial.Serial(**self.serial_settings) as serial_handle:
|
||||
telegram = []
|
||||
|
||||
while True:
|
||||
line = serial_handle.readline()
|
||||
line = line.decode('ascii') # TODO move this to the parser?
|
||||
|
||||
# Telegrams need to be complete because the values belong to a
|
||||
# particular reading and can also be related to eachother.
|
||||
if not telegram and not is_start_of_telegram(line):
|
||||
continue
|
||||
|
||||
telegram.append(line)
|
||||
|
||||
if is_end_of_telegram(line):
|
||||
|
||||
try:
|
||||
yield self.telegram_parser.parse(telegram)
|
||||
except ParseError as e:
|
||||
logger.error('Failed to parse telegram: %s', e)
|
||||
|
||||
telegram = []
|
||||
|
||||
|
||||
class AsyncSerialReader(SerialReader):
|
||||
"""Serial reader using asyncio pyserial."""
|
||||
|
||||
PORT_KEY = 'url'
|
||||
|
||||
@asyncio.coroutine
|
||||
def read(self, queue):
|
||||
"""
|
||||
Read complete DSMR telegram's from the serial interface and parse it
|
||||
into CosemObject's and MbusObject's.
|
||||
|
||||
Instead of being a generator, values are pushed to provided queue for
|
||||
asynchronous processing.
|
||||
|
||||
:rtype Generator/Async
|
||||
"""
|
||||
# create Serial StreamReader
|
||||
conn = serial_asyncio.open_serial_connection(**self.serial_settings)
|
||||
reader, _ = yield from conn
|
||||
|
||||
telegram = []
|
||||
|
||||
while True:
|
||||
# read line if available or give control back to loop until
|
||||
# new data has arrived
|
||||
line = yield from reader.readline()
|
||||
line = line.decode('ascii')
|
||||
|
||||
# Telegrams need to be complete because the values belong to a
|
||||
# particular reading and can also be related to eachother.
|
||||
if not telegram and not is_start_of_telegram(line):
|
||||
continue
|
||||
|
||||
telegram.append(line)
|
||||
|
||||
if is_end_of_telegram(line):
|
||||
try:
|
||||
parsed_telegram = self.telegram_parser.parse(telegram)
|
||||
# push new parsed telegram onto queue
|
||||
queue.put_nowait(parsed_telegram)
|
||||
except ParseError as e:
|
||||
logger.warning('Failed to parse telegram: %s', e)
|
||||
|
||||
telegram = []
|
||||
@@ -1,9 +1,10 @@
|
||||
from decimal import Decimal
|
||||
from copy import deepcopy
|
||||
|
||||
from . import obis_references as obis
|
||||
from .parsers import CosemParser, ValueParser, MBusParser
|
||||
from .value_types import timestamp
|
||||
|
||||
from dsmr_parser import obis_references as obis
|
||||
from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser
|
||||
from dsmr_parser.value_types import timestamp
|
||||
from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS
|
||||
|
||||
"""
|
||||
dsmr_parser.telegram_specifications
|
||||
@@ -14,60 +15,144 @@ how the telegram lines are parsed.
|
||||
"""
|
||||
|
||||
V2_2 = {
|
||||
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
||||
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
||||
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
||||
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
||||
obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)),
|
||||
obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)),
|
||||
obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)),
|
||||
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
|
||||
obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)),
|
||||
obis.DEVICE_TYPE: CosemParser(ValueParser(str)),
|
||||
obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)),
|
||||
obis.GAS_METER_READING: MBusParser(
|
||||
ValueParser(timestamp),
|
||||
ValueParser(int),
|
||||
ValueParser(int),
|
||||
ValueParser(int),
|
||||
ValueParser(str),
|
||||
ValueParser(Decimal),
|
||||
),
|
||||
'checksum_support': False,
|
||||
'objects': {
|
||||
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
||||
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
||||
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
||||
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
||||
obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)),
|
||||
obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)),
|
||||
obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)),
|
||||
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
|
||||
obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)),
|
||||
obis.DEVICE_TYPE: CosemParser(ValueParser(str)),
|
||||
obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)),
|
||||
obis.GAS_METER_READING: MBusParser(
|
||||
ValueParser(timestamp),
|
||||
ValueParser(str), # changed to str see issue60
|
||||
ValueParser(int),
|
||||
ValueParser(int),
|
||||
ValueParser(str), # obis ref
|
||||
ValueParser(str), # unit, position 5
|
||||
ValueParser(Decimal), # meter reading, position 6
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
V3 = V2_2
|
||||
|
||||
V4 = {
|
||||
obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
|
||||
obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
|
||||
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
||||
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
||||
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
||||
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
||||
obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
|
||||
# POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO
|
||||
obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)),
|
||||
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
|
||||
obis.DEVICE_TYPE: CosemParser(ValueParser(int)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)),
|
||||
obis.HOURLY_GAS_METER_READING: MBusParser(ValueParser(timestamp),
|
||||
ValueParser(Decimal))
|
||||
'checksum_support': True,
|
||||
'objects': {
|
||||
obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
|
||||
obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
|
||||
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
||||
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
||||
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
||||
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
||||
obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.POWER_EVENT_FAILURE_LOG:
|
||||
ProfileGenericParser(BUFFER_TYPES,
|
||||
PG_HEAD_PARSERS,
|
||||
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS),
|
||||
obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)),
|
||||
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
|
||||
obis.DEVICE_TYPE: CosemParser(ValueParser(int)),
|
||||
obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)),
|
||||
obis.HOURLY_GAS_METER_READING: MBusParser(
|
||||
ValueParser(timestamp),
|
||||
ValueParser(Decimal)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
V5 = {
|
||||
'checksum_support': True,
|
||||
'objects': {
|
||||
obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
|
||||
obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
|
||||
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
||||
obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
||||
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
||||
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
||||
obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.POWER_EVENT_FAILURE_LOG:
|
||||
ProfileGenericParser(BUFFER_TYPES,
|
||||
PG_HEAD_PARSERS,
|
||||
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS),
|
||||
obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)),
|
||||
obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
|
||||
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
|
||||
obis.DEVICE_TYPE: CosemParser(ValueParser(int)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)),
|
||||
obis.HOURLY_GAS_METER_READING: MBusParser(
|
||||
ValueParser(timestamp),
|
||||
ValueParser(Decimal)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ALL = (V2_2, V3, V4, V5)
|
||||
|
||||
|
||||
BELGIUM_FLUVIUS = deepcopy(V5)
|
||||
BELGIUM_FLUVIUS['objects'].update({
|
||||
obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser(
|
||||
ValueParser(timestamp),
|
||||
ValueParser(Decimal)
|
||||
)
|
||||
})
|
||||
|
||||
LUXEMBOURG_SMARTY = deepcopy(V5)
|
||||
LUXEMBOURG_SMARTY['objects'].update({
|
||||
obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
||||
obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)),
|
||||
obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)),
|
||||
})
|
||||
|
||||
@@ -4,15 +4,15 @@ setup(
|
||||
name='dsmr-parser',
|
||||
description='Library to parse Dutch Smart Meter Requirements (DSMR)',
|
||||
author='Nigel Dokter',
|
||||
author_email='nigeldokter@gmail.com',
|
||||
author_email='nigel@nldr.net',
|
||||
url='https://github.com/ndokter/dsmr_parser',
|
||||
version='0.5',
|
||||
packages=find_packages(),
|
||||
version='0.27',
|
||||
packages=find_packages(exclude=('test', 'test.*')),
|
||||
install_requires=[
|
||||
'pyserial>=3,<4',
|
||||
'pyserial-asyncio<1',
|
||||
'pytz',
|
||||
'PyCRC>=1.2,<2'
|
||||
'Tailer==0.4.1'
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
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'
|
||||
)
|
||||
|
||||
TELEGRAM_V3 = (
|
||||
'/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'
|
||||
)
|
||||
|
||||
TELEGRAM_V4_2 = (
|
||||
'/KFM5KAIFA-METER\r\n'
|
||||
'\r\n'
|
||||
'1-3:0.2.8(42)\r\n'
|
||||
'0-0:1.0.0(161113205757W)\r\n'
|
||||
'0-0:96.1.1(3960221976967177082151037881335713)\r\n'
|
||||
'1-0:1.8.1(001581.123*kWh)\r\n'
|
||||
'1-0:1.8.2(001435.706*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(0002)\r\n'
|
||||
'1-0:1.7.0(02.027*kW)\r\n'
|
||||
'1-0:2.7.0(00.000*kW)\r\n'
|
||||
'0-0:96.7.21(00015)\r\n'
|
||||
'0-0:96.7.9(00007)\r\n'
|
||||
'1-0:99.97.0(3)(0-0:96.7.19)(000104180320W)(0000237126*s)(000101000001W)'
|
||||
'(2147583646*s)(000102000003W)(2317482647*s)\r\n'
|
||||
'1-0:32.32.0(00000)\r\n'
|
||||
'1-0:52.32.0(00000)\r\n'
|
||||
'1-0:72.32.0(00000)\r\n'
|
||||
'1-0:32.36.0(00000)\r\n'
|
||||
'1-0:52.36.0(00000)\r\n'
|
||||
'1-0:72.36.0(00000)\r\n'
|
||||
'0-0:96.13.1()\r\n'
|
||||
'0-0:96.13.0()\r\n'
|
||||
'1-0:31.7.0(000*A)\r\n'
|
||||
'1-0:51.7.0(006*A)\r\n'
|
||||
'1-0:71.7.0(002*A)\r\n'
|
||||
'1-0:21.7.0(00.170*kW)\r\n'
|
||||
'1-0:22.7.0(00.000*kW)\r\n'
|
||||
'1-0:41.7.0(01.247*kW)\r\n'
|
||||
'1-0:42.7.0(00.000*kW)\r\n'
|
||||
'1-0:61.7.0(00.209*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(4819243993373755377509728609491464)\r\n'
|
||||
'0-1:24.2.1(161129200000W)(00981.443*m3)\r\n'
|
||||
'!6796\r\n'
|
||||
)
|
||||
|
||||
TELEGRAM_V5 = (
|
||||
'/ISk5\\2MT382-1000\r\n'
|
||||
'\r\n'
|
||||
'1-3:0.2.8(50)\r\n'
|
||||
'0-0:1.0.0(170102192002W)\r\n'
|
||||
'0-0:96.1.1(4B384547303034303436333935353037)\r\n'
|
||||
'1-0:1.8.1(000004.426*kWh)\r\n'
|
||||
'1-0:1.8.2(000002.399*kWh)\r\n'
|
||||
'1-0:2.8.1(000002.444*kWh)\r\n'
|
||||
'1-0:2.8.2(000000.000*kWh)\r\n'
|
||||
'0-0:96.14.0(0002)\r\n'
|
||||
'1-0:1.7.0(00.244*kW)\r\n'
|
||||
'1-0:2.7.0(00.000*kW)\r\n'
|
||||
'0-0:96.7.21(00013)\r\n'
|
||||
'0-0:96.7.9(00000)\r\n'
|
||||
'1-0:99.97.0(0)(0-0:96.7.19)\r\n'
|
||||
'1-0:32.32.0(00000)\r\n'
|
||||
'1-0:52.32.0(00000)\r\n'
|
||||
'1-0:72.32.0(00000)\r\n'
|
||||
'1-0:32.36.0(00000)\r\n'
|
||||
'1-0:52.36.0(00000)\r\n'
|
||||
'1-0:72.36.0(00000)\r\n'
|
||||
'0-0:96.13.0()\r\n'
|
||||
'1-0:32.7.0(0230.0*V)\r\n'
|
||||
'1-0:52.7.0(0230.0*V)\r\n'
|
||||
'1-0:72.7.0(0229.0*V)\r\n'
|
||||
'1-0:31.7.0(0.48*A)\r\n'
|
||||
'1-0:51.7.0(0.44*A)\r\n'
|
||||
'1-0:71.7.0(0.86*A)\r\n'
|
||||
'1-0:21.7.0(00.070*kW)\r\n'
|
||||
'1-0:41.7.0(00.032*kW)\r\n'
|
||||
'1-0:61.7.0(00.142*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(3232323241424344313233343536373839)\r\n'
|
||||
'0-1:24.2.1(170102161005W)(00000.107*m3)\r\n'
|
||||
'0-2:24.1.0(003)\r\n'
|
||||
'0-2:96.1.0()\r\n'
|
||||
'!6EEE\r\n'
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.objects import Telegram
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from example_telegrams import TELEGRAM_V4_2
|
||||
parser = TelegramParser(telegram_specifications.V4)
|
||||
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
|
||||
|
||||
print(telegram)
|
||||
+78
-28
@@ -1,42 +1,92 @@
|
||||
import unittest
|
||||
|
||||
from dsmr_parser.parsers import TelegramParserV2_2
|
||||
from decimal import Decimal
|
||||
|
||||
from dsmr_parser.objects import MBusObject, CosemObject
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser import obis_references as obis
|
||||
|
||||
TELEGRAM_V2_2 = [
|
||||
'/ISk5\2MT382-1004',
|
||||
'',
|
||||
'0-0:96.1.1(00000000000000)',
|
||||
'1-0:1.8.1(00001.001*kWh)',
|
||||
'1-0:1.8.2(00001.001*kWh)',
|
||||
'1-0:2.8.1(00001.001*kWh)',
|
||||
'1-0:2.8.2(00001.001*kWh)',
|
||||
'0-0:96.14.0(0001)',
|
||||
'1-0:1.7.0(0001.01*kW)',
|
||||
'1-0:2.7.0(0000.00*kW)',
|
||||
'0-0:17.0.0(0999.00*kW)',
|
||||
'0-0:96.3.10(1)',
|
||||
'0-0:96.13.1()',
|
||||
'0-0:96.13.0()',
|
||||
'0-1:24.1.0(3)',
|
||||
'0-1:96.1.0(000000000000)',
|
||||
'0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)',
|
||||
'(00001.001)',
|
||||
'0-1:24.4.0(1)',
|
||||
'!',
|
||||
]
|
||||
from test.example_telegrams import TELEGRAM_V2_2
|
||||
|
||||
|
||||
class TelegramParserV2_2Test(unittest.TestCase):
|
||||
""" Test parsing of a DSMR v2.2 telegram. """
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParserV2_2(telegram_specifications.V2_2)
|
||||
parser = TelegramParser(telegram_specifications.V2_2)
|
||||
result = parser.parse(TELEGRAM_V2_2)
|
||||
|
||||
assert float(result[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01
|
||||
assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
|
||||
# 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('1.001')
|
||||
|
||||
assert float(result[obis.GAS_METER_READING].value) == 1.001
|
||||
# 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('1.001')
|
||||
|
||||
# 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('1.001')
|
||||
|
||||
# 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('1.001')
|
||||
|
||||
# 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'
|
||||
|
||||
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].unit is None
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].value == '00000000000000'
|
||||
|
||||
# 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('1.01')
|
||||
|
||||
# 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')
|
||||
|
||||
# TEXT_MESSAGE_CODE (0-0:96.13.1)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE_CODE], CosemObject)
|
||||
assert result[obis.TEXT_MESSAGE_CODE].unit is None
|
||||
|
||||
# TEXT_MESSAGE (0-0:96.13.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert result[obis.TEXT_MESSAGE].unit is None
|
||||
assert result[obis.TEXT_MESSAGE].value is None
|
||||
|
||||
# DEVICE_TYPE (0-x:24.1.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert result[obis.DEVICE_TYPE].unit is None
|
||||
assert isinstance(result[obis.DEVICE_TYPE].value, str)
|
||||
assert result[obis.DEVICE_TYPE].value == '3'
|
||||
|
||||
# EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '000000000000'
|
||||
|
||||
# GAS_METER_READING (0-1:24.3.0)
|
||||
assert isinstance(result[obis.GAS_METER_READING], MBusObject)
|
||||
assert result[obis.GAS_METER_READING].unit == 'm3'
|
||||
assert isinstance(result[obis.GAS_METER_READING].value, Decimal)
|
||||
assert result[obis.GAS_METER_READING].value == Decimal('1.001')
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import unittest
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from dsmr_parser.objects import CosemObject, MBusObject
|
||||
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_V3
|
||||
|
||||
|
||||
class TelegramParserV3Test(unittest.TestCase):
|
||||
""" Test parsing of a DSMR v3 telegram. """
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.V3)
|
||||
result = parser.parse(TELEGRAM_V3)
|
||||
|
||||
# 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('12345.678')
|
||||
|
||||
# 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('12345.678')
|
||||
|
||||
# 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('12345.678')
|
||||
|
||||
# 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('12345.678')
|
||||
|
||||
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
|
||||
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject)
|
||||
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None
|
||||
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str)
|
||||
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0002'
|
||||
|
||||
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].unit is None
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].value == '4B384547303034303436333935353037'
|
||||
|
||||
# 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('1.19')
|
||||
|
||||
# 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')
|
||||
|
||||
# TEXT_MESSAGE_CODE (0-0:96.13.1)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE_CODE], CosemObject)
|
||||
assert result[obis.TEXT_MESSAGE_CODE].unit is None
|
||||
assert isinstance(result[obis.TEXT_MESSAGE_CODE].value, int)
|
||||
assert result[obis.TEXT_MESSAGE_CODE].value == 303132333435363738
|
||||
|
||||
# TEXT_MESSAGE (0-0:96.13.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert result[obis.TEXT_MESSAGE].unit is None
|
||||
assert isinstance(result[obis.TEXT_MESSAGE].value, str)
|
||||
assert result[obis.TEXT_MESSAGE].value == \
|
||||
'303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F' \
|
||||
'303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F' \
|
||||
'303132333435363738393A3B3C3D3E3F'
|
||||
|
||||
# DEVICE_TYPE (0-x:24.1.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert result[obis.DEVICE_TYPE].unit is None
|
||||
assert isinstance(result[obis.DEVICE_TYPE].value, str)
|
||||
assert result[obis.DEVICE_TYPE].value == '03'
|
||||
|
||||
# EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '3232323241424344313233343536373839'
|
||||
|
||||
# GAS_METER_READING (0-1:24.3.0)
|
||||
assert isinstance(result[obis.GAS_METER_READING], MBusObject)
|
||||
assert result[obis.GAS_METER_READING].unit == 'm3'
|
||||
assert isinstance(result[obis.GAS_METER_READING].value, Decimal)
|
||||
assert result[obis.GAS_METER_READING].value == Decimal('1.001')
|
||||
+49
-65
@@ -8,75 +8,13 @@ from dsmr_parser import obis_references as obis
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
|
||||
from dsmr_parser.objects import CosemObject, MBusObject
|
||||
from dsmr_parser.parsers import TelegramParser, TelegramParserV4
|
||||
|
||||
TELEGRAM_V4_2 = [
|
||||
'/KFM5KAIFA-METER\r\n',
|
||||
'\r\n',
|
||||
'1-3:0.2.8(42)\r\n',
|
||||
'0-0:1.0.0(161113205757W)\r\n',
|
||||
'0-0:96.1.1(3960221976967177082151037881335713)\r\n',
|
||||
'1-0:1.8.1(001581.123*kWh)\r\n',
|
||||
'1-0:1.8.2(001435.706*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(0002)\r\n',
|
||||
'1-0:1.7.0(02.027*kW)\r\n',
|
||||
'1-0:2.7.0(00.000*kW)\r\n',
|
||||
'0-0:96.7.21(00015)\r\n',
|
||||
'0-0:96.7.9(00007)\r\n',
|
||||
'1-0:99.97.0(3)(0-0:96.7.19)(000104180320W)(0000237126*s)(000101000001W)'
|
||||
'(2147583646*s)(000102000003W)(2317482647*s)\r\n',
|
||||
'1-0:32.32.0(00000)\r\n',
|
||||
'1-0:52.32.0(00000)\r\n',
|
||||
'1-0:72.32.0(00000)\r\n',
|
||||
'1-0:32.36.0(00000)\r\n',
|
||||
'1-0:52.36.0(00000)\r\n',
|
||||
'1-0:72.36.0(00000)\r\n',
|
||||
'0-0:96.13.1()\r\n',
|
||||
'0-0:96.13.0()\r\n',
|
||||
'1-0:31.7.0(000*A)\r\n',
|
||||
'1-0:51.7.0(006*A)\r\n',
|
||||
'1-0:71.7.0(002*A)\r\n',
|
||||
'1-0:21.7.0(00.170*kW)\r\n',
|
||||
'1-0:22.7.0(00.000*kW)\r\n',
|
||||
'1-0:41.7.0(01.247*kW)\r\n',
|
||||
'1-0:42.7.0(00.000*kW)\r\n',
|
||||
'1-0:61.7.0(00.209*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(4819243993373755377509728609491464)\r\n',
|
||||
'0-1:24.2.1(161129200000W)(00981.443*m3)\r\n',
|
||||
'!6796\r\n'
|
||||
]
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from test.example_telegrams import TELEGRAM_V4_2
|
||||
|
||||
|
||||
class TelegramParserV4_2Test(unittest.TestCase):
|
||||
""" Test parsing of a DSMR v4.2 telegram. """
|
||||
|
||||
def test_valid(self):
|
||||
# No exception is raised.
|
||||
TelegramParserV4.validate_telegram_checksum(
|
||||
TELEGRAM_V4_2
|
||||
)
|
||||
|
||||
def test_invalid(self):
|
||||
# Remove one the electricty used data value. This causes the checksum to
|
||||
# not match anymore.
|
||||
telegram = [line
|
||||
for line in TELEGRAM_V4_2
|
||||
if '1-0:1.8.1' not in line]
|
||||
|
||||
with self.assertRaises(InvalidChecksumError):
|
||||
TelegramParserV4.validate_telegram_checksum(telegram)
|
||||
|
||||
def test_missing_checksum(self):
|
||||
# Remove the checksum value causing a ParseError.
|
||||
telegram = TELEGRAM_V4_2[:-1]
|
||||
|
||||
with self.assertRaises(ParseError):
|
||||
TelegramParserV4.validate_telegram_checksum(telegram)
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.V4)
|
||||
result = parser.parse(TELEGRAM_V4_2)
|
||||
@@ -142,6 +80,12 @@ class TelegramParserV4_2Test(unittest.TestCase):
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)
|
||||
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0')
|
||||
|
||||
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
|
||||
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject)
|
||||
assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None
|
||||
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int)
|
||||
assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 15
|
||||
|
||||
# LONG_POWER_FAILURE_COUNT (96.7.9)
|
||||
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject)
|
||||
assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None
|
||||
@@ -194,8 +138,26 @@ class TelegramParserV4_2Test(unittest.TestCase):
|
||||
assert result[obis.TEXT_MESSAGE].unit is None
|
||||
assert result[obis.TEXT_MESSAGE].value is None
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('6')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('2')
|
||||
|
||||
# DEVICE_TYPE (0-x:24.1.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert isinstance(result[obis.DEVICE_TYPE], CosemObject)
|
||||
assert result[obis.DEVICE_TYPE].unit is None
|
||||
assert isinstance(result[obis.DEVICE_TYPE].value, int)
|
||||
assert result[obis.DEVICE_TYPE].value == 3
|
||||
@@ -259,3 +221,25 @@ class TelegramParserV4_2Test(unittest.TestCase):
|
||||
|
||||
# VALVE_POSITION_GAS (0-x:24.4.0)
|
||||
# TODO to be implemented
|
||||
|
||||
def test_checksum_valid(self):
|
||||
# No exception is raised.
|
||||
TelegramParser.validate_checksum(TELEGRAM_V4_2)
|
||||
|
||||
def test_checksum_invalid(self):
|
||||
# Remove the electricty used data value. This causes the checksum to
|
||||
# not match anymore.
|
||||
corrupted_telegram = TELEGRAM_V4_2.replace(
|
||||
'1-0:1.8.1(001581.123*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_V4_2.replace('!6796\r\n', '')
|
||||
|
||||
with self.assertRaises(ParseError):
|
||||
TelegramParser.validate_checksum(corrupted_telegram)
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import pytz
|
||||
|
||||
from dsmr_parser import obis_references as obis
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
|
||||
from dsmr_parser.objects import CosemObject, MBusObject
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from test.example_telegrams import TELEGRAM_V5
|
||||
|
||||
|
||||
class TelegramParserV5Test(unittest.TestCase):
|
||||
""" Test parsing of a DSMR v5.x telegram. """
|
||||
|
||||
def test_parse(self):
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
result = parser.parse(TELEGRAM_V5)
|
||||
|
||||
# P1_MESSAGE_HEADER (1-3:0.2.8)
|
||||
assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject)
|
||||
assert result[obis.P1_MESSAGE_HEADER].unit is None
|
||||
assert isinstance(result[obis.P1_MESSAGE_HEADER].value, str)
|
||||
assert result[obis.P1_MESSAGE_HEADER].value == '50'
|
||||
|
||||
# P1_MESSAGE_TIMESTAMP (0-0:1.0.0)
|
||||
assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP], CosemObject)
|
||||
assert result[obis.P1_MESSAGE_TIMESTAMP].unit is None
|
||||
assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP].value, datetime.datetime)
|
||||
assert result[obis.P1_MESSAGE_TIMESTAMP].value == \
|
||||
datetime.datetime(2017, 1, 2, 18, 20, 2, tzinfo=pytz.UTC)
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('4.426')
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('2.399')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('2.444')
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh'
|
||||
assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal)
|
||||
assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('0')
|
||||
|
||||
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
|
||||
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject)
|
||||
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None
|
||||
assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str)
|
||||
assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0002'
|
||||
|
||||
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].unit is None
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER].value == '4B384547303034303436333935353037'
|
||||
|
||||
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject)
|
||||
assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
|
||||
assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal)
|
||||
assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.244')
|
||||
|
||||
# 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')
|
||||
|
||||
# LONG_POWER_FAILURE_COUNT (96.7.9)
|
||||
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject)
|
||||
assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None
|
||||
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int)
|
||||
assert result[obis.LONG_POWER_FAILURE_COUNT].value == 0
|
||||
|
||||
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
|
||||
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject)
|
||||
assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None
|
||||
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int)
|
||||
assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 13
|
||||
|
||||
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SAG_L1_COUNT].value == 0
|
||||
|
||||
# VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SAG_L2_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SAG_L2_COUNT].value == 0
|
||||
|
||||
# VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SAG_L3_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SAG_L3_COUNT].value == 0
|
||||
|
||||
# VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SWELL_L1_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SWELL_L1_COUNT].value == 0
|
||||
|
||||
# VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SWELL_L2_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SWELL_L2_COUNT].value == 0
|
||||
|
||||
# VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT], CosemObject)
|
||||
assert result[obis.VOLTAGE_SWELL_L3_COUNT].unit is None
|
||||
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int)
|
||||
assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('230.0')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('230.0')
|
||||
|
||||
# INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('229.0')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.48')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.44')
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.86')
|
||||
|
||||
# TEXT_MESSAGE (0-0:96.13.0)
|
||||
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||
assert result[obis.TEXT_MESSAGE].unit is None
|
||||
assert result[obis.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)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.070')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.032')
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW'
|
||||
assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal)
|
||||
assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.142')
|
||||
|
||||
# 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')
|
||||
|
||||
# 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')
|
||||
|
||||
# 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')
|
||||
|
||||
# EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None
|
||||
assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str)
|
||||
assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '3232323241424344313233343536373839'
|
||||
|
||||
# HOURLY_GAS_METER_READING (0-1:24.2.1)
|
||||
assert isinstance(result[obis.HOURLY_GAS_METER_READING], MBusObject)
|
||||
assert result[obis.HOURLY_GAS_METER_READING].unit == 'm3'
|
||||
assert isinstance(result[obis.HOURLY_GAS_METER_READING].value, Decimal)
|
||||
assert result[obis.HOURLY_GAS_METER_READING].value == Decimal('0.107')
|
||||
|
||||
def test_checksum_valid(self):
|
||||
# No exception is raised.
|
||||
TelegramParser.validate_checksum(TELEGRAM_V5)
|
||||
|
||||
def test_checksum_invalid(self):
|
||||
# Remove the electricty used data value. This causes the checksum to
|
||||
# not match anymore.
|
||||
corrupted_telegram = TELEGRAM_V5.replace(
|
||||
'1-0:1.8.1(000004.426*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.replace('!6EEE\r\n', '')
|
||||
with self.assertRaises(ParseError):
|
||||
TelegramParser.validate_checksum(corrupted_telegram)
|
||||
@@ -0,0 +1,89 @@
|
||||
import unittest
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
|
||||
from dsmr_parser.objects import Telegram
|
||||
from dsmr_parser.objects import ProfileGenericObject
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from dsmr_parser.parsers import ProfileGenericParser
|
||||
from dsmr_parser.profile_generic_specifications import BUFFER_TYPES
|
||||
from dsmr_parser.profile_generic_specifications import PG_HEAD_PARSERS
|
||||
from dsmr_parser.profile_generic_specifications import PG_UNIDENTIFIED_BUFFERTYPE_PARSERS
|
||||
from test.example_telegrams import TELEGRAM_V5
|
||||
|
||||
|
||||
class TestParserCornerCases(unittest.TestCase):
|
||||
""" Test instantiation of Telegram object """
|
||||
|
||||
def test_power_event_log_empty_1(self):
|
||||
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
|
||||
parser = TelegramParser(telegram_specifications.V5)
|
||||
telegram = Telegram(TELEGRAM_V5, parser, telegram_specifications.V5)
|
||||
|
||||
object_type = ProfileGenericObject
|
||||
testitem = telegram.POWER_EVENT_FAILURE_LOG
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 0
|
||||
assert testitem.buffer_type == '0-0:96.7.19'
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 0
|
||||
|
||||
def test_power_event_log_empty_2(self):
|
||||
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
|
||||
object_type = ProfileGenericObject
|
||||
|
||||
# Power Event Log with 0 items and no object type
|
||||
pefl_line = r'1-0:99.97.0(0)()\r\n'
|
||||
testitem = pef_parser.parse(pefl_line)
|
||||
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 0
|
||||
assert testitem.buffer_type is None
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 0
|
||||
assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]
|
||||
json = testitem.to_json()
|
||||
assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}'
|
||||
|
||||
def test_power_event_log_null_values(self):
|
||||
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
|
||||
object_type = ProfileGenericObject
|
||||
|
||||
# Power Event Log with 1 item and no object type and nno values for the item
|
||||
pefl_line = r'1-0:99.97.0(1)()()()\r\n'
|
||||
testitem = pef_parser.parse(pefl_line)
|
||||
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 1
|
||||
assert testitem.buffer_type is None
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 1
|
||||
assert testitem.values == [{'value': 1, 'unit': None}, {'value': None, 'unit': None},
|
||||
{'value': None, 'unit': None}, {'value': None, 'unit': None}]
|
||||
json = testitem.to_json()
|
||||
assert json == \
|
||||
'{"buffer_length": 1, "buffer_type": null, "buffer": [{"datetime": null, "value": null, "unit": null}]}'
|
||||
|
||||
def test_power_event_log_brackets_only(self):
|
||||
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
|
||||
# Issue 57
|
||||
# Test of an ill formatted empty POWER_EVENT_FAILURE_LOG, observed on some smartmeters
|
||||
# The idea is that instead of failing, the parser converts it to an empty POWER_EVENT_FAILURE_LOG
|
||||
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
|
||||
object_type = ProfileGenericObject
|
||||
|
||||
pefl_line = r'1-0:99.97.0()\r\n'
|
||||
testitem = pef_parser.parse(pefl_line)
|
||||
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 0
|
||||
assert testitem.buffer_type is None
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 0
|
||||
assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]
|
||||
json = testitem.to_json()
|
||||
assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}'
|
||||
+39
-47
@@ -1,62 +1,54 @@
|
||||
"""Test DSMR serial protocol."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from dsmr_parser import obis_references as obis
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser.parsers import TelegramParserV2_2
|
||||
from dsmr_parser.protocol import DSMRProtocol
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from dsmr_parser.clients.protocol import DSMRProtocol
|
||||
|
||||
|
||||
TELEGRAM_V2_2 = [
|
||||
"/ISk5\2MT382-1004",
|
||||
"",
|
||||
"0-0:96.1.1(00000000000000)",
|
||||
"1-0:1.8.1(00001.001*kWh)",
|
||||
"1-0:1.8.2(00001.001*kWh)",
|
||||
"1-0:2.8.1(00001.001*kWh)",
|
||||
"1-0:2.8.2(00001.001*kWh)",
|
||||
"0-0:96.14.0(0001)",
|
||||
"1-0:1.7.0(0001.01*kW)",
|
||||
"1-0:2.7.0(0000.00*kW)",
|
||||
"0-0:17.0.0(0999.00*kW)",
|
||||
"0-0:96.3.10(1)",
|
||||
"0-0:96.13.1()",
|
||||
"0-0:96.13.0()",
|
||||
"0-1:24.1.0(3)",
|
||||
"0-1:96.1.0(000000000000)",
|
||||
"0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)",
|
||||
"(00001.001)",
|
||||
"0-1:24.4.0(1)",
|
||||
"!",
|
||||
]
|
||||
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'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def protocol():
|
||||
"""DSMRprotocol instance with mocked telegram_callback."""
|
||||
class ProtocolTest(unittest.TestCase):
|
||||
|
||||
parser = TelegramParserV2_2
|
||||
specification = telegram_specifications.V2_2
|
||||
def setUp(self):
|
||||
telegram_parser = TelegramParser(telegram_specifications.V2_2)
|
||||
self.protocol = DSMRProtocol(None, telegram_parser,
|
||||
telegram_callback=Mock())
|
||||
|
||||
telegram_parser = parser(specification)
|
||||
return DSMRProtocol(None, telegram_parser,
|
||||
telegram_callback=Mock())
|
||||
def test_complete_packet(self):
|
||||
"""Protocol should assemble incoming lines into complete packet."""
|
||||
|
||||
self.protocol.data_received(TELEGRAM_V2_2.encode('ascii'))
|
||||
|
||||
def test_complete_packet(protocol):
|
||||
"""Protocol should assemble incoming lines into complete packet."""
|
||||
telegram = self.protocol.telegram_callback.call_args_list[0][0][0]
|
||||
assert isinstance(telegram, dict)
|
||||
|
||||
for line in TELEGRAM_V2_2:
|
||||
protocol.data_received(bytes(line + '\r\n', 'ascii'))
|
||||
assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01
|
||||
assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
|
||||
|
||||
telegram = protocol.telegram_callback.call_args_list[0][0][0]
|
||||
assert isinstance(telegram, dict)
|
||||
|
||||
assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01
|
||||
assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
|
||||
|
||||
assert float(telegram[obis.GAS_METER_READING].value) == 1.001
|
||||
assert telegram[obis.GAS_METER_READING].unit == 'm3'
|
||||
assert float(telegram[obis.GAS_METER_READING].value) == 1.001
|
||||
assert telegram[obis.GAS_METER_READING].unit == 'm3'
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
import unittest
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
from dsmr_parser import telegram_specifications
|
||||
from dsmr_parser import obis_name_mapping
|
||||
from dsmr_parser.objects import CosemObject
|
||||
from dsmr_parser.objects import MBusObject
|
||||
from dsmr_parser.objects import Telegram
|
||||
from dsmr_parser.objects import ProfileGenericObject
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
from test.example_telegrams import TELEGRAM_V4_2
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class TelegramTest(unittest.TestCase):
|
||||
""" Test instantiation of Telegram object """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.item_names_tested = []
|
||||
super(TelegramTest, self).__init__(*args, **kwargs)
|
||||
|
||||
def verify_telegram_item(self, telegram, testitem_name, object_type, unit_val, value_type, value_val):
|
||||
testitem = eval("telegram.{}".format(testitem_name))
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.unit == unit_val
|
||||
assert isinstance(testitem.value, value_type)
|
||||
assert testitem.value == value_val
|
||||
self.item_names_tested.append(testitem_name)
|
||||
|
||||
def test_instantiate(self):
|
||||
parser = TelegramParser(telegram_specifications.V4)
|
||||
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
|
||||
|
||||
# P1_MESSAGE_HEADER (1-3:0.2.8)
|
||||
self.verify_telegram_item(telegram,
|
||||
'P1_MESSAGE_HEADER',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=str,
|
||||
value_val='42')
|
||||
|
||||
# P1_MESSAGE_TIMESTAMP (0-0:1.0.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'P1_MESSAGE_TIMESTAMP',
|
||||
CosemObject,
|
||||
unit_val=None,
|
||||
value_type=datetime.datetime,
|
||||
value_val=datetime.datetime(2016, 11, 13, 19, 57, 57, tzinfo=pytz.UTC))
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_USED_TARIFF_1',
|
||||
object_type=CosemObject,
|
||||
unit_val='kWh',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('1581.123'))
|
||||
|
||||
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_USED_TARIFF_2',
|
||||
object_type=CosemObject,
|
||||
unit_val='kWh',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('1435.706'))
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_DELIVERED_TARIFF_1',
|
||||
object_type=CosemObject,
|
||||
unit_val='kWh',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_DELIVERED_TARIFF_2',
|
||||
object_type=CosemObject,
|
||||
unit_val='kWh',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'ELECTRICITY_ACTIVE_TARIFF',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=str,
|
||||
value_val='0002')
|
||||
|
||||
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'EQUIPMENT_IDENTIFIER',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=str,
|
||||
value_val='3960221976967177082151037881335713')
|
||||
|
||||
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'CURRENT_ELECTRICITY_USAGE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('2.027'))
|
||||
|
||||
# CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'CURRENT_ELECTRICITY_DELIVERY',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
|
||||
self.verify_telegram_item(telegram,
|
||||
'SHORT_POWER_FAILURE_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=15)
|
||||
|
||||
# LONG_POWER_FAILURE_COUNT (96.7.9)
|
||||
self.verify_telegram_item(telegram,
|
||||
'LONG_POWER_FAILURE_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=7)
|
||||
|
||||
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SAG_L1_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SAG_L2_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SAG_L3_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SWELL_L1_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SWELL_L2_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'VOLTAGE_SWELL_L3_COUNT',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=0)
|
||||
|
||||
# TEXT_MESSAGE_CODE (0-0:96.13.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'TEXT_MESSAGE_CODE',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=type(None),
|
||||
value_val=None)
|
||||
|
||||
# TEXT_MESSAGE (0-0:96.13.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'TEXT_MESSAGE',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=type(None),
|
||||
value_val=None)
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_CURRENT_L1',
|
||||
object_type=CosemObject,
|
||||
unit_val='A',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_CURRENT_L2',
|
||||
object_type=CosemObject,
|
||||
unit_val='A',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('6'))
|
||||
|
||||
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_CURRENT_L3',
|
||||
object_type=CosemObject,
|
||||
unit_val='A',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('2'))
|
||||
|
||||
# DEVICE_TYPE (0-x:24.1.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'DEVICE_TYPE',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=int,
|
||||
value_val=3)
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0.170'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('1.247'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0.209'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
|
||||
object_type=CosemObject,
|
||||
unit_val='kW',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('0'))
|
||||
|
||||
# EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)
|
||||
self.verify_telegram_item(telegram,
|
||||
'EQUIPMENT_IDENTIFIER_GAS',
|
||||
object_type=CosemObject,
|
||||
unit_val=None,
|
||||
value_type=str,
|
||||
value_val='4819243993373755377509728609491464')
|
||||
|
||||
# HOURLY_GAS_METER_READING (0-1:24.2.1)
|
||||
self.verify_telegram_item(telegram,
|
||||
'HOURLY_GAS_METER_READING',
|
||||
object_type=MBusObject,
|
||||
unit_val='m3',
|
||||
value_type=Decimal,
|
||||
value_val=Decimal('981.443'))
|
||||
|
||||
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
|
||||
testitem_name = 'POWER_EVENT_FAILURE_LOG'
|
||||
object_type = ProfileGenericObject
|
||||
testitem = eval("telegram.{}".format(testitem_name))
|
||||
assert isinstance(testitem, object_type)
|
||||
assert testitem.buffer_length == 3
|
||||
assert testitem.buffer_type == '0-0:96.7.19'
|
||||
buffer = testitem.buffer
|
||||
assert isinstance(testitem.buffer, list)
|
||||
assert len(buffer) == 3
|
||||
assert all([isinstance(item, MBusObject) for item in buffer])
|
||||
date0 = datetime.datetime(2000, 1, 4, 17, 3, 20, tzinfo=datetime.timezone.utc)
|
||||
date1 = datetime.datetime(1999, 12, 31, 23, 0, 1, tzinfo=datetime.timezone.utc)
|
||||
date2 = datetime.datetime(2000, 1, 1, 23, 0, 3, tzinfo=datetime.timezone.utc)
|
||||
assert buffer[0].datetime == date0
|
||||
assert buffer[1].datetime == date1
|
||||
assert buffer[2].datetime == date2
|
||||
assert buffer[0].value == 237126
|
||||
assert buffer[1].value == 2147583646
|
||||
assert buffer[2].value == 2317482647
|
||||
assert all([isinstance(item.value, int) for item in buffer])
|
||||
assert all([isinstance(item.unit, str) for item in buffer])
|
||||
assert all([(item.unit == 's') for item in buffer])
|
||||
self.item_names_tested.append(testitem_name)
|
||||
|
||||
# check if all items in telegram V4 specification are covered
|
||||
V4_name_list = [obis_name_mapping.EN[signature] for signature, parser in
|
||||
telegram_specifications.V4['objects'].items()]
|
||||
V4_name_set = set(V4_name_list)
|
||||
item_names_tested_set = set(self.item_names_tested)
|
||||
|
||||
assert item_names_tested_set == V4_name_set
|
||||
@@ -0,0 +1,105 @@
|
||||
import unittest
|
||||
|
||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||
from test.example_telegrams import TELEGRAM_V2_2, TELEGRAM_V4_2
|
||||
|
||||
|
||||
class TelegramBufferTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.telegram_buffer = TelegramBuffer()
|
||||
|
||||
def test_v22_telegram(self):
|
||||
self.telegram_buffer.append(TELEGRAM_V2_2)
|
||||
|
||||
telegram = next(self.telegram_buffer.get_all())
|
||||
|
||||
self.assertEqual(telegram, TELEGRAM_V2_2)
|
||||
self.assertEqual(self.telegram_buffer._buffer, '')
|
||||
|
||||
def test_v42_telegram(self):
|
||||
self.telegram_buffer.append(TELEGRAM_V4_2)
|
||||
|
||||
telegram = next(self.telegram_buffer.get_all())
|
||||
|
||||
self.assertEqual(telegram, TELEGRAM_V4_2)
|
||||
self.assertEqual(self.telegram_buffer._buffer, '')
|
||||
|
||||
def test_multiple_mixed_telegrams(self):
|
||||
self.telegram_buffer.append(
|
||||
''.join((TELEGRAM_V2_2, TELEGRAM_V4_2, TELEGRAM_V2_2))
|
||||
)
|
||||
|
||||
telegrams = list(self.telegram_buffer.get_all())
|
||||
|
||||
self.assertListEqual(
|
||||
telegrams,
|
||||
[
|
||||
TELEGRAM_V2_2,
|
||||
TELEGRAM_V4_2,
|
||||
TELEGRAM_V2_2
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(self.telegram_buffer._buffer, '')
|
||||
|
||||
def test_v42_telegram_preceded_with_unclosed_telegram(self):
|
||||
# There are unclosed telegrams at the start of the buffer.
|
||||
incomplete_telegram = TELEGRAM_V4_2[:-1]
|
||||
|
||||
self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2)
|
||||
|
||||
telegram = next(self.telegram_buffer.get_all())
|
||||
|
||||
self.assertEqual(telegram, TELEGRAM_V4_2)
|
||||
self.assertEqual(self.telegram_buffer._buffer, '')
|
||||
|
||||
def test_v42_telegram_preceded_with_unopened_telegram(self):
|
||||
# There is unopened telegrams at the start of the buffer indicating that
|
||||
# the buffer was being filled while the telegram was outputted halfway.
|
||||
incomplete_telegram = TELEGRAM_V4_2[1:]
|
||||
|
||||
self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2)
|
||||
|
||||
telegram = next(self.telegram_buffer.get_all())
|
||||
|
||||
self.assertEqual(telegram, TELEGRAM_V4_2)
|
||||
self.assertEqual(self.telegram_buffer._buffer, '')
|
||||
|
||||
def test_v42_telegram_trailed_by_unclosed_telegram(self):
|
||||
incomplete_telegram = TELEGRAM_V4_2[:-1]
|
||||
|
||||
self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram)
|
||||
|
||||
telegram = next(self.telegram_buffer.get_all())
|
||||
|
||||
self.assertEqual(telegram, TELEGRAM_V4_2)
|
||||
self.assertEqual(self.telegram_buffer._buffer, incomplete_telegram)
|
||||
|
||||
def test_v42_telegram_trailed_by_unopened_telegram(self):
|
||||
incomplete_telegram = TELEGRAM_V4_2[1:]
|
||||
|
||||
self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram)
|
||||
|
||||
telegram = next(self.telegram_buffer.get_all())
|
||||
|
||||
self.assertEqual(telegram, TELEGRAM_V4_2)
|
||||
self.assertEqual(self.telegram_buffer._buffer, incomplete_telegram)
|
||||
|
||||
def test_v42_telegram_adding_line_by_line(self):
|
||||
for line in TELEGRAM_V4_2.splitlines(keepends=True):
|
||||
self.telegram_buffer.append(line)
|
||||
|
||||
telegram = next(self.telegram_buffer.get_all())
|
||||
|
||||
self.assertEqual(telegram, TELEGRAM_V4_2)
|
||||
self.assertEqual(self.telegram_buffer._buffer, '')
|
||||
|
||||
def test_v42_telegram_adding_char_by_char(self):
|
||||
for char in TELEGRAM_V4_2:
|
||||
self.telegram_buffer.append(char)
|
||||
|
||||
telegram = next(self.telegram_buffer.get_all())
|
||||
|
||||
self.assertEqual(telegram, TELEGRAM_V4_2)
|
||||
self.assertEqual(self.telegram_buffer._buffer, '')
|
||||
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = py34,py35
|
||||
envlist = py35,py36,py37,py38
|
||||
|
||||
[testenv]
|
||||
deps=
|
||||
@@ -9,13 +9,18 @@ deps=
|
||||
pytest-asyncio
|
||||
pytest-catchlog
|
||||
pytest-mock
|
||||
PyCRC
|
||||
commands=
|
||||
py.test --cov=dsmr_parser test {posargs}
|
||||
pylama dsmr_parser test
|
||||
|
||||
[pylama:dsmr_parser/clients/__init__.py]
|
||||
ignore = W0611
|
||||
|
||||
[pylama:dsmr_parser/parsers.py]
|
||||
ignore = W605
|
||||
|
||||
[pylama:pylint]
|
||||
max_line_length = 100
|
||||
max_line_length = 120
|
||||
|
||||
[pylama:pycodestyle]
|
||||
max_line_length = 100
|
||||
max_line_length = 120
|
||||
|
||||
Reference in New Issue
Block a user