Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29fc97a65c | ||
|
|
3b43cbf841 | ||
|
|
9d8cad8b46 | ||
|
|
0d6c763e86 | ||
|
|
8b60d48edd | ||
|
|
03b761e15b | ||
|
|
061334f702 | ||
|
|
920c9aedc2 | ||
|
|
991dd09e2a | ||
|
|
ce4d5b0e62 | ||
|
|
e512456cc2 | ||
|
|
3c9db523fa | ||
|
|
763237ef1d | ||
|
|
cdc9e395aa | ||
|
|
6dec45ae2c | ||
|
|
c2a67bff6d | ||
|
|
b3014823c1 | ||
|
|
4b392522c3 | ||
|
|
1c69b4e9ee | ||
|
|
bbea9de445 | ||
|
|
19d3f60aec | ||
|
|
b228bd524b | ||
|
|
220db73fbe | ||
|
|
81fd581e57 | ||
|
|
4df6ba75a2 | ||
|
|
819d0d0696 | ||
|
|
a3685f0310 | ||
|
|
5ae6ad4156 | ||
|
|
e330e9db21 | ||
|
|
0ac2990df2 | ||
|
|
e0d91f1ec4 | ||
|
|
c058feca5f | ||
|
|
6f3c74ce7c | ||
|
|
43a3cb7a96 | ||
|
|
5e88182301 | ||
|
|
e3569e0719 | ||
|
|
f8a3c76c68 | ||
|
|
a1d077d6f2 | ||
|
|
616db8b1cc | ||
|
|
927a4bc8e7 | ||
|
|
e7ff8f2444 | ||
|
|
f41fe0894c | ||
|
|
59dd0893a4 | ||
|
|
e3c8c92198 | ||
|
|
9363840042 | ||
|
|
4afab70dc0 | ||
|
|
9da4a3b0e6 | ||
|
|
defeed7e8a | ||
|
|
503cbd0ca7 | ||
|
|
ba29e34cf6 | ||
|
|
aa8ff29028 | ||
|
|
726ca507f7 | ||
|
|
9ee62f5228 | ||
|
|
fe5caa9126 | ||
|
|
cf771776cd | ||
|
|
447f2a24fb | ||
|
|
4a82066144 | ||
|
|
90eb6fb3fe |
@@ -0,0 +1,2 @@
|
||||
[run]
|
||||
branch = True
|
||||
@@ -1,2 +1,8 @@
|
||||
.idea
|
||||
*.pyc
|
||||
.tox
|
||||
.cache
|
||||
*.egg-info
|
||||
/.project
|
||||
/.pydevproject
|
||||
/.coverage
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
language: python
|
||||
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
|
||||
install: pip install tox-travis codecov
|
||||
|
||||
script: tox
|
||||
|
||||
after_success:
|
||||
- codecov
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: 2.7
|
||||
@@ -0,0 +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)
|
||||
|
||||
- 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
|
||||
+16
-13
@@ -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 version 4 and is 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.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from .protocol import create_dsmr_reader, create_tcp_dsmr_reader
|
||||
|
||||
|
||||
def console():
|
||||
"""Output DSMR data to 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()
|
||||
|
||||
if args.verbose:
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.ERROR
|
||||
logging.basicConfig(level=level)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
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()
|
||||
@@ -1,2 +1,6 @@
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidChecksumError(ParseError):
|
||||
pass
|
||||
|
||||
@@ -27,6 +27,10 @@ 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'
|
||||
|
||||
ELECTRICITY_USED_TARIFF_ALL = (
|
||||
ELECTRICITY_USED_TARIFF_1,
|
||||
|
||||
@@ -19,6 +19,21 @@ class MBusObject(DSMRObject):
|
||||
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']
|
||||
|
||||
|
||||
class CosemObject(DSMRObject):
|
||||
|
||||
@property
|
||||
|
||||
+77
-5
@@ -1,9 +1,11 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .objects import MBusObject, CosemObject
|
||||
from .exceptions import ParseError
|
||||
from PyCRC.CRC16 import CRC16
|
||||
|
||||
from .objects import MBusObject, MBusObjectV2_2, CosemObject
|
||||
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,6 +30,9 @@ class TelegramParser(object):
|
||||
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
|
||||
@@ -36,7 +40,7 @@ class TelegramParser(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,6 +51,70 @@ 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."""
|
||||
|
||||
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):
|
||||
|
||||
def __init__(self, *value_formats):
|
||||
@@ -85,7 +153,11 @@ class MBusParser(DSMRObjectParser):
|
||||
"""
|
||||
|
||||
def parse(self, line):
|
||||
return MBusObject(self._parse(line))
|
||||
values = self._parse(line)
|
||||
if len(values) == 2:
|
||||
return MBusObject(values)
|
||||
else:
|
||||
return MBusObjectV2_2(values)
|
||||
|
||||
|
||||
class CosemParser(DSMRObjectParser):
|
||||
|
||||
@@ -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()
|
||||
+88
-5
@@ -1,6 +1,24 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import serial
|
||||
import serial_asyncio
|
||||
|
||||
from dsmr_parser.parsers import TelegramParser
|
||||
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,
|
||||
@@ -14,19 +32,34 @@ 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.telegram_parser = TelegramParser(telegram_specification)
|
||||
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):
|
||||
"""
|
||||
@@ -40,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.
|
||||
@@ -50,6 +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 = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from .obis_references import *
|
||||
from . import obis_references as obis
|
||||
from .parsers import CosemParser, ValueParser, MBusParser
|
||||
from .value_types import timestamp
|
||||
|
||||
@@ -13,36 +13,61 @@ This module contains DSMR telegram specifications. Each specifications describes
|
||||
how the telegram lines are parsed.
|
||||
"""
|
||||
|
||||
V4 = {
|
||||
P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
|
||||
P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
|
||||
ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||
ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||
ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
||||
EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
||||
CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
||||
CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
||||
LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
|
||||
# POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO
|
||||
VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)),
|
||||
VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)),
|
||||
VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)),
|
||||
VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)),
|
||||
VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)),
|
||||
VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)),
|
||||
TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)),
|
||||
TEXT_MESSAGE: CosemParser(ValueParser(str)),
|
||||
DEVICE_TYPE: CosemParser(ValueParser(int)),
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)),
|
||||
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)),
|
||||
EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)),
|
||||
HOURLY_GAS_METER_READING: MBusParser(ValueParser(timestamp),
|
||||
ValueParser(Decimal))
|
||||
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),
|
||||
),
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@ import pytz
|
||||
|
||||
|
||||
def timestamp(value):
|
||||
|
||||
naive_datetime = datetime.datetime.strptime(value[:-1], '%y%m%d%H%M%S')
|
||||
is_dst = value[12] == 'S' # assume format 160322150000W
|
||||
|
||||
# TODO comment on this exception
|
||||
if len(value) == 13:
|
||||
is_dst = value[12] == 'S' # assume format 160322150000W
|
||||
else:
|
||||
is_dst = False
|
||||
|
||||
local_tz = pytz.timezone('Europe/Amsterdam')
|
||||
localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst)
|
||||
|
||||
@@ -4,10 +4,17 @@ setup(
|
||||
name='dsmr-parser',
|
||||
description='Library to parse Dutch Smart Meter Requirements (DSMR)',
|
||||
author='Nigel Dokter',
|
||||
version='0.1',
|
||||
author_email='nigeldokter@gmail.com',
|
||||
url='https://github.com/ndokter/dsmr_parser',
|
||||
version='0.5',
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
'pyserial==3.0.1',
|
||||
'pytz==2016.3'
|
||||
]
|
||||
'pyserial>=3,<4',
|
||||
'pyserial-asyncio<1',
|
||||
'pytz',
|
||||
'PyCRC>=1.2,<2'
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -0,0 +1,21 @@
|
||||
[tox]
|
||||
envlist = py34,py35
|
||||
|
||||
[testenv]
|
||||
deps=
|
||||
pytest
|
||||
pytest-cov
|
||||
pylama
|
||||
pytest-asyncio
|
||||
pytest-catchlog
|
||||
pytest-mock
|
||||
PyCRC
|
||||
commands=
|
||||
py.test --cov=dsmr_parser test {posargs}
|
||||
pylama dsmr_parser test
|
||||
|
||||
[pylama:pylint]
|
||||
max_line_length = 100
|
||||
|
||||
[pylama:pycodestyle]
|
||||
max_line_length = 100
|
||||
Reference in New Issue
Block a user