Compare commits

...
46 Commits
Author SHA1 Message Date
Nigel Dokter 29fc97a65c updated changelog 2017-01-04 20:02:08 +01:00
Nigel Dokter 3b43cbf841 all tests are written using unittest.TestCase now 2017-01-04 19:55:54 +01:00
Nigel Dokter 9d8cad8b46 Merge pull request #15 from AlexMekkering/telegram_pass
Pass lines to parser including line endings
2017-01-04 19:19:07 +01:00
Nigel Dokter 0d6c763e86 Merge pull request #12 from aequitas/tcp
Add support for TCP connections
2017-01-04 19:16:26 +01:00
Alex Mekkering 8b60d48edd pycodestyle fixes 2017-01-04 15:01:20 +01:00
Alex Mekkering 03b761e15b Pass lines to parser including line endings 2017-01-04 14:49:18 +01:00
Nigel Dokter 061334f702 Merge pull request #14 from ndokter/revert-13-crc
Revert "Fixed CRC calculation"
2017-01-04 11:58:19 +01:00
Nigel Dokter 920c9aedc2 Revert "Fixed CRC calculation" 2017-01-04 11:58:03 +01:00
Nigel Dokter 991dd09e2a Merge pull request #13 from AlexMekkering/crc
Fixed CRC calculation
2017-01-04 11:07:45 +01:00
Alex Mekkering ce4d5b0e62 Corrected unit test for failing CRC 2017-01-04 10:51:29 +01:00
Alex Mekkering e512456cc2 Fixed CRC calculation 2017-01-04 10:21:47 +01:00
Johan Bloemberg 3c9db523fa Fix tpyo. 2017-01-03 22:27:39 +01:00
Johan Bloemberg 763237ef1d Add TCP arguments to console. Implement reconnect logic in protocol. 2017-01-03 21:27:10 +01:00
Johan Bloemberg cdc9e395aa Add support for TCP connections. 2017-01-03 17:56:24 +01:00
Nigel Dokter 6dec45ae2c Merge branch 'crc_check' 2016-12-29 19:32:24 +01:00
Nigel Dokter c2a67bff6d version v0.5 changelog 2016-12-29 19:30:35 +01:00
Nigel Dokter b3014823c1 bugfix; updated async client to CRC check 2016-12-29 19:20:50 +01:00
Nigel Dokter 4b392522c3 Removed todo list from readme 2016-12-29 10:08:07 +01:00
Nigel Dokter 1c69b4e9ee added telegram CRC verification 2016-12-28 20:29:34 +01:00
Nigel Dokter bbea9de445 Merge pull request #9 from dennissiemensma/code-coverage
Code coverage (Tox, Travis & Codecov.io)
2016-11-27 21:01:27 +01:00
Dennis Siemensma 19d3f60aec Ignore .coverage file 2016-11-27 20:40:52 +01:00
Dennis Siemensma b228bd524b Add code coverage with Codecov in Travis 2016-11-27 20:29:49 +01:00
Dennis Siemensma 220db73fbe Add code coverage with pytest-cov 2016-11-27 20:07:13 +01:00
Nigel Dokter 81fd581e57 pep8 2016-11-26 15:58:24 +01:00
Nigel Dokter 4df6ba75a2 used python unittest for the tests 2016-11-26 15:33:58 +01:00
Nigel Dokter 819d0d0696 updated version number 2016-11-22 19:54:19 +01:00
Nigel Dokter a3685f0310 added Travis CI badge 2016-11-21 22:31:22 +01:00
Nigel Dokter 5ae6ad4156 typo 2016-11-21 21:49:17 +01:00
Nigel Dokter e330e9db21 updated changelog 2016-11-21 21:48:17 +01:00
Nigel Dokter 0ac2990df2 accepted changed from pull request instead 2016-11-21 21:46:21 +01:00
Nigel Dokter e0d91f1ec4 Merge pull request #5 from aequitas/master
Even parity offers better compatibility for v2.2.
2016-11-21 21:45:38 +01:00
Nigel Dokter c058feca5f make serial.EVEN the default parity for DSMR v2.2 2016-11-21 21:30:56 +01:00
Nigel Dokter 6f3c74ce7c Merge pull request #8 from aequitas/async_protocol
Async protocol
2016-11-21 20:39:57 +01:00
Nigel Dokter 43a3cb7a96 Merge pull request #7 from aequitas/error_handling
Add error handling to async serial.
2016-11-21 20:28:11 +01:00
Johan Bloemberg 5e88182301 Complete telegram handling logic. 2016-11-21 17:02:32 +01:00
Johan Bloemberg e3569e0719 Add asyncio protocol implementation. 2016-11-21 15:48:29 +01:00
Johan Bloemberg f8a3c76c68 wip async test 2016-11-21 14:35:35 +01:00
Johan Bloemberg a1d077d6f2 Fix style. 2016-11-21 14:19:12 +01:00
Johan Bloemberg 616db8b1cc Add error handling to async serial. 2016-11-21 14:11:30 +01:00
Nigel Dokter 927a4bc8e7 added DSMR v4 parsing unit test; added alternative serial settings for DSMR v2 and v4; 2016-11-20 12:44:45 +01:00
Johan Bloemberg e7ff8f2444 Even parity offers better compatibility for v2.2. 2016-11-15 23:00:59 +01:00
Nigel Dokter f41fe0894c changelog 2016-11-12 10:56:16 +01:00
Nigel Dokter 59dd0893a4 Merge pull request #3 from aequitas/asyncio
Add asyncio reader.
2016-11-12 10:51:10 +01:00
Johan Bloemberg e3c8c92198 Linting and tpyo 2016-11-12 01:36:36 +01:00
Johan Bloemberg 9363840042 Project maintenance. 2016-11-12 01:34:32 +01:00
Johan Bloemberg 4afab70dc0 Add asyncio reader. 2016-11-12 01:25:56 +01:00
18 changed files with 723 additions and 81 deletions
+2
View File
@@ -0,0 +1,2 @@
[run]
branch = True
+3
View File
@@ -3,3 +3,6 @@
.tox
.cache
*.egg-info
/.project
/.pydevproject
/.coverage
+8 -1
View File
@@ -1,10 +1,17 @@
language: python
python:
- 2.7
- 3.4
- 3.5
install: pip install tox-travis
install: pip install tox-travis codecov
script: tox
after_success:
- codecov
matrix:
allow_failures:
- python: 2.7
+21 -2
View File
@@ -1,9 +1,28 @@
Change Log
----------
**0.6** (2017-01-04)
- Fixed bug in CRC checksum verification for the asyncio client (`pull request #15 <https://github.com/ndokter/dsmr_parser/pull/15>`_)
- Support added for TCP connections using the asyncio client (`pull request #12 <https://github.com/ndokter/dsmr_parser/pull/12/>`_)
**0.5** (2016-12-29)
- CRC checksum verification for DSMR v4 telegrams (`issue #10 <https://github.com/ndokter/dsmr_parser/issues/10>`_)
**0.4** (2016-11-21)
- DSMR v2.2 serial settings now uses parity serial.EVEN by default (`pull request #5 <https://github.com/ndokter/dsmr_parser/pull/5>`_)
- improved asyncio reader and improve it's error handling (`pull request #8 <https://github.com/ndokter/dsmr_parser/pull/8>`_)
**0.3** (2016-11-12)
- asyncio reader for non-blocking reads (`pull request #3 <https://github.com/ndokter/dsmr_parser/pull/3>`_)
**0.2** (2016-11-08)
- User https://github.com/aequitas added support for DMSR version 2.2
- support for DMSR version 2.2 (`pull request #2 <https://github.com/ndokter/dsmr_parser/pull/2>`_)
**0.1** (2016-08-22)
- Initial version with a serial reader and support for DSMR version 4.x
- initial version with a serial reader and support for DSMR version 4.x
+16 -13
View File
@@ -4,6 +4,9 @@ DSMR Parser
.. image:: https://img.shields.io/pypi/v/dsmr-parser.svg
:target: https://pypi.python.org/pypi/dsmr-parser
.. image:: https://travis-ci.org/ndokter/dsmr_parser.svg?branch=master
: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.
@@ -11,7 +14,7 @@ also includes a serial client 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.5
DSMR Parser currently supports DSMR versions 2.2 and 4.x. It has been tested with Python 3.4 and 3.5.
Examples
@@ -22,7 +25,7 @@ Using the serial reader to connect to your smart meter and parse it's telegrams:
.. code-block:: python
from dsmr_parser import telegram_specifications
from dsmr_parser.obis_references import P1_MESSAGE_TIMESTAMP
from dsmr_parser import obis_references
from dsmr_parser.serial import SerialReader, SERIAL_SETTINGS_V4
serial_reader = SerialReader(
@@ -34,19 +37,19 @@ Using the serial reader to connect to your smart meter and parse it's telegrams:
for telegram in serial_reader.read():
# The telegram message timestamp.
message_datetime = telegram[P1_MESSAGE_TIMESTAMP]
message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP]
# Using the active tariff to determine the electricity being used and
# delivered for the right tariff.
tariff = telegram[ELECTRICITY_ACTIVE_TARIFF]
tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF]
tariff = int(tariff.value)
electricity_used_total \
= telegram[ELECTRICITY_USED_TARIFF_ALL[tariff - 1]]
= telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[tariff - 1]]
electricity_delivered_total = \
telegram[ELECTRICITY_DELIVERED_TARIFF_ALL[tariff - 1]]
telegram[obis_referencesELECTRICITY_DELIVERED_TARIFF_ALL[tariff - 1]]
gas_reading = telegram[HOURLY_GAS_METER_READING]
gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING]
# See dsmr_reader.obis_references for all readable telegram values.
@@ -60,10 +63,10 @@ To install DSMR Parser:
$ pip install dsmr-parser
Known issues
------------
TODO
----
- add unit tests
- verify telegram checksum
- improve ease of use
If the serial settings SERIAL_SETTINGS_V2_2 or SERIAL_SETTINGS_V4 don't work.
Make sure to try and replace the parity settings to EVEN or NONE.
It's possible that alternative settings will be added in the future if these
settings don't work for the majority of meters.
+45 -12
View File
@@ -1,6 +1,9 @@
import argparse
from dsmr_parser.serial import SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, SerialReader
from dsmr_parser import telegram_specifications
import asyncio
import logging
from functools import partial
from .protocol import create_dsmr_reader, create_tcp_dsmr_reader
def console():
@@ -9,24 +12,54 @@ def console():
parser = argparse.ArgumentParser(description=console.__doc__)
parser.add_argument('--device', default='/dev/ttyUSB0',
help='port to read DSMR data from')
parser.add_argument('--host', default=None,
help='alternatively connect using TCP host.')
parser.add_argument('--port', default=None,
help='TCP port to use for connection')
parser.add_argument('--version', default='2.2', choices=['2.2', '4'],
help='DSMR version (2.2, 4)')
parser.add_argument('--verbose', '-v', action='count')
args = parser.parse_args()
settings = {
'2.2': (SERIAL_SETTINGS_V2_2, telegram_specifications.V2_2),
'4': (SERIAL_SETTINGS_V4, telegram_specifications.V4),
}
if args.verbose:
level = logging.DEBUG
else:
level = logging.ERROR
logging.basicConfig(level=level)
serial_reader = SerialReader(
device=args.device,
serial_settings=settings[args.version][0],
telegram_specification=settings[args.version][1],
)
loop = asyncio.get_event_loop()
for telegram in serial_reader.read():
def print_callback(telegram):
"""Callback that prints telegram values."""
for obiref, obj in telegram.items():
if obj:
print(obj.value, obj.unit)
print()
# create tcp or serial connection depending on args
if args.host and args.port:
create_connection = partial(create_tcp_dsmr_reader,
args.host, args.port, args.version,
print_callback, loop=loop)
else:
create_connection = partial(create_dsmr_reader,
args.device, args.version,
print_callback, loop=loop)
try:
# connect and keep connected until interrupted by ctrl-c
while True:
# create serial or tcp connection
conn = create_connection()
transport, protocol = loop.run_until_complete(conn)
# wait until connection it closed
loop.run_until_complete(protocol.wait_closed())
# wait 5 seconds before attempting reconnect
loop.run_until_complete(asyncio.sleep(5))
except KeyboardInterrupt:
# cleanup connection after user initiated shutdown
transport.close()
loop.run_until_complete(asyncio.sleep(0))
finally:
loop.close()
+4
View File
@@ -1,2 +1,6 @@
class ParseError(Exception):
pass
class InvalidChecksumError(ParseError):
pass
+52 -4
View File
@@ -1,8 +1,10 @@
import logging
import re
from PyCRC.CRC16 import CRC16
from .objects import MBusObject, MBusObjectV2_2, CosemObject
from .exceptions import ParseError
from .exceptions import ParseError, InvalidChecksumError
from .obis_references import GAS_METER_READING
logger = logging.getLogger(__name__)
@@ -18,7 +20,6 @@ class TelegramParser(object):
self.telegram_specification = telegram_specification
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
@@ -29,14 +30,17 @@ class TelegramParser(object):
telegram = {}
for line_value in line_values:
obis_reference, dsmr_object = self.parse_line(line_value.strip())
# 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)
logger.debug('Parsing line \'%s\'', line_value)
obis_reference, parser = self._find_line_parser(line_value)
@@ -47,7 +51,51 @@ class TelegramParser(object):
return obis_reference, parser.parse(line_value)
class TelegramParserV4(TelegramParser):
@staticmethod
def validate_telegram_checksum(line_values):
"""
:type line_values: list
:raises ParseError:
:raises InvalidChecksumError:
"""
full_telegram = ''.join(line_values)
# Extract the bytes that count towards the checksum.
checksum_contents = re.search(r'\/.+\!', full_telegram, re.DOTALL)
# Extract the hexadecimal checksum value itself.
checksum_hex = re.search(r'((?<=\!)[0-9A-Z]{4}(?=\r\n))+', full_telegram)
if not checksum_contents or not checksum_hex:
raise ParseError(
'Failed to perform CRC validation because the telegram is '
'incomplete. The checksum and/or content values are missing.'
)
calculated_crc = CRC16().calculate(checksum_contents.group(0))
expected_crc = checksum_hex.group(0)
expected_crc = int(expected_crc, base=16)
if calculated_crc != expected_crc:
raise InvalidChecksumError(
"Invalid telegram. The CRC checksum '{}' does not match the "
"expected '{}'".format(
calculated_crc,
expected_crc
)
)
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."""
+125
View File
@@ -0,0 +1,125 @@
"""Asyncio protocol implementation for handling telegrams."""
import asyncio
import logging
from functools import partial
from serial_asyncio import create_serial_connection
from . import telegram_specifications
from .exceptions import ParseError
from .parsers import TelegramParserV2_2, TelegramParserV4
from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4,
is_end_of_telegram, is_start_of_telegram)
def create_dsmr_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
serial_settings = SERIAL_SETTINGS_V2_2
elif dsmr_version == '4':
specifications = telegram_specifications.V4
telegram_parser = TelegramParserV4
serial_settings = SERIAL_SETTINGS_V4
protocol = partial(DSMRProtocol, loop, telegram_parser(specifications),
telegram_callback=telegram_callback)
return protocol, serial_settings
def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):
"""Creates a DSMR asyncio protocol coroutine using serial port."""
protocol, serial_settings = create_dsmr_protocol(
dsmr_version, telegram_callback, loop=None)
serial_settings['url'] = port
conn = create_serial_connection(loop, protocol, **serial_settings)
return conn
def create_tcp_dsmr_reader(host, port, dsmr_version,
telegram_callback, loop=None):
"""Creates a DSMR asyncio protocol coroutine using TCP connection."""
protocol, _ = create_dsmr_protocol(
dsmr_version, telegram_callback, loop=None)
conn = loop.create_connection(protocol, host, port)
return conn
class DSMRProtocol(asyncio.Protocol):
"""Assemble and handle incoming data into complete DSM telegrams."""
transport = None
telegram_callback = None
def __init__(self, loop, telegram_parser, telegram_callback=None):
"""Initialize class."""
self.loop = loop
self.log = logging.getLogger(__name__)
self.telegram_parser = telegram_parser
# callback to call on complete telegram
self.telegram_callback = telegram_callback
# buffer to keep incoming telegram lines
self.telegram = []
# buffer to keep incomplete incoming data
self.buffer = ''
# keep a lock until the connection is closed
self._closed = asyncio.Event()
def connection_made(self, transport):
"""Just logging for now."""
self.transport = transport
self.log.debug('connected')
def data_received(self, data):
"""Add incoming data to buffer."""
data = data.decode()
self.log.debug('received data: %s', data.strip())
self.buffer += data
self.handle_lines()
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 = []
def connection_lost(self, exc):
"""Stop when connection is lost."""
if exc:
self.log.exception('disconnected due to exception')
else:
self.log.info('disconnected because of close/abort.')
self._closed.set()
def handle_telegram(self, telegram):
"""Send off parsed telegram to handling callback."""
self.log.debug('got telegram: %s', telegram)
if self.telegram_callback:
self.telegram_callback(telegram)
@asyncio.coroutine
def wait_closed(self):
"""Wait until connection is closed."""
yield from self._closed.wait()
+74 -5
View File
@@ -1,11 +1,19 @@
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__)
from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2
SERIAL_SETTINGS_V2_2 = {
'baudrate': 9600,
'bytesize': serial.SEVENBITS,
'parity': serial.PARITY_NONE,
'parity': serial.PARITY_EVEN,
'stopbits': serial.STOPBITS_ONE,
'xonxoff': 0,
'rtscts': 0,
@@ -24,23 +32,33 @@ SERIAL_SETTINGS_V4 = {
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['port'] = device
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):
@@ -55,7 +73,7 @@ class SerialReader(object):
while True:
line = serial_handle.readline()
line = line.decode('ascii')
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.
@@ -65,5 +83,56 @@ class SerialReader(object):
telegram.append(line)
if is_end_of_telegram(line):
yield self.telegram_parser.parse(telegram)
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 = []
+2 -1
View File
@@ -4,8 +4,9 @@ import pytz
def timestamp(value):
naive_datetime = datetime.datetime.strptime(value[:-1], '%y%m%d%H%M%S')
# TODO comment on this exception
if len(value) == 13:
is_dst = value[12] == 'S' # assume format 160322150000W
else:
+4 -2
View File
@@ -6,11 +6,13 @@ setup(
author='Nigel Dokter',
author_email='nigeldokter@gmail.com',
url='https://github.com/ndokter/dsmr_parser',
version='0.2',
version='0.5',
packages=find_packages(),
install_requires=[
'pyserial>=3,<4',
'pytz'
'pyserial-asyncio<1',
'pytz',
'PyCRC>=1.2,<2'
],
entry_points={
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
View File
-40
View File
@@ -1,40 +0,0 @@
"""Test telegram parsing."""
from dsmr_parser.parsers import TelegramParserV2_2
from dsmr_parser import telegram_specifications
from dsmr_parser.obis_references import CURRENT_ELECTRICITY_USAGE, GAS_METER_READING
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)",
"!",
]
def test_parse_v2_2():
"""Test if telegram parsing results in correct results."""
parser = TelegramParserV2_2(telegram_specifications.V2_2)
result = parser.parse(TELEGRAM_V2_2)
assert float(result[CURRENT_ELECTRICITY_USAGE].value) == 1.01
assert result[CURRENT_ELECTRICITY_USAGE].unit == 'kW'
assert float(result[GAS_METER_READING].value) == 1.001
assert result[GAS_METER_READING].unit == 'm3'
+42
View File
@@ -0,0 +1,42 @@
import unittest
from dsmr_parser.parsers import TelegramParserV2_2
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)',
'!',
]
class TelegramParserV2_2Test(unittest.TestCase):
""" Test parsing of a DSMR v2.2 telegram. """
def test_parse(self):
parser = TelegramParserV2_2(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'
assert float(result[obis.GAS_METER_READING].value) == 1.001
assert result[obis.GAS_METER_READING].unit == 'm3'
+261
View File
@@ -0,0 +1,261 @@
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, 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'
]
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)
# 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 == '42'
# 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(2016, 11, 13, 19, 57, 57, 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('1581.123')
# 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('1435.706')
# 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('0')
# 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 == '3960221976967177082151037881335713'
# 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('2.027')
# 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 == 7
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject)
assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT].value, int)
assert result[obis.VOLTAGE_SAG_L1_COUNT].value == 0
# VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)
assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT], CosemObject)
assert result[obis.VOLTAGE_SAG_L2_COUNT].unit is None
assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT].value, int)
assert result[obis.VOLTAGE_SAG_L2_COUNT].value == 0
# VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)
assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT], CosemObject)
assert result[obis.VOLTAGE_SAG_L3_COUNT].unit is None
assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT].value, int)
assert result[obis.VOLTAGE_SAG_L3_COUNT].value == 0
# VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)
assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT], CosemObject)
assert result[obis.VOLTAGE_SWELL_L1_COUNT].unit is None
assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT].value, int)
assert result[obis.VOLTAGE_SWELL_L1_COUNT].value == 0
# VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)
assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT], CosemObject)
assert result[obis.VOLTAGE_SWELL_L2_COUNT].unit is None
assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT].value, int)
assert result[obis.VOLTAGE_SWELL_L2_COUNT].value == 0
# VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT], CosemObject)
assert result[obis.VOLTAGE_SWELL_L3_COUNT].unit is None
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int)
assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0
# TEXT_MESSAGE_CODE (0-0:96.13.1)
assert isinstance(result[obis.TEXT_MESSAGE_CODE], CosemObject)
assert result[obis.TEXT_MESSAGE_CODE].unit is None
assert result[obis.TEXT_MESSAGE_CODE].value 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, 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.170')
# 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('1.247')
# 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.209')
# 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 == '4819243993373755377509728609491464'
# 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('981.443')
# POWER_EVENT_FAILURE_LOG (99.97.0)
# TODO to be implemented
# ACTUAL_TRESHOLD_ELECTRICITY (0-0:17.0.0)
# TODO to be implemented
# ACTUAL_SWITCH_POSITION (0-0:96.3.10)
# TODO to be implemented
# VALVE_POSITION_GAS (0-x:24.4.0)
# TODO to be implemented
+58
View File
@@ -0,0 +1,58 @@
"""Test DSMR serial protocol."""
import unittest
from unittest.mock import Mock
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
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)",
"!",
]
class ProtocolTest(unittest.TestCase):
def setUp(self):
parser = TelegramParserV2_2
specification = telegram_specifications.V2_2
telegram_parser = parser(specification)
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'))
telegram = self.protocol.telegram_callback.call_args_list[0][0][0]
assert isinstance(telegram, dict)
assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01
assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
assert float(telegram[obis.GAS_METER_READING].value) == 1.001
assert telegram[obis.GAS_METER_READING].unit == 'm3'
+6 -1
View File
@@ -4,9 +4,14 @@ envlist = py34,py35
[testenv]
deps=
pytest
pytest-cov
pylama
pytest-asyncio
pytest-catchlog
pytest-mock
PyCRC
commands=
py.test test {posargs}
py.test --cov=dsmr_parser test {posargs}
pylama dsmr_parser test
[pylama:pylint]