Compare commits

...
61 Commits
Author SHA1 Message Date
Nigel c04b0a5add Updated version number 2019-03-04 20:31:51 +01:00
Nigel Dokter ed2290a597 Merge pull request #33 from mleenaerts/patch-1
Fix DSMR v5.0 serial settings which were not used
2019-01-22 19:27:56 +01:00
Mark Leenaerts 85c67464a1 Fix DSMR v5.0 serial settings which were not used
While analysing some CRC check errors I encounter within the home-assistant plugin (which uses this component) I encountered this oversight in the code.
2019-01-19 16:11:12 +01:00
Nigel Dokter 48783acc00 Update CHANGELOG.rst 2018-09-23 13:01:22 +02:00
Nigel Dokter 41f6a7ac16 Update CHANGELOG.rst 2018-09-23 13:01:04 +02:00
Nigel Dokter 59811dbf9f Update setup.py 2018-09-23 12:59:02 +02:00
Nigel Dokter 9b09969763 Merge pull request #31 from tneele/master
Add serial settings for DSMR v5.0
2018-09-23 12:50:01 +02:00
Nigel Dokter 64f5dc2e87 Merge pull request #32 from bossjl/master
Lux-creos-obis-1.8.0
2018-09-23 12:49:42 +02:00
bossjl 887dd3a2aa Lux-creos-obis-1.8.0 2018-09-21 18:37:32 +02:00
Thomas Neele ad6ab304f5 Added serial settings for DSMR v5.0 2018-09-15 21:23:22 +02:00
Nigel Dokter 8582395017 Merge pull request #30 from jk-5/sagemcom-t210-d
Added missing values for 3-phase sagemcom TD210-D meter
2018-09-12 23:45:29 +02:00
jk-5 3327c78c0e Updated unittests 2018-09-06 15:49:11 +02:00
jk-5 472c54968e Corrected meter types 2018-09-06 15:40:02 +02:00
jk-5 d534b1d8b0 Added missing values for 3-phase sagemcom TD210-D meter 2018-09-06 14:53:30 +02:00
Nigel Dokter d94bc8de03 updated version 2017-09-18 12:02:36 +02:00
Nigel Dokter 41381f1705 Merge pull request #26 from vavdb/patch-1
Optional NUL after checksum
2017-09-18 11:55:31 +02:00
Vincent van den Braken e3203a5334 Optional NUL after checksum
My smart meter returns 00 0D 0A after the checksum, not just 0D 0A.
2017-09-15 13:48:10 +02:00
Nigel Dokter c78ebe3e2d fixed import errors 2017-06-05 21:06:48 +02:00
Nigel Dokter d6e28db116 log checksum errors as warning; dont force full telegram signatures; removed unused code for automatic telegram version detection; 2017-06-05 21:02:59 +02:00
Nigel Dokter 5f1afeb1f8 Merge pull request #25 from AlexMekkering/optional_telegram_lines
Support optional telegram signatures
2017-06-05 20:45:38 +02:00
Alex Mekkering dcb59fddb1 Support optional telegram signatures 2017-06-05 09:07:47 +02:00
Nigel Dokter 9b0a85e84b updated version 2017-05-12 21:37:54 +02:00
Nigel Dokter 1dca6ab396 Merge pull request #23 from aequitas/dsmr5
Add DSMR5 option to protocol.
2017-05-12 21:34:06 +02:00
Johan Bloemberg efc09df71f Add DSMR5 option to protocol. 2017-05-12 20:30:53 +02:00
Nigel Dokter de5c884d8d Small comment improvement 2017-02-27 20:44:18 +01:00
Nigel Dokter d2f57a8926 fixed pep8 2017-02-27 20:35:19 +01:00
Nigel Dokter 3d64fea247 Updated README 2017-02-27 20:26:15 +01:00
Nigel Dokter 46860e04c1 Formatting 2017-02-03 22:42:31 +01:00
Nigel Dokter 148bdabc12 Updated docs 2017-02-03 22:40:39 +01:00
Nigel Dokter 9d20bb8ad5 experimenting with version detection 2017-01-28 17:01:33 +01:00
Nigel Dokter 24ab9aa712 experimenting with version detection 2017-01-28 17:01:02 +01:00
Nigel Dokter 0c40070752 fixed changelog date 2017-01-26 21:18:56 +01:00
Nigel Dokter 6c8a9dcbdb added python 3.6 support 2017-01-26 19:06:03 +01:00
Nigel Dokter c1a6b930c8 try python 3.6 support 2017-01-26 19:04:01 +01:00
Nigel Dokter 9e74c4c23c adjusting supported python versions 2017-01-26 19:02:15 +01:00
Nigel Dokter b4a520c8b4 updated readme 2017-01-26 19:00:31 +01:00
Nigel Dokter a88dfe1a41 added DSMR v3 specification; updated changelog; 2017-01-26 18:50:30 +01:00
Nigel Dokter c4dcc73191 added DSMR v3 specification 2017-01-26 18:02:21 +01:00
Nigel Dokter c4caf54576 pep8 2017-01-25 19:34:38 +01:00
Nigel Dokter 8a868ce826 pep8 2017-01-25 19:33:42 +01:00
Nigel Dokter 9623f3b092 added unit test for DSMR v5 parsing 2017-01-25 19:32:30 +01:00
Nigel Dokter adcfdfe2ae fixed v2 specification typo; fixed wrongly edited obis reference; 2017-01-22 16:42:02 +01:00
Nigel Dokter 45ee8dbb32 added basic config for DSMR v5 specification; added DSMR v5 example telegram for testing; 2017-01-22 16:39:16 +01:00
Nigel Dokter 7a4c204850 added code comments 2017-01-21 10:42:17 +01:00
Nigel Dokter 45f5fe2c36 define checksum support in telegram specification; moved telegram v2 exception temporarily from parser to MBUSObject; 2017-01-21 10:33:17 +01:00
Nigel Dokter 07634abed1 Progress on removing TelegramParserV2_2 and TelegramParserV4 in favor of a generic TelegramParser 2017-01-20 23:02:19 +01:00
Nigel Dokter e2e4bb36a2 updated changelog 2017-01-14 20:10:42 +01:00
Nigel Dokter 4eeefec426 update version number to 0.7 2017-01-11 17:44:48 +01:00
Nigel Dokter f3d8311ac2 updated readme and changelog for upcoming breaking API changes; skip pylama check for unused imports in clients module; 2017-01-11 17:40:25 +01:00
Nigel Dokter 9b488e74f8 renamed serial.py to serial_.py 2017-01-10 20:57:50 +01:00
Nigel Dokter fadf206715 moved serial clients to own package 2017-01-10 20:09:33 +01:00
Nigel Dokter e97ab7c7ea import issues 2017-01-09 21:47:51 +01:00
Nigel Dokter 759e0a0d92 removed absolute imports back to relative due to serial.py importing from serial 2017-01-09 21:31:08 +01:00
Nigel Dokter 21334e5a0a changed relative imports to absolute; renamed TelegramBuffer.put to TelegramBuffer.append; 2017-01-09 20:15:55 +01:00
Nigel Dokter 11672d0512 pep8 2017-01-08 11:44:20 +01:00
Nigel Dokter 87a5a2d2fa dev progress 2017-01-08 11:28:15 +01:00
Nigel Dokter 663024239f dev progress 2017-01-08 11:24:04 +01:00
Nigel Dokter 0e7819b535 dev progress 2017-01-07 22:29:02 +01:00
Nigel Dokter 60317a0dc5 dev progress 2017-01-07 21:26:21 +01:00
Nigel Dokter d990a316ad finishing implementation of TelegramBuffer 2017-01-07 11:25:43 +01:00
Nigel Dokter f10032f701 refactored TelegramParser.parse to accept a str instead of list 2017-01-05 21:24:41 +01:00
23 changed files with 1327 additions and 524 deletions
+1
View File
@@ -4,6 +4,7 @@ python:
- 2.7
- 3.4
- 3.5
- 3.6
install: pip install tox-travis codecov
+43
View File
@@ -1,6 +1,49 @@
Change Log
----------
**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)
- bugix: 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>`_)
+112 -27
View File
@@ -8,50 +8,135 @@ 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 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.serial 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
+2 -2
View File
@@ -1,9 +1,9 @@
from functools import partial
import argparse
import asyncio
import logging
from functools import partial
from .protocol import create_dsmr_reader, create_tcp_dsmr_reader
from dsmr_parser.clients import create_dsmr_reader, create_tcp_dsmr_reader
def console():
+5
View File
@@ -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
@@ -1,31 +1,36 @@
"""Asyncio protocol implementation for handling telegrams."""
from functools import partial
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)
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':
specifications = telegram_specifications.V2_2
telegram_parser = TelegramParserV2_2
specification = telegram_specifications.V2_2
serial_settings = SERIAL_SETTINGS_V2_2
elif dsmr_version == '4':
specifications = telegram_specifications.V4
telegram_parser = TelegramParserV4
specification = telegram_specifications.V4
serial_settings = SERIAL_SETTINGS_V4
elif dsmr_version == '5':
specification = telegram_specifications.V5
serial_settings = SERIAL_SETTINGS_V5
else:
raise NotImplementedError("No telegram parser found for version: %s",
dsmr_version)
protocol = partial(DSMRProtocol, loop, telegram_parser(specifications),
protocol = partial(DSMRProtocol, loop, TelegramParser(specification),
telegram_callback=telegram_callback)
return protocol, serial_settings
@@ -63,10 +68,8 @@ class DSMRProtocol(asyncio.Protocol):
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 = ''
self.telegram_buffer = TelegramBuffer()
# keep a lock until the connection is closed
self._closed = asyncio.Event()
@@ -77,32 +80,12 @@ class DSMRProtocol(asyncio.Protocol):
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()
data = data.decode('ascii')
self.log.debug('received data: %s', data)
self.telegram_buffer.append(data)
def handle_lines(self):
"""Assemble incoming data into single lines."""
crlf = "\r\n"
while crlf in self.buffer:
line, self.buffer = self.buffer.split(crlf, 1)
self.log.debug('got line: %s', line)
line += crlf # add the trailing crlf again
# 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 = []
for telegram in self.telegram_buffer.get_all():
self.handle_telegram(telegram)
def connection_lost(self, exc):
"""Stop when connection is lost."""
@@ -116,8 +99,14 @@ class DSMRProtocol(asyncio.Protocol):
"""Send off parsed telegram to handling callback."""
self.log.debug('got telegram: %s', telegram)
if self.telegram_callback:
self.telegram_callback(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):
+78
View File
@@ -0,0 +1,78 @@
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
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()
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.readline()
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)
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)
+32
View File
@@ -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
}
+57
View File
@@ -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
)
+53 -33
View File
@@ -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,
+18 -19
View File
@@ -1,4 +1,7 @@
class DSMRObject(object):
"""
Represents all data from a single telegram line.
"""
def __init__(self, values):
self.values = values
@@ -12,26 +15,23 @@ 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[5]['value']
else:
return self.values[1]['value']
@property
def unit(self):
return self.values[1]['unit']
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']
# 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[4]['value']
else:
return self.values[1]['unit']
class CosemObject(DSMRObject):
@@ -46,5 +46,4 @@ class CosemObject(DSMRObject):
class ProfileGeneric(DSMRObject):
pass
# TODO implement
pass # TODO implement
+67 -81
View File
@@ -3,71 +3,75 @@ import re
from PyCRC.CRC16 import CRC16
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
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
logger = logging.getLogger(__name__)
class TelegramParser(object):
def __init__(self, telegram_specification):
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(
@@ -76,8 +80,7 @@ class TelegramParserV4(TelegramParser):
)
calculated_crc = CRC16().calculate(checksum_contents.group(0))
expected_crc = checksum_hex.group(0)
expected_crc = int(expected_crc, base=16)
expected_crc = int(checksum_hex.group(0), base=16)
if calculated_crc != expected_crc:
raise InvalidChecksumError(
@@ -88,34 +91,11 @@ class TelegramParserV4(TelegramParser):
)
)
def parse(self, line_values):
self.validate_telegram_checksum(line_values)
return super().parse(line_values)
class TelegramParserV2_2(TelegramParser):
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))
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
@@ -153,11 +133,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 +149,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):
@@ -210,6 +187,15 @@ class ProfileGenericParser(DSMRObjectParser):
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
-138
View File
@@ -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 = []
+113 -56
View File
@@ -1,8 +1,8 @@
from decimal import Decimal
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
from dsmr_parser.value_types import timestamp
"""
@@ -14,60 +14,117 @@ 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(int),
ValueParser(int),
ValueParser(int),
ValueParser(str),
ValueParser(Decimal),
),
}
}
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.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)
)
}
}
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)),
# 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.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)
+2 -2
View File
@@ -4,9 +4,9 @@ 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.6',
version='0.13',
packages=find_packages(),
install_requires=[
'pyserial>=3,<4',
+130
View File
@@ -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'
'!87B3\r\n'
)
+78 -28
View File
@@ -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')
+98
View File
@@ -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')
+24 -64
View File
@@ -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)
@@ -259,3 +197,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)
+247
View File
@@ -0,0 +1,247 @@
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.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)
+28 -32
View File
@@ -1,52 +1,48 @@
"""Test DSMR serial protocol."""
import unittest
from unittest.mock import Mock
import unittest
from dsmr_parser import obis_references as obis
from dsmr_parser import telegram_specifications
from dsmr_parser.parsers import 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'
)
class ProtocolTest(unittest.TestCase):
def setUp(self):
parser = TelegramParserV2_2
specification = telegram_specifications.V2_2
telegram_parser = parser(specification)
telegram_parser = TelegramParser(telegram_specifications.V2_2)
self.protocol = DSMRProtocol(None, telegram_parser,
telegram_callback=Mock())
def test_complete_packet(self):
"""Protocol should assemble incoming lines into complete packet."""
for line in TELEGRAM_V2_2:
self.protocol.data_received(bytes(line + '\r\n', 'ascii'))
self.protocol.data_received(TELEGRAM_V2_2.encode('ascii'))
telegram = self.protocol.telegram_callback.call_args_list[0][0][0]
assert isinstance(telegram, dict)
+105
View File
@@ -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, '')
+4 -1
View File
@@ -1,5 +1,5 @@
[tox]
envlist = py34,py35
envlist = py34,py35,p36
[testenv]
deps=
@@ -14,6 +14,9 @@ commands=
py.test --cov=dsmr_parser test {posargs}
pylama dsmr_parser test
[pylama:dsmr_parser/clients/__init__.py]
ignore = W0611
[pylama:pylint]
max_line_length = 100