Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b0a85e84b | ||
|
|
1dca6ab396 | ||
|
|
efc09df71f | ||
|
|
de5c884d8d | ||
|
|
d2f57a8926 | ||
|
|
3d64fea247 | ||
|
|
46860e04c1 | ||
|
|
148bdabc12 | ||
|
|
9d20bb8ad5 | ||
|
|
24ab9aa712 |
@@ -1,6 +1,10 @@
|
||||
Change Log
|
||||
----------
|
||||
|
||||
**0.9** (2017-05-12)
|
||||
|
||||
- added DSMR v5 serial settings
|
||||
|
||||
**0.8** (2017-01-26)
|
||||
|
||||
- added support for DSMR v3
|
||||
|
||||
+111
-26
@@ -8,7 +8,7 @@ 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
|
||||
@@ -17,41 +17,126 @@ Features
|
||||
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 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 telegram_specifications
|
||||
from dsmr_parser import obis_references
|
||||
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
|
||||
)
|
||||
# The telegram message timestamp.
|
||||
message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP]
|
||||
|
||||
for telegram in serial_reader.read():
|
||||
# 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)
|
||||
|
||||
# The telegram message timestamp.
|
||||
message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP]
|
||||
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]]
|
||||
|
||||
# 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)
|
||||
gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING]
|
||||
|
||||
electricity_used_total \
|
||||
= telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[tariff - 1]]
|
||||
electricity_delivered_total = \
|
||||
telegram[obis_referencesELECTRICITY_DELIVERED_TARIFF_ALL[tariff - 1]]
|
||||
|
||||
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
|
||||
|
||||
@@ -23,6 +23,9 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None):
|
||||
elif dsmr_version == '4':
|
||||
specification = telegram_specifications.V4
|
||||
serial_settings = SERIAL_SETTINGS_V4
|
||||
elif dsmr_version == '5':
|
||||
specification = telegram_specifications.V5
|
||||
serial_settings = SERIAL_SETTINGS_V4
|
||||
else:
|
||||
raise NotImplementedError("No telegram parser found for version: %s",
|
||||
dsmr_version)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
class DSMRObject(object):
|
||||
"""
|
||||
Represents all data from a single telegram line.
|
||||
"""
|
||||
|
||||
def __init__(self, values):
|
||||
self.values = values
|
||||
|
||||
+33
-4
@@ -27,8 +27,8 @@ class TelegramParser(object):
|
||||
|
||||
The telegram str type makes python 2.x integration easier.
|
||||
|
||||
:param str telegram: full telegram from start ('/') to checksum
|
||||
('!ABCD') including line endings inbetween the telegram's lines
|
||||
: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:
|
||||
{
|
||||
@@ -51,8 +51,12 @@ class TelegramParser(object):
|
||||
for signature, parser in self.telegram_specification['objects'].items():
|
||||
match = re.search(signature, telegram_data, re.DOTALL)
|
||||
|
||||
if match:
|
||||
telegram[signature] = parser.parse(match.group(0))
|
||||
# All telegram specification lines/signatures are expected to be
|
||||
# present.
|
||||
if not match:
|
||||
raise ParseError('Telegram specification does not match '
|
||||
'telegram data')
|
||||
telegram[signature] = parser.parse(match.group(0))
|
||||
|
||||
return telegram
|
||||
|
||||
@@ -90,6 +94,31 @@ class TelegramParser(object):
|
||||
)
|
||||
|
||||
|
||||
def match_telegram_specification(telegram_data):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
# Prevent circular import
|
||||
from dsmr_parser import telegram_specifications
|
||||
|
||||
for specification in telegram_specifications.ALL:
|
||||
try:
|
||||
TelegramParser(specification).parse(telegram_data)
|
||||
except ParseError:
|
||||
pass
|
||||
else:
|
||||
return specification
|
||||
|
||||
|
||||
class DSMRObjectParser(object):
|
||||
"""
|
||||
Parses an object (can also be see as a 'line') from a telegram.
|
||||
|
||||
@@ -42,34 +42,7 @@ V2_2 = {
|
||||
}
|
||||
}
|
||||
|
||||
V3 = {
|
||||
'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(int),
|
||||
ValueParser(int),
|
||||
ValueParser(int),
|
||||
ValueParser(str),
|
||||
ValueParser(Decimal),
|
||||
),
|
||||
}
|
||||
}
|
||||
V3 = V2_2
|
||||
|
||||
V4 = {
|
||||
'checksum_support': True,
|
||||
@@ -145,3 +118,5 @@ V5 = {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ALL = (V2_2, V3, V4, V5)
|
||||
|
||||
@@ -6,7 +6,7 @@ setup(
|
||||
author='Nigel Dokter',
|
||||
author_email='nigeldokter@gmail.com',
|
||||
url='https://github.com/ndokter/dsmr_parser',
|
||||
version='0.8',
|
||||
version='0.9',
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
'pyserial>=3,<4',
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import unittest
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user