Compare commits
25
Commits
v0.7
...
development
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d216996fe6 | ||
|
|
aef37837c5 | ||
|
|
6305d3d37f | ||
|
|
d2f57a8926 | ||
|
|
3d64fea247 | ||
|
|
46860e04c1 | ||
|
|
148bdabc12 | ||
|
|
9d20bb8ad5 | ||
|
|
24ab9aa712 | ||
|
|
0c40070752 | ||
|
|
6c8a9dcbdb | ||
|
|
c1a6b930c8 | ||
|
|
9e74c4c23c | ||
|
|
b4a520c8b4 | ||
|
|
a88dfe1a41 | ||
|
|
c4dcc73191 | ||
|
|
c4caf54576 | ||
|
|
8a868ce826 | ||
|
|
9623f3b092 | ||
|
|
adcfdfe2ae | ||
|
|
45ee8dbb32 | ||
|
|
7a4c204850 | ||
|
|
45f5fe2c36 | ||
|
|
07634abed1 | ||
|
|
e2e4bb36a2 |
@@ -4,6 +4,7 @@ python:
|
|||||||
- 2.7
|
- 2.7
|
||||||
- 3.4
|
- 3.4
|
||||||
- 3.5
|
- 3.5
|
||||||
|
- 3.6
|
||||||
|
|
||||||
install: pip install tox-travis codecov
|
install: pip install tox-travis codecov
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -1,9 +1,23 @@
|
|||||||
Change Log
|
Change Log
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
**0.9** (2017-03-02)
|
||||||
|
|
||||||
|
- allow the telegram specification to optionally be autodetected
|
||||||
|
|
||||||
|
**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)
|
**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.
|
- 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:**
|
**IMPORTANT: this release has the following backwards incompatible changes:**
|
||||||
|
|
||||||
|
|||||||
+96
-12
@@ -8,24 +8,26 @@ DSMR Parser
|
|||||||
:target: https://travis-ci.org/ndokter/dsmr_parser
|
:target: https://travis-ci.org/ndokter/dsmr_parser
|
||||||
|
|
||||||
A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It
|
A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It
|
||||||
also includes a serial client to directly read and parse smart meter data.
|
also includes client implementation to directly read and parse smart meter data.
|
||||||
|
|
||||||
|
|
||||||
Features
|
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
|
.. code-block:: python
|
||||||
|
|
||||||
from dsmr_parser import telegram_specifications
|
from dsmr_parser import telegram_specifications
|
||||||
from dsmr_parser import obis_references
|
|
||||||
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V4
|
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V4
|
||||||
|
|
||||||
serial_reader = SerialReader(
|
serial_reader = SerialReader(
|
||||||
@@ -35,23 +37,105 @@ Using the serial reader to connect to your smart meter and parse it's telegrams:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for telegram in serial_reader.read():
|
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
|
||||||
|
|
||||||
|
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 object
|
||||||
|
---------------
|
||||||
|
|
||||||
|
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 obis_references
|
||||||
|
|
||||||
# The telegram message timestamp.
|
# The telegram message timestamp.
|
||||||
message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP]
|
message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP]
|
||||||
|
|
||||||
# Using the active tariff to determine the electricity being used and
|
# Using the active tariff to determine the electricity being used and
|
||||||
# delivered for the right tariff.
|
# delivered for the right tariff.
|
||||||
tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF]
|
active_tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF]
|
||||||
tariff = int(tariff.value)
|
active_tariff = int(tariff.value)
|
||||||
|
|
||||||
electricity_used_total \
|
electricity_used_total = telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[active_tariff - 1]]
|
||||||
= telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[tariff - 1]]
|
electricity_delivered_total = telegram[obis_references.ELECTRICITY_DELIVERED_TARIFF_ALL[active_tariff - 1]]
|
||||||
electricity_delivered_total = \
|
|
||||||
telegram[obis_referencesELECTRICITY_DELIVERED_TARIFF_ALL[tariff - 1]]
|
|
||||||
|
|
||||||
gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING]
|
gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING]
|
||||||
|
|
||||||
# See dsmr_reader.obis_references for all readable telegram values.
|
# See dsmr_reader.obis_references for all readable telegram values.
|
||||||
|
# Note that the avilable values differ per DSMR version.
|
||||||
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from serial_asyncio import create_serial_connection
|
|||||||
from dsmr_parser import telegram_specifications
|
from dsmr_parser import telegram_specifications
|
||||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||||
from dsmr_parser.exceptions import ParseError
|
from dsmr_parser.exceptions import ParseError
|
||||||
from dsmr_parser.parsers import TelegramParserV2_2, TelegramParserV4
|
from dsmr_parser.parsers import TelegramParser
|
||||||
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
|
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
|
||||||
SERIAL_SETTINGS_V4
|
SERIAL_SETTINGS_V4
|
||||||
|
|
||||||
@@ -18,18 +18,16 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None):
|
|||||||
"""Creates a DSMR asyncio protocol."""
|
"""Creates a DSMR asyncio protocol."""
|
||||||
|
|
||||||
if dsmr_version == '2.2':
|
if dsmr_version == '2.2':
|
||||||
specifications = telegram_specifications.V2_2
|
specification = telegram_specifications.V2_2
|
||||||
telegram_parser = TelegramParserV2_2
|
|
||||||
serial_settings = SERIAL_SETTINGS_V2_2
|
serial_settings = SERIAL_SETTINGS_V2_2
|
||||||
elif dsmr_version == '4':
|
elif dsmr_version == '4':
|
||||||
specifications = telegram_specifications.V4
|
specification = telegram_specifications.V4
|
||||||
telegram_parser = TelegramParserV4
|
|
||||||
serial_settings = SERIAL_SETTINGS_V4
|
serial_settings = SERIAL_SETTINGS_V4
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("No telegram parser found for version: %s",
|
raise NotImplementedError("No telegram parser found for version: %s",
|
||||||
dsmr_version)
|
dsmr_version)
|
||||||
|
|
||||||
protocol = partial(DSMRProtocol, loop, telegram_parser(specifications),
|
protocol = partial(DSMRProtocol, loop, TelegramParser(specification),
|
||||||
telegram_callback=telegram_callback)
|
telegram_callback=telegram_callback)
|
||||||
|
|
||||||
return protocol, serial_settings
|
return protocol, serial_settings
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import serial_asyncio
|
|||||||
|
|
||||||
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
|
||||||
from dsmr_parser.exceptions import ParseError
|
from dsmr_parser.exceptions import ParseError
|
||||||
from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2, \
|
from dsmr_parser.parsers import TelegramParser
|
||||||
TelegramParserV4
|
|
||||||
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
|
|
||||||
SERIAL_SETTINGS_V4
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -21,14 +18,7 @@ class SerialReader(object):
|
|||||||
self.serial_settings = serial_settings
|
self.serial_settings = serial_settings
|
||||||
self.serial_settings[self.PORT_KEY] = device
|
self.serial_settings[self.PORT_KEY] = device
|
||||||
|
|
||||||
if serial_settings is SERIAL_SETTINGS_V2_2:
|
self.telegram_parser = TelegramParser(telegram_specification)
|
||||||
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)
|
|
||||||
self.telegram_buffer = TelegramBuffer()
|
self.telegram_buffer = TelegramBuffer()
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ class ParseError(Exception):
|
|||||||
|
|
||||||
class InvalidChecksumError(ParseError):
|
class InvalidChecksumError(ParseError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramSpecificationMatchError(ParseError):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -1,36 +1,48 @@
|
|||||||
P1_MESSAGE_HEADER = r'1-3:0\.2\.8'
|
"""
|
||||||
P1_MESSAGE_TIMESTAMP = r'0-0:1\.0\.0'
|
Contains the signatures of each telegram line.
|
||||||
ELECTRICITY_USED_TARIFF_1 = r'1-0:1\.8\.1'
|
|
||||||
ELECTRICITY_USED_TARIFF_2 = r'1-0:1\.8\.2'
|
Previously contained the channel + obis reference signatures, but has been
|
||||||
ELECTRICITY_DELIVERED_TARIFF_1 = r'1-0:2\.8\.1'
|
refactored to full line signatures to maintain backwards compatibility.
|
||||||
ELECTRICITY_DELIVERED_TARIFF_2 = r'1-0:2\.8\.2'
|
Might be refactored in a backwards incompatible way as soon as proper telegram
|
||||||
ELECTRICITY_ACTIVE_TARIFF = r'0-0:96\.14\.0'
|
objects are introduced.
|
||||||
EQUIPMENT_IDENTIFIER = r'0-0:96\.1\.1'
|
"""
|
||||||
CURRENT_ELECTRICITY_USAGE = r'1-0:1\.7\.0'
|
P1_MESSAGE_HEADER = r'\d-\d:0\.2\.8.+?\r\n'
|
||||||
CURRENT_ELECTRICITY_DELIVERY = r'1-0:2\.7\.0'
|
P1_MESSAGE_TIMESTAMP = r'\d-\d:1\.0\.0.+?\r\n'
|
||||||
LONG_POWER_FAILURE_COUNT = r'96\.7\.9'
|
ELECTRICITY_USED_TARIFF_1 = r'\d-\d:1\.8\.1.+?\r\n'
|
||||||
POWER_EVENT_FAILURE_LOG = r'99\.97\.0'
|
ELECTRICITY_USED_TARIFF_2 = r'\d-\d:1\.8\.2.+?\r\n'
|
||||||
VOLTAGE_SAG_L1_COUNT = r'1-0:32\.32\.0'
|
ELECTRICITY_DELIVERED_TARIFF_1 = r'\d-\d:2\.8\.1.+?\r\n'
|
||||||
VOLTAGE_SAG_L2_COUNT = r'1-0:52\.32\.0'
|
ELECTRICITY_DELIVERED_TARIFF_2 = r'\d-\d:2\.8\.2.+?\r\n'
|
||||||
VOLTAGE_SAG_L3_COUNT = r'1-0:72\.32\.0'
|
ELECTRICITY_ACTIVE_TARIFF = r'\d-\d:96\.14\.0.+?\r\n'
|
||||||
VOLTAGE_SWELL_L1_COUNT = r'1-0:32\.36\.0'
|
EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n'
|
||||||
VOLTAGE_SWELL_L2_COUNT = r'1-0:52\.36\.0'
|
CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n'
|
||||||
VOLTAGE_SWELL_L3_COUNT = r'1-0:72\.36\.0'
|
CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n'
|
||||||
TEXT_MESSAGE_CODE = r'0-0:96\.13\.1'
|
LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n'
|
||||||
TEXT_MESSAGE = r'0-0:96\.13\.0'
|
POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n'
|
||||||
DEVICE_TYPE = r'0-\d:24\.1\.0'
|
VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n'
|
||||||
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'1-0:21\.7\.0'
|
VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n'
|
||||||
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'1-0:41\.7\.0'
|
VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n'
|
||||||
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'1-0:61\.7\.0'
|
VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n'
|
||||||
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'1-0:22\.7\.0'
|
VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n'
|
||||||
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'1-0:42\.7\.0'
|
VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n'
|
||||||
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'1-0:62\.7\.0'
|
TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n'
|
||||||
EQUIPMENT_IDENTIFIER_GAS = r'0-\d:96\.1\.0'
|
TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n'
|
||||||
HOURLY_GAS_METER_READING = r'0-1:24\.2\.1'
|
DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n'
|
||||||
GAS_METER_READING = r'0-\d:24\.3\.0'
|
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n'
|
||||||
ACTUAL_TRESHOLD_ELECTRICITY = r'0-0:17\.0\.0'
|
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n'
|
||||||
ACTUAL_SWITCH_POSITION = r'0-0:96\.3\.10'
|
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n'
|
||||||
VALVE_POSITION_GAS = r'0-\d:24\.4\.0'
|
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_ALL = (
|
||||||
ELECTRICITY_USED_TARIFF_1,
|
ELECTRICITY_USED_TARIFF_1,
|
||||||
|
|||||||
+16
-17
@@ -1,4 +1,7 @@
|
|||||||
class DSMRObject(object):
|
class DSMRObject(object):
|
||||||
|
"""
|
||||||
|
Represents all data from a single telegram line.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, values):
|
def __init__(self, values):
|
||||||
self.values = values
|
self.values = values
|
||||||
@@ -12,26 +15,23 @@ class MBusObject(DSMRObject):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
|
# 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]['value']
|
return self.values[1]['value']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit(self):
|
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.
|
||||||
class MBusObjectV2_2(DSMRObject):
|
if len(self.values) != 2: # v2
|
||||||
|
|
||||||
@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']
|
return self.values[4]['value']
|
||||||
|
else:
|
||||||
|
return self.values[1]['unit']
|
||||||
|
|
||||||
|
|
||||||
class CosemObject(DSMRObject):
|
class CosemObject(DSMRObject):
|
||||||
@@ -46,5 +46,4 @@ class CosemObject(DSMRObject):
|
|||||||
|
|
||||||
|
|
||||||
class ProfileGeneric(DSMRObject):
|
class ProfileGeneric(DSMRObject):
|
||||||
pass
|
pass # TODO implement
|
||||||
# TODO implement
|
|
||||||
|
|||||||
+69
-70
@@ -3,69 +3,69 @@ import re
|
|||||||
|
|
||||||
from PyCRC.CRC16 import CRC16
|
from PyCRC.CRC16 import CRC16
|
||||||
|
|
||||||
from dsmr_parser.objects import MBusObject, MBusObjectV2_2, CosemObject
|
from dsmr_parser.objects import MBusObject, CosemObject
|
||||||
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
|
from dsmr_parser.exceptions import ParseError, InvalidChecksumError, \
|
||||||
from dsmr_parser.obis_references import GAS_METER_READING
|
TelegramSpecificationMatchError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TelegramParser(object):
|
class TelegramParser(object):
|
||||||
|
|
||||||
def __init__(self, telegram_specification):
|
def __init__(self, telegram_specification=None, apply_checksum_validation=True):
|
||||||
"""
|
"""
|
||||||
:param telegram_specification: determines how the telegram is parsed
|
:param telegram_specification: determines how the telegram is parsed.
|
||||||
|
Will attempt to autodetect if omitted.
|
||||||
|
:param apply_checksum_validation: validate checksum if applicable for
|
||||||
|
telegram DSMR version (v4 and up).
|
||||||
:type telegram_specification: dict
|
:type telegram_specification: dict
|
||||||
"""
|
"""
|
||||||
self.telegram_specification = telegram_specification
|
self.telegram_specification = telegram_specification
|
||||||
|
self.apply_checksum_validation = apply_checksum_validation
|
||||||
|
|
||||||
def _find_line_parser(self, line_value):
|
def parse(self, telegram_data):
|
||||||
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, telegram):
|
|
||||||
"""
|
"""
|
||||||
Parse telegram from string to dict.
|
Parse telegram from string to dict.
|
||||||
|
|
||||||
The telegram str type makes python 2.x integration easier.
|
The telegram str type makes python 2.x integration easier.
|
||||||
|
|
||||||
:param str telegram: full telegram from start ('/') to checksum
|
:param str telegram_data: full telegram from start ('/') to checksum
|
||||||
('!ABCD') including line endings inbetween the telegram's lines
|
('!ABCD') including line endings in between the telegram's lines
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
:returns: Shortened example:
|
:returns: Shortened example:
|
||||||
{
|
{
|
||||||
..
|
..
|
||||||
r'0-0:96\.1\.1': <CosemObject>, # EQUIPMENT_IDENTIFIER
|
r'\d-\d:96\.1\.1.+?\r\n': <CosemObject>, # EQUIPMENT_IDENTIFIER
|
||||||
r'1-0:1\.8\.1': <CosemObject>, # ELECTRICITY_USED_TARIFF_1
|
r'\d-\d:1\.8\.1.+?\r\n': <CosemObject>, # ELECTRICITY_USED_TARIFF_1
|
||||||
r'0-\d:24\.3\.0': <MBusObject>, # GAS_METER_READING
|
r'\d-\d:24\.3\.0.+?\r\n.+?\r\n': <MBusObject>, # GAS_METER_READING
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
|
:raises ParseError:
|
||||||
"""
|
"""
|
||||||
telegram_lines = telegram.splitlines()
|
if not self.telegram_specification:
|
||||||
parsed_lines = map(self.parse_line, telegram_lines)
|
self.telegram_specification = \
|
||||||
|
match_telegram_specification(telegram_data)
|
||||||
|
|
||||||
return {obis_reference: dsmr_object
|
if self.apply_checksum_validation \
|
||||||
for obis_reference, dsmr_object in parsed_lines}
|
and self.telegram_specification['checksum_support']:
|
||||||
|
self.validate_checksum(telegram_data)
|
||||||
|
|
||||||
def parse_line(self, line):
|
telegram = {}
|
||||||
logger.debug("Parsing line '%s'", line)
|
|
||||||
|
|
||||||
obis_reference, parser = self._find_line_parser(line)
|
for signature, parser in self.telegram_specification['objects'].items():
|
||||||
|
match = re.search(signature, telegram_data, re.DOTALL)
|
||||||
|
|
||||||
if not obis_reference:
|
# All telegram specification lines/signatures are expected to be
|
||||||
logger.debug("No line class found for: '%s'", line)
|
# present.
|
||||||
return None, None
|
if not match:
|
||||||
|
raise ParseError('Telegram specification does not match '
|
||||||
|
'telegram data')
|
||||||
|
telegram[signature] = parser.parse(match.group(0))
|
||||||
|
|
||||||
return obis_reference, parser.parse(line)
|
return telegram
|
||||||
|
|
||||||
|
|
||||||
class TelegramParserV4(TelegramParser):
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_telegram_checksum(telegram):
|
def validate_checksum(telegram):
|
||||||
"""
|
"""
|
||||||
:param str telegram:
|
:param str telegram:
|
||||||
:raises ParseError:
|
:raises ParseError:
|
||||||
@@ -97,47 +97,41 @@ class TelegramParserV4(TelegramParser):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse(self, telegram):
|
|
||||||
|
def match_telegram_specification(telegram_data):
|
||||||
"""
|
"""
|
||||||
:param str telegram:
|
Find telegram specification that matches the telegram data by trying all
|
||||||
|
specifications.
|
||||||
|
|
||||||
|
Could be further optimized to check the actual 0.2.8 OBIS reference which
|
||||||
|
is available for DSMR version 4 and up.
|
||||||
|
|
||||||
|
:param str telegram_data: full telegram from start ('/') to checksum
|
||||||
|
('!ABCD') including line endings in between the telegram's lines
|
||||||
|
:return: telegram specification
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
self.validate_telegram_checksum(telegram)
|
# Prevent circular import
|
||||||
|
from dsmr_parser import telegram_specifications
|
||||||
|
|
||||||
return super().parse(telegram)
|
for specification in telegram_specifications.ALL:
|
||||||
|
try:
|
||||||
|
TelegramParser(specification).parse(telegram_data)
|
||||||
class TelegramParserV2_2(TelegramParser):
|
except ParseError:
|
||||||
|
pass
|
||||||
def parse(self, telegram):
|
|
||||||
"""
|
|
||||||
:param str telegram:
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO fix this in the specification: telegram_specifications.V2_2
|
|
||||||
def join_lines(telegram):
|
|
||||||
"""Join lines for gas meter."""
|
|
||||||
join_next = re.compile(GAS_METER_READING)
|
|
||||||
|
|
||||||
join = None
|
|
||||||
for line_value in telegram.splitlines():
|
|
||||||
if join:
|
|
||||||
yield join + line_value
|
|
||||||
join = None
|
|
||||||
elif join_next.match(line_value):
|
|
||||||
join = line_value
|
|
||||||
else:
|
else:
|
||||||
yield line_value
|
return specification
|
||||||
|
|
||||||
# TODO temporary workaround
|
raise TelegramSpecificationMatchError(
|
||||||
lines = join_lines(telegram)
|
'Could automatically match telegram specification. Make sure the data'
|
||||||
telegram = '\r\n'.join(lines)
|
'is not corrupt. Alternatively manually specify one.'
|
||||||
|
)
|
||||||
return super().parse(telegram)
|
|
||||||
|
|
||||||
|
|
||||||
class DSMRObjectParser(object):
|
class DSMRObjectParser(object):
|
||||||
|
"""
|
||||||
|
Parses an object (can also be see as a 'line') from a telegram.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *value_formats):
|
def __init__(self, *value_formats):
|
||||||
self.value_formats = value_formats
|
self.value_formats = value_formats
|
||||||
@@ -175,11 +169,7 @@ class MBusParser(DSMRObjectParser):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def parse(self, line):
|
def parse(self, line):
|
||||||
values = self._parse(line)
|
return MBusObject(self._parse(line))
|
||||||
if len(values) == 2:
|
|
||||||
return MBusObject(values)
|
|
||||||
else:
|
|
||||||
return MBusObjectV2_2(values)
|
|
||||||
|
|
||||||
|
|
||||||
class CosemParser(DSMRObjectParser):
|
class CosemParser(DSMRObjectParser):
|
||||||
@@ -232,6 +222,15 @@ class ProfileGenericParser(DSMRObjectParser):
|
|||||||
|
|
||||||
|
|
||||||
class ValueParser(object):
|
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):
|
def __init__(self, coerce_type):
|
||||||
self.coerce_type = coerce_type
|
self.coerce_type = coerce_type
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ how the telegram lines are parsed.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
V2_2 = {
|
V2_2 = {
|
||||||
|
'checksum_support': False,
|
||||||
|
'objects': {
|
||||||
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
||||||
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||||
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||||
@@ -37,17 +39,22 @@ V2_2 = {
|
|||||||
ValueParser(str),
|
ValueParser(str),
|
||||||
ValueParser(Decimal),
|
ValueParser(Decimal),
|
||||||
),
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
V3 = V2_2
|
||||||
|
|
||||||
V4 = {
|
V4 = {
|
||||||
|
'checksum_support': True,
|
||||||
|
'objects': {
|
||||||
obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
|
obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
|
||||||
obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
|
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_1: CosemParser(ValueParser(Decimal)),
|
||||||
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||||
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||||
obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||||
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
||||||
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
|
||||||
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
||||||
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
||||||
obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
|
obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
|
||||||
@@ -68,6 +75,48 @@ V4 = {
|
|||||||
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||||
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||||
obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)),
|
obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)),
|
||||||
obis.HOURLY_GAS_METER_READING: MBusParser(ValueParser(timestamp),
|
obis.HOURLY_GAS_METER_READING: MBusParser(
|
||||||
ValueParser(Decimal))
|
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_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)),
|
||||||
|
# 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: 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)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ setup(
|
|||||||
author='Nigel Dokter',
|
author='Nigel Dokter',
|
||||||
author_email='nigeldokter@gmail.com',
|
author_email='nigeldokter@gmail.com',
|
||||||
url='https://github.com/ndokter/dsmr_parser',
|
url='https://github.com/ndokter/dsmr_parser',
|
||||||
version='0.7',
|
version='0.9',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pyserial>=3,<4',
|
'pyserial>=3,<4',
|
||||||
|
|||||||
@@ -21,6 +21,31 @@ TELEGRAM_V2_2 = (
|
|||||||
'!\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 = (
|
TELEGRAM_V4_2 = (
|
||||||
'/KFM5KAIFA-METER\r\n'
|
'/KFM5KAIFA-METER\r\n'
|
||||||
'\r\n'
|
'\r\n'
|
||||||
@@ -60,3 +85,46 @@ TELEGRAM_V4_2 = (
|
|||||||
'0-1:24.2.1(161129200000W)(00981.443*m3)\r\n'
|
'0-1:24.2.1(161129200000W)(00981.443*m3)\r\n'
|
||||||
'!6796\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'
|
||||||
|
'!87B3\r\n'
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from dsmr_parser.exceptions import TelegramSpecificationMatchError
|
||||||
|
from dsmr_parser.parsers import match_telegram_specification
|
||||||
|
from dsmr_parser import telegram_specifications
|
||||||
|
from test import example_telegrams
|
||||||
|
|
||||||
|
|
||||||
|
class MatchTelegramSpecificationTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_v2_2(self):
|
||||||
|
assert match_telegram_specification(example_telegrams.TELEGRAM_V2_2) \
|
||||||
|
== telegram_specifications.V2_2
|
||||||
|
|
||||||
|
def test_v3(self):
|
||||||
|
assert match_telegram_specification(example_telegrams.TELEGRAM_V3) \
|
||||||
|
== telegram_specifications.V3
|
||||||
|
|
||||||
|
def test_v4_2(self):
|
||||||
|
assert match_telegram_specification(example_telegrams.TELEGRAM_V4_2) \
|
||||||
|
== telegram_specifications.V4
|
||||||
|
|
||||||
|
def test_v5(self):
|
||||||
|
assert match_telegram_specification(example_telegrams.TELEGRAM_V5) \
|
||||||
|
== telegram_specifications.V5
|
||||||
|
|
||||||
|
def test_malformed_telegram(self):
|
||||||
|
with self.assertRaises(TelegramSpecificationMatchError):
|
||||||
|
match_telegram_specification(example_telegrams.TELEGRAM_V5[:-4])
|
||||||
+87
-6
@@ -1,20 +1,101 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from test.example_telegrams import TELEGRAM_V2_2
|
from decimal import Decimal
|
||||||
from dsmr_parser.parsers import TelegramParserV2_2
|
|
||||||
|
from dsmr_parser.objects import MBusObject, CosemObject
|
||||||
|
from dsmr_parser.parsers import TelegramParser
|
||||||
from dsmr_parser import telegram_specifications
|
from dsmr_parser import telegram_specifications
|
||||||
from dsmr_parser import obis_references as obis
|
from dsmr_parser import obis_references as obis
|
||||||
|
from test.example_telegrams import TELEGRAM_V2_2
|
||||||
|
|
||||||
|
|
||||||
class TelegramParserV2_2Test(unittest.TestCase):
|
class TelegramParserV2_2Test(unittest.TestCase):
|
||||||
""" Test parsing of a DSMR v2.2 telegram. """
|
""" Test parsing of a DSMR v2.2 telegram. """
|
||||||
|
|
||||||
|
def test_telegram_specification_matching(self):
|
||||||
|
parser = TelegramParser()
|
||||||
|
parser.parse(TELEGRAM_V2_2)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
parser.telegram_specification,
|
||||||
|
telegram_specifications.V2_2
|
||||||
|
)
|
||||||
|
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
parser = TelegramParserV2_2(telegram_specifications.V2_2)
|
parser = TelegramParser(telegram_specifications.V2_2)
|
||||||
result = parser.parse(TELEGRAM_V2_2)
|
result = parser.parse(TELEGRAM_V2_2)
|
||||||
|
|
||||||
assert float(result[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01
|
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
|
||||||
assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
|
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 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,107 @@
|
|||||||
|
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_telegram_specification_matching(self):
|
||||||
|
parser = TelegramParser()
|
||||||
|
parser.parse(TELEGRAM_V3)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
parser.telegram_specification,
|
||||||
|
telegram_specifications.V3
|
||||||
|
)
|
||||||
|
|
||||||
|
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')
|
||||||
+30
-21
@@ -4,39 +4,26 @@ import unittest
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from test.example_telegrams import TELEGRAM_V4_2
|
|
||||||
from dsmr_parser import obis_references as obis
|
from dsmr_parser import obis_references as obis
|
||||||
from dsmr_parser import telegram_specifications
|
from dsmr_parser import telegram_specifications
|
||||||
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
|
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
|
||||||
from dsmr_parser.objects import CosemObject, MBusObject
|
from dsmr_parser.objects import CosemObject, MBusObject
|
||||||
from dsmr_parser.parsers import TelegramParser, TelegramParserV4
|
from dsmr_parser.parsers import TelegramParser
|
||||||
|
from test.example_telegrams import TELEGRAM_V4_2
|
||||||
|
|
||||||
|
|
||||||
class TelegramParserV4_2Test(unittest.TestCase):
|
class TelegramParserV4_2Test(unittest.TestCase):
|
||||||
""" Test parsing of a DSMR v4.2 telegram. """
|
""" Test parsing of a DSMR v4.2 telegram. """
|
||||||
|
|
||||||
def test_valid(self):
|
def test_telegram_specification_matching(self):
|
||||||
# No exception is raised.
|
parser = TelegramParser()
|
||||||
TelegramParserV4.validate_telegram_checksum(TELEGRAM_V4_2)
|
parser.parse(TELEGRAM_V4_2)
|
||||||
|
|
||||||
def test_invalid(self):
|
self.assertEqual(
|
||||||
# Remove the electricty used data value. This causes the checksum to
|
parser.telegram_specification,
|
||||||
# not match anymore.
|
telegram_specifications.V4
|
||||||
corrupted_telegram = TELEGRAM_V4_2.replace(
|
|
||||||
'1-0:1.8.1(001581.123*kWh)\r\n',
|
|
||||||
''
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaises(InvalidChecksumError):
|
|
||||||
TelegramParserV4.validate_telegram_checksum(corrupted_telegram)
|
|
||||||
|
|
||||||
def test_missing_checksum(self):
|
|
||||||
# Remove the checksum value causing a ParseError.
|
|
||||||
corrupted_telegram = TELEGRAM_V4_2.replace('!6796\r\n', '')
|
|
||||||
|
|
||||||
with self.assertRaises(ParseError):
|
|
||||||
TelegramParserV4.validate_telegram_checksum(corrupted_telegram)
|
|
||||||
|
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
parser = TelegramParser(telegram_specifications.V4)
|
parser = TelegramParser(telegram_specifications.V4)
|
||||||
result = parser.parse(TELEGRAM_V4_2)
|
result = parser.parse(TELEGRAM_V4_2)
|
||||||
@@ -219,3 +206,25 @@ class TelegramParserV4_2Test(unittest.TestCase):
|
|||||||
|
|
||||||
# VALVE_POSITION_GAS (0-x:24.4.0)
|
# VALVE_POSITION_GAS (0-x:24.4.0)
|
||||||
# TODO to be implemented
|
# 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,214 @@
|
|||||||
|
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_telegram_specification_matching(self):
|
||||||
|
parser = TelegramParser()
|
||||||
|
parser.parse(TELEGRAM_V5)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
parser.telegram_specification,
|
||||||
|
telegram_specifications.V5
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# TEXT_MESSAGE (0-0:96.13.0)
|
||||||
|
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||||
|
assert result[obis.TEXT_MESSAGE].unit is None
|
||||||
|
assert result[obis.TEXT_MESSAGE].value is None
|
||||||
|
|
||||||
|
# DEVICE_TYPE (0-x:24.1.0)
|
||||||
|
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
|
||||||
|
assert result[obis.DEVICE_TYPE].unit is None
|
||||||
|
assert isinstance(result[obis.DEVICE_TYPE].value, int)
|
||||||
|
assert result[obis.DEVICE_TYPE].value == 3
|
||||||
|
|
||||||
|
# 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('!87B3\r\n', '')
|
||||||
|
|
||||||
|
with self.assertRaises(ParseError):
|
||||||
|
TelegramParser.validate_checksum(corrupted_telegram)
|
||||||
@@ -4,7 +4,7 @@ import unittest
|
|||||||
|
|
||||||
from dsmr_parser import obis_references as obis
|
from dsmr_parser import obis_references as obis
|
||||||
from dsmr_parser import telegram_specifications
|
from dsmr_parser import telegram_specifications
|
||||||
from dsmr_parser.parsers import TelegramParserV2_2
|
from dsmr_parser.parsers import TelegramParser
|
||||||
from dsmr_parser.clients.protocol import DSMRProtocol
|
from dsmr_parser.clients.protocol import DSMRProtocol
|
||||||
|
|
||||||
|
|
||||||
@@ -35,10 +35,7 @@ TELEGRAM_V2_2 = (
|
|||||||
class ProtocolTest(unittest.TestCase):
|
class ProtocolTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
parser = TelegramParserV2_2
|
telegram_parser = TelegramParser(telegram_specifications.V2_2)
|
||||||
specification = telegram_specifications.V2_2
|
|
||||||
|
|
||||||
telegram_parser = parser(specification)
|
|
||||||
self.protocol = DSMRProtocol(None, telegram_parser,
|
self.protocol = DSMRProtocol(None, telegram_parser,
|
||||||
telegram_callback=Mock())
|
telegram_callback=Mock())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user