From fe278c2d3dcf97d25dc6dcc888ccf7f610ece515 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 22 Aug 2016 20:16:11 +0200 Subject: [PATCH 001/152] Initial commit --- .gitignore | 2 + LICENSE | 21 ++++ README.md | 66 ++++++++++ dsmr_parser/__init__.py | 0 dsmr_parser/exceptions.py | 2 + dsmr_parser/obis_references.py | 38 ++++++ dsmr_parser/objects.py | 35 ++++++ dsmr_parser/parsers.py | 159 +++++++++++++++++++++++++ dsmr_parser/serial.py | 55 +++++++++ dsmr_parser/telegram_specifications.py | 48 ++++++++ dsmr_parser/value_types.py | 14 +++ setup.cfg | 2 + setup.py | 13 ++ 13 files changed, 455 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dsmr_parser/__init__.py create mode 100644 dsmr_parser/exceptions.py create mode 100644 dsmr_parser/obis_references.py create mode 100644 dsmr_parser/objects.py create mode 100644 dsmr_parser/parsers.py create mode 100644 dsmr_parser/serial.py create mode 100644 dsmr_parser/telegram_specifications.py create mode 100644 dsmr_parser/value_types.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c10666e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +*.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f272df9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2016 Nigel Dokter http://nldr.net + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6d7493 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +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. + + +Features +-------- + +DSMR Parser currently supports DSMR version 4 and is tested with Python 3.5 + + +Examples +-------- + +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.serial 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(): + + # The telegram message timestamp. + message_datetime = telegram[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 = int(tariff.value) + + electricity_used_total \ + = telegram[ELECTRICITY_USED_TARIFF_ALL[tariff - 1]] + electricity_delivered_total = \ + telegram[ELECTRICITY_DELIVERED_TARIFF_ALL[tariff - 1]] + + gas_reading = telegram[HOURLY_GAS_METER_READING] + + # See dsmr_reader.obis_references for all readable telegram values. + + +Installation +------------ + +To install DSMR Parser: + +.. code-block:: bash + + $ pip install dsmr-parser + + +TODO +---- + +- add unit tests +- verify telegram checksum +- improve ease of use diff --git a/dsmr_parser/__init__.py b/dsmr_parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dsmr_parser/exceptions.py b/dsmr_parser/exceptions.py new file mode 100644 index 0000000..831cca9 --- /dev/null +++ b/dsmr_parser/exceptions.py @@ -0,0 +1,2 @@ +class ParseError(Exception): + pass diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py new file mode 100644 index 0000000..7fbb24e --- /dev/null +++ b/dsmr_parser/obis_references.py @@ -0,0 +1,38 @@ +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' + +ELECTRICITY_USED_TARIFF_ALL = ( + ELECTRICITY_USED_TARIFF_1, + ELECTRICITY_USED_TARIFF_2 +) +ELECTRICITY_DELIVERED_TARIFF_ALL = ( + ELECTRICITY_DELIVERED_TARIFF_1, + ELECTRICITY_DELIVERED_TARIFF_2 +) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py new file mode 100644 index 0000000..5024dba --- /dev/null +++ b/dsmr_parser/objects.py @@ -0,0 +1,35 @@ +class DSMRObject(object): + + def __init__(self, values): + self.values = values + + +class MBusObject(DSMRObject): + + @property + def datetime(self): + return self.values[0]['value'] + + @property + def value(self): + return self.values[1]['value'] + + @property + def unit(self): + return self.values[1]['unit'] + + +class CosemObject(DSMRObject): + + @property + def value(self): + return self.values[0]['value'] + + @property + def unit(self): + return self.values[0]['unit'] + + +class ProfileGeneric(DSMRObject): + pass + # TODO implement diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py new file mode 100644 index 0000000..ae08e27 --- /dev/null +++ b/dsmr_parser/parsers.py @@ -0,0 +1,159 @@ +import logging +import re + +from .objects import MBusObject, CosemObject +from .exceptions import ParseError + + +logger = logging.getLogger(__name__) + + +class TelegramParser(object): + + def __init__(self, telegram_specification): + """ + :param telegram_specification: determines how the telegram is parsed + :type telegram_specification: dict + """ + 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 + + return None, None + + def parse(self, line_values): + telegram = {} + + for line_value in line_values: + 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 DSMRObjectParser(object): + + def __init__(self, *value_formats): + self.value_formats = value_formats + + def _parse(self, line): + # Match value groups, but exclude the parentheses + pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*]{0,}(?=\)))+') + values = re.findall(pattern, line) + + # Convert empty value groups to None for clarity. + values = [None if value == '' else value for value in values] + + if not values or len(values) != len(self.value_formats): + raise ParseError("Invalid '%s' line for '%s'", line, self) + + return [self.value_formats[i].parse(value) + for i, value in enumerate(values)] + + +class MBusParser(DSMRObjectParser): + """ + Gas meter value parser. + + These are lines with a timestamp and gas meter value. + + Line format: + 'ID (TST) (Mv1*U1)' + + 1 2 3 4 + + 1) OBIS Reduced ID-code + 2) Time Stamp (TST) of capture time of measurement value + 3) Measurement value 1 (most recent entry of buffer attribute without unit) + 4) Unit of measurement values (Unit of capture objects attribute) + """ + + def parse(self, line): + return MBusObject(self._parse(line)) + + +class CosemParser(DSMRObjectParser): + """ + Cosem object parser. + + These are data objects with a single value that optionally have a unit of + measurement. + + Line format: + ID (Mv*U) + + 1 23 45 + + 1) OBIS Reduced ID-code + 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 + """ + + def parse(self, line): + return CosemObject(self._parse(line)) + + +class ProfileGenericParser(DSMRObjectParser): + """ + Power failure log parser. + + These are data objects with multiple repeating groups of values. + + Line format: + ID (z) (ID1) (TST) (Bv1*U1) (TST) (Bvz*Uz) + + 1 2 3 4 5 6 7 8 9 + + 1) OBIS Reduced ID-code + 2) Number of values z (max 10). + 3) Identifications of buffer values (OBIS Reduced ID codes of capture objects attribute) + 4) Time Stamp (TST) of power failure end time + 5) Buffer value 1 (most recent entry of buffer attribute without unit) + 6) Unit of buffer values (Unit of capture objects attribute) + 7) Time Stamp (TST) of power failure end time + 8) Buffer value 2 (oldest entry of buffer attribute without unit) + 9) Unit of buffer values (Unit of capture objects attribute) + """ + + def parse(self, line): + raise NotImplementedError() + + +class ValueParser(object): + + def __init__(self, coerce_type): + self.coerce_type = coerce_type + + def parse(self, value): + + unit_of_measurement = None + + if value and '*' in value: + value, unit_of_measurement = value.split('*') + + # A value group is not required to have a value, and then coercing does + # not apply. + value = self.coerce_type(value) if value is not None else value + + return { + 'value': value, + 'unit': unit_of_measurement + } diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py new file mode 100644 index 0000000..ee80a67 --- /dev/null +++ b/dsmr_parser/serial.py @@ -0,0 +1,55 @@ +import serial + +from dsmr_parser.parsers import TelegramParser + +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): + return line.startswith('/') + + +def is_end_of_telegram(line): + return line.startswith('!') + + +class SerialReader(object): + + def __init__(self, device, serial_settings, telegram_specification): + self.serial_settings = serial_settings + self.serial_settings['port'] = device + self.telegram_parser = TelegramParser(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') + + # 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): + yield self.telegram_parser.parse(telegram) + telegram = [] + diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py new file mode 100644 index 0000000..bcab475 --- /dev/null +++ b/dsmr_parser/telegram_specifications.py @@ -0,0 +1,48 @@ +from decimal import Decimal + +from .obis_references import * +from .parsers import CosemParser, ValueParser, MBusParser +from .value_types import timestamp + + +""" +dsmr_parser.telegram_specifications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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)) +} + diff --git a/dsmr_parser/value_types.py b/dsmr_parser/value_types.py new file mode 100644 index 0000000..4154d50 --- /dev/null +++ b/dsmr_parser/value_types.py @@ -0,0 +1,14 @@ +import datetime + +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 + + local_tz = pytz.timezone('Europe/Amsterdam') + localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst) + + return localized_datetime.astimezone(pytz.utc) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b88034e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f95903a --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup, find_packages + +setup( + name='dsmr-parser', + description='Library to parse Dutch Smart Meter Requirements (DSMR)', + author='Nigel Dokter', + version='0.1', + packages=find_packages(), + install_requires=[ + 'pyserial==3.0.1', + 'pytz==2016.3' + ] +) From f4424d663a8fa55a925f2e56d6ae2739dc19b4ea Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 22 Aug 2016 20:18:17 +0200 Subject: [PATCH 002/152] Changed readme from .md to .rst --- README.md => README.rst | 0 setup.cfg | 2 -- 2 files changed, 2 deletions(-) rename README.md => README.rst (100%) delete mode 100644 setup.cfg diff --git a/README.md b/README.rst similarity index 100% rename from README.md rename to README.rst diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b88034e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md From 6343bce2ee4be843ff0de094287accd90a17cb3d Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 22 Aug 2016 20:38:16 +0200 Subject: [PATCH 003/152] Added pypi badge --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index e6d7493..9184170 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,9 @@ DSMR Parser =========== +.. image:: https://img.shields.io/pypi/v/dsmr-parser.svg + :target: https://pypi.python.org/pypi/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. From 90eb6fb3fe0d090b2f81e16f770d45ddbfffa0a2 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Sun, 6 Nov 2016 20:40:01 +0100 Subject: [PATCH 004/152] Add V2.2 telegram implementation and console read. --- dsmr_parser/__main__.py | 35 ++++++++++++++++++++++++++ dsmr_parser/obis_references.py | 4 +++ dsmr_parser/serial.py | 10 ++++++++ dsmr_parser/telegram_specifications.py | 18 +++++++++++++ setup.py | 5 +++- 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 dsmr_parser/__main__.py diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py new file mode 100644 index 0000000..b3e127c --- /dev/null +++ b/dsmr_parser/__main__.py @@ -0,0 +1,35 @@ +import argparse +from dsmr_parser.serial import SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, SerialReader +from dsmr_parser import telegram_specifications +from dsmr_parser.obis_references import P1_MESSAGE_TIMESTAMP + +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('--version', default='2.2', choices=['2.2', '4'], + help='DSMR version (2.2, 4)') + + args = parser.parse_args() + + version = 'V' + args.version.replace('.', '_') + + settings = { + '2.2': (SERIAL_SETTINGS_V2_2, telegram_specifications.V2_2), + '4': (SERIAL_SETTINGS_V4, telegram_specifications.V4), + } + + serial_reader = SerialReader( + device=args.device, + serial_settings=settings[args.version][0], + telegram_specification=settings[args.version][1], + ) + + for telegram in serial_reader.read(): + for obiref, obj in telegram.items(): + if obj: + print(obj.value, obj.unit) + print() + diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 7fbb24e..f99d007 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -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, diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index ee80a67..a4726c6 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -2,6 +2,16 @@ import serial from dsmr_parser.parsers import TelegramParser +SERIAL_SETTINGS_V2_2 = { + 'baudrate': 9600, + 'bytesize': serial.SEVENBITS, + 'parity': serial.PARITY_NONE, + 'stopbits': serial.STOPBITS_ONE, + 'xonxoff': 0, + 'rtscts': 0, + 'timeout': 20 +} + SERIAL_SETTINGS_V4 = { 'baudrate': 115200, 'bytesize': serial.SEVENBITS, diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index bcab475..b6f5071 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -13,6 +13,24 @@ This module contains DSMR telegram specifications. Each specifications describes how the telegram lines are parsed. """ +V2_2 = { + EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + 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)), + CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), + ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), + TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), + TEXT_MESSAGE: CosemParser(ValueParser(str)), + EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), + DEVICE_TYPE: CosemParser(ValueParser(str)), + VALVE_POSITION_GAS: CosemParser(ValueParser(str)), +} + V4 = { P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), diff --git a/setup.py b/setup.py index f95903a..c0f2d76 100644 --- a/setup.py +++ b/setup.py @@ -9,5 +9,8 @@ setup( install_requires=[ 'pyserial==3.0.1', 'pytz==2016.3' - ] + ], + entry_points={ + 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] + }, ) From 4a82066144d30da0267d7ecc76c4653ecb2bd593 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Mon, 7 Nov 2016 19:59:39 +0100 Subject: [PATCH 005/152] Add test/style suite. --- .gitignore | 2 + dsmr_parser/__main__.py | 5 +- dsmr_parser/serial.py | 1 - dsmr_parser/telegram_specifications.py | 99 ++++++++++++++------------ setup.py | 4 +- tox.ini | 16 +++++ 6 files changed, 74 insertions(+), 53 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index c10666e..a66f5e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea *.pyc +.tox +.cache diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index b3e127c..92c0dce 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -1,7 +1,7 @@ import argparse from dsmr_parser.serial import SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, SerialReader from dsmr_parser import telegram_specifications -from dsmr_parser.obis_references import P1_MESSAGE_TIMESTAMP + def console(): """Output DSMR data to console.""" @@ -14,8 +14,6 @@ def console(): args = parser.parse_args() - version = 'V' + args.version.replace('.', '_') - settings = { '2.2': (SERIAL_SETTINGS_V2_2, telegram_specifications.V2_2), '4': (SERIAL_SETTINGS_V4, telegram_specifications.V4), @@ -32,4 +30,3 @@ def console(): if obj: print(obj.value, obj.unit) print() - diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index a4726c6..faa6d94 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -62,4 +62,3 @@ class SerialReader(object): if is_end_of_telegram(line): yield self.telegram_parser.parse(telegram) telegram = [] - diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index b6f5071..958153b 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -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 @@ -14,53 +14,60 @@ how the telegram lines are parsed. """ V2_2 = { - EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - 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)), - CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), - ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), - TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), - TEXT_MESSAGE: CosemParser(ValueParser(str)), - EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), - DEVICE_TYPE: CosemParser(ValueParser(str)), - VALVE_POSITION_GAS: CosemParser(ValueParser(str)), + 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 = { - 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)), + 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 - 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)) + 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)) } - diff --git a/setup.py b/setup.py index c0f2d76..8f55bfd 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ setup( version='0.1', packages=find_packages(), install_requires=[ - 'pyserial==3.0.1', - 'pytz==2016.3' + 'pyserial>=3.2.1', + 'pytz' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..eb471d4 --- /dev/null +++ b/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist = py35 + +[testenv] +deps= + pytest + pylama +commands= + py.test test {posargs} + pylama dsmr_parser test + +[pylama:pylint] +max_line_length = 100 + +[pylama:pycodestyle] +max_line_length = 100 From 447f2a24fb2ce996ae13d1048ac8c630e6abe3d2 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Mon, 7 Nov 2016 20:00:10 +0100 Subject: [PATCH 006/152] Add test and exceptions for V2_2 implementation. --- dsmr_parser/objects.py | 15 +++++++++++++++ dsmr_parser/parsers.py | 32 ++++++++++++++++++++++++++++---- dsmr_parser/serial.py | 9 +++++++-- dsmr_parser/value_types.py | 5 ++++- test/test_parse.py | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 test/test_parse.py diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 5024dba..e96df9f 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -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]['unit'] + + class CosemObject(DSMRObject): @property diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index ae08e27..4b49a68 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -1,9 +1,9 @@ import logging import re -from .objects import MBusObject, CosemObject +from .objects import MBusObject, MBusObjectV2_2, CosemObject from .exceptions import ParseError - +from .obis_references import GAS_METER_READING logger = logging.getLogger(__name__) @@ -29,7 +29,7 @@ class TelegramParser(object): telegram = {} for line_value in line_values: - obis_reference, dsmr_object = self.parse_line(line_value) + obis_reference, dsmr_object = self.parse_line(line_value.strip()) telegram[obis_reference] = dsmr_object @@ -47,6 +47,26 @@ class TelegramParser(object): return obis_reference, parser.parse(line_value) +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 +105,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): diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index faa6d94..114244c 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -1,6 +1,6 @@ import serial -from dsmr_parser.parsers import TelegramParser +from dsmr_parser.parsers import TelegramParser, V3TelegramParser SERIAL_SETTINGS_V2_2 = { 'baudrate': 9600, @@ -36,7 +36,12 @@ class SerialReader(object): def __init__(self, device, serial_settings, telegram_specification): self.serial_settings = serial_settings self.serial_settings['port'] = device - self.telegram_parser = TelegramParser(telegram_specification) + + if serial_settings is SERIAL_SETTINGS_V2_2: + telegram_parser = V3TelegramParser + else: + telegram_parser = TelegramParser + self.telegram_parser = telegram_parser(telegram_specification) def read(self): """ diff --git a/dsmr_parser/value_types.py b/dsmr_parser/value_types.py index 4154d50..48a9146 100644 --- a/dsmr_parser/value_types.py +++ b/dsmr_parser/value_types.py @@ -6,7 +6,10 @@ 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 + 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) diff --git a/test/test_parse.py b/test/test_parse.py new file mode 100644 index 0000000..460b509 --- /dev/null +++ b/test/test_parse.py @@ -0,0 +1,38 @@ +"""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 float(result[GAS_METER_READING].value) == 1.001 From cf771776cdc4d8e3323292782e78541d99448705 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Mon, 7 Nov 2016 20:04:59 +0100 Subject: [PATCH 007/152] Fix console imports. --- .gitignore | 1 + dsmr_parser/serial.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a66f5e5..83f3764 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.pyc .tox .cache +*.egg-info diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 114244c..3e7aa70 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -1,6 +1,6 @@ import serial -from dsmr_parser.parsers import TelegramParser, V3TelegramParser +from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2 SERIAL_SETTINGS_V2_2 = { 'baudrate': 9600, @@ -38,7 +38,7 @@ class SerialReader(object): self.serial_settings['port'] = device if serial_settings is SERIAL_SETTINGS_V2_2: - telegram_parser = V3TelegramParser + telegram_parser = TelegramParserV2_2 else: telegram_parser = TelegramParser self.telegram_parser = telegram_parser(telegram_specification) From fe5caa9126e9de709ca63b5f86aaf064571c0fa7 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Mon, 7 Nov 2016 20:08:28 +0100 Subject: [PATCH 008/152] Test and fix parsing of gas unit. --- dsmr_parser/objects.py | 2 +- test/test_parse.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index e96df9f..f09fda5 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -31,7 +31,7 @@ class MBusObjectV2_2(DSMRObject): @property def unit(self): - return self.values[4]['unit'] + return self.values[4]['value'] class CosemObject(DSMRObject): diff --git a/test/test_parse.py b/test/test_parse.py index 460b509..433fd15 100644 --- a/test/test_parse.py +++ b/test/test_parse.py @@ -35,4 +35,6 @@ def test_parse_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' From 9ee62f5228d2bb160592d0e8137db4b43f8856bb Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Tue, 8 Nov 2016 10:18:42 +0100 Subject: [PATCH 009/152] Add python3.4 to the test. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index eb471d4..9bdde36 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35 +envlist = py34,py35 [testenv] deps= From 726ca507f708d716906bb9c9a56f2f7e47a6123b Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Tue, 8 Nov 2016 10:21:59 +0100 Subject: [PATCH 010/152] Do not pin pyserial to one specific version. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8f55bfd..51ae369 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( version='0.1', packages=find_packages(), install_requires=[ - 'pyserial>=3.2.1', + 'pyserial>=3,<4', 'pytz' ], entry_points={ From aa8ff2902838790f846efa94313df8da4bc98be1 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Tue, 8 Nov 2016 10:24:20 +0100 Subject: [PATCH 011/152] Add travis config. --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e09c73f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - 2.7 + - 3.4 + - 3.5 +install: pip install tox-travis +script: tox +matrix: + allow_failures: + - python: 2.7 From 503cbd0ca7debffa5aede51b838a139366407dae Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 8 Nov 2016 19:26:21 +0100 Subject: [PATCH 012/152] updated changelog and setup --- CHANGELOG.rst | 9 +++++++++ setup.py | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..14fc532 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,9 @@ +Change Log +---------- +**0.2** (2016-11-08) + +- User https://github.com/aequitas added support for DMSR version 2.2 + +**0.1** (2016-08-22) + +- Initial version with a serial reader and support for DSMR version 4.x diff --git a/setup.py b/setup.py index 51ae369..d02d7a3 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,9 @@ 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.2', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From defeed7e8a73db38cb87ed4e4b7f7a345619475b Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 8 Nov 2016 19:33:43 +0100 Subject: [PATCH 013/152] updated readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9184170..375d8b1 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,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.2. It has been tested with Python 3.5 Examples From 9da4a3b0e67d455825243745f20b480d94bf9775 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 8 Nov 2016 19:34:20 +0100 Subject: [PATCH 014/152] typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 375d8b1..f63d373 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,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.2. 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.5 Examples From 4afab70dc094871b6534131278b31826a4343c1e Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Sat, 12 Nov 2016 00:45:55 +0100 Subject: [PATCH 015/152] Add asyncio reader. --- dsmr_parser/serial.py | 48 +++++++++++++++++++++++++++++++++++++++++-- setup.py | 1 + 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 3e7aa70..0d716fd 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -1,5 +1,6 @@ import serial - +import asyncio +import serial_asyncio from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2 SERIAL_SETTINGS_V2_2 = { @@ -33,9 +34,11 @@ def is_end_of_telegram(line): 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 @@ -67,3 +70,44 @@ class SerialReader(object): if is_end_of_telegram(line): yield self.telegram_parser.parse(telegram) 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): + # push new parsed telegram onto queue + queue.put_nowait(self.telegram_parser.parse(telegram)) + telegram = [] + diff --git a/setup.py b/setup.py index d02d7a3..cc79d2d 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ setup( packages=find_packages(), install_requires=[ 'pyserial>=3,<4', + 'pyserial-asyncio<1' 'pytz' ], entry_points={ From 9363840042c54551958855d7936d92cab5f0a382 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Sat, 12 Nov 2016 01:34:32 +0100 Subject: [PATCH 016/152] Project maintenance. --- CHANGELOG.rst | 4 ++++ README.rst | 2 +- setup.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 14fc532..dbebfea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,9 @@ Change Log ---------- +**0.3** + +- Added asyncio reader for non-blocking reads. + **0.2** (2016-11-08) - User https://github.com/aequitas added support for DMSR version 2.2 diff --git a/README.rst b/README.rst index f63d373..23aff2f 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,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.5 and 3.4. Examples diff --git a/setup.py b/setup.py index cc79d2d..855f005 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.2', + version='0.3', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From e3c8c921986cac05a75223c2b53cf75669151c20 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Sat, 12 Nov 2016 01:36:36 +0100 Subject: [PATCH 017/152] Linting and tpyo --- dsmr_parser/serial.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 0d716fd..6f47b1c 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -71,6 +71,7 @@ class SerialReader(object): yield self.telegram_parser.parse(telegram) telegram = [] + class AsyncSerialReader(SerialReader): """Serial reader using asyncio pyserial.""" @@ -110,4 +111,3 @@ class AsyncSerialReader(SerialReader): # push new parsed telegram onto queue queue.put_nowait(self.telegram_parser.parse(telegram)) telegram = [] - diff --git a/setup.py b/setup.py index 855f005..3a919fa 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( packages=find_packages(), install_requires=[ 'pyserial>=3,<4', - 'pyserial-asyncio<1' + 'pyserial-asyncio<1', 'pytz' ], entry_points={ From f41fe0894c5a60d4822b247022e555909072842f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 12 Nov 2016 10:56:16 +0100 Subject: [PATCH 018/152] changelog --- CHANGELOG.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dbebfea..881fd97 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,12 +1,12 @@ Change Log ---------- -**0.3** +**0.3** (2016-11-12) -- Added asyncio reader for non-blocking reads. +- Added asyncio reader for non-blocking reads. (thanks to https://github.com/aequitas) **0.2** (2016-11-08) -- User https://github.com/aequitas added support for DMSR version 2.2 +- Added support for DMSR version 2.2 (thanks to https://github.com/aequitas) **0.1** (2016-08-22) From e7ff8f2444276eeb1c786a55cf8f67dd25582417 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Tue, 15 Nov 2016 23:00:59 +0100 Subject: [PATCH 019/152] Even parity offers better compatibility for v2.2. --- dsmr_parser/serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 3e7aa70..3ddfc87 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -5,7 +5,7 @@ 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, From 927a4bc8e7131c3f0863e126f56238d9f652b248 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 20 Nov 2016 12:44:45 +0100 Subject: [PATCH 020/152] added DSMR v4 parsing unit test; added alternative serial settings for DSMR v2 and v4; --- README.rst | 6 +- dsmr_parser/parsers.py | 2 +- dsmr_parser/serial.py | 21 ++ test/__init__.py | 0 test/{test_parse.py => test_parse_v2_2.py} | 15 +- test/test_parse_v4_2.py | 231 +++++++++++++++++++++ 6 files changed, 265 insertions(+), 10 deletions(-) create mode 100644 test/__init__.py rename test/{test_parse.py => test_parse_v2_2.py} (69%) create mode 100644 test/test_parse_v4_2.py diff --git a/README.rst b/README.rst index 23aff2f..b6feed0 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,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 and 3.4. +DSMR Parser currently supports DSMR versions 2.2 and 4.x. It has been tested with Python 3.4 and 3.5. Examples @@ -50,6 +50,9 @@ Using the serial reader to connect to your smart meter and parse it's telegrams: # See dsmr_reader.obis_references for all readable telegram values. +The dsmr_parser.serial module contains multiple settings that should work in +most cases. For example: if SERIAL_SETTINGS_V4 doesn't work, then try +SERIAL_SETTINGS_V4_EVEN too. Installation ------------ @@ -64,6 +67,5 @@ To install DSMR Parser: TODO ---- -- add unit tests - verify telegram checksum - improve ease of use diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 4b49a68..0e86a2a 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -36,7 +36,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) diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 6f47b1c..f9276f7 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -13,7 +13,27 @@ SERIAL_SETTINGS_V2_2 = { 'timeout': 20 } +SERIAL_SETTINGS_V2_2_EVEN = { + '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_NONE, + 'stopbits': serial.STOPBITS_ONE, + 'xonxoff': 0, + 'rtscts': 0, + 'timeout': 20 +} + +SERIAL_SETTINGS_V4_EVEN = { 'baudrate': 115200, 'bytesize': serial.SEVENBITS, 'parity': serial.PARITY_EVEN, @@ -24,6 +44,7 @@ SERIAL_SETTINGS_V4 = { } + def is_start_of_telegram(line): return line.startswith('/') diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_parse.py b/test/test_parse_v2_2.py similarity index 69% rename from test/test_parse.py rename to test/test_parse_v2_2.py index 433fd15..92717e1 100644 --- a/test/test_parse.py +++ b/test/test_parse_v2_2.py @@ -1,8 +1,8 @@ -"""Test telegram parsing.""" +"""Test parsing of a DSMR v2.2 telegram.""" 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 +from dsmr_parser import obis_references as obis TELEGRAM_V2_2 = [ "/ISk5\2MT382-1004", @@ -28,13 +28,14 @@ TELEGRAM_V2_2 = [ ] -def test_parse_v2_2(): +def test_parse(): """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' + 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' diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py new file mode 100644 index 0000000..14730a9 --- /dev/null +++ b/test/test_parse_v4_2.py @@ -0,0 +1,231 @@ +"""Test parsing of a DSMR v4.2 telegram.""" +import datetime + +from decimal import Decimal +import pytz + +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 + +TELEGRAM_V4_2 = [ + '1-3:0.2.8(42)', + '0-0:1.0.0(161113205757W)', + '0-0:96.1.1(1231231231231231231231231231231231)', + '1-0:1.8.1(001511.267*kWh)', + '1-0:1.8.2(001265.173*kWh)', + '1-0:2.8.1(000000.000*kWh)', + '1-0:2.8.2(000000.000*kWh)', + '0-0:96.14.0(0001)', + '1-0:1.7.0(00.235*kW)', + '1-0:2.7.0(00.000*kW)', + '0-0:96.7.21(00015)', + '0-0:96.7.9(00007)', + '1-0:99.97.0(3)(0-0:96.7.19)(000103180420W)(0000237126*s)(000101000001W)(2147483647*s)(000101000001W)(2147483647*s)', + '1-0:32.32.0(00000)', + '1-0:52.32.0(00000)', + '1-0:72.32.0(00000)', + '1-0:32.36.0(00000)', + '1-0:52.36.0(00000)', + '1-0:72.36.0(00000)', + '0-0:96.13.1()', + '0-0:96.13.0()', + '1-0:31.7.0(000*A)', + '1-0:51.7.0(000*A)', + '1-0:71.7.0(000*A)', + '1-0:21.7.0(00.095*kW)', + '1-0:22.7.0(00.000*kW)', + '1-0:41.7.0(00.025*kW)', + '1-0:42.7.0(00.000*kW)', + '1-0:61.7.0(00.115*kW)', + '1-0:62.7.0(00.000*kW)', + '0-1:24.1.0(003)', + '0-1:96.1.0(3404856892390357246729543587524029)', + '0-1:24.2.1(161113200000W)(00915.219*m3)', + '!5D83', +] + + +def test_parse(): + 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('1511.267') + + # 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('1265.173') + + # 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 == '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 == '1231231231231231231231231231231231' + + # 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.235') + + # 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.095') + + # 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.025') + + # 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.115') + + # 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 == '3404856892390357246729543587524029' + + # 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('915.219') + + # 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 From 616db8b1cc1013220222d8130c129109f7fb96f5 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Mon, 21 Nov 2016 13:59:48 +0100 Subject: [PATCH 021/152] Add error handling to async serial. --- dsmr_parser/serial.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index f9276f7..fbe51e6 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -1,8 +1,15 @@ -import serial import asyncio +import logging + +import serial + import serial_asyncio +from dsmr_parser.exceptions import ParseError from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2 +logger = logging.getLogger(__name__) + + SERIAL_SETTINGS_V2_2 = { 'baudrate': 9600, 'bytesize': serial.SEVENBITS, @@ -44,7 +51,6 @@ SERIAL_SETTINGS_V4_EVEN = { } - def is_start_of_telegram(line): return line.startswith('/') @@ -129,6 +135,11 @@ class AsyncSerialReader(SerialReader): telegram.append(line) if is_end_of_telegram(line): - # push new parsed telegram onto queue - queue.put_nowait(self.telegram_parser.parse(telegram)) + try: + parsed_telegram = self.telegram_parser.parse(telegram) + # push new parsed telegram onto queue + queue.put_nowait(parsed_telegram) + except ParseError: + logger.exception("failed to parse telegram") + telegram = [] From a1d077d6f24e8d8e1ba83eaf52996ee0dbbd776e Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Mon, 21 Nov 2016 14:19:12 +0100 Subject: [PATCH 022/152] Fix style. --- test/test_parse_v4_2.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 14730a9..36b1402 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -1,13 +1,13 @@ """Test parsing of a DSMR v4.2 telegram.""" import datetime - from decimal import Decimal + import pytz +from dsmr_parser import obis_references as obis +from dsmr_parser import telegram_specifications 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 TELEGRAM_V4_2 = [ '1-3:0.2.8(42)', @@ -22,7 +22,8 @@ TELEGRAM_V4_2 = [ '1-0:2.7.0(00.000*kW)', '0-0:96.7.21(00015)', '0-0:96.7.9(00007)', - '1-0:99.97.0(3)(0-0:96.7.19)(000103180420W)(0000237126*s)(000101000001W)(2147483647*s)(000101000001W)(2147483647*s)', + ('1-0:99.97.0(3)(0-0:96.7.19)(000103180420W)(0000237126*s)' + '(000101000001W)(2147483647*s)(000101000001W)(2147483647*s)'), '1-0:32.32.0(00000)', '1-0:52.32.0(00000)', '1-0:72.32.0(00000)', From f8a3c76c684146621a0793616cd00dc940ccd54a Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Wed, 16 Nov 2016 10:16:45 +0100 Subject: [PATCH 023/152] wip async test --- .envrc | 1 + test/test_async.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 2 ++ 3 files changed, 60 insertions(+) create mode 100644 .envrc create mode 100644 test/test_async.py diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..94840b3 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +layout python3 diff --git a/test/test_async.py b/test/test_async.py new file mode 100644 index 0000000..b8a05cb --- /dev/null +++ b/test/test_async.py @@ -0,0 +1,57 @@ +"""Test async read/parse.""" + +import pytest +import asyncio +from dsmr_parser.serial import AsyncSerialReader, SERIAL_SETTINGS_V2_2 +from dsmr_parser.obis_references import CURRENT_ELECTRICITY_USAGE, GAS_METER_READING +from dsmr_parser import telegram_specifications + +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)", + "!", +] + + +@pytest.mark.asyncio +def test_async_read(event_loop, mocker): + """Test async read and parse.""" + + mock_open_serial_connection = mocker.patch('serial_asyncio.open_serial_connection') + mock_open_serial_connection.return_value = (mocker.stub(), None) + + queue = asyncio.Queue() + + serial_reader = AsyncSerialReader( + device='/dev/ttyUSB0', + serial_settings=SERIAL_SETTINGS_V2_2, + telegram_specification=telegram_specifications.V2_2, + ) + + event_loop.run_until_complete(serial_reader.read(queue)) + + assert not queue.get_nowait() + + result = yield from queue.get() + + 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' diff --git a/tox.ini b/tox.ini index 9bdde36..9acedbc 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ envlist = py34,py35 deps= pytest pylama + pytest-asyncio + pytest-mock commands= py.test test {posargs} pylama dsmr_parser test From e3569e07199ad4db955017a88bdaef413e8c828a Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Mon, 21 Nov 2016 15:47:29 +0100 Subject: [PATCH 024/152] Add asyncio protocol implementation. --- .envrc | 1 - dsmr_parser/__main__.py | 30 ++++++++------ dsmr_parser/protocol.py | 90 +++++++++++++++++++++++++++++++++++++++++ test/test_async.py | 57 -------------------------- test/test_protocol.py | 39 ++++++++++++++++++ tox.ini | 1 + 6 files changed, 148 insertions(+), 70 deletions(-) delete mode 100644 .envrc create mode 100644 dsmr_parser/protocol.py delete mode 100644 test/test_async.py create mode 100644 test/test_protocol.py diff --git a/.envrc b/.envrc deleted file mode 100644 index 94840b3..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -layout python3 diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index 92c0dce..5cb6b72 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -1,6 +1,8 @@ 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 .protocol import create_dsmr_reader def console(): @@ -11,22 +13,26 @@ def console(): help='port to read DSMR data from') 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() + + conn = create_dsmr_reader(args.device, args.version, print_callback, loop=loop) + + loop.create_task(conn) + loop.run_forever() diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py new file mode 100644 index 0000000..a4e3c1a --- /dev/null +++ b/dsmr_parser/protocol.py @@ -0,0 +1,90 @@ +"""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 TelegramParser, TelegramParserV2_2 +from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, + is_end_of_telegram) + + +def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): + """Creates a DSMR asyncio protocol coroutine.""" + + 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 = TelegramParser + serial_settings = SERIAL_SETTINGS_V4 + + serial_settings['url'] = port + + protocol = partial(DSMRProtocol, loop, telegram_parser(specifications), + telegram_callback=telegram_callback) + + conn = create_serial_connection(loop, protocol, **serial_settings) + + 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 = '' + + 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.""" + while "\r\n" in self.buffer: + line, self.buffer = self.buffer.split("\r\n", 1) + self.log.debug('got line: %s', line) + 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") + + def connection_lost(self, exc): + """Stop when connection is lost.""" + self.log.error('disconnected') + + 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) diff --git a/test/test_async.py b/test/test_async.py deleted file mode 100644 index b8a05cb..0000000 --- a/test/test_async.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Test async read/parse.""" - -import pytest -import asyncio -from dsmr_parser.serial import AsyncSerialReader, SERIAL_SETTINGS_V2_2 -from dsmr_parser.obis_references import CURRENT_ELECTRICITY_USAGE, GAS_METER_READING -from dsmr_parser import telegram_specifications - -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)", - "!", -] - - -@pytest.mark.asyncio -def test_async_read(event_loop, mocker): - """Test async read and parse.""" - - mock_open_serial_connection = mocker.patch('serial_asyncio.open_serial_connection') - mock_open_serial_connection.return_value = (mocker.stub(), None) - - queue = asyncio.Queue() - - serial_reader = AsyncSerialReader( - device='/dev/ttyUSB0', - serial_settings=SERIAL_SETTINGS_V2_2, - telegram_specification=telegram_specifications.V2_2, - ) - - event_loop.run_until_complete(serial_reader.read(queue)) - - assert not queue.get_nowait() - - result = yield from queue.get() - - 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' diff --git a/test/test_protocol.py b/test/test_protocol.py new file mode 100644 index 0000000..79e7aa7 --- /dev/null +++ b/test/test_protocol.py @@ -0,0 +1,39 @@ +"""Test DSMR serial protocol.""" + +from unittest.mock import Mock + +import pytest +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 .test_parse_v2_2 import TELEGRAM_V2_2 + + +@pytest.fixture +def protocol(): + """DSMRprotocol instance with mocked telegram_callback.""" + + parser = TelegramParserV2_2 + specification = telegram_specifications.V2_2 + + telegram_parser = parser(specification) + return DSMRProtocol(None, telegram_parser, + telegram_callback=Mock()) + + +def test_complete_packet(protocol): + """Protocol should assemble incoming lines into complete packet.""" + + for line in TELEGRAM_V2_2: + protocol.data_received(bytes(line + '\r\n', 'ascii')) + + telegram = 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' diff --git a/tox.ini b/tox.ini index 9acedbc..616fc67 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ deps= pytest pylama pytest-asyncio + pytest-catchlog pytest-mock commands= py.test test {posargs} From 5e8818230183dac0aa6018172066bba2a294c959 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Mon, 21 Nov 2016 17:02:32 +0100 Subject: [PATCH 025/152] Complete telegram handling logic. --- dsmr_parser/protocol.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index a4e3c1a..812d17c 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -10,7 +10,7 @@ from . import telegram_specifications from .exceptions import ParseError from .parsers import TelegramParser, TelegramParserV2_2 from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, - is_end_of_telegram) + is_end_of_telegram, is_start_of_telegram) def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): @@ -70,6 +70,12 @@ class DSMRProtocol(asyncio.Protocol): while "\r\n" in self.buffer: line, self.buffer = self.buffer.split("\r\n", 1) self.log.debug('got line: %s', line) + + # 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: @@ -77,6 +83,7 @@ class DSMRProtocol(asyncio.Protocol): 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.""" From c058feca5f55e43d3e6837e439711761940db3f1 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 21 Nov 2016 21:30:56 +0100 Subject: [PATCH 026/152] make serial.EVEN the default parity for DSMR v2.2 --- CHANGELOG.rst | 11 ++++++++--- README.rst | 22 +++++++++++++--------- dsmr_parser/serial.py | 20 -------------------- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 881fd97..c4c8635 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,13 +1,18 @@ Change Log ---------- + +**0.4** (2016-11-21) +- DSMR v2.2 serial settings now uses parity serial.EVEN by default (`issue #4 `_) +- improved asyncio reader and improve it's error handling (`pull request #5 `_) + **0.3** (2016-11-12) -- Added asyncio reader for non-blocking reads. (thanks to https://github.com/aequitas) +- asyncio reader for non-blocking reads. (`pull request #3 `_) **0.2** (2016-11-08) -- Added support for DMSR version 2.2 (thanks to https://github.com/aequitas) +- support for DMSR version 2.2 (`pull request #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 diff --git a/README.rst b/README.rst index b6feed0..d750628 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,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,25 +34,22 @@ 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. -The dsmr_parser.serial module contains multiple settings that should work in -most cases. For example: if SERIAL_SETTINGS_V4 doesn't work, then try -SERIAL_SETTINGS_V4_EVEN too. Installation ------------ @@ -63,6 +60,13 @@ To install DSMR Parser: $ pip install dsmr-parser +Known issues +------------ + +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. TODO ---- diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index fbe51e6..30dc451 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -11,16 +11,6 @@ logger = logging.getLogger(__name__) SERIAL_SETTINGS_V2_2 = { - 'baudrate': 9600, - 'bytesize': serial.SEVENBITS, - 'parity': serial.PARITY_NONE, - 'stopbits': serial.STOPBITS_ONE, - 'xonxoff': 0, - 'rtscts': 0, - 'timeout': 20 -} - -SERIAL_SETTINGS_V2_2_EVEN = { 'baudrate': 9600, 'bytesize': serial.SEVENBITS, 'parity': serial.PARITY_EVEN, @@ -31,16 +21,6 @@ SERIAL_SETTINGS_V2_2_EVEN = { } SERIAL_SETTINGS_V4 = { - 'baudrate': 115200, - 'bytesize': serial.SEVENBITS, - 'parity': serial.PARITY_NONE, - 'stopbits': serial.STOPBITS_ONE, - 'xonxoff': 0, - 'rtscts': 0, - 'timeout': 20 -} - -SERIAL_SETTINGS_V4_EVEN = { 'baudrate': 115200, 'bytesize': serial.SEVENBITS, 'parity': serial.PARITY_EVEN, From 0ac2990df2e8c459306cf8aaf171ec92f8004c57 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 21 Nov 2016 21:46:21 +0100 Subject: [PATCH 027/152] accepted changed from pull request instead --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c4c8635..850d0c1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,7 @@ Change Log ---------- **0.4** (2016-11-21) -- DSMR v2.2 serial settings now uses parity serial.EVEN by default (`issue #4 `_) +- DSMR v2.2 serial settings now uses parity serial.EVEN by default (`issue #4 `_) - improved asyncio reader and improve it's error handling (`pull request #5 `_) **0.3** (2016-11-12) From e330e9db21e5a5230bdf283d56f0447ad61dbdd8 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 21 Nov 2016 21:48:17 +0100 Subject: [PATCH 028/152] updated changelog --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 850d0c1..21f9a40 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,8 +2,8 @@ Change Log ---------- **0.4** (2016-11-21) -- DSMR v2.2 serial settings now uses parity serial.EVEN by default (`issue #4 `_) -- improved asyncio reader and improve it's error handling (`pull request #5 `_) +- DSMR v2.2 serial settings now uses parity serial.EVEN by default (`pull request #5 `_) +- improved asyncio reader and improve it's error handling (`pull request #8 `_) **0.3** (2016-11-12) From 5ae6ad41567143c73c6bd074c9baa4857cf496ca Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 21 Nov 2016 21:49:17 +0100 Subject: [PATCH 029/152] typo --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 21f9a40..dfdb440 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,7 @@ Change Log ---------- **0.4** (2016-11-21) + - DSMR v2.2 serial settings now uses parity serial.EVEN by default (`pull request #5 `_) - improved asyncio reader and improve it's error handling (`pull request #8 `_) From a3685f031021bdb8ea5937a01128089be3da99db Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 21 Nov 2016 22:31:22 +0100 Subject: [PATCH 030/152] added Travis CI badge --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index d750628..a1f06a0 100644 --- a/README.rst +++ b/README.rst @@ -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. From 819d0d0696762428c71b1c29fa5953fdbb5d6abc Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 22 Nov 2016 19:54:19 +0100 Subject: [PATCH 031/152] updated version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3a919fa..04bb7b9 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.3', + version='0.4', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 4df6ba75a22606d9fec14db3a4552dfe400b317c Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 26 Nov 2016 15:33:58 +0100 Subject: [PATCH 032/152] used python unittest for the tests --- test/test_parse_v2_2.py | 19 +-- test/test_parse_v4_2.py | 306 ++++++++++++++++++++-------------------- 2 files changed, 164 insertions(+), 161 deletions(-) diff --git a/test/test_parse_v2_2.py b/test/test_parse_v2_2.py index 92717e1..4031961 100644 --- a/test/test_parse_v2_2.py +++ b/test/test_parse_v2_2.py @@ -1,4 +1,4 @@ -"""Test parsing of a DSMR v2.2 telegram.""" +import unittest from dsmr_parser.parsers import TelegramParserV2_2 from dsmr_parser import telegram_specifications @@ -28,14 +28,15 @@ TELEGRAM_V2_2 = [ ] -def test_parse(): - """Test if telegram parsing results in correct results.""" +class TelegramParserV2_2Test(unittest.TestCase): + """ Test parsing of a DSMR v2.2 telegram. """ - parser = TelegramParserV2_2(telegram_specifications.V2_2) - result = parser.parse(TELEGRAM_V2_2) + 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.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' + assert float(result[obis.GAS_METER_READING].value) == 1.001 + assert result[obis.GAS_METER_READING].unit == 'm3' diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 36b1402..36773e7 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -1,6 +1,6 @@ -"""Test parsing of a DSMR v4.2 telegram.""" -import datetime from decimal import Decimal +import datetime +import unittest import pytz @@ -47,186 +47,188 @@ TELEGRAM_V4_2 = [ '!5D83', ] +class TelegramParserV4_2Test(unittest.TestCase): + """ Test parsing of a DSMR v4.2 telegram. """ -def test_parse(): - parser = TelegramParser(telegram_specifications.V4) - result = parser.parse(TELEGRAM_V4_2) + 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_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) + # 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('1511.267') + # 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('1511.267') - # 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('1265.173') + # 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('1265.173') - # 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_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_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 == '0001' + # 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 == '1231231231231231231231231231231231' + # 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 == '1231231231231231231231231231231231' - # 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.235') + # 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.235') - # 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') + # 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 + # 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_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_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_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_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_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 + # 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_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 + # 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 + # 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.095') + # 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.095') - # 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.025') + # 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.025') - # 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.115') + # 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.115') - # 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_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_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') + # 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 == '3404856892390357246729543587524029' + # 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 == '3404856892390357246729543587524029' - # 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('915.219') + # 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('915.219') - # POWER_EVENT_FAILURE_LOG (99.97.0) - # TODO to be implemented + # POWER_EVENT_FAILURE_LOG (99.97.0) + # TODO to be implemented - # ACTUAL_TRESHOLD_ELECTRICITY (0-0:17.0.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 + # ACTUAL_SWITCH_POSITION (0-0:96.3.10) + # TODO to be implemented - # VALVE_POSITION_GAS (0-x:24.4.0) - # TODO to be implemented + # VALVE_POSITION_GAS (0-x:24.4.0) + # TODO to be implemented From 81fd581e57987ecb35ab875c4c202a9f2b8a483d Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 26 Nov 2016 15:58:24 +0100 Subject: [PATCH 033/152] pep8 --- test/test_parse_v4_2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 36773e7..bb0b67a 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -47,6 +47,7 @@ TELEGRAM_V4_2 = [ '!5D83', ] + class TelegramParserV4_2Test(unittest.TestCase): """ Test parsing of a DSMR v4.2 telegram. """ From 220db73fbed357acbb9c92a236dc4166b8bb0534 Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Sun, 27 Nov 2016 20:07:13 +0100 Subject: [PATCH 034/152] Add code coverage with pytest-cov --- .coverage | 1 + .coveragerc | 2 ++ .gitignore | 2 ++ tox.ini | 3 ++- 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .coverage create mode 100644 .coveragerc diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..fa5354f --- /dev/null +++ b/.coverage @@ -0,0 +1 @@ +!coverage.py: This is a private format, don't read it directly!{"arcs": {"/home/dennis/workspace/dsmr_parser/dsmr_parser/objects.py": [[4, -3], [1, 3], [22, 24], [24, 28], [48, -1], [28, 32], [22, 37], [-13, 15], [13, 17], [32, -22], [-37, 37], [39, 43], [7, 22], [-22, 22], [-39, 41], [-3, 4], [7, 9], [48, 49], [9, 13], [-1, 1], [-48, 48], [15, -13], [-43, 45], [37, 48], [34, -32], [-28, 30], [37, 39], [3, -1], [-7, 7], [45, -43], [19, -17], [17, -7], [1, 7], [30, -28], [49, -48], [-17, 19], [-32, 34], [41, -39], [43, -37]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/exceptions.py": [[1, 2], [-1, 1], [2, -1], [1, -1]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/obis_references.py": [[17, 18], [10, 11], [21, 22], [23, 24], [25, 26], [5, 6], [33, 36], [28, 29], [9, 10], [22, 23], [2, 3], [13, 14], [29, 30], [3, 4], [11, 12], [19, 20], [18, 19], [16, 17], [1, 2], [8, 9], [-1, 1], [6, 7], [12, 13], [15, 16], [27, 28], [24, 25], [4, 5], [40, 41], [41, -1], [37, 40], [30, 31], [14, 15], [31, 32], [20, 21], [7, 8], [26, 27], [36, 37], [32, 33]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/telegram_specifications.py": [[1, 3], [22, 23], [23, 24], [28, 29], [18, 19], [71, 72], [31, 32], [19, 20], [61, 62], [60, 61], [32, 33], [24, 25], [-1, 1], [65, 66], [58, 59], [64, 65], [72, -1], [52, 53], [55, 56], [66, 67], [27, 28], [56, 57], [38, 43], [33, 34], [47, 48], [4, 5], [63, 64], [20, 21], [35, 36], [36, 37], [48, 49], [68, 69], [21, 22], [25, 26], [29, 30], [5, 17], [53, 55], [62, 63], [50, 51], [70, 71], [69, 70], [37, 38], [43, 44], [17, 18], [26, 27], [45, 46], [59, 60], [49, 50], [57, 58], [44, 45], [67, 68], [30, 31], [46, 47], [51, 52], [3, 4], [34, 35]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/parsers.py": [[86, 87], [41, 43], [135, -134], [-169, 171], [78, 81], [38, -11], [-166, 167], [87, -75], [83, 86], [24, -20], [81, -81], [-86, 86], [28, 38], [54, 67], [109, 110], [44, 45], [90, 115], [36, -28], [109, 112], [138, 164], [86, -86], [75, -70], [105, 107], [18, -13], [59, 62], [-115, 115], [13, 20], [-90, 90], [-20, 22], [107, -90], [-81, 81], [-28, 29], [81, 83], [167, -166], [67, -51], [4, 5], [173, 174], [72, 75], [-50, 50], [63, 58], [51, -50], [110, -107], [11, 50], [43, 44], [26, -20], [70, 72], [-51, 54], [6, 8], [181, 182], [45, -38], [34, 31], [112, -107], [134, -115], [11, 13], [171, 173], [-75, 77], [81, 81], [-72, 73], [-13, 18], [22, 23], [158, 160], [5, 6], [23, 22], [22, 26], [169, -164], [-134, 135], [115, 138], [-138, 138], [31, 32], [73, -72], [-107, 108], [39, 41], [1, 2], [-1, 1], [138, 158], [58, 59], [164, 166], [108, 109], [29, 31], [90, 105], [178, 181], [50, 70], [-11, 11], [-54, 55], [32, 34], [55, 57], [160, -138], [166, 169], [60, 61], [8, 11], [62, 65], [31, 36], [62, 63], [-164, 164], [50, 51], [58, -54], [-38, 39], [70, 90], [23, 24], [87, 86], [-70, 70], [182, -169], [132, 134], [77, 78], [61, 58], [174, 178], [115, 132], [2, 4], [57, 58], [65, 58], [20, 28], [173, 178], [43, 47], [59, 60], [164, -1], [47, -38]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/value_types.py": [[9, 10], [-1, 1], [1, 3], [12, 14], [6, -1], [8, 9], [-6, 8], [10, 14], [14, 15], [3, 6], [17, -6], [9, 12], [15, 17]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/serial.py": [[16, 17], [34, 38], [44, 46], [39, -38], [28, 29], [30, 34], [46, 56], [15, 16], [-38, 39], [85, 87], [14, 15], [-82, 82], [56, -42], [42, 82], [26, 27], [17, 18], [19, 20], [1, 2], [25, 26], [-1, 1], [-42, 42], [6, 7], [4, 6], [20, 24], [-34, 35], [27, 28], [38, 42], [42, 44], [8, 10], [35, -34], [24, 25], [29, 30], [87, -82], [10, 14], [82, 83], [18, 19], [82, -1], [7, 8], [2, 4], [83, 85]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/__main__.py": [], "/home/dennis/workspace/dsmr_parser/dsmr_parser/protocol.py": [[54, -44], [1, 3], [10, 11], [94, 96], [80, 70], [86, 70], [41, 42], [46, 47], [83, 86], [12, 16], [92, -38], [63, 64], [-68, 70], [39, 41], [65, 66], [-1, 1], [56, 61], [70, -68], [42, 44], [44, 56], [68, 88], [3, 4], [-38, 38], [47, 48], [-44, 46], [80, 81], [52, 54], [88, 92], [4, 5], [97, -92], [38, -1], [9, 10], [-61, 63], [66, -61], [96, 97], [70, 71], [11, 12], [-92, 94], [7, 9], [64, 65], [16, 38], [76, 79], [38, 39], [72, 76], [71, 72], [5, 7], [48, 50], [81, 82], [82, 83], [61, 68], [79, 80], [50, 52]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/__init__.py": [[-1, 1], [1, -1]]}} \ No newline at end of file diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..398ff08 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +branch = True diff --git a/.gitignore b/.gitignore index 83f3764..7c84973 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .tox .cache *.egg-info +/.project +/.pydevproject diff --git a/tox.ini b/tox.ini index 616fc67..88cbe5c 100644 --- a/tox.ini +++ b/tox.ini @@ -4,12 +4,13 @@ envlist = py34,py35 [testenv] deps= pytest + pytest-cov pylama pytest-asyncio pytest-catchlog pytest-mock commands= - py.test test {posargs} + py.test --cov=dsmr_parser test {posargs} pylama dsmr_parser test [pylama:pylint] From b228bd524b819cfa45f6c7ca984500fa8ba2ee3a Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Sun, 27 Nov 2016 20:29:49 +0100 Subject: [PATCH 035/152] Add code coverage with Codecov in Travis --- .travis.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e09c73f..83613fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 From 19d3f60aec236735d380f6a13b8631b289027edf Mon Sep 17 00:00:00 2001 From: Dennis Siemensma Date: Sun, 27 Nov 2016 20:40:52 +0100 Subject: [PATCH 036/152] Ignore .coverage file --- .coverage | 1 - .gitignore | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index fa5354f..0000000 --- a/.coverage +++ /dev/null @@ -1 +0,0 @@ -!coverage.py: This is a private format, don't read it directly!{"arcs": {"/home/dennis/workspace/dsmr_parser/dsmr_parser/objects.py": [[4, -3], [1, 3], [22, 24], [24, 28], [48, -1], [28, 32], [22, 37], [-13, 15], [13, 17], [32, -22], [-37, 37], [39, 43], [7, 22], [-22, 22], [-39, 41], [-3, 4], [7, 9], [48, 49], [9, 13], [-1, 1], [-48, 48], [15, -13], [-43, 45], [37, 48], [34, -32], [-28, 30], [37, 39], [3, -1], [-7, 7], [45, -43], [19, -17], [17, -7], [1, 7], [30, -28], [49, -48], [-17, 19], [-32, 34], [41, -39], [43, -37]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/exceptions.py": [[1, 2], [-1, 1], [2, -1], [1, -1]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/obis_references.py": [[17, 18], [10, 11], [21, 22], [23, 24], [25, 26], [5, 6], [33, 36], [28, 29], [9, 10], [22, 23], [2, 3], [13, 14], [29, 30], [3, 4], [11, 12], [19, 20], [18, 19], [16, 17], [1, 2], [8, 9], [-1, 1], [6, 7], [12, 13], [15, 16], [27, 28], [24, 25], [4, 5], [40, 41], [41, -1], [37, 40], [30, 31], [14, 15], [31, 32], [20, 21], [7, 8], [26, 27], [36, 37], [32, 33]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/telegram_specifications.py": [[1, 3], [22, 23], [23, 24], [28, 29], [18, 19], [71, 72], [31, 32], [19, 20], [61, 62], [60, 61], [32, 33], [24, 25], [-1, 1], [65, 66], [58, 59], [64, 65], [72, -1], [52, 53], [55, 56], [66, 67], [27, 28], [56, 57], [38, 43], [33, 34], [47, 48], [4, 5], [63, 64], [20, 21], [35, 36], [36, 37], [48, 49], [68, 69], [21, 22], [25, 26], [29, 30], [5, 17], [53, 55], [62, 63], [50, 51], [70, 71], [69, 70], [37, 38], [43, 44], [17, 18], [26, 27], [45, 46], [59, 60], [49, 50], [57, 58], [44, 45], [67, 68], [30, 31], [46, 47], [51, 52], [3, 4], [34, 35]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/parsers.py": [[86, 87], [41, 43], [135, -134], [-169, 171], [78, 81], [38, -11], [-166, 167], [87, -75], [83, 86], [24, -20], [81, -81], [-86, 86], [28, 38], [54, 67], [109, 110], [44, 45], [90, 115], [36, -28], [109, 112], [138, 164], [86, -86], [75, -70], [105, 107], [18, -13], [59, 62], [-115, 115], [13, 20], [-90, 90], [-20, 22], [107, -90], [-81, 81], [-28, 29], [81, 83], [167, -166], [67, -51], [4, 5], [173, 174], [72, 75], [-50, 50], [63, 58], [51, -50], [110, -107], [11, 50], [43, 44], [26, -20], [70, 72], [-51, 54], [6, 8], [181, 182], [45, -38], [34, 31], [112, -107], [134, -115], [11, 13], [171, 173], [-75, 77], [81, 81], [-72, 73], [-13, 18], [22, 23], [158, 160], [5, 6], [23, 22], [22, 26], [169, -164], [-134, 135], [115, 138], [-138, 138], [31, 32], [73, -72], [-107, 108], [39, 41], [1, 2], [-1, 1], [138, 158], [58, 59], [164, 166], [108, 109], [29, 31], [90, 105], [178, 181], [50, 70], [-11, 11], [-54, 55], [32, 34], [55, 57], [160, -138], [166, 169], [60, 61], [8, 11], [62, 65], [31, 36], [62, 63], [-164, 164], [50, 51], [58, -54], [-38, 39], [70, 90], [23, 24], [87, 86], [-70, 70], [182, -169], [132, 134], [77, 78], [61, 58], [174, 178], [115, 132], [2, 4], [57, 58], [65, 58], [20, 28], [173, 178], [43, 47], [59, 60], [164, -1], [47, -38]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/value_types.py": [[9, 10], [-1, 1], [1, 3], [12, 14], [6, -1], [8, 9], [-6, 8], [10, 14], [14, 15], [3, 6], [17, -6], [9, 12], [15, 17]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/serial.py": [[16, 17], [34, 38], [44, 46], [39, -38], [28, 29], [30, 34], [46, 56], [15, 16], [-38, 39], [85, 87], [14, 15], [-82, 82], [56, -42], [42, 82], [26, 27], [17, 18], [19, 20], [1, 2], [25, 26], [-1, 1], [-42, 42], [6, 7], [4, 6], [20, 24], [-34, 35], [27, 28], [38, 42], [42, 44], [8, 10], [35, -34], [24, 25], [29, 30], [87, -82], [10, 14], [82, 83], [18, 19], [82, -1], [7, 8], [2, 4], [83, 85]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/__main__.py": [], "/home/dennis/workspace/dsmr_parser/dsmr_parser/protocol.py": [[54, -44], [1, 3], [10, 11], [94, 96], [80, 70], [86, 70], [41, 42], [46, 47], [83, 86], [12, 16], [92, -38], [63, 64], [-68, 70], [39, 41], [65, 66], [-1, 1], [56, 61], [70, -68], [42, 44], [44, 56], [68, 88], [3, 4], [-38, 38], [47, 48], [-44, 46], [80, 81], [52, 54], [88, 92], [4, 5], [97, -92], [38, -1], [9, 10], [-61, 63], [66, -61], [96, 97], [70, 71], [11, 12], [-92, 94], [7, 9], [64, 65], [16, 38], [76, 79], [38, 39], [72, 76], [71, 72], [5, 7], [48, 50], [81, 82], [82, 83], [61, 68], [79, 80], [50, 52]], "/home/dennis/workspace/dsmr_parser/dsmr_parser/__init__.py": [[-1, 1], [1, -1]]}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7c84973..1da5fee 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ *.egg-info /.project /.pydevproject +/.coverage From 1c69b4e9ee08bc3a5c6e88bcaf06633f5f7664dc Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 28 Dec 2016 20:29:34 +0100 Subject: [PATCH 037/152] added telegram CRC verification --- dsmr_parser/exceptions.py | 4 ++ dsmr_parser/parsers.py | 54 ++++++++++++++++- dsmr_parser/serial.py | 29 ++++++--- dsmr_parser/value_types.py | 3 +- setup.py | 5 +- test/test_parse_v2_2.py | 40 ++++++------- test/test_parse_v4_2.py | 118 ++++++++++++++++++++++--------------- test/test_protocol.py | 25 +++++++- tox.ini | 1 + 9 files changed, 198 insertions(+), 81 deletions(-) diff --git a/dsmr_parser/exceptions.py b/dsmr_parser/exceptions.py index 831cca9..a5fa8f4 100644 --- a/dsmr_parser/exceptions.py +++ b/dsmr_parser/exceptions.py @@ -1,2 +1,6 @@ class ParseError(Exception): pass + + +class InvalidChecksumError(ParseError): + pass diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 0e86a2a..6b513b9 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -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,7 +30,10 @@ 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 @@ -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(self, TelegramParserV4).parse(line_values) + + class TelegramParserV2_2(TelegramParser): + def parse(self, line_values): """Join lines for gas meter.""" diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 30dc451..fa70c81 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -1,11 +1,11 @@ import asyncio import logging - import serial - import serial_asyncio + from dsmr_parser.exceptions import ParseError -from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2 +from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2, \ + TelegramParserV4 logger = logging.getLogger(__name__) @@ -32,15 +32,20 @@ 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): @@ -49,8 +54,11 @@ class SerialReader(object): 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): @@ -65,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. @@ -75,7 +83,12 @@ 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 = [] @@ -119,7 +132,7 @@ class AsyncSerialReader(SerialReader): parsed_telegram = self.telegram_parser.parse(telegram) # push new parsed telegram onto queue queue.put_nowait(parsed_telegram) - except ParseError: - logger.exception("failed to parse telegram") + except ParseError as e: + logger.warning('Failed to parse telegram: %s', e) telegram = [] diff --git a/dsmr_parser/value_types.py b/dsmr_parser/value_types.py index 48a9146..4bc9ef3 100644 --- a/dsmr_parser/value_types.py +++ b/dsmr_parser/value_types.py @@ -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: diff --git a/setup.py b/setup.py index 04bb7b9..dca1c3d 100644 --- a/setup.py +++ b/setup.py @@ -6,12 +6,13 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.4', + version='0.5', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', 'pyserial-asyncio<1', - 'pytz' + 'pytz', + 'PyCRC>=1.2,<2' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] diff --git a/test/test_parse_v2_2.py b/test/test_parse_v2_2.py index 4031961..b36a466 100644 --- a/test/test_parse_v2_2.py +++ b/test/test_parse_v2_2.py @@ -5,26 +5,26 @@ 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)", - "!", + '/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)', + '!', ] diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index bb0b67a..58e757e 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -6,51 +6,77 @@ 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 dsmr_parser.parsers import TelegramParser, TelegramParserV4 TELEGRAM_V4_2 = [ - '1-3:0.2.8(42)', - '0-0:1.0.0(161113205757W)', - '0-0:96.1.1(1231231231231231231231231231231231)', - '1-0:1.8.1(001511.267*kWh)', - '1-0:1.8.2(001265.173*kWh)', - '1-0:2.8.1(000000.000*kWh)', - '1-0:2.8.2(000000.000*kWh)', - '0-0:96.14.0(0001)', - '1-0:1.7.0(00.235*kW)', - '1-0:2.7.0(00.000*kW)', - '0-0:96.7.21(00015)', - '0-0:96.7.9(00007)', - ('1-0:99.97.0(3)(0-0:96.7.19)(000103180420W)(0000237126*s)' - '(000101000001W)(2147483647*s)(000101000001W)(2147483647*s)'), - '1-0:32.32.0(00000)', - '1-0:52.32.0(00000)', - '1-0:72.32.0(00000)', - '1-0:32.36.0(00000)', - '1-0:52.36.0(00000)', - '1-0:72.36.0(00000)', - '0-0:96.13.1()', - '0-0:96.13.0()', - '1-0:31.7.0(000*A)', - '1-0:51.7.0(000*A)', - '1-0:71.7.0(000*A)', - '1-0:21.7.0(00.095*kW)', - '1-0:22.7.0(00.000*kW)', - '1-0:41.7.0(00.025*kW)', - '1-0:42.7.0(00.000*kW)', - '1-0:61.7.0(00.115*kW)', - '1-0:62.7.0(00.000*kW)', - '0-1:24.1.0(003)', - '0-1:96.1.0(3404856892390357246729543587524029)', - '0-1:24.2.1(161113200000W)(00915.219*m3)', - '!5D83', + '/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) @@ -72,13 +98,13 @@ class TelegramParserV4_2Test(unittest.TestCase): 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('1511.267') + 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('1265.173') + 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) @@ -96,19 +122,19 @@ class TelegramParserV4_2Test(unittest.TestCase): 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' + 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 == '1231231231231231231231231231231231' + 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('0.235') + 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) @@ -178,19 +204,19 @@ class TelegramParserV4_2Test(unittest.TestCase): 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.095') + 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('0.025') + 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.115') + 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) @@ -214,13 +240,13 @@ class TelegramParserV4_2Test(unittest.TestCase): 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 == '3404856892390357246729543587524029' + 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('915.219') + assert result[obis.HOURLY_GAS_METER_READING].value == Decimal('981.443') # POWER_EVENT_FAILURE_LOG (99.97.0) # TODO to be implemented diff --git a/test/test_protocol.py b/test/test_protocol.py index 79e7aa7..121e4cd 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -3,12 +3,35 @@ from unittest.mock import Mock import pytest + 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 .test_parse_v2_2 import TELEGRAM_V2_2 + +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)", + "!", +] @pytest.fixture diff --git a/tox.ini b/tox.ini index 616fc67..0f667a1 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ deps= pytest-asyncio pytest-catchlog pytest-mock + PyCRC commands= py.test test {posargs} pylama dsmr_parser test From 4b392522c3d0e680567cddce69cb0926f781adce Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 29 Dec 2016 10:08:07 +0100 Subject: [PATCH 038/152] Removed todo list from readme --- README.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.rst b/README.rst index a1f06a0..ca62ede 100644 --- a/README.rst +++ b/README.rst @@ -70,9 +70,3 @@ 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. - -TODO ----- - -- verify telegram checksum -- improve ease of use From b3014823c1893df13b47618b8f71044d6e65f003 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 29 Dec 2016 19:20:50 +0100 Subject: [PATCH 039/152] bugfix; updated async client to CRC check --- dsmr_parser/parsers.py | 2 +- dsmr_parser/protocol.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 6b513b9..cf91e2c 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -91,7 +91,7 @@ class TelegramParserV4(TelegramParser): def parse(self, line_values): self.validate_telegram_checksum(line_values) - return super(self, TelegramParserV4).parse(line_values) + return super().parse(line_values) class TelegramParserV2_2(TelegramParser): diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index 812d17c..d2270e0 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -8,9 +8,15 @@ from serial_asyncio import create_serial_connection from . import telegram_specifications from .exceptions import ParseError -from .parsers import TelegramParser, TelegramParserV2_2 -from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, - is_end_of_telegram, is_start_of_telegram) +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_reader(port, dsmr_version, telegram_callback, loop=None): @@ -22,7 +28,7 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): serial_settings = SERIAL_SETTINGS_V2_2 elif dsmr_version == '4': specifications = telegram_specifications.V4 - telegram_parser = TelegramParser + telegram_parser = TelegramParserV4 serial_settings = SERIAL_SETTINGS_V4 serial_settings['url'] = port From c2a67bff6da6690ffa21cba507a06fe015c90808 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 29 Dec 2016 19:30:35 +0100 Subject: [PATCH 040/152] version v0.5 changelog --- CHANGELOG.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dfdb440..52481b1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.5** (2016-12-29) + +- CRC checksum verification for DSMR v4 telegrams (`issue #10 `_) + **0.4** (2016-11-21) - DSMR v2.2 serial settings now uses parity serial.EVEN by default (`pull request #5 `_) @@ -8,7 +12,7 @@ Change Log **0.3** (2016-11-12) -- asyncio reader for non-blocking reads. (`pull request #3 `_) +- asyncio reader for non-blocking reads (`pull request #3 `_) **0.2** (2016-11-08) From cdc9e395aa4d3ef37f9bddb2d4ce904a21eec3eb Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Thu, 8 Dec 2016 22:14:43 +0100 Subject: [PATCH 041/152] Add support for TCP connections. --- dsmr_parser/protocol.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index d2270e0..ea9d867 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -19,8 +19,8 @@ from .serial import ( ) -def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): - """Creates a DSMR asyncio protocol coroutine.""" +def creater_dsmr_protocol(dsmr_version, telegram_callback, loop=None): + """Creates a DSMR asyncio protocol.""" if dsmr_version == '2.2': specifications = telegram_specifications.V2_2 @@ -31,13 +31,27 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): telegram_parser = TelegramParserV4 serial_settings = SERIAL_SETTINGS_V4 - serial_settings['url'] = port - protocol = partial(DSMRProtocol, loop, telegram_parser(specifications), telegram_callback=telegram_callback) - conn = create_serial_connection(loop, protocol, **serial_settings) + 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 = creater_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, _ = creater_dsmr_protocol( + dsmr_version, telegram_callback, loop=None) + conn = loop.create_connection(protocol, host, port) return conn From 763237ef1d433fbc3118b42b6d5774a51d104869 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Tue, 3 Jan 2017 21:27:10 +0100 Subject: [PATCH 042/152] Add TCP arguments to console. Implement reconnect logic in protocol. --- dsmr_parser/__main__.py | 35 +++++++++++++++++++++++++++++++---- dsmr_parser/protocol.py | 28 +++++++++++++++++----------- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index 5cb6b72..8813731 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -1,8 +1,9 @@ import argparse import asyncio import logging +from functools import partial -from .protocol import create_dsmr_reader +from .protocol import create_dsmr_reader, create_tcp_dsmr_reader def console(): @@ -11,6 +12,10 @@ 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') @@ -32,7 +37,29 @@ def console(): print(obj.value, obj.unit) print() - conn = create_dsmr_reader(args.device, args.version, print_callback, loop=loop) + # 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) - loop.create_task(conn) - loop.run_forever() + 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() diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index ea9d867..f2a0e96 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -8,15 +8,9 @@ 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 .parsers import TelegramParserV2_2, TelegramParserV4 +from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, + is_end_of_telegram, is_start_of_telegram) def creater_dsmr_protocol(dsmr_version, telegram_callback, loop=None): @@ -47,7 +41,8 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): return conn -def create_tcp_dsmr_reader(host, port, dsmr_version, telegram_callback, loop=None): +def create_tcp_dsmr_reader(host, port, dsmr_version, + telegram_callback, loop=None): """Creates a DSMR asyncio protocol coroutine using TCP connection.""" protocol, _ = creater_dsmr_protocol( dsmr_version, telegram_callback, loop=None) @@ -72,6 +67,8 @@ class DSMRProtocol(asyncio.Protocol): 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.""" @@ -107,7 +104,11 @@ class DSMRProtocol(asyncio.Protocol): def connection_lost(self, exc): """Stop when connection is lost.""" - self.log.error('disconnected') + 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.""" @@ -115,3 +116,8 @@ class DSMRProtocol(asyncio.Protocol): if self.telegram_callback: self.telegram_callback(telegram) + + @asyncio.coroutine + def wait_closed(self): + """Wait until connection is closed.""" + yield from self._closed.wait() From 3c9db523fa76180beccc0777444dd9d753260f42 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Tue, 3 Jan 2017 22:27:39 +0100 Subject: [PATCH 043/152] Fix tpyo. --- dsmr_parser/protocol.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index f2a0e96..336f7e7 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -13,7 +13,7 @@ from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, is_end_of_telegram, is_start_of_telegram) -def creater_dsmr_protocol(dsmr_version, telegram_callback, loop=None): +def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): """Creates a DSMR asyncio protocol.""" if dsmr_version == '2.2': @@ -33,7 +33,7 @@ def creater_dsmr_protocol(dsmr_version, telegram_callback, loop=None): def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): """Creates a DSMR asyncio protocol coroutine using serial port.""" - protocol, serial_settings = creater_dsmr_protocol( + protocol, serial_settings = create_dsmr_protocol( dsmr_version, telegram_callback, loop=None) serial_settings['url'] = port @@ -44,7 +44,7 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): def create_tcp_dsmr_reader(host, port, dsmr_version, telegram_callback, loop=None): """Creates a DSMR asyncio protocol coroutine using TCP connection.""" - protocol, _ = creater_dsmr_protocol( + protocol, _ = create_dsmr_protocol( dsmr_version, telegram_callback, loop=None) conn = loop.create_connection(protocol, host, port) return conn From e512456cc2acfe0df34778273da04a389e9db04c Mon Sep 17 00:00:00 2001 From: Alex Mekkering Date: Wed, 4 Jan 2017 10:21:47 +0100 Subject: [PATCH 044/152] Fixed CRC calculation --- dsmr_parser/parsers.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index cf91e2c..431b61c 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -61,22 +61,19 @@ class TelegramParserV4(TelegramParser): :raises InvalidChecksumError: """ - full_telegram = ''.join(line_values) + full_telegram = '\r\n'.join(line_values) # Extract the bytes that count towards the checksum. - checksum_contents = re.search(r'\/.+\!', full_telegram, re.DOTALL) + contents = re.search(r'(\/.+\!)([0-9A-Z]{4})', 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: + if not contents: 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) + calculated_crc = CRC16().calculate(contents.group(1)) + expected_crc = contents.group(2) expected_crc = int(expected_crc, base=16) if calculated_crc != expected_crc: From ce4d5b0e62670c922c372eb29ad3463a84dc98ca Mon Sep 17 00:00:00 2001 From: Alex Mekkering Date: Wed, 4 Jan 2017 10:51:29 +0100 Subject: [PATCH 045/152] Corrected unit test for failing CRC --- test/test_parse_v4_2.py | 72 ++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 58e757e..897c5f3 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -11,43 +11,43 @@ 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', + '/KFM5KAIFA-METER', + '', + '1-3:0.2.8(42)', + '0-0:1.0.0(161113205757W)', + '0-0:96.1.1(3960221976967177082151037881335713)', + '1-0:1.8.1(001581.123*kWh)', + '1-0:1.8.2(001435.706*kWh)', + '1-0:2.8.1(000000.000*kWh)', + '1-0:2.8.2(000000.000*kWh)', + '0-0:96.14.0(0002)', + '1-0:1.7.0(02.027*kW)', + '1-0:2.7.0(00.000*kW)', + '0-0:96.7.21(00015)', + '0-0:96.7.9(00007)', '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' + '(2147583646*s)(000102000003W)(2317482647*s)', + '1-0:32.32.0(00000)', + '1-0:52.32.0(00000)', + '1-0:72.32.0(00000)', + '1-0:32.36.0(00000)', + '1-0:52.36.0(00000)', + '1-0:72.36.0(00000)', + '0-0:96.13.1()', + '0-0:96.13.0()', + '1-0:31.7.0(000*A)', + '1-0:51.7.0(006*A)', + '1-0:71.7.0(002*A)', + '1-0:21.7.0(00.170*kW)', + '1-0:22.7.0(00.000*kW)', + '1-0:41.7.0(01.247*kW)', + '1-0:42.7.0(00.000*kW)', + '1-0:61.7.0(00.209*kW)', + '1-0:62.7.0(00.000*kW)', + '0-1:24.1.0(003)', + '0-1:96.1.0(4819243993373755377509728609491464)', + '0-1:24.2.1(161129200000W)(00981.443*m3)', + '!6796' ] From 920c9aedc2ff7075b76304ddae097e98fefbe4ef Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 4 Jan 2017 11:58:03 +0100 Subject: [PATCH 046/152] Revert "Fixed CRC calculation" --- dsmr_parser/parsers.py | 13 +++++--- test/test_parse_v4_2.py | 72 ++++++++++++++++++++--------------------- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 431b61c..cf91e2c 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -61,19 +61,22 @@ class TelegramParserV4(TelegramParser): :raises InvalidChecksumError: """ - full_telegram = '\r\n'.join(line_values) + full_telegram = ''.join(line_values) # Extract the bytes that count towards the checksum. - contents = re.search(r'(\/.+\!)([0-9A-Z]{4})', full_telegram, re.DOTALL) + checksum_contents = re.search(r'\/.+\!', full_telegram, re.DOTALL) - if not contents: + # 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(contents.group(1)) - expected_crc = contents.group(2) + 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: diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 897c5f3..58e757e 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -11,43 +11,43 @@ from dsmr_parser.objects import CosemObject, MBusObject from dsmr_parser.parsers import TelegramParser, TelegramParserV4 TELEGRAM_V4_2 = [ - '/KFM5KAIFA-METER', - '', - '1-3:0.2.8(42)', - '0-0:1.0.0(161113205757W)', - '0-0:96.1.1(3960221976967177082151037881335713)', - '1-0:1.8.1(001581.123*kWh)', - '1-0:1.8.2(001435.706*kWh)', - '1-0:2.8.1(000000.000*kWh)', - '1-0:2.8.2(000000.000*kWh)', - '0-0:96.14.0(0002)', - '1-0:1.7.0(02.027*kW)', - '1-0:2.7.0(00.000*kW)', - '0-0:96.7.21(00015)', - '0-0:96.7.9(00007)', + '/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)', - '1-0:32.32.0(00000)', - '1-0:52.32.0(00000)', - '1-0:72.32.0(00000)', - '1-0:32.36.0(00000)', - '1-0:52.36.0(00000)', - '1-0:72.36.0(00000)', - '0-0:96.13.1()', - '0-0:96.13.0()', - '1-0:31.7.0(000*A)', - '1-0:51.7.0(006*A)', - '1-0:71.7.0(002*A)', - '1-0:21.7.0(00.170*kW)', - '1-0:22.7.0(00.000*kW)', - '1-0:41.7.0(01.247*kW)', - '1-0:42.7.0(00.000*kW)', - '1-0:61.7.0(00.209*kW)', - '1-0:62.7.0(00.000*kW)', - '0-1:24.1.0(003)', - '0-1:96.1.0(4819243993373755377509728609491464)', - '0-1:24.2.1(161129200000W)(00981.443*m3)', - '!6796' + '(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 03b761e15b20a378d2279e7041574359cf37dfdf Mon Sep 17 00:00:00 2001 From: Alex Mekkering Date: Wed, 4 Jan 2017 14:49:18 +0100 Subject: [PATCH 047/152] Pass lines to parser including line endings --- dsmr_parser/protocol.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index d2270e0..0712189 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -73,9 +73,11 @@ class DSMRProtocol(asyncio.Protocol): def handle_lines(self): """Assemble incoming data into single lines.""" - while "\r\n" in self.buffer: - line, self.buffer = self.buffer.split("\r\n", 1) + 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. From 8b60d48edd8fccf584243874875f0c6e21da26ce Mon Sep 17 00:00:00 2001 From: Alex Mekkering Date: Wed, 4 Jan 2017 15:01:20 +0100 Subject: [PATCH 048/152] pycodestyle fixes --- dsmr_parser/protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index 0712189..60c2cc8 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -77,7 +77,7 @@ class DSMRProtocol(asyncio.Protocol): 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 + 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. From 3b43cbf841b91a796309b04e07ebbfe510ddeb6a Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 4 Jan 2017 19:55:54 +0100 Subject: [PATCH 049/152] all tests are written using unittest.TestCase now --- test/test_protocol.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/test/test_protocol.py b/test/test_protocol.py index 121e4cd..41c68b4 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -1,9 +1,7 @@ """Test DSMR serial protocol.""" - +import unittest from unittest.mock import Mock -import pytest - from dsmr_parser import obis_references as obis from dsmr_parser import telegram_specifications from dsmr_parser.parsers import TelegramParserV2_2 @@ -34,29 +32,27 @@ TELEGRAM_V2_2 = [ ] -@pytest.fixture -def protocol(): - """DSMRprotocol instance with mocked telegram_callback.""" +class ProtocolTest(unittest.TestCase): - parser = TelegramParserV2_2 - specification = telegram_specifications.V2_2 + def setUp(self): + parser = TelegramParserV2_2 + specification = telegram_specifications.V2_2 - telegram_parser = parser(specification) - return DSMRProtocol(None, telegram_parser, - telegram_callback=Mock()) + 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.""" -def test_complete_packet(protocol): - """Protocol should assemble incoming lines into complete packet.""" + for line in TELEGRAM_V2_2: + self.protocol.data_received(bytes(line + '\r\n', 'ascii')) - for line in TELEGRAM_V2_2: - protocol.data_received(bytes(line + '\r\n', 'ascii')) + telegram = self.protocol.telegram_callback.call_args_list[0][0][0] + assert isinstance(telegram, dict) - telegram = 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.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' + assert float(telegram[obis.GAS_METER_READING].value) == 1.001 + assert telegram[obis.GAS_METER_READING].unit == 'm3' From 29fc97a65cf3bd6f39f99309a8c9eecbea314d67 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 4 Jan 2017 20:02:08 +0100 Subject: [PATCH 050/152] updated changelog --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 52481b1..5f28f0c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Change Log ---------- +**0.6** (2017-01-04) + +- Fixed bug in CRC checksum verification for the asyncio client (`pull request #15 `_) +- Support added for TCP connections using the asyncio client (`pull request #12 `_) + **0.5** (2016-12-29) - CRC checksum verification for DSMR v4 telegrams (`issue #10 `_) From 1373d570d25815003413bd5f4116934fbf5a284d Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 4 Jan 2017 20:06:09 +0100 Subject: [PATCH 051/152] updated version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dca1c3d..f60708f 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.5', + version='0.6', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From f10032f701544453ca21c28fc7b3ba478e2b7192 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 5 Jan 2017 21:24:41 +0100 Subject: [PATCH 052/152] refactored TelegramParser.parse to accept a str instead of list --- dsmr_parser/parsers.py | 88 +++++++++++++++++++++++--------------- dsmr_parser/protocol.py | 16 ++++--- dsmr_parser/serial.py | 42 +++++++++---------- test/test_parse_v2_2.py | 44 +++++++++---------- test/test_parse_v4_2.py | 93 ++++++++++++++++++++--------------------- test/test_protocol.py | 51 +++++++++++----------- 6 files changed, 180 insertions(+), 154 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index cf91e2c..a215a98 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -26,48 +26,58 @@ class TelegramParser(object): return None, None - def parse(self, line_values): - telegram = {} + def parse(self, telegram): + """ + Parse telegram from string to dict. - for line_value in line_values: - # TODO temporarily strip newline characters. - line_value = line_value.strip() + The telegram str type makes python 2.x integration easier. - obis_reference, dsmr_object = self.parse_line(line_value) + :param str telegram: full telegram from start ('/') to checksum + ('!ABCD') including line endings inbetween the telegram's lines + :rtype: dict + :returns: Shortened example: + { + .. + r'0-0:96\.1\.1': , # EQUIPMENT_IDENTIFIER + r'1-0:1\.8\.1': , # ELECTRICITY_USED_TARIFF_1 + r'0-\d:24\.3\.0': , # GAS_METER_READING + .. + } + """ + telegram_lines = telegram.splitlines() + parsed_lines = map(self.parse_line, telegram_lines) - telegram[obis_reference] = dsmr_object + return {obis_reference: dsmr_object + for obis_reference, dsmr_object in parsed_lines} - return telegram + def parse_line(self, line): + logger.debug("Parsing line '%s'", line) - def parse_line(self, line_value): - logger.debug('Parsing line \'%s\'', line_value) + obis_reference, parser = self._find_line_parser(line) - obis_reference, parser = self._find_line_parser(line_value) - - if not parser: - logger.warning("No line class found for: '%s'", line_value) + if not obis_reference: + logger.debug("No line class found for: '%s'", line) return None, None - return obis_reference, parser.parse(line_value) + return obis_reference, parser.parse(line) class TelegramParserV4(TelegramParser): @staticmethod - def validate_telegram_checksum(line_values): + def validate_telegram_checksum(telegram): """ - :type line_values: list + :param str telegram: :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 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 +86,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,31 +97,44 @@ class TelegramParserV4(TelegramParser): ) ) - def parse(self, line_values): - self.validate_telegram_checksum(line_values) + def parse(self, telegram): + """ + :param str telegram: + :rtype: dict + """ + self.validate_telegram_checksum(telegram) - return super().parse(line_values) + return super().parse(telegram) class TelegramParserV2_2(TelegramParser): - def parse(self, line_values): - """Join lines for gas meter.""" + def parse(self, telegram): + """ + :param str telegram: + :rtype: dict + """ - def join_lines(line_values): + # TODO fix this in the specification: telegram_specifications.V2_2 + def join_lines(telegram): + """Join lines for gas meter.""" join_next = re.compile(GAS_METER_READING) join = None - for line_value in line_values: + for line_value in telegram.splitlines(): if join: - yield join.strip() + line_value + yield join + line_value join = None elif join_next.match(line_value): join = line_value else: yield line_value - return super().parse(join_lines(line_values)) + # TODO temporary workaround + lines = join_lines(telegram) + telegram = '\r\n'.join(lines) + + return super().parse(telegram) class DSMRObjectParser(object): diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index 5d1a8c7..dc5c54e 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -24,6 +24,9 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): specifications = telegram_specifications.V4 telegram_parser = TelegramParserV4 serial_settings = SERIAL_SETTINGS_V4 + else: + raise NotImplementedError("No telegram parser found for version: %s", + dsmr_version) protocol = partial(DSMRProtocol, loop, telegram_parser(specifications), telegram_callback=telegram_callback) @@ -64,7 +67,7 @@ class DSMRProtocol(asyncio.Protocol): # callback to call on complete telegram self.telegram_callback = telegram_callback # buffer to keep incoming telegram lines - self.telegram = [] + self.telegram = '' # buffer to keep incomplete incoming data self.buffer = '' # keep a lock until the connection is closed @@ -77,7 +80,7 @@ class DSMRProtocol(asyncio.Protocol): def data_received(self, data): """Add incoming data to buffer.""" - data = data.decode() + data = data.decode('ascii') self.log.debug('received data: %s', data.strip()) self.buffer += data self.handle_lines() @@ -95,13 +98,16 @@ class DSMRProtocol(asyncio.Protocol): if not self.telegram and not is_start_of_telegram(line): continue - self.telegram.append(line) + self.telegram += line + if is_end_of_telegram(line): try: parsed_telegram = self.telegram_parser.parse(self.telegram) + except ParseError as e: + self.log.error('Failed to parse telegram: %s', e) + else: self.handle_telegram(parsed_telegram) - except ParseError: - self.log.exception("failed to parse telegram") + self.telegram = [] def connection_lost(self, exc): diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index fa70c81..edd0efd 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -33,14 +33,16 @@ SERIAL_SETTINGS_V4 = { def is_start_of_telegram(line): """ - :type line: line + :param bytes line: series of bytes representing a line. + Example: b'/KFM5KAIFA-METER\r\n' """ return line.startswith('/') def is_end_of_telegram(line): """ - :type line: line + :param bytes line: series of bytes representing a line. + Example: b'!7B05\r\n' """ return line.startswith('!') @@ -66,30 +68,28 @@ class SerialReader(object): Read complete DSMR telegram's from the serial interface and parse it into CosemObject's and MbusObject's - :rtype dict + :rtype: generator """ with serial.Serial(**self.serial_settings) as serial_handle: - telegram = [] + telegram = '' while True: line = serial_handle.readline() - line = line.decode('ascii') # TODO move this to the parser? + line.decode('ascii') - # Telegrams need to be complete because the values belong to a - # particular reading and can also be related to eachother. + # Build up buffer from the start of the telegram. if not telegram and not is_start_of_telegram(line): continue - telegram.append(line) + telegram += 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 = [] + telegram = '' class AsyncSerialReader(SerialReader): @@ -106,33 +106,33 @@ class AsyncSerialReader(SerialReader): Instead of being a generator, values are pushed to provided queue for asynchronous processing. - :rtype Generator/Async + :rtype: None """ # create Serial StreamReader conn = serial_asyncio.open_serial_connection(**self.serial_settings) reader, _ = yield from conn - telegram = [] + telegram = '' while True: - # read line if available or give control back to loop until - # new data has arrived + # 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. + # Build up buffer from the start of the telegram. if not telegram and not is_start_of_telegram(line): continue - telegram.append(line) + telegram += 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) + # 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) - telegram = [] + telegram = '' diff --git a/test/test_parse_v2_2.py b/test/test_parse_v2_2.py index b36a466..1d6b504 100644 --- a/test/test_parse_v2_2.py +++ b/test/test_parse_v2_2.py @@ -4,28 +4,28 @@ 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)', - '!', -] +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 TelegramParserV2_2Test(unittest.TestCase): diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 58e757e..4a2085d 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -10,45 +10,45 @@ 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', +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', + '(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): @@ -56,26 +56,25 @@ class TelegramParserV4_2Test(unittest.TestCase): def test_valid(self): # No exception is raised. - TelegramParserV4.validate_telegram_checksum( - TELEGRAM_V4_2 - ) + TelegramParserV4.validate_telegram_checksum(TELEGRAM_V4_2) def test_invalid(self): - # Remove one the electricty used data value. This causes the checksum to + # Remove 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] + corrupted_telegram = TELEGRAM_V4_2.replace( + '1-0:1.8.1(001581.123*kWh)\r\n', + '' + ) with self.assertRaises(InvalidChecksumError): - TelegramParserV4.validate_telegram_checksum(telegram) + TelegramParserV4.validate_telegram_checksum(corrupted_telegram) def test_missing_checksum(self): # Remove the checksum value causing a ParseError. - telegram = TELEGRAM_V4_2[:-1] + corrupted_telegram = TELEGRAM_V4_2.replace('!6796\r\n', '') with self.assertRaises(ParseError): - TelegramParserV4.validate_telegram_checksum(telegram) + TelegramParserV4.validate_telegram_checksum(corrupted_telegram) def test_parse(self): parser = TelegramParser(telegram_specifications.V4) diff --git a/test/test_protocol.py b/test/test_protocol.py index 41c68b4..d779bc7 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -1,35 +1,35 @@ -"""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 -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): @@ -45,8 +45,7 @@ class ProtocolTest(unittest.TestCase): 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) From d990a316ad9b15c5b4f7fcee3aec4068e69f0ef0 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 7 Jan 2017 11:25:43 +0100 Subject: [PATCH 053/152] finishing implementation of TelegramBuffer --- dsmr_parser/serial.py | 48 +++++++++++++++++++++ test/example_telegrams.py | 62 ++++++++++++++++++++++++++++ test/telegram_buffer.py | 87 +++++++++++++++++++++++++++++++++++++++ test/test_parse_v2_2.py | 24 +---------- test/test_parse_v4_2.py | 41 +----------------- 5 files changed, 199 insertions(+), 63 deletions(-) create mode 100644 test/example_telegrams.py create mode 100644 test/telegram_buffer.py diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index edd0efd..3e51f48 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -1,5 +1,6 @@ import asyncio import logging +import re import serial import serial_asyncio @@ -136,3 +137,50 @@ class AsyncSerialReader(SerialReader): logger.warning('Failed to parse telegram: %s', e) telegram = '' + + +class TelegramBuffer(object): + + def __init__(self, callback): + self._callback = callback + self._buffer = '' + + def append(self, data): + """ + Add telegram data to buffer. The callback is called with a full telegram + when data is complete. + :param str data: chars or lines of telegram data + :return: + """ + self._buffer += data + + for telegram in self.find_telegrams(self._buffer): + self._callback(telegram) + self._remove(telegram) + + 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:] + + @staticmethod + def find_telegrams(buffer): + """ + Find complete telegrams from 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}\r\n', buffer, re.DOTALL) diff --git a/test/example_telegrams.py b/test/example_telegrams.py new file mode 100644 index 0000000..eb4e8d4 --- /dev/null +++ b/test/example_telegrams.py @@ -0,0 +1,62 @@ +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_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' +) diff --git a/test/telegram_buffer.py b/test/telegram_buffer.py new file mode 100644 index 0000000..92df5b9 --- /dev/null +++ b/test/telegram_buffer.py @@ -0,0 +1,87 @@ +from unittest import mock, TestCase +from unittest.mock import call + +from dsmr_parser.serial import TelegramBuffer +from test.example_telegrams import TELEGRAM_V2_2, TELEGRAM_V4_2 + + +class TelegramBufferTest(TestCase): + + def setUp(self): + self.callback = mock.MagicMock() + self.telegram_buffer = TelegramBuffer(self.callback) + + def test_v22_telegram(self): + self.telegram_buffer.append(TELEGRAM_V2_2) + + self.callback.assert_called_once_with(TELEGRAM_V2_2) + self.assertEqual(self.telegram_buffer._buffer, '') + + def test_v42_telegram(self): + self.telegram_buffer.append(TELEGRAM_V4_2) + + self.callback.assert_called_once_with(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)) + ) + + self.callback.assert_has_calls([ + call(TELEGRAM_V2_2), + call(TELEGRAM_V4_2), + call(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) + + self.callback.assert_called_once_with(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) + + self.callback.assert_called_once_with(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) + + self.callback.assert_called_once_with(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) + + self.callback.assert_called_once_with(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) + + self.callback.assert_called_once_with(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) + + self.callback.assert_called_once_with(TELEGRAM_V4_2) + self.assertEqual(self.telegram_buffer._buffer, '') diff --git a/test/test_parse_v2_2.py b/test/test_parse_v2_2.py index 1d6b504..eaaa7ee 100644 --- a/test/test_parse_v2_2.py +++ b/test/test_parse_v2_2.py @@ -1,32 +1,10 @@ import unittest +from .example_telegrams import TELEGRAM_V2_2 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\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 TelegramParserV2_2Test(unittest.TestCase): """ Test parsing of a DSMR v2.2 telegram. """ diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 4a2085d..a7904fc 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -4,52 +4,13 @@ import unittest import pytz +from .example_telegrams import TELEGRAM_V4_2 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. """ From 60317a0dc52cc63918e9b884fc1ebcbad99ea4ef Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 7 Jan 2017 21:26:21 +0100 Subject: [PATCH 054/152] dev progress --- dsmr_parser/protocol.py | 36 +++---------------- dsmr_parser/serial.py | 78 +++++++++++++++++------------------------ test/telegram_buffer.py | 2 -- 3 files changed, 36 insertions(+), 80 deletions(-) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index dc5c54e..aeffc10 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -10,7 +10,7 @@ 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) + is_end_of_telegram, is_start_of_telegram, TelegramBuffer) def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): @@ -66,10 +66,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(self.handle_telegram) # keep a lock until the connection is closed self._closed = asyncio.Event() @@ -81,34 +79,8 @@ class DSMRProtocol(asyncio.Protocol): def data_received(self, data): """Add incoming data to buffer.""" data = data.decode('ascii') - 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 += line - - if is_end_of_telegram(line): - try: - parsed_telegram = self.telegram_parser.parse(self.telegram) - except ParseError as e: - self.log.error('Failed to parse telegram: %s', e) - else: - self.handle_telegram(parsed_telegram) - - self.telegram = [] + self.log.debug('received data: %s', data) + self.telegram_buffer.append(data) def connection_lost(self, exc): """Stop when connection is lost.""" diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 3e51f48..8aebb03 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -63,6 +63,7 @@ class SerialReader(object): telegram_parser = TelegramParser self.telegram_parser = telegram_parser(telegram_specification) + self.telegram_buffer = TelegramBuffer(self.handle_telegram) def read(self): """ @@ -72,25 +73,15 @@ class SerialReader(object): :rtype: generator """ with serial.Serial(**self.serial_settings) as serial_handle: - telegram = '' - while True: - line = serial_handle.readline() - line.decode('ascii') + data = serial_handle.readline() + self.telegram_buffer.append(data.decode('ascii')) - # Build up buffer from the start of the telegram. - if not telegram and not is_start_of_telegram(line): - continue - - telegram += 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 = '' + def handle_telegram(self, telegram): + try: + yield self.telegram_parser.parse(telegram) + except ParseError as e: + logger.error('Failed to parse telegram: %s', e) class AsyncSerialReader(SerialReader): @@ -113,48 +104,40 @@ class AsyncSerialReader(SerialReader): 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') + data = yield from reader.readline() + self.telegram_buffer.append(data.decode('ascii')) - # Build up buffer from the start of the telegram. - if not telegram and not is_start_of_telegram(line): - continue - - telegram += line - - if is_end_of_telegram(line): - 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) - - telegram = '' + # TODO + # 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) class TelegramBuffer(object): + """ + Used as a buffer for a stream or telegram data. Returns telegram from buffer + when complete. + """ def __init__(self, callback): - self._callback = callback self._buffer = '' + self._callback = callback def append(self, data): """ - Add telegram data to buffer. The callback is called with a full telegram - when data is complete. - :param str data: chars or lines of telegram data - :return: + Add telegram data to buffer. + :param str data: chars, lines or full telegram strings of telegram data """ self._buffer += data - for telegram in self.find_telegrams(self._buffer): + for telegram in self._find_telegrams(): self._callback(telegram) self._remove(telegram) @@ -170,8 +153,7 @@ class TelegramBuffer(object): self._buffer = self._buffer[index:] - @staticmethod - def find_telegrams(buffer): + def _find_telegrams(self): """ Find complete telegrams from buffer from start ('/') till ending checksum ('!AB12\r\n'). @@ -183,4 +165,8 @@ class TelegramBuffer(object): # 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}\r\n', buffer, re.DOTALL) + return re.findall( + r'\/[^\/]+?\![A-F0-9]{0,4}\r\n', + self._buffer, + re.DOTALL + ) diff --git a/test/telegram_buffer.py b/test/telegram_buffer.py index 92df5b9..2cb479b 100644 --- a/test/telegram_buffer.py +++ b/test/telegram_buffer.py @@ -71,7 +71,6 @@ class TelegramBufferTest(TestCase): 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) @@ -79,7 +78,6 @@ class TelegramBufferTest(TestCase): 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) From 0e7819b535033349c096fb59c357e95c73a1ccfd Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 7 Jan 2017 22:29:02 +0100 Subject: [PATCH 055/152] dev progress --- dsmr_parser/protocol.py | 13 +++++--- dsmr_parser/serial.py | 51 ++++++++++++++++-------------- test/telegram_buffer.py | 69 +++++++++++++++++++++++++++-------------- 3 files changed, 82 insertions(+), 51 deletions(-) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index aeffc10..6342e9d 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -67,7 +67,7 @@ class DSMRProtocol(asyncio.Protocol): # callback to call on complete telegram self.telegram_callback = telegram_callback # buffer to keep incomplete incoming data - self.telegram_buffer = TelegramBuffer(self.handle_telegram) + self.telegram_buffer = TelegramBuffer() # keep a lock until the connection is closed self._closed = asyncio.Event() @@ -80,7 +80,8 @@ class DSMRProtocol(asyncio.Protocol): """Add incoming data to buffer.""" data = data.decode('ascii') self.log.debug('received data: %s', data) - self.telegram_buffer.append(data) + self.telegram_buffer.put(data) + map(self.handle_telegram, self.telegram_buffer.get_all()) def connection_lost(self, exc): """Stop when connection is lost.""" @@ -94,8 +95,12 @@ 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 ParseError: + self.log.exception("failed to parse telegram") + else: + self.telegram_callback(parsed_telegram) @asyncio.coroutine def wait_closed(self): diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 8aebb03..5ed809e 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -63,7 +63,7 @@ class SerialReader(object): telegram_parser = TelegramParser self.telegram_parser = telegram_parser(telegram_specification) - self.telegram_buffer = TelegramBuffer(self.handle_telegram) + self.telegram_buffer = TelegramBuffer() def read(self): """ @@ -75,13 +75,14 @@ class SerialReader(object): with serial.Serial(**self.serial_settings) as serial_handle: while True: data = serial_handle.readline() - self.telegram_buffer.append(data.decode('ascii')) + self.telegram_buffer.put(data.decode('ascii')) + + for telegram in self.telegram_buffer.get_all(): + try: + yield self.telegram_parser.parse(telegram) + except ParseError as e: + logger.error('Failed to parse telegram: %s', e) - def handle_telegram(self, telegram): - try: - yield self.telegram_parser.parse(telegram) - except ParseError as e: - logger.error('Failed to parse telegram: %s', e) class AsyncSerialReader(SerialReader): @@ -108,16 +109,16 @@ class AsyncSerialReader(SerialReader): # 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')) + self.telegram_buffer.put(data.decode('ascii')) - # TODO - # 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) + 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) class TelegramBuffer(object): @@ -126,21 +127,25 @@ class TelegramBuffer(object): when complete. """ - def __init__(self, callback): + def __init__(self): self._buffer = '' - self._callback = callback - def append(self, data): + 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 put(self, data): """ Add telegram data to buffer. :param str data: chars, lines or full telegram strings of telegram data """ self._buffer += data - for telegram in self._find_telegrams(): - self._callback(telegram) - self._remove(telegram) - def _remove(self, telegram): """ Remove telegram from buffer and incomplete data preceding it. This diff --git a/test/telegram_buffer.py b/test/telegram_buffer.py index 2cb479b..17a3643 100644 --- a/test/telegram_buffer.py +++ b/test/telegram_buffer.py @@ -8,40 +8,51 @@ from test.example_telegrams import TELEGRAM_V2_2, TELEGRAM_V4_2 class TelegramBufferTest(TestCase): def setUp(self): - self.callback = mock.MagicMock() - self.telegram_buffer = TelegramBuffer(self.callback) + self.telegram_buffer = TelegramBuffer() def test_v22_telegram(self): - self.telegram_buffer.append(TELEGRAM_V2_2) + self.telegram_buffer.put(TELEGRAM_V2_2) - self.callback.assert_called_once_with(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) + self.telegram_buffer.put(TELEGRAM_V4_2) - self.callback.assert_called_once_with(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( + self.telegram_buffer.put( ''.join((TELEGRAM_V2_2, TELEGRAM_V4_2, TELEGRAM_V2_2)) ) - self.callback.assert_has_calls([ - call(TELEGRAM_V2_2), - call(TELEGRAM_V4_2), - call(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) + self.telegram_buffer.put(incomplete_telegram + TELEGRAM_V4_2) - self.callback.assert_called_once_with(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): @@ -49,37 +60,47 @@ class TelegramBufferTest(TestCase): # 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) + self.telegram_buffer.put(incomplete_telegram + TELEGRAM_V4_2) - self.callback.assert_called_once_with(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) + self.telegram_buffer.put(TELEGRAM_V4_2 + incomplete_telegram) - self.callback.assert_called_once_with(TELEGRAM_V4_2) + 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) + self.telegram_buffer.put(TELEGRAM_V4_2 + incomplete_telegram) - self.callback.assert_called_once_with(TELEGRAM_V4_2) + 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) + self.telegram_buffer.put(line) - self.callback.assert_called_once_with(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_adding_char_by_char(self): for char in TELEGRAM_V4_2: - self.telegram_buffer.append(char) + self.telegram_buffer.put(char) - self.callback.assert_called_once_with(TELEGRAM_V4_2) + telegram = next(self.telegram_buffer.get_all()) + + self.assertEqual(telegram, TELEGRAM_V4_2) self.assertEqual(self.telegram_buffer._buffer, '') From 663024239ffb574485ac3bc6f34fe80e06de3e59 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 8 Jan 2017 11:24:04 +0100 Subject: [PATCH 056/152] dev progress --- test/{telegram_buffer.py => test_telegram_buffer.py} | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename test/{telegram_buffer.py => test_telegram_buffer.py} (97%) diff --git a/test/telegram_buffer.py b/test/test_telegram_buffer.py similarity index 97% rename from test/telegram_buffer.py rename to test/test_telegram_buffer.py index 17a3643..8bbdcd5 100644 --- a/test/telegram_buffer.py +++ b/test/test_telegram_buffer.py @@ -1,11 +1,10 @@ -from unittest import mock, TestCase -from unittest.mock import call +import unittest from dsmr_parser.serial import TelegramBuffer from test.example_telegrams import TELEGRAM_V2_2, TELEGRAM_V4_2 -class TelegramBufferTest(TestCase): +class TelegramBufferTest(unittest.TestCase): def setUp(self): self.telegram_buffer = TelegramBuffer() From 87a5a2d2facac24169e8fc204f7b337544af35d8 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 8 Jan 2017 11:28:15 +0100 Subject: [PATCH 057/152] dev progress --- dsmr_parser/protocol.py | 7 ++++--- dsmr_parser/serial.py | 24 ++++-------------------- test/test_parse_v2_2.py | 2 +- test/test_parse_v4_2.py | 2 +- 4 files changed, 10 insertions(+), 25 deletions(-) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index 6342e9d..2ee9c32 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -9,8 +9,7 @@ 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, TelegramBuffer) +from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4,TelegramBuffer) def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): @@ -81,7 +80,9 @@ class DSMRProtocol(asyncio.Protocol): data = data.decode('ascii') self.log.debug('received data: %s', data) self.telegram_buffer.put(data) - map(self.handle_telegram, self.telegram_buffer.get_all()) + + for telegram in self.telegram_buffer.get_all(): + self.handle_telegram(telegram) def connection_lost(self, exc): """Stop when connection is lost.""" diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 5ed809e..54128b3 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -32,22 +32,6 @@ SERIAL_SETTINGS_V4 = { } -def is_start_of_telegram(line): - """ - :param bytes line: series of bytes representing a line. - Example: b'/KFM5KAIFA-METER\r\n' - """ - return line.startswith('/') - - -def is_end_of_telegram(line): - """ - :param bytes line: series of bytes representing a line. - Example: b'!7B05\r\n' - """ - return line.startswith('!') - - class SerialReader(object): PORT_KEY = 'port' @@ -123,8 +107,8 @@ class AsyncSerialReader(SerialReader): class TelegramBuffer(object): """ - Used as a buffer for a stream or telegram data. Returns telegram from buffer - when complete. + Used as a buffer for a stream of telegram data. Constructs full telegram + strings from the buffered data and returns it. """ def __init__(self): @@ -132,7 +116,7 @@ class TelegramBuffer(object): def get_all(self): """ - Remove complete telegrams from buffer and yield them + Remove complete telegrams from buffer and yield them. :rtype generator: """ for telegram in self._find_telegrams(): @@ -160,7 +144,7 @@ class TelegramBuffer(object): def _find_telegrams(self): """ - Find complete telegrams from buffer from start ('/') till ending + Find complete telegrams in buffer from start ('/') till ending checksum ('!AB12\r\n'). :rtype: list """ diff --git a/test/test_parse_v2_2.py b/test/test_parse_v2_2.py index eaaa7ee..26c6d49 100644 --- a/test/test_parse_v2_2.py +++ b/test/test_parse_v2_2.py @@ -1,6 +1,6 @@ import unittest -from .example_telegrams import TELEGRAM_V2_2 +from test.example_telegrams import TELEGRAM_V2_2 from dsmr_parser.parsers import TelegramParserV2_2 from dsmr_parser import telegram_specifications from dsmr_parser import obis_references as obis diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index a7904fc..1048f87 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -4,7 +4,7 @@ import unittest import pytz -from .example_telegrams import TELEGRAM_V4_2 +from test.example_telegrams import TELEGRAM_V4_2 from dsmr_parser import obis_references as obis from dsmr_parser import telegram_specifications from dsmr_parser.exceptions import InvalidChecksumError, ParseError From 11672d05127b48123251d7fe839e0a1d48b4e930 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 8 Jan 2017 11:44:20 +0100 Subject: [PATCH 058/152] pep8 --- dsmr_parser/protocol.py | 2 +- dsmr_parser/serial.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index 2ee9c32..f31748f 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -9,7 +9,7 @@ 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,TelegramBuffer) +from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, TelegramBuffer) def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 54128b3..1d92ba8 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -68,7 +68,6 @@ class SerialReader(object): logger.error('Failed to parse telegram: %s', e) - class AsyncSerialReader(SerialReader): """Serial reader using asyncio pyserial.""" From 21334e5a0ae4e64985e2d6bc9512ac5610be65a4 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 9 Jan 2017 20:15:55 +0100 Subject: [PATCH 059/152] changed relative imports to absolute; renamed TelegramBuffer.put to TelegramBuffer.append; --- dsmr_parser/__main__.py | 2 +- dsmr_parser/parsers.py | 6 +++--- dsmr_parser/protocol.py | 11 ++++++----- dsmr_parser/serial.py | 6 +++--- dsmr_parser/telegram_specifications.py | 6 +++--- test/test_telegram_buffer.py | 18 +++++++++--------- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index 8813731..03c9f1d 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -3,7 +3,7 @@ import asyncio import logging from functools import partial -from .protocol import create_dsmr_reader, create_tcp_dsmr_reader +from dsmr_parser.protocol import create_dsmr_reader, create_tcp_dsmr_reader def console(): diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index a215a98..d36cdb6 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,9 +3,9 @@ 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, MBusObjectV2_2, CosemObject +from dsmr_parser.exceptions import ParseError, InvalidChecksumError +from dsmr_parser.obis_references import GAS_METER_READING logger = logging.getLogger(__name__) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index f31748f..08a8732 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -6,10 +6,11 @@ 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, TelegramBuffer) +from dsmr_parser import telegram_specifications +from dsmr_parser.exceptions import ParseError +from dsmr_parser.parsers import TelegramParserV2_2, TelegramParserV4 +from dsmr_parser.serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, + TelegramBuffer) def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): @@ -79,7 +80,7 @@ class DSMRProtocol(asyncio.Protocol): """Add incoming data to buffer.""" data = data.decode('ascii') self.log.debug('received data: %s', data) - self.telegram_buffer.put(data) + self.telegram_buffer.append(data) for telegram in self.telegram_buffer.get_all(): self.handle_telegram(telegram) diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 1d92ba8..93e31b9 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -59,7 +59,7 @@ class SerialReader(object): with serial.Serial(**self.serial_settings) as serial_handle: while True: data = serial_handle.readline() - self.telegram_buffer.put(data.decode('ascii')) + self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): try: @@ -92,7 +92,7 @@ class AsyncSerialReader(SerialReader): # Read line if available or give control back to loop until new # data has arrived. data = yield from reader.readline() - self.telegram_buffer.put(data.decode('ascii')) + self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): try: @@ -122,7 +122,7 @@ class TelegramBuffer(object): self._remove(telegram) yield telegram - def put(self, data): + def append(self, data): """ Add telegram data to buffer. :param str data: chars, lines or full telegram strings of telegram data diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 958153b..adc5e62 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -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 """ diff --git a/test/test_telegram_buffer.py b/test/test_telegram_buffer.py index 8bbdcd5..7ad28ee 100644 --- a/test/test_telegram_buffer.py +++ b/test/test_telegram_buffer.py @@ -10,7 +10,7 @@ class TelegramBufferTest(unittest.TestCase): self.telegram_buffer = TelegramBuffer() def test_v22_telegram(self): - self.telegram_buffer.put(TELEGRAM_V2_2) + self.telegram_buffer.append(TELEGRAM_V2_2) telegram = next(self.telegram_buffer.get_all()) @@ -18,7 +18,7 @@ class TelegramBufferTest(unittest.TestCase): self.assertEqual(self.telegram_buffer._buffer, '') def test_v42_telegram(self): - self.telegram_buffer.put(TELEGRAM_V4_2) + self.telegram_buffer.append(TELEGRAM_V4_2) telegram = next(self.telegram_buffer.get_all()) @@ -26,7 +26,7 @@ class TelegramBufferTest(unittest.TestCase): self.assertEqual(self.telegram_buffer._buffer, '') def test_multiple_mixed_telegrams(self): - self.telegram_buffer.put( + self.telegram_buffer.append( ''.join((TELEGRAM_V2_2, TELEGRAM_V4_2, TELEGRAM_V2_2)) ) @@ -47,7 +47,7 @@ class TelegramBufferTest(unittest.TestCase): # There are unclosed telegrams at the start of the buffer. incomplete_telegram = TELEGRAM_V4_2[:-1] - self.telegram_buffer.put(incomplete_telegram + TELEGRAM_V4_2) + self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2) telegram = next(self.telegram_buffer.get_all()) @@ -59,7 +59,7 @@ class TelegramBufferTest(unittest.TestCase): # the buffer was being filled while the telegram was outputted halfway. incomplete_telegram = TELEGRAM_V4_2[1:] - self.telegram_buffer.put(incomplete_telegram + TELEGRAM_V4_2) + self.telegram_buffer.append(incomplete_telegram + TELEGRAM_V4_2) telegram = next(self.telegram_buffer.get_all()) @@ -69,7 +69,7 @@ class TelegramBufferTest(unittest.TestCase): def test_v42_telegram_trailed_by_unclosed_telegram(self): incomplete_telegram = TELEGRAM_V4_2[:-1] - self.telegram_buffer.put(TELEGRAM_V4_2 + incomplete_telegram) + self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram) telegram = next(self.telegram_buffer.get_all()) @@ -79,7 +79,7 @@ class TelegramBufferTest(unittest.TestCase): def test_v42_telegram_trailed_by_unopened_telegram(self): incomplete_telegram = TELEGRAM_V4_2[1:] - self.telegram_buffer.put(TELEGRAM_V4_2 + incomplete_telegram) + self.telegram_buffer.append(TELEGRAM_V4_2 + incomplete_telegram) telegram = next(self.telegram_buffer.get_all()) @@ -88,7 +88,7 @@ class TelegramBufferTest(unittest.TestCase): def test_v42_telegram_adding_line_by_line(self): for line in TELEGRAM_V4_2.splitlines(keepends=True): - self.telegram_buffer.put(line) + self.telegram_buffer.append(line) telegram = next(self.telegram_buffer.get_all()) @@ -97,7 +97,7 @@ class TelegramBufferTest(unittest.TestCase): def test_v42_telegram_adding_char_by_char(self): for char in TELEGRAM_V4_2: - self.telegram_buffer.put(char) + self.telegram_buffer.append(char) telegram = next(self.telegram_buffer.get_all()) From 759e0a0d922521c4009937d01dce78aa5b383e12 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 9 Jan 2017 21:31:08 +0100 Subject: [PATCH 060/152] removed absolute imports back to relative due to serial.py importing from serial --- dsmr_parser/__main__.py | 2 +- dsmr_parser/parsers.py | 6 +++--- dsmr_parser/protocol.py | 9 ++++----- dsmr_parser/serial.py | 5 ++--- dsmr_parser/telegram_specifications.py | 6 +++--- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index 03c9f1d..8813731 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -3,7 +3,7 @@ import asyncio import logging from functools import partial -from dsmr_parser.protocol import create_dsmr_reader, create_tcp_dsmr_reader +from .protocol import create_dsmr_reader, create_tcp_dsmr_reader def console(): diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index d36cdb6..a215a98 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,9 +3,9 @@ import re from PyCRC.CRC16 import CRC16 -from dsmr_parser.objects import MBusObject, MBusObjectV2_2, CosemObject -from dsmr_parser.exceptions import ParseError, InvalidChecksumError -from dsmr_parser.obis_references import GAS_METER_READING +from .objects import MBusObject, MBusObjectV2_2, CosemObject +from .exceptions import ParseError, InvalidChecksumError +from .obis_references import GAS_METER_READING logger = logging.getLogger(__name__) diff --git a/dsmr_parser/protocol.py b/dsmr_parser/protocol.py index 08a8732..24c3184 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/protocol.py @@ -6,11 +6,10 @@ from functools import partial from serial_asyncio import create_serial_connection -from dsmr_parser import telegram_specifications -from dsmr_parser.exceptions import ParseError -from dsmr_parser.parsers import TelegramParserV2_2, TelegramParserV4 -from dsmr_parser.serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, - TelegramBuffer) +from . import telegram_specifications +from .exceptions import ParseError +from .parsers import TelegramParserV2_2, TelegramParserV4 +from .serial import (SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, TelegramBuffer) def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index 93e31b9..da26308 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -4,9 +4,8 @@ import re import serial import serial_asyncio -from dsmr_parser.exceptions import ParseError -from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2, \ - TelegramParserV4 +from .exceptions import ParseError +from .parsers import TelegramParser, TelegramParserV2_2, TelegramParserV4 logger = logging.getLogger(__name__) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index adc5e62..958153b 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -1,8 +1,8 @@ from decimal import Decimal -from dsmr_parser import obis_references as obis -from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser -from dsmr_parser.value_types import timestamp +from . import obis_references as obis +from .parsers import CosemParser, ValueParser, MBusParser +from .value_types import timestamp """ From e97ab7c7ea29f9658e56ccb3a85e35238dc5f9dc Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 9 Jan 2017 21:47:51 +0100 Subject: [PATCH 061/152] import issues --- dsmr_parser/serial.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/serial.py b/dsmr_parser/serial.py index da26308..93e31b9 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -4,8 +4,9 @@ import re import serial import serial_asyncio -from .exceptions import ParseError -from .parsers import TelegramParser, TelegramParserV2_2, TelegramParserV4 +from dsmr_parser.exceptions import ParseError +from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2, \ + TelegramParserV4 logger = logging.getLogger(__name__) From fadf20671553cf325c638c2551f50712b74e343b Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 10 Jan 2017 20:09:33 +0100 Subject: [PATCH 062/152] moved serial clients to own package --- dsmr_parser/__main__.py | 4 +- dsmr_parser/clients/__init__.py | 5 ++ dsmr_parser/{ => clients}/protocol.py | 10 ++-- dsmr_parser/{ => clients}/serial.py | 79 ++------------------------ dsmr_parser/clients/settings.py | 22 +++++++ dsmr_parser/clients/telegram_buffer.py | 57 +++++++++++++++++++ dsmr_parser/parsers.py | 6 +- dsmr_parser/telegram_specifications.py | 6 +- test/test_protocol.py | 2 +- test/test_telegram_buffer.py | 2 +- 10 files changed, 104 insertions(+), 89 deletions(-) create mode 100644 dsmr_parser/clients/__init__.py rename dsmr_parser/{ => clients}/protocol.py (91%) rename dsmr_parser/{ => clients}/serial.py (55%) create mode 100644 dsmr_parser/clients/settings.py create mode 100644 dsmr_parser/clients/telegram_buffer.py diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py index 8813731..8d9da8b 100644 --- a/dsmr_parser/__main__.py +++ b/dsmr_parser/__main__.py @@ -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(): diff --git a/dsmr_parser/clients/__init__.py b/dsmr_parser/clients/__init__.py new file mode 100644 index 0000000..9b8a4b0 --- /dev/null +++ b/dsmr_parser/clients/__init__.py @@ -0,0 +1,5 @@ +from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ + SERIAL_SETTINGS_V4 +from dsmr_parser.clients.serial import SerialReader, AsyncSerialReader +from dsmr_parser.clients.protocol import create_dsmr_protocol, \ + create_dsmr_reader, create_tcp_dsmr_reader diff --git a/dsmr_parser/protocol.py b/dsmr_parser/clients/protocol.py similarity index 91% rename from dsmr_parser/protocol.py rename to dsmr_parser/clients/protocol.py index 24c3184..26ff573 100644 --- a/dsmr_parser/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -6,10 +6,12 @@ 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, TelegramBuffer) +from dsmr_parser import telegram_specifications +from dsmr_parser.clients.telegram_buffer import TelegramBuffer +from dsmr_parser.exceptions import ParseError +from dsmr_parser.parsers import TelegramParserV2_2, TelegramParserV4 +from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ + SERIAL_SETTINGS_V4 def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): diff --git a/dsmr_parser/serial.py b/dsmr_parser/clients/serial.py similarity index 55% rename from dsmr_parser/serial.py rename to dsmr_parser/clients/serial.py index 93e31b9..f5fa6a9 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/clients/serial.py @@ -1,37 +1,19 @@ import asyncio import logging -import re import serial import serial_asyncio +from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2, \ TelegramParserV4 +from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ + SERIAL_SETTINGS_V4 + 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 -} - - class SerialReader(object): PORT_KEY = 'port' @@ -104,57 +86,4 @@ class AsyncSerialReader(SerialReader): logger.warning('Failed to parse telegram: %s', e) -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}\r\n', - self._buffer, - re.DOTALL - ) diff --git a/dsmr_parser/clients/settings.py b/dsmr_parser/clients/settings.py new file mode 100644 index 0000000..2c2677c --- /dev/null +++ b/dsmr_parser/clients/settings.py @@ -0,0 +1,22 @@ +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 +} diff --git a/dsmr_parser/clients/telegram_buffer.py b/dsmr_parser/clients/telegram_buffer.py new file mode 100644 index 0000000..78a98eb --- /dev/null +++ b/dsmr_parser/clients/telegram_buffer.py @@ -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}\r\n', + self._buffer, + re.DOTALL + ) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index a215a98..d36cdb6 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,9 +3,9 @@ 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, MBusObjectV2_2, CosemObject +from dsmr_parser.exceptions import ParseError, InvalidChecksumError +from dsmr_parser.obis_references import GAS_METER_READING logger = logging.getLogger(__name__) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 958153b..adc5e62 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -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 """ diff --git a/test/test_protocol.py b/test/test_protocol.py index d779bc7..71137e2 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -5,7 +5,7 @@ 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.clients.protocol import DSMRProtocol TELEGRAM_V2_2 = ( diff --git a/test/test_telegram_buffer.py b/test/test_telegram_buffer.py index 7ad28ee..ef1cb4b 100644 --- a/test/test_telegram_buffer.py +++ b/test/test_telegram_buffer.py @@ -1,6 +1,6 @@ import unittest -from dsmr_parser.serial import TelegramBuffer +from dsmr_parser.clients.telegram_buffer import TelegramBuffer from test.example_telegrams import TELEGRAM_V2_2, TELEGRAM_V4_2 From 9b488e74f84d5fad7d977f8a31774006a55e0ed9 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 10 Jan 2017 20:57:50 +0100 Subject: [PATCH 063/152] renamed serial.py to serial_.py --- dsmr_parser/clients/__init__.py | 2 +- dsmr_parser/clients/{serial.py => serial_.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename dsmr_parser/clients/{serial.py => serial_.py} (100%) diff --git a/dsmr_parser/clients/__init__.py b/dsmr_parser/clients/__init__.py index 9b8a4b0..2a3b1fd 100644 --- a/dsmr_parser/clients/__init__.py +++ b/dsmr_parser/clients/__init__.py @@ -1,5 +1,5 @@ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ SERIAL_SETTINGS_V4 -from dsmr_parser.clients.serial import SerialReader, AsyncSerialReader +from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader from dsmr_parser.clients.protocol import create_dsmr_protocol, \ create_dsmr_reader, create_tcp_dsmr_reader diff --git a/dsmr_parser/clients/serial.py b/dsmr_parser/clients/serial_.py similarity index 100% rename from dsmr_parser/clients/serial.py rename to dsmr_parser/clients/serial_.py From f3d8311ac24943ce3d8eabd353097a8945ab405c Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 11 Jan 2017 17:40:25 +0100 Subject: [PATCH 064/152] updated readme and changelog for upcoming breaking API changes; skip pylama check for unused imports in clients module; --- CHANGELOG.rst | 10 ++++++++++ README.rst | 2 +- dsmr_parser/clients/protocol.py | 2 +- dsmr_parser/clients/serial_.py | 3 --- tox.ini | 3 +++ 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5f28f0c..310379e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ Change Log ---------- +**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. + +**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 `_) diff --git a/README.rst b/README.rst index ca62ede..b2136fd 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ Using the serial reader to connect to your smart meter and parse it's telegrams: from dsmr_parser import telegram_specifications from dsmr_parser import obis_references - from dsmr_parser.serial import SerialReader, SERIAL_SETTINGS_V4 + from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V4 serial_reader = SerialReader( device='/dev/ttyUSB0', diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 26ff573..87293d8 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -1,8 +1,8 @@ """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 diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index f5fa6a9..e9f9221 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -84,6 +84,3 @@ class AsyncSerialReader(SerialReader): ) except ParseError as e: logger.warning('Failed to parse telegram: %s', e) - - - diff --git a/tox.ini b/tox.ini index 39ff113..1402405 100644 --- a/tox.ini +++ b/tox.ini @@ -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 From 4eeefec426fc1048267f4d5a835a39c7e151f654 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 11 Jan 2017 17:44:48 +0100 Subject: [PATCH 065/152] update version number to 0.7 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f60708f..fba27c8 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.6', + version='0.7', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From e2e4bb36a205a858f0e230320892d30898484881 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 14 Jan 2017 20:10:42 +0100 Subject: [PATCH 066/152] updated changelog --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 310379e..599c0f4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,7 @@ Change Log **0.7** (2017-01-14) -- Internal refactoring related to the way clients feed their data into the parse module. Clients can now supply the telegram data in single characters, lines (which was common) or complete telegram strings. +- Internal refactoring related to the way clients feed their data into the parse module. Clients can now supply the telegram data in single characters, lines (which was common) or complete telegram strings. (`pull request #17 `_) **IMPORTANT: this release has the following backwards incompatible changes:** From 07634abed1170fb779c5830f39e0fe1f2a33f611 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Fri, 20 Jan 2017 23:02:19 +0100 Subject: [PATCH 067/152] Progress on removing TelegramParserV2_2 and TelegramParserV4 in favor of a generic TelegramParser --- dsmr_parser/clients/protocol.py | 10 ++--- dsmr_parser/clients/serial_.py | 14 +----- dsmr_parser/obis_references.py | 74 +++++++++++++++++-------------- dsmr_parser/parsers.py | 77 ++++++--------------------------- test/test_parse_v2_2.py | 6 +-- test/test_parse_v4_2.py | 10 ++--- test/test_protocol.py | 7 +-- 7 files changed, 70 insertions(+), 128 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 87293d8..8f55376 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -9,7 +9,7 @@ from serial_asyncio import create_serial_connection from dsmr_parser import telegram_specifications from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError -from dsmr_parser.parsers import TelegramParserV2_2, TelegramParserV4 +from dsmr_parser.parsers import TelegramParser from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ SERIAL_SETTINGS_V4 @@ -18,18 +18,16 @@ 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 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 diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index e9f9221..d69cac3 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -5,10 +5,7 @@ import serial_asyncio from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError -from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2, \ - TelegramParserV4 -from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ - SERIAL_SETTINGS_V4 +from dsmr_parser.parsers import TelegramParser logger = logging.getLogger(__name__) @@ -21,14 +18,7 @@ class SerialReader(object): 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) + self.telegram_parser = TelegramParser(telegram_specification) self.telegram_buffer = TelegramBuffer() def read(self): diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index f99d007..392b83c 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -1,36 +1,44 @@ -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_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' +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' +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' +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' ELECTRICITY_USED_TARIFF_ALL = ( ELECTRICITY_USED_TARIFF_1, diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index d36cdb6..2cf6ab9 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -5,28 +5,22 @@ from PyCRC.CRC16 import CRC16 from dsmr_parser.objects import MBusObject, MBusObjectV2_2, CosemObject from dsmr_parser.exceptions import ParseError, InvalidChecksumError -from dsmr_parser.obis_references import GAS_METER_READING logger = logging.getLogger(__name__) class TelegramParser(object): - def __init__(self, telegram_specification): + def __init__(self, telegram_specification, + enable_checksum_validation=False): """ :param telegram_specification: determines how the telegram is parsed :type telegram_specification: dict """ self.telegram_specification = telegram_specification + self.enable_checksum_validation = enable_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, telegram): + def parse(self, telegram_data): """ Parse telegram from string to dict. @@ -44,28 +38,22 @@ class TelegramParser(object): .. } """ - telegram_lines = telegram.splitlines() - parsed_lines = map(self.parse_line, telegram_lines) - return {obis_reference: dsmr_object - for obis_reference, dsmr_object in parsed_lines} + if self.enable_checksum_validation: + self.validate_checksum(telegram_data) - def parse_line(self, line): - logger.debug("Parsing line '%s'", line) + telegram = {} - obis_reference, parser = self._find_line_parser(line) + for signature, parser in self.telegram_specification.items(): + match = re.search(signature, telegram_data, re.DOTALL) - if not obis_reference: - logger.debug("No line class found for: '%s'", line) - return None, None + if match: + telegram[signature] = parser.parse(match.group(0)) - return obis_reference, parser.parse(line) - - -class TelegramParserV4(TelegramParser): + return telegram @staticmethod - def validate_telegram_checksum(telegram): + def validate_checksum(telegram): """ :param str telegram: :raises ParseError: @@ -97,45 +85,6 @@ class TelegramParserV4(TelegramParser): ) ) - def parse(self, telegram): - """ - :param str telegram: - :rtype: dict - """ - self.validate_telegram_checksum(telegram) - - return super().parse(telegram) - - -class TelegramParserV2_2(TelegramParser): - - def parse(self, telegram): - """ - :param str telegram: - :rtype: dict - """ - - # TODO fix this in the specification: telegram_specifications.V2_2 - def join_lines(telegram): - """Join lines for gas meter.""" - join_next = re.compile(GAS_METER_READING) - - join = None - for line_value in telegram.splitlines(): - if join: - yield join + line_value - join = None - elif join_next.match(line_value): - join = line_value - else: - yield line_value - - # TODO temporary workaround - lines = join_lines(telegram) - telegram = '\r\n'.join(lines) - - return super().parse(telegram) - class DSMRObjectParser(object): diff --git a/test/test_parse_v2_2.py b/test/test_parse_v2_2.py index 26c6d49..5ae5485 100644 --- a/test/test_parse_v2_2.py +++ b/test/test_parse_v2_2.py @@ -1,16 +1,16 @@ import unittest -from test.example_telegrams import TELEGRAM_V2_2 -from dsmr_parser.parsers import TelegramParserV2_2 +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_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 diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 1048f87..1e04f11 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -4,12 +4,12 @@ import unittest import pytz -from test.example_telegrams import TELEGRAM_V4_2 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 +from dsmr_parser.parsers import TelegramParser +from test.example_telegrams import TELEGRAM_V4_2 class TelegramParserV4_2Test(unittest.TestCase): @@ -17,7 +17,7 @@ class TelegramParserV4_2Test(unittest.TestCase): def test_valid(self): # No exception is raised. - TelegramParserV4.validate_telegram_checksum(TELEGRAM_V4_2) + TelegramParser.validate_checksum(TELEGRAM_V4_2) def test_invalid(self): # Remove the electricty used data value. This causes the checksum to @@ -28,14 +28,14 @@ class TelegramParserV4_2Test(unittest.TestCase): ) with self.assertRaises(InvalidChecksumError): - TelegramParserV4.validate_telegram_checksum(corrupted_telegram) + TelegramParser.validate_checksum(corrupted_telegram) def test_missing_checksum(self): # Remove the checksum value causing a ParseError. corrupted_telegram = TELEGRAM_V4_2.replace('!6796\r\n', '') with self.assertRaises(ParseError): - TelegramParserV4.validate_telegram_checksum(corrupted_telegram) + TelegramParser.validate_checksum(corrupted_telegram) def test_parse(self): parser = TelegramParser(telegram_specifications.V4) diff --git a/test/test_protocol.py b/test/test_protocol.py index 71137e2..2fb14e0 100644 --- a/test/test_protocol.py +++ b/test/test_protocol.py @@ -4,7 +4,7 @@ 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.parsers import TelegramParser from dsmr_parser.clients.protocol import DSMRProtocol @@ -35,10 +35,7 @@ TELEGRAM_V2_2 = ( 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()) From 45f5fe2c3691fc2df9a6d24120b0db3ba8543b05 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 21 Jan 2017 10:33:17 +0100 Subject: [PATCH 068/152] define checksum support in telegram specification; moved telegram v2 exception temporarily from parser to MBUSObject; --- dsmr_parser/objects.py | 31 ++++--- dsmr_parser/parsers.py | 28 +++---- dsmr_parser/telegram_specifications.py | 112 +++++++++++++------------ test/test_parse_v4_2.py | 44 +++++----- 4 files changed, 109 insertions(+), 106 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index f09fda5..890dd50 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -12,26 +12,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): diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 2cf6ab9..069c02a 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,7 +3,7 @@ import re from PyCRC.CRC16 import CRC16 -from dsmr_parser.objects import MBusObject, MBusObjectV2_2, CosemObject +from dsmr_parser.objects import MBusObject, CosemObject from dsmr_parser.exceptions import ParseError, InvalidChecksumError logger = logging.getLogger(__name__) @@ -11,14 +11,15 @@ logger = logging.getLogger(__name__) class TelegramParser(object): - def __init__(self, telegram_specification, - enable_checksum_validation=False): + 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.enable_checksum_validation = enable_checksum_validation + self.apply_checksum_validation = apply_checksum_validation def parse(self, telegram_data): """ @@ -32,19 +33,22 @@ class TelegramParser(object): :returns: Shortened example: { .. - r'0-0:96\.1\.1': , # EQUIPMENT_IDENTIFIER - r'1-0:1\.8\.1': , # ELECTRICITY_USED_TARIFF_1 - r'0-\d:24\.3\.0': , # GAS_METER_READING + r'\d-\d:96\.1\.1.+?\r\n': , # EQUIPMENT_IDENTIFIER + r'\d-\d:1\.8\.1.+?\r\n': , # ELECTRICITY_USED_TARIFF_1 + r'\d-\d:24\.3\.0.+?\r\n.+?\r\n': , # GAS_METER_READING .. } + :raises ParseError: + :raises InvalidChecksumError: """ - if self.enable_checksum_validation: + if self.apply_checksum_validation \ + and self.telegram_specification['checksum_support']: self.validate_checksum(telegram_data) telegram = {} - for signature, parser in self.telegram_specification.items(): + for signature, parser in self.telegram_specification['objects'].items(): match = re.search(signature, telegram_data, re.DOTALL) if match: @@ -124,11 +128,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): diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index adc5e62..b4f48c2 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -14,60 +14,66 @@ 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), + ), + } } 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.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)) + } } diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 1e04f11..681783b 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -15,28 +15,6 @@ 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. - TelegramParser.validate_checksum(TELEGRAM_V4_2) - - def test_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_missing_checksum(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) - def test_parse(self): parser = TelegramParser(telegram_specifications.V4) result = parser.parse(TELEGRAM_V4_2) @@ -219,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) From 7a4c2048508047bd2a04f8fe817a3addfd9ebd66 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 21 Jan 2017 10:42:17 +0100 Subject: [PATCH 069/152] added code comments --- dsmr_parser/objects.py | 3 +-- dsmr_parser/parsers.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 890dd50..9489d97 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -43,5 +43,4 @@ class CosemObject(DSMRObject): class ProfileGeneric(DSMRObject): - pass - # TODO implement + pass # TODO implement diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 069c02a..478b518 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -91,6 +91,9 @@ class TelegramParser(object): 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 @@ -181,6 +184,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 From 45ee8dbb32b24c3b5dcddbb8e77610f7e09f66c7 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 22 Jan 2017 16:39:16 +0100 Subject: [PATCH 070/152] added basic config for DSMR v5 specification; added DSMR v5 example telegram for testing; --- dsmr_parser/obis_references.py | 3 +- dsmr_parser/telegram_specifications.py | 47 +++++++++++++++++++++++--- test/example_telegrams.py | 43 +++++++++++++++++++++++ test/test_parse_v5.py | 21 ++++++++++++ 4 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 test/test_parse_v5.py diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 392b83c..40475ec 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -34,8 +34,9 @@ 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 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' +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' diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index b4f48c2..fc6c662 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -15,7 +15,7 @@ how the telegram lines are parsed. V2_2 = { 'checksum_support': False, - 'objects': { + 'object_signatures': { obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), @@ -47,12 +47,12 @@ V4 = { '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.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)), @@ -73,7 +73,46 @@ V4 = { 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)) + 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_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), + obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), + # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO + obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)), + obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), + obis.DEVICE_TYPE: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), + obis.HOURLY_GAS_METER_READING: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ) } } diff --git a/test/example_telegrams.py b/test/example_telegrams.py index eb4e8d4..55e7897 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -60,3 +60,46 @@ TELEGRAM_V4_2 = ( '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' +) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py new file mode 100644 index 0000000..b337e20 --- /dev/null +++ b/test/test_parse_v5.py @@ -0,0 +1,21 @@ +import unittest + +from dsmr_parser import obis_references as obis +from dsmr_parser import telegram_specifications +from dsmr_parser.objects import CosemObject +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' From adcfdfe2ae268da37db669f0e79052fc9d608f4f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 22 Jan 2017 16:42:02 +0100 Subject: [PATCH 071/152] fixed v2 specification typo; fixed wrongly edited obis reference; --- dsmr_parser/obis_references.py | 2 +- dsmr_parser/telegram_specifications.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 40475ec..c33f2a5 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -36,7 +36,7 @@ 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 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)' +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' diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index fc6c662..646a2da 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -15,7 +15,7 @@ how the telegram lines are parsed. V2_2 = { 'checksum_support': False, - 'object_signatures': { + 'objects': { obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), From 9623f3b092701ff489d830e5781037303b641549 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 25 Jan 2017 19:32:30 +0100 Subject: [PATCH 072/152] added unit test for DSMR v5 parsing --- test/test_parse_v5.py | 188 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 1 deletion(-) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index b337e20..b276f8b 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -1,8 +1,15 @@ import unittest +from decimal import Decimal + +import datetime + +import pytz + from dsmr_parser import obis_references as obis from dsmr_parser import telegram_specifications -from dsmr_parser.objects import CosemObject +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 @@ -19,3 +26,182 @@ class TelegramParserV5Test(unittest.TestCase): assert result[obis.P1_MESSAGE_HEADER].unit is None assert isinstance(result[obis.P1_MESSAGE_HEADER].value, str) assert result[obis.P1_MESSAGE_HEADER].value == '50' + + # P1_MESSAGE_TIMESTAMP (0-0:1.0.0) + assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP], CosemObject) + assert result[obis.P1_MESSAGE_TIMESTAMP].unit is None + assert isinstance(result[obis.P1_MESSAGE_TIMESTAMP].value, datetime.datetime) + assert result[obis.P1_MESSAGE_TIMESTAMP].value == \ + datetime.datetime(2017, 1, 2, 18, 20, 2, tzinfo=pytz.UTC) + + # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1], CosemObject) + assert result[obis.ELECTRICITY_USED_TARIFF_1].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_1].value, Decimal) + assert result[obis.ELECTRICITY_USED_TARIFF_1].value == Decimal('4.426') + + # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2], CosemObject) + assert result[obis.ELECTRICITY_USED_TARIFF_2].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_USED_TARIFF_2].value, Decimal) + assert result[obis.ELECTRICITY_USED_TARIFF_2].value == Decimal('2.399') + + # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1], CosemObject) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value, Decimal) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_1].value == Decimal('2.444') + + # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2], CosemObject) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].unit == 'kWh' + assert isinstance(result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value, Decimal) + assert result[obis.ELECTRICITY_DELIVERED_TARIFF_2].value == Decimal('0') + + # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) + assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF], CosemObject) + assert result[obis.ELECTRICITY_ACTIVE_TARIFF].unit is None + assert isinstance(result[obis.ELECTRICITY_ACTIVE_TARIFF].value, str) + assert result[obis.ELECTRICITY_ACTIVE_TARIFF].value == '0002' + + # EQUIPMENT_IDENTIFIER (0-0:96.1.1) + assert isinstance(result[obis.EQUIPMENT_IDENTIFIER], CosemObject) + assert result[obis.EQUIPMENT_IDENTIFIER].unit is None + assert isinstance(result[obis.EQUIPMENT_IDENTIFIER].value, str) + assert result[obis.EQUIPMENT_IDENTIFIER].value == '4B384547303034303436333935353037' + + # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) + assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE], CosemObject) + assert result[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW' + assert isinstance(result[obis.CURRENT_ELECTRICITY_USAGE].value, Decimal) + assert result[obis.CURRENT_ELECTRICITY_USAGE].value == Decimal('0.244') + + # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) + assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY], CosemObject) + assert result[obis.CURRENT_ELECTRICITY_DELIVERY].unit == 'kW' + assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal) + assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0') + + # LONG_POWER_FAILURE_COUNT (96.7.9) + assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject) + assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None + assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int) + assert result[obis.LONG_POWER_FAILURE_COUNT].value == 0 + + # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0) + assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject) + assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None + assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT].value, int) + assert result[obis.VOLTAGE_SAG_L1_COUNT].value == 0 + + # VOLTAGE_SAG_L2_COUNT (1-0:52.32.0) + assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT], CosemObject) + assert result[obis.VOLTAGE_SAG_L2_COUNT].unit is None + assert isinstance(result[obis.VOLTAGE_SAG_L2_COUNT].value, int) + assert result[obis.VOLTAGE_SAG_L2_COUNT].value == 0 + + # VOLTAGE_SAG_L3_COUNT (1-0:72.32.0) + assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT], CosemObject) + assert result[obis.VOLTAGE_SAG_L3_COUNT].unit is None + assert isinstance(result[obis.VOLTAGE_SAG_L3_COUNT].value, int) + assert result[obis.VOLTAGE_SAG_L3_COUNT].value == 0 + + # VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0) + assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT], CosemObject) + assert result[obis.VOLTAGE_SWELL_L1_COUNT].unit is None + assert isinstance(result[obis.VOLTAGE_SWELL_L1_COUNT].value, int) + assert result[obis.VOLTAGE_SWELL_L1_COUNT].value == 0 + + # VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0) + assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT], CosemObject) + assert result[obis.VOLTAGE_SWELL_L2_COUNT].unit is None + assert isinstance(result[obis.VOLTAGE_SWELL_L2_COUNT].value, int) + assert result[obis.VOLTAGE_SWELL_L2_COUNT].value == 0 + + # VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0) + assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT], CosemObject) + assert result[obis.VOLTAGE_SWELL_L3_COUNT].unit is None + assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int) + assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0 + + # TEXT_MESSAGE (0-0:96.13.0) + assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) + assert result[obis.TEXT_MESSAGE].unit is None + assert result[obis.TEXT_MESSAGE].value is None + + # DEVICE_TYPE (0-x:24.1.0) + assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) + assert result[obis.DEVICE_TYPE].unit is None + assert isinstance(result[obis.DEVICE_TYPE].value, int) + assert result[obis.DEVICE_TYPE].value == 3 + + # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE].value == Decimal('0.070') + + # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE].value == Decimal('0.032') + + # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE].value == Decimal('0.142') + + # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE].value == Decimal('0') + + # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE].value == Decimal('0') + + # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0) + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], CosemObject) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].unit == 'kW' + assert isinstance(result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value, Decimal) + assert result[obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE].value == Decimal('0') + + # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) + assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS], CosemObject) + assert result[obis.EQUIPMENT_IDENTIFIER_GAS].unit is None + assert isinstance(result[obis.EQUIPMENT_IDENTIFIER_GAS].value, str) + assert result[obis.EQUIPMENT_IDENTIFIER_GAS].value == '3232323241424344313233343536373839' + + # HOURLY_GAS_METER_READING (0-1:24.2.1) + assert isinstance(result[obis.HOURLY_GAS_METER_READING], MBusObject) + assert result[obis.HOURLY_GAS_METER_READING].unit == 'm3' + assert isinstance(result[obis.HOURLY_GAS_METER_READING].value, Decimal) + assert result[obis.HOURLY_GAS_METER_READING].value == Decimal('0.107') + + + def test_checksum_valid(self): + # No exception is raised. + TelegramParser.validate_checksum(TELEGRAM_V5) + + def test_checksum_invalid(self): + # Remove the electricty used data value. This causes the checksum to + # not match anymore. + corrupted_telegram = TELEGRAM_V5.replace( + '1-0:1.8.1(000004.426*kWh)\r\n', + '' + ) + + with self.assertRaises(InvalidChecksumError): + TelegramParser.validate_checksum(corrupted_telegram) + + def test_checksum_missing(self): + # Remove the checksum value causing a ParseError. + corrupted_telegram = TELEGRAM_V5.replace('!87B3\r\n', '') + + with self.assertRaises(ParseError): + TelegramParser.validate_checksum(corrupted_telegram) From 8a868ce82683af94ff2ab9c3ce87558bc8b08317 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 25 Jan 2017 19:33:42 +0100 Subject: [PATCH 073/152] pep8 --- test/test_parse_v5.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index b276f8b..a7dc719 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -1,8 +1,7 @@ -import unittest - from decimal import Decimal import datetime +import unittest import pytz From c4caf54576b99102f58082d408d0d8f581ba5074 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Wed, 25 Jan 2017 19:34:38 +0100 Subject: [PATCH 074/152] pep8 --- test/test_parse_v5.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index a7dc719..550fde9 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -181,7 +181,6 @@ class TelegramParserV5Test(unittest.TestCase): 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. From c4dcc7319106a62edc3754a36425ef1b46222a2f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 26 Jan 2017 18:02:21 +0100 Subject: [PATCH 075/152] added DSMR v3 specification --- test/test_parse_v3.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/test_parse_v3.py diff --git a/test/test_parse_v3.py b/test/test_parse_v3.py new file mode 100644 index 0000000..e69de29 From a88dfe1a4180f9f86e1795b036188a51416e51f5 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 26 Jan 2017 18:50:30 +0100 Subject: [PATCH 076/152] added DSMR v3 specification; updated changelog; --- CHANGELOG.rst | 10 +++ dsmr_parser/obis_references.py | 5 +- dsmr_parser/telegram_specifications.py | 29 ++++++++ setup.py | 2 +- test/example_telegrams.py | 25 +++++++ test/test_parse_v2_2.py | 78 +++++++++++++++++++- test/test_parse_v3.py | 98 ++++++++++++++++++++++++++ 7 files changed, 242 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 599c0f4..3100b59 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ Change Log ---------- +**0.8** (2017-01-29) + +- 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 `_) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index c33f2a5..6791f1e 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -34,13 +34,16 @@ 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 +# 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, ELECTRICITY_USED_TARIFF_2 diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 646a2da..43642f9 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -42,6 +42,35 @@ V2_2 = { } } +V3 = { + 'checksum_support': False, + 'objects': { + obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), + obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), + obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), + obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), + obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), + obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), + obis.DEVICE_TYPE: CosemParser(ValueParser(str)), + obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), + obis.GAS_METER_READING: MBusParser( + ValueParser(timestamp), + ValueParser(int), + ValueParser(int), + ValueParser(int), + ValueParser(str), + ValueParser(Decimal), + ), + } +} + V4 = { 'checksum_support': True, 'objects': { diff --git a/setup.py b/setup.py index fba27c8..a4b5fdd 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.7', + version='0.8', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', diff --git a/test/example_telegrams.py b/test/example_telegrams.py index 55e7897..2df8606 100644 --- a/test/example_telegrams.py +++ b/test/example_telegrams.py @@ -21,6 +21,31 @@ TELEGRAM_V2_2 = ( '!\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' diff --git a/test/test_parse_v2_2.py b/test/test_parse_v2_2.py index 5ae5485..e7203ab 100644 --- a/test/test_parse_v2_2.py +++ b/test/test_parse_v2_2.py @@ -1,5 +1,8 @@ import unittest +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 @@ -13,8 +16,77 @@ class TelegramParserV2_2Test(unittest.TestCase): 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') diff --git a/test/test_parse_v3.py b/test/test_parse_v3.py index e69de29..c50a86e 100644 --- a/test/test_parse_v3.py +++ b/test/test_parse_v3.py @@ -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') From b4a520c8b4f684a382eacb1fe3080ebeb068cd8e Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 26 Jan 2017 19:00:31 +0100 Subject: [PATCH 077/152] updated readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b2136fd..eed7802 100644 --- a/README.rst +++ b/README.rst @@ -14,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.4 and 3.5. +DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.5. Examples From 9e74c4c23c95439a99595fa3a28511b86496eea1 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 26 Jan 2017 19:02:15 +0100 Subject: [PATCH 078/152] adjusting supported python versions --- README.rst | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index eed7802..f550688 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ also includes a serial client to directly read and parse smart meter data. Features -------- -DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.5. +DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.4 and 3.5. Examples diff --git a/tox.ini b/tox.ini index 1402405..75afbbe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34,py35 +envlist = py34,py35,py36 [testenv] deps= From c1a6b930c8b64bf2f133d2fda58545090869e19c Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 26 Jan 2017 19:04:01 +0100 Subject: [PATCH 079/152] try python 3.6 support --- .travis.yml | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 83613fa..311a607 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - 2.7 - 3.4 - 3.5 + - 3.6 install: pip install tox-travis codecov diff --git a/tox.ini b/tox.ini index 75afbbe..95660fe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34,py35,py36 +envlist = py34,py35,p36 [testenv] deps= From 6c8a9dcbdbbd46b92c889c0b960b4d9fae595cec Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 26 Jan 2017 19:06:03 +0100 Subject: [PATCH 080/152] added python 3.6 support --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f550688..2fdd090 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ also includes a serial client to directly read and parse smart meter data. Features -------- -DSMR Parser supports DSMR versions 2, 3, 4 and 5. 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 From 0c40070752a73f9f72a39d0820312a6b141e8d74 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 26 Jan 2017 21:18:56 +0100 Subject: [PATCH 081/152] fixed changelog date --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3100b59..1d34527 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Change Log ---------- -**0.8** (2017-01-29) +**0.8** (2017-01-26) - added support for DSMR v3 - added support for DSMR v5 From 24ab9aa712bc22b82e977877c2615a586f1fb2db Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 28 Jan 2017 17:01:02 +0100 Subject: [PATCH 082/152] experimenting with version detection --- test/test_match_telegram_specification.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/test_match_telegram_specification.py diff --git a/test/test_match_telegram_specification.py b/test/test_match_telegram_specification.py new file mode 100644 index 0000000..e69de29 From 9d20bb8ad564743ab2f816097cf9b74537fe2779 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 28 Jan 2017 17:01:33 +0100 Subject: [PATCH 083/152] experimenting with version detection --- dsmr_parser/objects.py | 3 ++ dsmr_parser/parsers.py | 37 ++++++++++++++++++++--- dsmr_parser/telegram_specifications.py | 31 ++----------------- test/test_match_telegram_specification.py | 25 +++++++++++++++ 4 files changed, 64 insertions(+), 32 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 9489d97..e6706c4 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,4 +1,7 @@ class DSMRObject(object): + """ + Represents all data from a single telegram line. + """ def __init__(self, values): self.values = values diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 478b518..6a9f465 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -27,8 +27,8 @@ class TelegramParser(object): The telegram str type makes python 2.x integration easier. - :param str telegram: full telegram from start ('/') to checksum - ('!ABCD') including line endings inbetween the telegram's lines + :param str telegram_data: full telegram from start ('/') to checksum + ('!ABCD') including line endings in between the telegram's lines :rtype: dict :returns: Shortened example: { @@ -51,8 +51,12 @@ class TelegramParser(object): for signature, parser in self.telegram_specification['objects'].items(): match = re.search(signature, telegram_data, re.DOTALL) - if match: - telegram[signature] = parser.parse(match.group(0)) + # All telegram specification lines/signatures are expected to be + # present. + if not match: + raise ParseError('Telegram specification does not match ' + 'telegram data') + telegram[signature] = parser.parse(match.group(0)) return telegram @@ -90,6 +94,31 @@ class TelegramParser(object): ) +def match_telegram_specification(telegram_data): + """ + Find telegram specification that matches the telegram data by trying all + specifications. + + Could be further optimized to check the actual 0.2.8 OBIS reference which + is available for DSMR version 4 and up. + + :param str telegram_data: full telegram from start ('/') to checksum + ('!ABCD') including line endings in between the telegram's lines + :return: telegram specification + :rtype: dict + """ + # Prevent circular import + from dsmr_parser import telegram_specifications + + for specification in telegram_specifications.ALL: + try: + TelegramParser(specification).parse(telegram_data) + except ParseError: + pass + else: + return specification + + class DSMRObjectParser(object): """ Parses an object (can also be see as a 'line') from a telegram. diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 43642f9..5222786 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -42,34 +42,7 @@ V2_2 = { } } -V3 = { - 'checksum_support': False, - 'objects': { - obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), - obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), - obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), - obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), - obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), - obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), - obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), - obis.DEVICE_TYPE: CosemParser(ValueParser(str)), - obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), - obis.GAS_METER_READING: MBusParser( - ValueParser(timestamp), - ValueParser(int), - ValueParser(int), - ValueParser(int), - ValueParser(str), - ValueParser(Decimal), - ), - } -} +V3 = V2_2 V4 = { 'checksum_support': True, @@ -145,3 +118,5 @@ V5 = { ) } } + +ALL = (V2_2, V3, V4, V5) diff --git a/test/test_match_telegram_specification.py b/test/test_match_telegram_specification.py index e69de29..1385b82 100644 --- a/test/test_match_telegram_specification.py +++ b/test/test_match_telegram_specification.py @@ -0,0 +1,25 @@ +import unittest + +from dsmr_parser.parsers import match_telegram_specification +from dsmr_parser import telegram_specifications +from test import example_telegrams + + +class MatchTelegramSpecificationTest(unittest.TestCase): + + + def test_v2_2(self): + assert match_telegram_specification(example_telegrams.TELEGRAM_V2_2) \ + == telegram_specifications.V2_2 + + def test_v3(self): + assert match_telegram_specification(example_telegrams.TELEGRAM_V3) \ + == telegram_specifications.V3 + + def test_v4_2(self): + assert match_telegram_specification(example_telegrams.TELEGRAM_V4_2) \ + == telegram_specifications.V4 + + def test_v5(self): + assert match_telegram_specification(example_telegrams.TELEGRAM_V5) \ + == telegram_specifications.V5 From 148bdabc122b8bed4b77dadf310cbe8586eb40ef Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Fri, 3 Feb 2017 22:40:39 +0100 Subject: [PATCH 084/152] Updated docs --- README.rst | 134 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index 2fdd090..b960c04 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ DSMR Parser :target: https://travis-ci.org/ndokter/dsmr_parser A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It -also includes a serial client to directly read and parse smart meter data. +also includes client implementation to directly read and parse smart meter data. Features @@ -17,41 +17,123 @@ Features DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.4, 3.5 and 3.6. -Examples --------- +Client module usage +------------------- -Using the serial reader to connect to your smart meter and parse it's telegrams: +**Serial client** + +The most simple client is the serial client. 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 + 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 parsed DSMR v3 telegram + {'\\d-\\d:17\\.0\\.0.+?\\r\\n': , + '\\d-\\d:1\\.7\\.0.+?\\r\\n': , + '\\d-\\d:1\\.8\\.1.+?\\r\\n': , + '\\d-\\d:1\\.8\\.2.+?\\r\\n': , + '\\d-\\d:24\\.1\\.0.+?\\r\\n': , + '\\d-\\d:24\\.3\\.0.+?\\r\\n.+?\\r\\n': , + '\\d-\\d:24\\.4\\.0.+?\\r\\n': , + '\\d-\\d:2\\.7\\.0.+?\\r\\n': , + '\\d-\\d:2\\.8\\.1.+?\\r\\n': , + '\\d-\\d:2\\.8\\.2.+?\\r\\n': , + '\\d-\\d:96\\.13\\.0.+?\\r\\n': , + '\\d-\\d:96\\.13\\.1.+?\\r\\n': , + '\\d-\\d:96\\.14\\.0.+?\\r\\n': , + '\\d-\\d:96\\.1\\.0.+?\\r\\n': , + '\\d-\\d:96\\.1\\.1.+?\\r\\n': , + '\\d-\\d:96\\.3\\.10.+?\\r\\n': } + +Example to get values: .. code-block:: python - from dsmr_parser import telegram_specifications from dsmr_parser import obis_references - from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V4 - serial_reader = SerialReader( - device='/dev/ttyUSB0', - serial_settings=SERIAL_SETTINGS_V4, - telegram_specification=telegram_specifications.V4 - ) + # The telegram message timestamp. + message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP] - for telegram in serial_reader.read(): + # Using the active tariff to determine the electricity being used and + # delivered for the right tariff. + active_tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF] + active_tariff = int(tariff.value) - # The telegram message timestamp. - message_datetime = telegram[obis_references.P1_MESSAGE_TIMESTAMP] + electricity_used_total = telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[active_tariff - 1]] + electricity_delivered_total = telegram[obis_references.ELECTRICITY_DELIVERED_TARIFF_ALL[active_tariff - 1]] - # Using the active tariff to determine the electricity being used and - # delivered for the right tariff. - tariff = telegram[obis_references.ELECTRICITY_ACTIVE_TARIFF] - tariff = int(tariff.value) + gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING] - electricity_used_total \ - = telegram[obis_references.ELECTRICITY_USED_TARIFF_ALL[tariff - 1]] - electricity_delivered_total = \ - telegram[obis_referencesELECTRICITY_DELIVERED_TARIFF_ALL[tariff - 1]] - - gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING] - - # See dsmr_reader.obis_references for all readable telegram values. + # See dsmr_reader.obis_references for all readable telegram values. Installation From 46860e04c19ef8d98e4969d027393e39acc4ae4b Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Fri, 3 Feb 2017 22:42:31 +0100 Subject: [PATCH 085/152] Formatting --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b960c04..9abba4a 100644 --- a/README.rst +++ b/README.rst @@ -53,6 +53,7 @@ into a dictionary. from dsmr_parser import telegram_specifications from dsmr_parser.parsers import TelegramParser + telegram_str = ( '/ISk5\2MT382-1000\r\n' '\r\n' @@ -77,12 +78,12 @@ into a dictionary. '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 --------------- From 3d64fea24785e20863deb04f2c48525ce8b25fb4 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 27 Feb 2017 20:26:15 +0100 Subject: [PATCH 086/152] Updated README --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9abba4a..749b118 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Client module usage **Serial client** -The most simple client is the serial client. It should be run in a separate +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 @@ -97,7 +97,7 @@ contains 'kW', 'A', 'kWh' or 'm3'. .. code-block:: python - # Contents of parsed DSMR v3 telegram + # Contents of a parsed DSMR v3 telegram {'\\d-\\d:17\\.0\\.0.+?\\r\\n': , '\\d-\\d:1\\.7\\.0.+?\\r\\n': , '\\d-\\d:1\\.8\\.1.+?\\r\\n': , @@ -115,7 +115,7 @@ contains 'kW', 'A', 'kWh' or 'm3'. '\\d-\\d:96\\.1\\.1.+?\\r\\n': , '\\d-\\d:96\\.3\\.10.+?\\r\\n': } -Example to get values: +Example to get some of the values: .. code-block:: python @@ -135,6 +135,7 @@ Example to get values: gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING] # See dsmr_reader.obis_references for all readable telegram values. + # Note that the avilable values differ per DSMR version. Installation From d2f57a89266d6f7cdbe4fa6d88308f164d1ebe49 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 27 Feb 2017 20:35:19 +0100 Subject: [PATCH 087/152] fixed pep8 --- test/test_match_telegram_specification.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_match_telegram_specification.py b/test/test_match_telegram_specification.py index 1385b82..357495a 100644 --- a/test/test_match_telegram_specification.py +++ b/test/test_match_telegram_specification.py @@ -7,7 +7,6 @@ from test import example_telegrams class MatchTelegramSpecificationTest(unittest.TestCase): - def test_v2_2(self): assert match_telegram_specification(example_telegrams.TELEGRAM_V2_2) \ == telegram_specifications.V2_2 From de5c884d8d4c8cc789155fd37d03caf31d997452 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 27 Feb 2017 20:44:18 +0100 Subject: [PATCH 088/152] Small comment improvement --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 749b118..7b53ee4 100644 --- a/README.rst +++ b/README.rst @@ -54,6 +54,7 @@ into a dictionary. 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' From efc09df71f3a5480f68b2f3127175f47912b7a5c Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Fri, 12 May 2017 20:30:53 +0200 Subject: [PATCH 089/152] Add DSMR5 option to protocol. --- dsmr_parser/clients/protocol.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 8f55376..d63a3fc 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -23,6 +23,9 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): elif dsmr_version == '4': specification = telegram_specifications.V4 serial_settings = SERIAL_SETTINGS_V4 + elif dsmr_version == '5': + specification = telegram_specifications.V5 + serial_settings = SERIAL_SETTINGS_V4 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) From 9b0a85e84b2bdce88c1925985e7c9b9846e8b58f Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Fri, 12 May 2017 21:37:54 +0200 Subject: [PATCH 090/152] updated version --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d34527..3617fb4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.9** (2017-05-12) + +- added DSMR v5 serial settings + **0.8** (2017-01-26) - added support for DSMR v3 diff --git a/setup.py b/setup.py index a4b5fdd..b8dc651 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.8', + version='0.9', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From dcb59fddb1a6f5efe791ff94cd0d6d9de12621d2 Mon Sep 17 00:00:00 2001 From: Alex Mekkering Date: Mon, 5 Jun 2017 09:07:47 +0200 Subject: [PATCH 091/152] Support optional telegram signatures --- dsmr_parser/parsers.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 6a9f465..c38dcf0 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -51,12 +51,10 @@ class TelegramParser(object): for signature, parser in self.telegram_specification['objects'].items(): match = re.search(signature, telegram_data, re.DOTALL) - # All telegram specification lines/signatures are expected to be - # present. - if not match: - raise ParseError('Telegram specification does not match ' - 'telegram data') - telegram[signature] = parser.parse(match.group(0)) + # 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 From d6e28db116d4394cd80a70cec15ad2e532af0ae9 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 5 Jun 2017 21:02:59 +0200 Subject: [PATCH 092/152] log checksum errors as warning; dont force full telegram signatures; removed unused code for automatic telegram version detection; --- CHANGELOG.rst | 6 +++++ dsmr_parser/clients/protocol.py | 2 ++ dsmr_parser/clients/serial_.py | 2 ++ dsmr_parser/parsers.py | 32 +++-------------------- setup.py | 2 +- test/test_match_telegram_specification.py | 24 ----------------- 6 files changed, 15 insertions(+), 53 deletions(-) delete mode 100644 test/test_match_telegram_specification.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3617fb4..0da194d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Change Log ---------- +**0.10** (2017-06-05) + +- bugix: don't force full telegram signatures (`pull request #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 diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index d63a3fc..68f2434 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -101,6 +101,8 @@ class DSMRProtocol(asyncio.Protocol): 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: diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index d69cac3..ddc0a14 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -36,6 +36,8 @@ class SerialReader(object): 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) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index c38dcf0..087d9e0 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -92,31 +92,6 @@ class TelegramParser(object): ) -def match_telegram_specification(telegram_data): - """ - Find telegram specification that matches the telegram data by trying all - specifications. - - Could be further optimized to check the actual 0.2.8 OBIS reference which - is available for DSMR version 4 and up. - - :param str telegram_data: full telegram from start ('/') to checksum - ('!ABCD') including line endings in between the telegram's lines - :return: telegram specification - :rtype: dict - """ - # Prevent circular import - from dsmr_parser import telegram_specifications - - for specification in telegram_specifications.ALL: - try: - TelegramParser(specification).parse(telegram_data) - except ParseError: - pass - else: - return specification - - class DSMRObjectParser(object): """ Parses an object (can also be see as a 'line') from a telegram. @@ -174,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): diff --git a/setup.py b/setup.py index b8dc651..176233c 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.9', + version='0.10', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', diff --git a/test/test_match_telegram_specification.py b/test/test_match_telegram_specification.py deleted file mode 100644 index 357495a..0000000 --- a/test/test_match_telegram_specification.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from dsmr_parser.parsers import match_telegram_specification -from dsmr_parser import telegram_specifications -from test import example_telegrams - - -class MatchTelegramSpecificationTest(unittest.TestCase): - - def test_v2_2(self): - assert match_telegram_specification(example_telegrams.TELEGRAM_V2_2) \ - == telegram_specifications.V2_2 - - def test_v3(self): - assert match_telegram_specification(example_telegrams.TELEGRAM_V3) \ - == telegram_specifications.V3 - - def test_v4_2(self): - assert match_telegram_specification(example_telegrams.TELEGRAM_V4_2) \ - == telegram_specifications.V4 - - def test_v5(self): - assert match_telegram_specification(example_telegrams.TELEGRAM_V5) \ - == telegram_specifications.V5 From c78ebe3e2d2dcad1512ff11aab3a50bef2646ff2 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 5 Jun 2017 21:06:48 +0200 Subject: [PATCH 093/152] fixed import errors --- dsmr_parser/clients/protocol.py | 2 +- dsmr_parser/clients/serial_.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 68f2434..0834051 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -8,7 +8,7 @@ from serial_asyncio import create_serial_connection from dsmr_parser import telegram_specifications from dsmr_parser.clients.telegram_buffer import TelegramBuffer -from dsmr_parser.exceptions import ParseError +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 diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index ddc0a14..1d7be89 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -4,7 +4,7 @@ import serial import serial_asyncio from dsmr_parser.clients.telegram_buffer import TelegramBuffer -from dsmr_parser.exceptions import ParseError +from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.parsers import TelegramParser From e3203a5334a3dc95160f45f3ca3aaddc971d0b1f Mon Sep 17 00:00:00 2001 From: Vincent van den Braken Date: Fri, 15 Sep 2017 13:48:10 +0200 Subject: [PATCH 094/152] Optional NUL after checksum My smart meter returns 00 0D 0A after the checksum, not just 0D 0A. --- dsmr_parser/clients/telegram_buffer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/clients/telegram_buffer.py b/dsmr_parser/clients/telegram_buffer.py index 78a98eb..5933296 100644 --- a/dsmr_parser/clients/telegram_buffer.py +++ b/dsmr_parser/clients/telegram_buffer.py @@ -51,7 +51,7 @@ class TelegramBuffer(object): # - The checksum is optional '{0,4}' because not all telegram versions # support it. return re.findall( - r'\/[^\/]+?\![A-F0-9]{0,4}\r\n', + r'\/[^\/]+?\![A-F0-9]{0,4}\0?\r\n', self._buffer, re.DOTALL ) From d94bc8de03586b53959b5b320528606f78971a9a Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Mon, 18 Sep 2017 12:02:36 +0200 Subject: [PATCH 095/152] updated version --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0da194d..10d1169 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.11** (2017-09-18) + +- NULL value fix in checksum (`pull request #26 `_) + **0.10** (2017-06-05) - bugix: don't force full telegram signatures (`pull request #25 `_) diff --git a/setup.py b/setup.py index 176233c..63fc719 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.10', + version='0.11', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From d534b1d8b064f62257feda6828dc9f810a85389d Mon Sep 17 00:00:00 2001 From: jk-5 Date: Thu, 6 Sep 2018 14:53:30 +0200 Subject: [PATCH 096/152] Added missing values for 3-phase sagemcom TD210-D meter --- dsmr_parser/obis_references.py | 7 +++++++ dsmr_parser/telegram_specifications.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 6791f1e..14f696b 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -17,6 +17,7 @@ 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' @@ -24,6 +25,12 @@ 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' diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 5222786..19b0028 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -96,6 +96,7 @@ V5 = { 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)), @@ -103,6 +104,12 @@ V5 = { 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(int)), + obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(int)), obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), obis.DEVICE_TYPE: CosemParser(ValueParser(int)), obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), From 472c54968e72c630707d3c54d58a6f13f4de3f68 Mon Sep 17 00:00:00 2001 From: jk-5 Date: Thu, 6 Sep 2018 15:40:02 +0200 Subject: [PATCH 097/152] Corrected meter types --- dsmr_parser/telegram_specifications.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 19b0028..1e28fce 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -104,12 +104,12 @@ V5 = { 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(int)), - obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(int)), - obis.INSTANTANEOUS_CURRENT_L3: 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)), From 3327c78c0ee4f19f909e5951cbb5710dc811ffb0 Mon Sep 17 00:00:00 2001 From: jk-5 Date: Thu, 6 Sep 2018 15:49:11 +0200 Subject: [PATCH 098/152] Updated unittests --- test/test_parse_v5.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index 550fde9..e9cfbc1 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -87,6 +87,12 @@ class TelegramParserV5Test(unittest.TestCase): 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 @@ -123,6 +129,42 @@ class TelegramParserV5Test(unittest.TestCase): 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 From ad6ab304f5c28279bd8c22d760883775c939adce Mon Sep 17 00:00:00 2001 From: Thomas Neele Date: Sat, 15 Sep 2018 21:18:36 +0200 Subject: [PATCH 099/152] Added serial settings for DSMR v5.0 --- dsmr_parser/clients/__init__.py | 2 +- dsmr_parser/clients/settings.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/clients/__init__.py b/dsmr_parser/clients/__init__.py index 2a3b1fd..7323ecd 100644 --- a/dsmr_parser/clients/__init__.py +++ b/dsmr_parser/clients/__init__.py @@ -1,5 +1,5 @@ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ - SERIAL_SETTINGS_V4 + 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 diff --git a/dsmr_parser/clients/settings.py b/dsmr_parser/clients/settings.py index 2c2677c..26502d0 100644 --- a/dsmr_parser/clients/settings.py +++ b/dsmr_parser/clients/settings.py @@ -20,3 +20,13 @@ SERIAL_SETTINGS_V4 = { '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 +} From 887dd3a2aa820ed4b33a776a3805c5d86dae9b86 Mon Sep 17 00:00:00 2001 From: bossjl Date: Fri, 21 Sep 2018 18:37:32 +0200 Subject: [PATCH 100/152] Lux-creos-obis-1.8.0 --- dsmr_parser/obis_references.py | 1 + dsmr_parser/telegram_specifications.py | 1 + 2 files changed, 2 insertions(+) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 14f696b..5050f43 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -8,6 +8,7 @@ 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' diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 1e28fce..e2c0bf5 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -88,6 +88,7 @@ V5 = { 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)), From 59811dbf9fe11483c5827e9088e4f1728d31a11b Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 23 Sep 2018 12:59:02 +0200 Subject: [PATCH 101/152] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 63fc719..aa2b577 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigeldokter@gmail.com', url='https://github.com/ndokter/dsmr_parser', - version='0.11', + version='0.12', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 41f6a7ac168a5b306f4cb41a0e562aaf06a92504 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 23 Sep 2018 13:01:04 +0200 Subject: [PATCH 102/152] Update CHANGELOG.rst --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 10d1169..234c994 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.12** (2018-09-23) +- Add serial settings for DSMR v5.0 (`pull request #31 `_). +- Lux-creos-obis-1.8.0 (`pull request #32 `_). + **0.11** (2017-09-18) - NULL value fix in checksum (`pull request #26 `_) From 48783acc00dceecf35a01447a04d186f9d796e75 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sun, 23 Sep 2018 13:01:22 +0200 Subject: [PATCH 103/152] Update CHANGELOG.rst --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 234c994..32629c4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,7 @@ Change Log ---------- **0.12** (2018-09-23) + - Add serial settings for DSMR v5.0 (`pull request #31 `_). - Lux-creos-obis-1.8.0 (`pull request #32 `_). From 85c67464a1a9ff4e606e155bc5ff707140c23702 Mon Sep 17 00:00:00 2001 From: Mark Leenaerts Date: Sat, 19 Jan 2019 16:11:12 +0100 Subject: [PATCH 104/152] 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. --- dsmr_parser/clients/protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 0834051..141a389 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -11,7 +11,7 @@ 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_V4, SERIAL_SETTINGS_V5 def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): @@ -25,7 +25,7 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): serial_settings = SERIAL_SETTINGS_V4 elif dsmr_version == '5': specification = telegram_specifications.V5 - serial_settings = SERIAL_SETTINGS_V4 + serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) From c04b0a5add58ce70153eede1a87ca171876b61c7 Mon Sep 17 00:00:00 2001 From: Nigel Date: Mon, 4 Mar 2019 20:31:51 +0100 Subject: [PATCH 105/152] Updated version number --- CHANGELOG.rst | 4 ++++ setup.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 32629c4..f7d822c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.13** (2019-03-04) + +- Fix DSMR v5.0 serial settings which were not used (`pull request #33 `_). + **0.12** (2018-09-23) - Add serial settings for DSMR v5.0 (`pull request #31 `_). diff --git a/setup.py b/setup.py index aa2b577..e375858 100644 --- a/setup.py +++ b/setup.py @@ -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.12', + version='0.13', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 8bdf77c78d938281bbe29b6c4d785877435dcdf6 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sat, 6 Apr 2019 12:56:27 +0200 Subject: [PATCH 106/152] ensure build and dist directories are not synced --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1da5fee..20de282 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /.project /.pydevproject /.coverage +build/ +dist/ From c36f68a88423e9e656658db0afdd46fd7b350ff6 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Thu, 6 Jun 2019 05:41:55 +0200 Subject: [PATCH 107/152] working version of the Telegram object --- dsmr_parser/clients/serial_.py | 20 ++++++++++++ dsmr_parser/obis_name_mapping.py | 54 ++++++++++++++++++++++++++++++++ dsmr_parser/objects.py | 54 ++++++++++++++++++++++++++++++++ dsmr_parser/parsers.py | 2 +- test/test_telegram.py | 30 ++++++++++++++++++ tox.ini | 2 +- 6 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 dsmr_parser/obis_name_mapping.py create mode 100644 test/test_telegram.py diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 1d7be89..c252f6f 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -6,6 +6,7 @@ import serial_asyncio from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.parsers import TelegramParser +from dsmr_parser.objects import Telegram logger = logging.getLogger(__name__) @@ -41,6 +42,25 @@ class SerialReader(object): except ParseError as e: logger.error('Failed to parse telegram: %s', e) + def read_as_object(self): + """ + Read complete DSMR telegram's from the serial interface and return a Telegram object. + + :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 Telegram(telegram, telegram_parser, telegram_specification) + 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.""" diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py new file mode 100644 index 0000000..8f72654 --- /dev/null +++ b/dsmr_parser/obis_name_mapping.py @@ -0,0 +1,54 @@ +from dsmr_parser import obis_references as obis + +""" +dsmr_parser.obis_name_mapping +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module contains a mapping of obis references to names. +""" + +EN = { + obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', + obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', + obis.ELECTRICITY_IMPORTED_TOTAL : 'ELECTRICITY_IMPORTED_TOTAL', + obis.ELECTRICITY_USED_TARIFF_1 : 'ELECTRICITY_USED_TARIFF_1', + obis.ELECTRICITY_USED_TARIFF_2 : 'ELECTRICITY_USED_TARIFF_2', + obis.ELECTRICITY_DELIVERED_TARIFF_1 : 'ELECTRICITY_DELIVERED_TARIFF_1', + obis.ELECTRICITY_DELIVERED_TARIFF_2 : 'ELECTRICITY_DELIVERED_TARIFF_2', + obis.ELECTRICITY_ACTIVE_TARIFF : 'ELECTRICITY_ACTIVE_TARIFF', + obis.EQUIPMENT_IDENTIFIER : 'EQUIPMENT_IDENTIFIER', + obis.CURRENT_ELECTRICITY_USAGE : 'CURRENT_ELECTRICITY_USAGE', + obis.CURRENT_ELECTRICITY_DELIVERY : 'CURRENT_ELECTRICITY_DELIVERY', + obis.LONG_POWER_FAILURE_COUNT : 'LONG_POWER_FAILURE_COUNT', + obis.SHORT_POWER_FAILURE_COUNT : 'SHORT_POWER_FAILURE_COUNT', + obis.POWER_EVENT_FAILURE_LOG : 'POWER_EVENT_FAILURE_LOG', + obis.VOLTAGE_SAG_L1_COUNT : 'VOLTAGE_SAG_L1_COUNT', + obis.VOLTAGE_SAG_L2_COUNT : 'VOLTAGE_SAG_L2_COUNT', + obis.VOLTAGE_SAG_L3_COUNT : 'VOLTAGE_SAG_L3_COUNT', + obis.VOLTAGE_SWELL_L1_COUNT : 'VOLTAGE_SWELL_L1_COUNT', + obis.VOLTAGE_SWELL_L2_COUNT : 'VOLTAGE_SWELL_L2_COUNT', + obis.VOLTAGE_SWELL_L3_COUNT : 'VOLTAGE_SWELL_L3_COUNT', + obis.INSTANTANEOUS_VOLTAGE_L1 : 'INSTANTANEOUS_VOLTAGE_L1', + obis.INSTANTANEOUS_VOLTAGE_L2 : 'INSTANTANEOUS_VOLTAGE_L2', + obis.INSTANTANEOUS_VOLTAGE_L3 : 'INSTANTANEOUS_VOLTAGE_L3', + obis.INSTANTANEOUS_CURRENT_L1 : 'INSTANTANEOUS_CURRENT_L1', + obis.INSTANTANEOUS_CURRENT_L2 : 'INSTANTANEOUS_CURRENT_L2', + obis.INSTANTANEOUS_CURRENT_L3 : 'INSTANTANEOUS_CURRENT_L3', + obis.TEXT_MESSAGE_CODE : 'TEXT_MESSAGE_CODE', + obis.TEXT_MESSAGE : 'TEXT_MESSAGE', + obis.DEVICE_TYPE : 'DEVICE_TYPE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + obis.EQUIPMENT_IDENTIFIER_GAS : 'EQUIPMENT_IDENTIFIER_GAS', + obis.HOURLY_GAS_METER_READING : 'HOURLY_GAS_METER_READING', + obis.GAS_METER_READING : 'GAS_METER_READING', + obis.ACTUAL_TRESHOLD_ELECTRICITY : 'ACTUAL_TRESHOLD_ELECTRICITY', + obis.ACTUAL_SWITCH_POSITION : 'ACTUAL_SWITCH_POSITION', + obis.VALVE_POSITION_GAS : 'VALVE_POSITION_GAS' +} + +REVERSE_EN = dict([ (v,k) for k,v in EN.items()]) \ No newline at end of file diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index e6706c4..940318a 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,3 +1,57 @@ +import dsmr_parser.obis_name_mapping + +class Telegram(object): + """ + Container for raw and parsed telegram data. + Initializing: + from dsmr_parser import telegram_specifications + from dsmr_parser.exceptions import InvalidChecksumError, ParseError + from dsmr_parser.objects import CosemObject, MBusObject, Telegram + from dsmr_parser.parsers import TelegramParser + from test.example_telegrams import TELEGRAM_V4_2 + parser = TelegramParser(telegram_specifications.V4) + telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) + + Attributes can be accessed on a telegram object by addressing by their english name, for example: + telegram.ELECTRICITY_USED_TARIFF_1 + + All attributes in a telegram can be iterated over, for example: + [k for k,v in telegram] + yields: + ['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...] + """ + def __init__(self, telegram_data, telegram_parser, telegram_specification): + self._telegram_data = telegram_data + self._telegram_specification = telegram_specification + self._telegram_parser = telegram_parser + self._obis_name_mapping = dsmr_parser.obis_name_mapping.EN + self._reverse_obis_name_mapping = dsmr_parser.obis_name_mapping.REVERSE_EN + self._dictionary = self._telegram_parser.parse(telegram_data) + self._item_names = self._get_item_names() + + def __getattr__(self, name): + ''' will only get called for undefined attributes ''' + obis_reference = self._reverse_obis_name_mapping[name] + value = self._dictionary[obis_reference] + setattr(self, name, value) + return value + + def _get_item_names(self): + return [self._obis_name_mapping[k] for k, v in self._dictionary.items()] + + def __iter__(self): + for attr in self._item_names: + value = getattr(self, attr) + yield attr, value + + def __str__(self): + output = "" + for attr,value in self: + output += " " if not output == "" else "" + output += "{}: \t {} \t[{}]\n".format(attr,str(value.value),str(value.unit)) + return output + + class DSMRObject(object): """ Represents all data from a single telegram line. diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 087d9e0..4b415f6 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,7 +3,7 @@ import re from PyCRC.CRC16 import CRC16 -from dsmr_parser.objects import MBusObject, CosemObject +from dsmr_parser.objects import MBusObject, CosemObject, Telegram from dsmr_parser.exceptions import ParseError, InvalidChecksumError logger = logging.getLogger(__name__) diff --git a/test/test_telegram.py b/test/test_telegram.py new file mode 100644 index 0000000..d0e1042 --- /dev/null +++ b/test/test_telegram.py @@ -0,0 +1,30 @@ +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, Telegram +from dsmr_parser.parsers import TelegramParser +from test.example_telegrams import TELEGRAM_V4_2 + +class TelegramTest(unittest.TestCase): + """ Test instantiation of Telegram object """ + + def test_instantiate(self): + parser = TelegramParser(telegram_specifications.V4) + #result = parser.parse(TELEGRAM_V4_2) + telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) + + + + + # 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' diff --git a/tox.ini b/tox.ini index 95660fe..23fe214 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ commands= pylama dsmr_parser test [pylama:dsmr_parser/clients/__init__.py] -ignore = W0611 +ignore = W0611,W0605 [pylama:pylint] max_line_length = 100 From 83886247216b439af43160841ae69221d146cf49 Mon Sep 17 00:00:00 2001 From: Thomas Neele Date: Mon, 22 Jul 2019 21:33:15 +0200 Subject: [PATCH 108/152] Read more data from serial port at once A telegram can contain dozens of lines. Reading them one by one is somewhat inefficient. With this change, the client tries to read all data that is available. This significantly reduced CPU load for me. --- dsmr_parser/clients/serial_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 1d7be89..9939194 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -30,7 +30,7 @@ class SerialReader(object): """ with serial.Serial(**self.serial_settings) as serial_handle: while True: - data = serial_handle.readline() + data = serial_handle.read(max(1, min(1024, serial_handle.in_waiting))) self.telegram_buffer.append(data.decode('ascii')) for telegram in self.telegram_buffer.get_all(): From f9e9e70771598f4316e6b64b6d6016925f50c3e4 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Thu, 3 Oct 2019 21:33:11 +0200 Subject: [PATCH 109/152] experimentation file --- test/experiment_telegram.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test/experiment_telegram.py diff --git a/test/experiment_telegram.py b/test/experiment_telegram.py new file mode 100644 index 0000000..3450a05 --- /dev/null +++ b/test/experiment_telegram.py @@ -0,0 +1,14 @@ +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, Telegram +from dsmr_parser.parsers import TelegramParser +from test.example_telegrams import TELEGRAM_V4_2 +parser = TelegramParser(telegram_specifications.V4) +telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) + +print(telegram) \ No newline at end of file From b31bcd61fc918fe5b5bc3c0e1cff78fa6947ab3a Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 8 Oct 2019 22:13:58 +0200 Subject: [PATCH 110/152] Updated version number --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f7d822c..5fac8e7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.14** (2019-10-08) + +- Changed serial reading to reduce CPU usage (`pull request #37 `_). + **0.13** (2019-03-04) - Fix DSMR v5.0 serial settings which were not used (`pull request #33 `_). diff --git a/setup.py b/setup.py index e375858..b20a2a8 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.13', + version='0.14', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From f7ba363b93598c98ce46230994c4fcfb619a559d Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 25 Nov 2019 01:37:48 +0100 Subject: [PATCH 111/152] small fixes --- dsmr_parser/clients/serial_.py | 2 +- test/experiment_telegram.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index c252f6f..319becc 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -55,7 +55,7 @@ class SerialReader(object): for telegram in self.telegram_buffer.get_all(): try: - yield Telegram(telegram, telegram_parser, telegram_specification) + yield Telegram(telegram, self.telegram_parser, self.telegram_specification) except InvalidChecksumError as e: logger.warning(str(e)) except ParseError as e: diff --git a/test/experiment_telegram.py b/test/experiment_telegram.py index 3450a05..2649f51 100644 --- a/test/experiment_telegram.py +++ b/test/experiment_telegram.py @@ -7,7 +7,7 @@ from dsmr_parser import telegram_specifications from dsmr_parser.exceptions import InvalidChecksumError, ParseError from dsmr_parser.objects import CosemObject, MBusObject, Telegram from dsmr_parser.parsers import TelegramParser -from test.example_telegrams import TELEGRAM_V4_2 +from example_telegrams import TELEGRAM_V4_2 parser = TelegramParser(telegram_specifications.V4) telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) From 8ba400800b7f5b663e000919219f506b30ca8cc0 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 25 Nov 2019 01:53:54 +0100 Subject: [PATCH 112/152] small fixes --- dsmr_parser/clients/serial_.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dsmr_parser/clients/serial_.py b/dsmr_parser/clients/serial_.py index 319becc..265cedd 100644 --- a/dsmr_parser/clients/serial_.py +++ b/dsmr_parser/clients/serial_.py @@ -21,6 +21,7 @@ class SerialReader(object): self.telegram_parser = TelegramParser(telegram_specification) self.telegram_buffer = TelegramBuffer() + self.telegram_specification = telegram_specification def read(self): """ From d712d468ac5c3eee28d03e3fa893b0a2fa00fc75 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 25 Nov 2019 20:44:25 +0100 Subject: [PATCH 113/152] remove useless space --- dsmr_parser/objects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 940318a..e7374ab 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -47,7 +47,6 @@ class Telegram(object): def __str__(self): output = "" for attr,value in self: - output += " " if not output == "" else "" output += "{}: \t {} \t[{}]\n".format(attr,str(value.value),str(value.unit)) return output From a137ef0e02fa97dd11a1e28a25a0318cc9a4dc1b Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 1 Dec 2019 17:47:22 +0100 Subject: [PATCH 114/152] add some documentation for the use of the telegram as an object --- README.rst | 114 ++++++++++++++++++++++++++++++++++++++++- dsmr_parser/objects.py | 2 +- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7b53ee4..5f0d7d6 100644 --- a/README.rst +++ b/README.rst @@ -85,8 +85,8 @@ into a dictionary. telegram = parser.parse(telegram_str) print(telegram) # see 'Telegram object' docs below -Telegram object ---------------- +Telegram dictionary +------------------- A dictionary of which the key indicates the field type. These regex values correspond to one of dsmr_parser.obis_reference constants. @@ -138,6 +138,116 @@ Example to get some of the values: # See dsmr_reader.obis_references for all readable telegram values. # Note that the avilable values differ per DSMR version. +Telegram as an Object +--------------------- +An object version of the telegram is available as well. + + +.. code-block:: python + + # DSMR v4.2 p1 using dsmr_parser and telegram objects + + from dsmr_parser import telegram_specifications + from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5 + from dsmr_parser.objects import CosemObject, MBusObject, Telegram + from dsmr_parser.parsers import TelegramParser + import os + + serial_reader = SerialReader( + device='/dev/ttyUSB0', + serial_settings=SERIAL_SETTINGS_V5, + telegram_specification=telegram_specifications.V4 + ) + + # telegram = next(serial_reader.read_as_object()) + # print(telegram) + + for telegram in serial_reader.read_as_object(): + os.system('clear') + print(telegram) + +Example of output of print of the telegram object: + +.. code-block:: console + + P1_MESSAGE_HEADER: 42 [None] + P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None] + EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None] + ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh] + ELECTRICITY_USED_TARIFF_2: 1435.706 [kWh] + ELECTRICITY_DELIVERED_TARIFF_1: 0.000 [kWh] + ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh] + ELECTRICITY_ACTIVE_TARIFF: 0002 [None] + CURRENT_ELECTRICITY_USAGE: 2.027 [kW] + CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW] + LONG_POWER_FAILURE_COUNT: 7 [None] + VOLTAGE_SAG_L1_COUNT: 0 [None] + VOLTAGE_SAG_L2_COUNT: 0 [None] + VOLTAGE_SAG_L3_COUNT: 0 [None] + VOLTAGE_SWELL_L1_COUNT: 0 [None] + VOLTAGE_SWELL_L2_COUNT: 0 [None] + VOLTAGE_SWELL_L3_COUNT: 0 [None] + TEXT_MESSAGE_CODE: None [None] + TEXT_MESSAGE: None [None] + DEVICE_TYPE: 3 [None] + INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.170 [kW] + INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 1.247 [kW] + INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.209 [kW] + INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW] + INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW] + INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW] + EQUIPMENT_IDENTIFIER_GAS: 4819243993373755377509728609491464 [None] + HOURLY_GAS_METER_READING: 981.443 [m3] + +Accessing the telegrams information as attributes directly: + +.. code-block:: python + + telegram + Out[3]: + telegram.CURRENT_ELECTRICITY_USAGE.value + Out[4]: Decimal('2.027') + telegram.CURRENT_ELECTRICITY_USAGE.unit + Out[5]: 'kW' + +The telegram object has an iterator, can be used to find all the elements in the current telegram: + +.. code-block:: python + + for attr, value in telegram: + print(attr) + + Out[7]: + P1_MESSAGE_HEADER + P1_MESSAGE_TIMESTAMP + EQUIPMENT_IDENTIFIER + ELECTRICITY_USED_TARIFF_1 + ELECTRICITY_USED_TARIFF_2 + ELECTRICITY_DELIVERED_TARIFF_1 + ELECTRICITY_DELIVERED_TARIFF_2 + ELECTRICITY_ACTIVE_TARIFF + CURRENT_ELECTRICITY_USAGE + CURRENT_ELECTRICITY_DELIVERY + LONG_POWER_FAILURE_COUNT + VOLTAGE_SAG_L1_COUNT + VOLTAGE_SAG_L2_COUNT + VOLTAGE_SAG_L3_COUNT + VOLTAGE_SWELL_L1_COUNT + VOLTAGE_SWELL_L2_COUNT + VOLTAGE_SWELL_L3_COUNT + TEXT_MESSAGE_CODE + TEXT_MESSAGE + DEVICE_TYPE + INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE + INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE + INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE + INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE + INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE + INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE + EQUIPMENT_IDENTIFIER_GAS + HOURLY_GAS_METER_READING + + Installation ------------ diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index e7374ab..07d576d 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -46,7 +46,7 @@ class Telegram(object): def __str__(self): output = "" - for attr,value in self: + for attr, value in self: output += "{}: \t {} \t[{}]\n".format(attr,str(value.value),str(value.unit)) return output From 1b522fc7f088f22a8b54df5605dada8fc79cb5f7 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 1 Dec 2019 18:34:21 +0100 Subject: [PATCH 115/152] add some documentation for the use of the telegram as an object --- README.rst | 65 ++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/README.rst b/README.rst index 5f0d7d6..11ce600 100644 --- a/README.rst +++ b/README.rst @@ -210,43 +210,40 @@ Accessing the telegrams information as attributes directly: telegram.CURRENT_ELECTRICITY_USAGE.unit Out[5]: 'kW' -The telegram object has an iterator, can be used to find all the elements in the current telegram: +The telegram object has an iterator, can be used to find all the information elements in the current telegram: .. code-block:: python - for attr, value in telegram: - print(attr) - - Out[7]: - P1_MESSAGE_HEADER - P1_MESSAGE_TIMESTAMP - EQUIPMENT_IDENTIFIER - ELECTRICITY_USED_TARIFF_1 - ELECTRICITY_USED_TARIFF_2 - ELECTRICITY_DELIVERED_TARIFF_1 - ELECTRICITY_DELIVERED_TARIFF_2 - ELECTRICITY_ACTIVE_TARIFF - CURRENT_ELECTRICITY_USAGE - CURRENT_ELECTRICITY_DELIVERY - LONG_POWER_FAILURE_COUNT - VOLTAGE_SAG_L1_COUNT - VOLTAGE_SAG_L2_COUNT - VOLTAGE_SAG_L3_COUNT - VOLTAGE_SWELL_L1_COUNT - VOLTAGE_SWELL_L2_COUNT - VOLTAGE_SWELL_L3_COUNT - TEXT_MESSAGE_CODE - TEXT_MESSAGE - DEVICE_TYPE - INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE - INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE - INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE - INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE - INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE - INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE - EQUIPMENT_IDENTIFIER_GAS - HOURLY_GAS_METER_READING - + [attr for attr, value in telegram] + Out[11]: + ['P1_MESSAGE_HEADER', + 'P1_MESSAGE_TIMESTAMP', + 'EQUIPMENT_IDENTIFIER', + 'ELECTRICITY_USED_TARIFF_1', + 'ELECTRICITY_USED_TARIFF_2', + 'ELECTRICITY_DELIVERED_TARIFF_1', + 'ELECTRICITY_DELIVERED_TARIFF_2', + 'ELECTRICITY_ACTIVE_TARIFF', + 'CURRENT_ELECTRICITY_USAGE', + 'CURRENT_ELECTRICITY_DELIVERY', + 'LONG_POWER_FAILURE_COUNT', + 'VOLTAGE_SAG_L1_COUNT', + 'VOLTAGE_SAG_L2_COUNT', + 'VOLTAGE_SAG_L3_COUNT', + 'VOLTAGE_SWELL_L1_COUNT', + 'VOLTAGE_SWELL_L2_COUNT', + 'VOLTAGE_SWELL_L3_COUNT', + 'TEXT_MESSAGE_CODE', + 'TEXT_MESSAGE', + 'DEVICE_TYPE', + 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + 'EQUIPMENT_IDENTIFIER_GAS', + 'HOURLY_GAS_METER_READING'] Installation From ea804d485ebbf5f8bb98b5902e2a32ae69f21040 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 1 Dec 2019 18:39:21 +0100 Subject: [PATCH 116/152] add some documentation for the use of the telegram as an object --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 11ce600..47fc884 100644 --- a/README.rst +++ b/README.rst @@ -205,10 +205,12 @@ Accessing the telegrams information as attributes directly: telegram Out[3]: + telegram.CURRENT_ELECTRICITY_USAGE + Out[4]: telegram.CURRENT_ELECTRICITY_USAGE.value - Out[4]: Decimal('2.027') + Out[5]: Decimal('2.027') telegram.CURRENT_ELECTRICITY_USAGE.unit - Out[5]: 'kW' + Out[6]: 'kW' The telegram object has an iterator, can be used to find all the information elements in the current telegram: From aa4f9fb96751bcd2d210e347376ad71498afb6da Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 1 Dec 2019 18:55:53 +0100 Subject: [PATCH 117/152] update version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b20a2a8..673267f 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.14', + version='0.15', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 5313edd6cb84ace9a2549a0e482103d738338ace Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 9 Dec 2019 22:31:40 +0100 Subject: [PATCH 118/152] add file reader --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 20de282..4dfc343 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ /.coverage build/ dist/ +*.*~ +*~ \ No newline at end of file From 3c78b0b6c4f17246fa4a6a1d0e9db095ce987cbc Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 9 Dec 2019 22:36:46 +0100 Subject: [PATCH 119/152] add file reader --- dsmr_parser/clients/filereader.py | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 dsmr_parser/clients/filereader.py diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py new file mode 100644 index 0000000..ee1fc90 --- /dev/null +++ b/dsmr_parser/clients/filereader.py @@ -0,0 +1,35 @@ +from dsmr_parser import telegram_specifications +from dsmr_parser.clients.telegram_buffer import TelegramBuffer +from dsmr_parser.exceptions import ParseError, InvalidChecksumError +from dsmr_parser.objects import CosemObject, MBusObject, Telegram +from dsmr_parser.parsers import TelegramParser +import os + +logger = logging.getLogger(__name__) + +class FileReader(object): + + def __init__(self, file, telegram_specification): + self._file = file + self.telegram_parser = TelegramParser(telegram_specification) + self.telegram_buffer = TelegramBuffer() + self.telegram_specification = telegram_specification + + def read_as_object(self): + """ + Read complete DSMR telegram's from a file and return a Telegram object. + :rtype: generator + """ + with open(self._file,"rb") as file_handle: + while True: + data = file_handle.readline() + str = data.decode() + self.telegram_buffer.append(str) + + for telegram in self.telegram_buffer.get_all(): + try: + yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + except InvalidChecksumError as e: + logger.warning(str(e)) + except ParseError as e: + logger.error('Failed to parse telegram: %s', e) From a65949823d9303055ff31dfe6cd4083c146721d7 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 9 Dec 2019 23:27:57 +0100 Subject: [PATCH 120/152] fix filereader --- dsmr_parser/clients/filereader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index ee1fc90..30396d9 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -1,9 +1,9 @@ -from dsmr_parser import telegram_specifications +import logging + from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError -from dsmr_parser.objects import CosemObject, MBusObject, Telegram +from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser -import os logger = logging.getLogger(__name__) From c1d7ba151d7bb7f3a58b38d44f8f0c880da0f365 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 9 Dec 2019 23:48:48 +0100 Subject: [PATCH 121/152] add some documentation for the file reader --- dsmr_parser/clients/filereader.py | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index 30396d9..dae492d 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -8,6 +8,44 @@ from dsmr_parser.parsers import TelegramParser logger = logging.getLogger(__name__) class FileReader(object): + """ + Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects + for each read telegram. + Usage: + from dsmr_parser import telegram_specifications + from dsmr_parser.clients.filereader import FileReader + + if __name__== "__main__": + + infile = '/data/smartmeter/readings.txt' + + file_reader = FileReader( + file = infile, + telegram_specification = telegram_specifications.V4 + ) + + for telegram in file_reader.read_as_object(): + print(telegram) + + The file can be created like: + from dsmr_parser import telegram_specifications + from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5 + + if __name__== "__main__": + + outfile = '/data/smartmeter/readings.txt' + + serial_reader = SerialReader( + device='/dev/ttyUSB0', + serial_settings=SERIAL_SETTINGS_V5, + telegram_specification=telegram_specifications.V4 + ) + + for telegram in serial_reader.read_as_object(): + f=open(outfile,"ab+") + f.write(telegram._telegram_data.encode()) + f.close() + """ def __init__(self, file, telegram_specification): self._file = file From 7c9c59308e2284a9807e1a1e21cc93bc00bbf138 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 12 Dec 2019 22:20:16 +0100 Subject: [PATCH 122/152] `create_tcp_dsmr_reader` accepts `loop=None` but always expects a loop. Fixes #36 --- dsmr_parser/clients/protocol.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 141a389..2c9650e 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -49,12 +49,13 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None): def create_tcp_dsmr_reader(host, port, dsmr_version, telegram_callback, loop=None): """Creates a DSMR asyncio protocol coroutine using TCP connection.""" + if not loop: + loop = asyncio.get_event_loop() protocol, _ = create_dsmr_protocol( - dsmr_version, telegram_callback, loop=None) + dsmr_version, telegram_callback, loop=loop) conn = loop.create_connection(protocol, host, port) return conn - class DSMRProtocol(asyncio.Protocol): """Assemble and handle incoming data into complete DSM telegrams.""" From 12aa003799ee3240f8c523e9133101dcd37778a1 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 12 Dec 2019 22:29:18 +0100 Subject: [PATCH 123/152] Updated version number --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5fac8e7..3b334c9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.15** (2019-12-12) + +- Fixed asyncio loop issue (`pull request #37 `_). + **0.14** (2019-10-08) - Changed serial reading to reduce CPU usage (`pull request #37 `_). diff --git a/setup.py b/setup.py index b20a2a8..673267f 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.14', + version='0.15', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From 3a8b4d24582d299751df0645ad4f5de7411a9baa Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Thu, 12 Dec 2019 22:33:14 +0100 Subject: [PATCH 124/152] Corrected changelog --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b334c9..584c593 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,7 @@ Change Log **0.15** (2019-12-12) -- Fixed asyncio loop issue (`pull request #37 `_). +- Fixed asyncio loop issue (`pull request #43 `_). **0.14** (2019-10-08) From 50cef2646b18171ec6b50e4abde75438813815b5 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 16 Dec 2019 15:30:55 +0100 Subject: [PATCH 125/152] add fileinputreader --- dsmr_parser/clients/filereader.py | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index dae492d..c2a35cb 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -71,3 +71,47 @@ class FileReader(object): logger.warning(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e) + +class FileInputReader(object): + """ + Filereader to read and parse raw telegram strings from stdin or files specified at the commandline + and instantiate Telegram objects for each read telegram. + Usage: + from dsmr_parser import telegram_specifications + from dsmr_parser.clients.filereader import FileInputReader + + if __name__== "__main__": + + fileinput_reader = FileReader( + file = infile, + telegram_specification = telegram_specifications.V4 + ) + + for telegram in fileinput_reader.read_as_object(): + print(telegram) + """ + + def __init__(self, telegram_specification): + self.telegram_parser = TelegramParser(telegram_specification) + self.telegram_buffer = TelegramBuffer() + self.telegram_specification = telegram_specification + + def read_as_object(self): + """ + Read complete DSMR telegram's from stdin of filearguments specified on teh command line + and return a Telegram object. + :rtype: generator + """ + with fileinput.input(mode='rb') as file_handle: + while True: + data = file_handle.readline() + str = data.decode() + self.telegram_buffer.append(str) + + for telegram in self.telegram_buffer.get_all(): + try: + yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + except InvalidChecksumError as e: + logger.warning(str(e)) + except ParseError as e: + logger.error('Failed to parse telegram: %s', e) From 43500e6bc25e596ef985b39481a86adedcfa541d Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 16 Dec 2019 15:36:13 +0100 Subject: [PATCH 126/152] fix failing import --- dsmr_parser/clients/filereader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index c2a35cb..f4ed6fd 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -1,4 +1,5 @@ import logging +import fileinput from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError From 2d4b0d8e7266acf4f5c938157d1cb203ebe6a426 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 16 Dec 2019 15:41:24 +0100 Subject: [PATCH 127/152] fix documentation FileInputReader --- dsmr_parser/clients/filereader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index f4ed6fd..e6eeb59 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -77,7 +77,7 @@ class FileInputReader(object): """ Filereader to read and parse raw telegram strings from stdin or files specified at the commandline and instantiate Telegram objects for each read telegram. - Usage: + Usage python script "syphon_smartmeter_readings_stdin.py": from dsmr_parser import telegram_specifications from dsmr_parser.clients.filereader import FileInputReader @@ -90,6 +90,10 @@ class FileInputReader(object): for telegram in fileinput_reader.read_as_object(): print(telegram) + + Command line: + tail -f /data/smartmeter/readings.txt | python3 syphon_smartmeter_readings_stdin.py + """ def __init__(self, telegram_specification): From 3bfb555d0ea94cb623bdd64ce1c1ad5f47eeb9e1 Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Thu, 19 Dec 2019 04:19:29 +0100 Subject: [PATCH 128/152] Add support for Belgian and Smarty meters --- dsmr_parser/clients/protocol.py | 3 +++ dsmr_parser/obis_references.py | 5 +++++ dsmr_parser/telegram_specifications.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 2c9650e..7e8e260 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -26,6 +26,9 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None): elif dsmr_version == '5': specification = telegram_specifications.V5 serial_settings = SERIAL_SETTINGS_V5 + elif dsmr_version == '5B': + specification = telegram_specifications.BELGIUM_FLUVIUS + serial_settings = SERIAL_SETTINGS_V5 else: raise NotImplementedError("No telegram parser found for version: %s", dsmr_version) diff --git a/dsmr_parser/obis_references.py b/dsmr_parser/obis_references.py index 5050f43..cb7b158 100644 --- a/dsmr_parser/obis_references.py +++ b/dsmr_parser/obis_references.py @@ -60,3 +60,8 @@ ELECTRICITY_DELIVERED_TARIFF_ALL = ( ELECTRICITY_DELIVERED_TARIFF_1, ELECTRICITY_DELIVERED_TARIFF_2 ) + +# Alternate codes for foreign countries. +BELGIUM_HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n' # Different code, same format. +LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL = r'\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+) +LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL = r'\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index e2c0bf5..a42806f 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -1,4 +1,5 @@ from decimal import Decimal +from copy import deepcopy from dsmr_parser import obis_references as obis from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser @@ -128,3 +129,18 @@ V5 = { } ALL = (V2_2, V3, V4, V5) + + +BELGIUM_FLUVIUS = deepcopy(V5) +BELGIUM_FLUVIUS['objects'].update({ + obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser( + ValueParser(timestamp), + ValueParser(Decimal) + ) +}) + +LUXEMBOURG_SMARTY = deepcopy(V5) +LUXEMBOURG_SMARTY['objects'].update({ + obis.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), + obis.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemParser(ValueParser(Decimal)), +}) From a01e67364630c10da635b9d43eaa535f05feff63 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 21 Dec 2019 15:02:13 +0100 Subject: [PATCH 129/152] updated changelog for new release --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 584c593..babab7c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.16** (2019-12-21) + +- Add support for Belgian and Smarty meters (`pull request #44 `_). + **0.15** (2019-12-12) - Fixed asyncio loop issue (`pull request #43 `_). diff --git a/setup.py b/setup.py index 673267f..fe434f6 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.15', + version='0.16', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From eac9681e0792294cd5fa384fc8f858cda6b77dc0 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Sat, 21 Dec 2019 17:37:50 +0100 Subject: [PATCH 130/152] updated changelog --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index babab7c..fe4d4ea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.17** (2019-12-21) + +- Add a true telegram object (`pull request #40 `_). + **0.16** (2019-12-21) - Add support for Belgian and Smarty meters (`pull request #44 `_). From 8c2485d70f5fe9743be05d6d7b9f72b339e6707c Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Tue, 24 Dec 2019 00:57:44 +0100 Subject: [PATCH 131/152] added file tail reader --- dsmr_parser/clients/filereader.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index e6eeb59..12164f2 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -1,5 +1,6 @@ import logging import fileinput +import tailer from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.exceptions import ParseError, InvalidChecksumError @@ -120,3 +121,31 @@ class FileInputReader(object): logger.warning(str(e)) except ParseError as e: logger.error('Failed to parse telegram: %s', e) + + +class FileTailReader(object): + + def __init__(self, file, telegram_specification): + self._file = file + self.telegram_parser = TelegramParser(telegram_specification) + self.telegram_buffer = TelegramBuffer() + self.telegram_specification = telegram_specification + + def read_as_object(self): + """ + Read complete DSMR telegram's from a files tail and return a Telegram object. + :rtype: generator + """ + with open(self._file,"rb") as file_handle: + for data in tailer.follow(file_handle): + str = data.decode() + self.telegram_buffer.append(str) + + for telegram in self.telegram_buffer.get_all(): + try: + yield Telegram(telegram, self.telegram_parser, self.telegram_specification) + except dsmr_parser.exceptions.InvalidChecksumError as e: + logger.warning(str(e)) + except dsmr_parser.exceptions.ParseError as e: + logger.error('Failed to parse telegram: %s', e) + From e6625df4a7ff66da22cab1c74e80f393a9e4cd02 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Fri, 27 Dec 2019 15:18:35 +0100 Subject: [PATCH 132/152] add documentation to FileTailReader --- dsmr_parser/clients/filereader.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index 12164f2..3869a8a 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -124,6 +124,25 @@ class FileInputReader(object): class FileTailReader(object): + """ + Filereader to read and parse raw telegram strings from the tail of a + given file and instantiate Telegram objects for each read telegram. + Usage python script "syphon_smartmeter_readings_stdin.py": + from dsmr_parser import telegram_specifications + from dsmr_parser.clients.filereader import FileTailReader + + if __name__== "__main__": + + infile = '/data/smartmeter/readings.txt' + + filetail_reader = FileTailReader( + file = infile, + telegram_specification = telegram_specifications.V5 + ) + + for telegram in filetail_reader.read_as_object(): + print(telegram) + """ def __init__(self, file, telegram_specification): self._file = file From 0675a6e2e7af1f626d90abc3cf37341ae26233dd Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Fri, 27 Dec 2019 16:22:10 +0100 Subject: [PATCH 133/152] add tailer dependency --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e1f0139..0c6015d 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,8 @@ setup( 'pyserial>=3,<4', 'pyserial-asyncio<1', 'pytz', - 'PyCRC>=1.2,<2' + 'PyCRC>=1.2,<2', + 'Tailer==0.4.1' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] From d714528c5a8ec8e4286176c5711398100bc2c23c Mon Sep 17 00:00:00 2001 From: Jean-Louis Dupond Date: Tue, 28 Jan 2020 17:26:45 +0100 Subject: [PATCH 134/152] Include needed PyCRC code --- dsmr_parser/parsers.py | 27 +++++++++++++++++++++++++-- setup.py | 1 - tox.ini | 1 - 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 4b415f6..4609287 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -1,7 +1,7 @@ import logging import re -from PyCRC.CRC16 import CRC16 +from ctypes import c_ushort from dsmr_parser.objects import MBusObject, CosemObject, Telegram from dsmr_parser.exceptions import ParseError, InvalidChecksumError @@ -79,7 +79,7 @@ class TelegramParser(object): 'incomplete. The checksum and/or content values are missing.' ) - calculated_crc = CRC16().calculate(checksum_contents.group(0)) + calculated_crc = TelegramParser.crc16(checksum_contents.group(0)) expected_crc = int(checksum_hex.group(0), base=16) if calculated_crc != expected_crc: @@ -91,6 +91,29 @@ class TelegramParser(object): ) ) + @staticmethod + def crc16(telegram): + crc16_tab = [] + + for i in range(0, 256): + crc = c_ushort(i).value + for j in range(0, 8): + if (crc & 0x0001): + crc = c_ushort(crc >> 1).value ^ 0xA001 + else: + crc = c_ushort(crc >> 1).value + crc16_tab.append(hex(crc)) + + crcValue = 0x0000 + + for c in telegram: + d = ord(c) + tmp = crcValue ^ d + rotated = c_ushort(crcValue >> 8).value + crcValue = rotated ^ int(crc16_tab[(tmp & 0x00ff)], 0) + + return crcValue + class DSMRObjectParser(object): """ diff --git a/setup.py b/setup.py index e1f0139..7c54d73 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,6 @@ setup( 'pyserial>=3,<4', 'pyserial-asyncio<1', 'pytz', - 'PyCRC>=1.2,<2' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] diff --git a/tox.ini b/tox.ini index 23fe214..3efa213 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ deps= pytest-asyncio pytest-catchlog pytest-mock - PyCRC commands= py.test --cov=dsmr_parser test {posargs} pylama dsmr_parser test From dc6c35a0b6dc5a96e15d8863933125dba61bf1d2 Mon Sep 17 00:00:00 2001 From: Nigel Dokter Date: Tue, 28 Jan 2020 19:40:27 +0100 Subject: [PATCH 135/152] Updated changelog --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fe4d4ea..0fda0e3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ---------- +**0.18** (2020-01-28) + +- PyCRC replacement (`pull request #48 `_). + **0.17** (2019-12-21) - Add a true telegram object (`pull request #40 `_). diff --git a/setup.py b/setup.py index 7c54d73..78eba62 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.17', + version='0.18', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From fee3f696c46a062c1ca717773372e777bc797d0e Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 2 Feb 2020 17:26:47 +0100 Subject: [PATCH 136/152] merged upstream 0.18 version and resolved conflict --- CHANGELOG.rst | 8 ++++++++ dsmr_parser/parsers.py | 27 +++++++++++++++++++++++++-- setup.py | 3 +-- tox.ini | 1 - 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index babab7c..0fda0e3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Change Log ---------- +**0.18** (2020-01-28) + +- PyCRC replacement (`pull request #48 `_). + +**0.17** (2019-12-21) + +- Add a true telegram object (`pull request #40 `_). + **0.16** (2019-12-21) - Add support for Belgian and Smarty meters (`pull request #44 `_). diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 4b415f6..4609287 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -1,7 +1,7 @@ import logging import re -from PyCRC.CRC16 import CRC16 +from ctypes import c_ushort from dsmr_parser.objects import MBusObject, CosemObject, Telegram from dsmr_parser.exceptions import ParseError, InvalidChecksumError @@ -79,7 +79,7 @@ class TelegramParser(object): 'incomplete. The checksum and/or content values are missing.' ) - calculated_crc = CRC16().calculate(checksum_contents.group(0)) + calculated_crc = TelegramParser.crc16(checksum_contents.group(0)) expected_crc = int(checksum_hex.group(0), base=16) if calculated_crc != expected_crc: @@ -91,6 +91,29 @@ class TelegramParser(object): ) ) + @staticmethod + def crc16(telegram): + crc16_tab = [] + + for i in range(0, 256): + crc = c_ushort(i).value + for j in range(0, 8): + if (crc & 0x0001): + crc = c_ushort(crc >> 1).value ^ 0xA001 + else: + crc = c_ushort(crc >> 1).value + crc16_tab.append(hex(crc)) + + crcValue = 0x0000 + + for c in telegram: + d = ord(c) + tmp = crcValue ^ d + rotated = c_ushort(crcValue >> 8).value + crcValue = rotated ^ int(crc16_tab[(tmp & 0x00ff)], 0) + + return crcValue + class DSMRObjectParser(object): """ diff --git a/setup.py b/setup.py index 0c6015d..beb8e57 100644 --- a/setup.py +++ b/setup.py @@ -6,13 +6,12 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.17', + version='0.19', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', 'pyserial-asyncio<1', 'pytz', - 'PyCRC>=1.2,<2', 'Tailer==0.4.1' ], entry_points={ diff --git a/tox.ini b/tox.ini index 23fe214..3efa213 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ deps= pytest-asyncio pytest-catchlog pytest-mock - PyCRC commands= py.test --cov=dsmr_parser test {posargs} pylama dsmr_parser test From b6537678a70961ecabfc618273a46f5bc1dabd15 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 2 Feb 2020 22:12:25 +0100 Subject: [PATCH 137/152] cleaned up based on pylama complaints / pinpointed to coverage version 4.5.4 as next version is incompatible --- dsmr_parser/clients/filereader.py | 11 +++-- dsmr_parser/clients/protocol.py | 1 + dsmr_parser/obis_name_mapping.py | 80 +++++++++++++++---------------- dsmr_parser/objects.py | 3 +- dsmr_parser/parsers.py | 2 +- test/experiment_telegram.py | 10 +--- test/test_telegram.py | 23 +++------ tox.ini | 12 +++-- 8 files changed, 67 insertions(+), 75 deletions(-) diff --git a/dsmr_parser/clients/filereader.py b/dsmr_parser/clients/filereader.py index 3869a8a..061eda7 100644 --- a/dsmr_parser/clients/filereader.py +++ b/dsmr_parser/clients/filereader.py @@ -9,6 +9,7 @@ from dsmr_parser.parsers import TelegramParser logger = logging.getLogger(__name__) + class FileReader(object): """ Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects @@ -60,7 +61,7 @@ class FileReader(object): Read complete DSMR telegram's from a file and return a Telegram object. :rtype: generator """ - with open(self._file,"rb") as file_handle: + with open(self._file, "rb") as file_handle: while True: data = file_handle.readline() str = data.decode() @@ -74,6 +75,7 @@ class FileReader(object): except ParseError as e: logger.error('Failed to parse telegram: %s', e) + class FileInputReader(object): """ Filereader to read and parse raw telegram strings from stdin or files specified at the commandline @@ -155,7 +157,7 @@ class FileTailReader(object): Read complete DSMR telegram's from a files tail and return a Telegram object. :rtype: generator """ - with open(self._file,"rb") as file_handle: + with open(self._file, "rb") as file_handle: for data in tailer.follow(file_handle): str = data.decode() self.telegram_buffer.append(str) @@ -163,8 +165,7 @@ class FileTailReader(object): for telegram in self.telegram_buffer.get_all(): try: yield Telegram(telegram, self.telegram_parser, self.telegram_specification) - except dsmr_parser.exceptions.InvalidChecksumError as e: + except InvalidChecksumError as e: logger.warning(str(e)) - except dsmr_parser.exceptions.ParseError as e: + except ParseError as e: logger.error('Failed to parse telegram: %s', e) - diff --git a/dsmr_parser/clients/protocol.py b/dsmr_parser/clients/protocol.py index 7e8e260..e43e230 100644 --- a/dsmr_parser/clients/protocol.py +++ b/dsmr_parser/clients/protocol.py @@ -59,6 +59,7 @@ def create_tcp_dsmr_reader(host, port, dsmr_version, conn = loop.create_connection(protocol, host, port) return conn + class DSMRProtocol(asyncio.Protocol): """Assemble and handle incoming data into complete DSM telegrams.""" diff --git a/dsmr_parser/obis_name_mapping.py b/dsmr_parser/obis_name_mapping.py index 8f72654..0401f5e 100644 --- a/dsmr_parser/obis_name_mapping.py +++ b/dsmr_parser/obis_name_mapping.py @@ -10,45 +10,45 @@ This module contains a mapping of obis references to names. EN = { obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER', obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP', - obis.ELECTRICITY_IMPORTED_TOTAL : 'ELECTRICITY_IMPORTED_TOTAL', - obis.ELECTRICITY_USED_TARIFF_1 : 'ELECTRICITY_USED_TARIFF_1', - obis.ELECTRICITY_USED_TARIFF_2 : 'ELECTRICITY_USED_TARIFF_2', - obis.ELECTRICITY_DELIVERED_TARIFF_1 : 'ELECTRICITY_DELIVERED_TARIFF_1', - obis.ELECTRICITY_DELIVERED_TARIFF_2 : 'ELECTRICITY_DELIVERED_TARIFF_2', - obis.ELECTRICITY_ACTIVE_TARIFF : 'ELECTRICITY_ACTIVE_TARIFF', - obis.EQUIPMENT_IDENTIFIER : 'EQUIPMENT_IDENTIFIER', - obis.CURRENT_ELECTRICITY_USAGE : 'CURRENT_ELECTRICITY_USAGE', - obis.CURRENT_ELECTRICITY_DELIVERY : 'CURRENT_ELECTRICITY_DELIVERY', - obis.LONG_POWER_FAILURE_COUNT : 'LONG_POWER_FAILURE_COUNT', - obis.SHORT_POWER_FAILURE_COUNT : 'SHORT_POWER_FAILURE_COUNT', - obis.POWER_EVENT_FAILURE_LOG : 'POWER_EVENT_FAILURE_LOG', - obis.VOLTAGE_SAG_L1_COUNT : 'VOLTAGE_SAG_L1_COUNT', - obis.VOLTAGE_SAG_L2_COUNT : 'VOLTAGE_SAG_L2_COUNT', - obis.VOLTAGE_SAG_L3_COUNT : 'VOLTAGE_SAG_L3_COUNT', - obis.VOLTAGE_SWELL_L1_COUNT : 'VOLTAGE_SWELL_L1_COUNT', - obis.VOLTAGE_SWELL_L2_COUNT : 'VOLTAGE_SWELL_L2_COUNT', - obis.VOLTAGE_SWELL_L3_COUNT : 'VOLTAGE_SWELL_L3_COUNT', - obis.INSTANTANEOUS_VOLTAGE_L1 : 'INSTANTANEOUS_VOLTAGE_L1', - obis.INSTANTANEOUS_VOLTAGE_L2 : 'INSTANTANEOUS_VOLTAGE_L2', - obis.INSTANTANEOUS_VOLTAGE_L3 : 'INSTANTANEOUS_VOLTAGE_L3', - obis.INSTANTANEOUS_CURRENT_L1 : 'INSTANTANEOUS_CURRENT_L1', - obis.INSTANTANEOUS_CURRENT_L2 : 'INSTANTANEOUS_CURRENT_L2', - obis.INSTANTANEOUS_CURRENT_L3 : 'INSTANTANEOUS_CURRENT_L3', - obis.TEXT_MESSAGE_CODE : 'TEXT_MESSAGE_CODE', - obis.TEXT_MESSAGE : 'TEXT_MESSAGE', - obis.DEVICE_TYPE : 'DEVICE_TYPE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE : 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', - obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE : 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', - obis.EQUIPMENT_IDENTIFIER_GAS : 'EQUIPMENT_IDENTIFIER_GAS', - obis.HOURLY_GAS_METER_READING : 'HOURLY_GAS_METER_READING', - obis.GAS_METER_READING : 'GAS_METER_READING', - obis.ACTUAL_TRESHOLD_ELECTRICITY : 'ACTUAL_TRESHOLD_ELECTRICITY', - obis.ACTUAL_SWITCH_POSITION : 'ACTUAL_SWITCH_POSITION', - obis.VALVE_POSITION_GAS : 'VALVE_POSITION_GAS' + obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL', + obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1', + obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2', + obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1', + obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2', + obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF', + obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER', + obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE', + obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY', + obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT', + obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT', + obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG', + obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT', + obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT', + obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT', + obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT', + obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT', + obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT', + obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1', + obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2', + obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3', + obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1', + obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2', + obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3', + obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE', + obis.TEXT_MESSAGE: 'TEXT_MESSAGE', + obis.DEVICE_TYPE: 'DEVICE_TYPE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS', + obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING', + obis.GAS_METER_READING: 'GAS_METER_READING', + obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY', + obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION', + obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS' } -REVERSE_EN = dict([ (v,k) for k,v in EN.items()]) \ No newline at end of file +REVERSE_EN = dict([(v, k) for k, v in EN.items()]) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 07d576d..e313cd5 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,5 +1,6 @@ import dsmr_parser.obis_name_mapping + class Telegram(object): """ Container for raw and parsed telegram data. @@ -47,7 +48,7 @@ class Telegram(object): def __str__(self): output = "" for attr, value in self: - output += "{}: \t {} \t[{}]\n".format(attr,str(value.value),str(value.unit)) + output += "{}: \t {} \t[{}]\n".format(attr, str(value.value), str(value.unit)) return output diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index 4609287..d9aeb5a 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,7 +3,7 @@ import re from ctypes import c_ushort -from dsmr_parser.objects import MBusObject, CosemObject, Telegram +from dsmr_parser.objects import MBusObject, CosemObject from dsmr_parser.exceptions import ParseError, InvalidChecksumError logger = logging.getLogger(__name__) diff --git a/test/experiment_telegram.py b/test/experiment_telegram.py index 2649f51..2892346 100644 --- a/test/experiment_telegram.py +++ b/test/experiment_telegram.py @@ -1,14 +1,8 @@ -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, Telegram +from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser from example_telegrams import TELEGRAM_V4_2 parser = TelegramParser(telegram_specifications.V4) telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) -print(telegram) \ No newline at end of file +print(telegram) diff --git a/test/test_telegram.py b/test/test_telegram.py index d0e1042..ea85704 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -1,30 +1,21 @@ -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, Telegram +from dsmr_parser.objects import CosemObject +from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser from test.example_telegrams import TELEGRAM_V4_2 + class TelegramTest(unittest.TestCase): """ Test instantiation of Telegram object """ def test_instantiate(self): parser = TelegramParser(telegram_specifications.V4) - #result = parser.parse(TELEGRAM_V4_2) telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) - - - # 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' + testitem = telegram.P1_MESSAGE_HEADER + assert isinstance(testitem, CosemObject) + assert testitem.unit is None + assert testitem.value == '42' diff --git a/tox.ini b/tox.ini index 3efa213..6b0f152 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -envlist = py34,py35,p36 +envlist = py34,py35,py36,py37 +requires = coverage<=4.5.4 [testenv] deps= @@ -14,10 +15,13 @@ commands= pylama dsmr_parser test [pylama:dsmr_parser/clients/__init__.py] -ignore = W0611,W0605 +ignore = W0611 + +[pylama:dsmr_parser/parsers.py] +ignore = W605 [pylama:pylint] -max_line_length = 100 +max_line_length = 120 [pylama:pycodestyle] -max_line_length = 100 +max_line_length = 120 From 5d88284d8d4dbb6bda1b5d484c6cb5869ae9ad19 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 2 Feb 2020 22:34:17 +0100 Subject: [PATCH 138/152] remove requires property again --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6b0f152..a3e12f0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist = py34,py35,py36,py37 -requires = coverage<=4.5.4 [testenv] deps= From a7b0b03391f4ed82f670abda49276e3d45518bbf Mon Sep 17 00:00:00 2001 From: lowdef Date: Sun, 2 Feb 2020 22:52:06 +0100 Subject: [PATCH 139/152] remove conflicting entries --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index beb8e57..a33f5a4 100644 --- a/setup.py +++ b/setup.py @@ -6,13 +6,12 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.19', + version='0.18', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', 'pyserial-asyncio<1', - 'pytz', - 'Tailer==0.4.1' + 'pytz' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] From fe710e927286cde8c2628af4017409ece7ab0142 Mon Sep 17 00:00:00 2001 From: lowdef Date: Sun, 2 Feb 2020 22:54:21 +0100 Subject: [PATCH 140/152] redo essential change --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a33f5a4..9c47c51 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,8 @@ setup( install_requires=[ 'pyserial>=3,<4', 'pyserial-asyncio<1', - 'pytz' + 'pytz', + 'Tailer==0.4.1' ], entry_points={ 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] From 88c9ccd83d3fc5807febfd31ffcbf9f240b88ed7 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Fri, 1 May 2020 20:36:35 +0200 Subject: [PATCH 141/152] Add following missing signatures to the V4 telegram specification SHORT_POWER_FAILURE_COUNT, INSTANTANEOUS_CURRENT_L1, INSTANTANEOUS_CURRENT_L2, INSTANTANEOUS_CURRENT_L3. --- dsmr_parser/telegram_specifications.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index a42806f..2e2ff45 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -58,6 +58,7 @@ V4 = { obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), @@ -69,6 +70,9 @@ V4 = { obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), obis.DEVICE_TYPE: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)), 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)), From c4331f6cd6d2b182b2ba9f511d0badf14d4b4cc6 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sat, 2 May 2020 15:50:00 +0200 Subject: [PATCH 142/152] add tests for the missing elements and correct some test bugs --- test/test_parse_v4_2.py | 26 +++- test/test_parse_v5.py | 2 +- test/test_telegram.py | 283 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 305 insertions(+), 6 deletions(-) diff --git a/test/test_parse_v4_2.py b/test/test_parse_v4_2.py index 681783b..cab34f7 100644 --- a/test/test_parse_v4_2.py +++ b/test/test_parse_v4_2.py @@ -80,6 +80,12 @@ class TelegramParserV4_2Test(unittest.TestCase): assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal) assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('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 == 15 + # 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 @@ -132,8 +138,26 @@ class TelegramParserV4_2Test(unittest.TestCase): assert result[obis.TEXT_MESSAGE].unit is None assert result[obis.TEXT_MESSAGE].value is None + # 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') + + # 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('6') + + # 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('2') + # DEVICE_TYPE (0-x:24.1.0) - assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) + assert isinstance(result[obis.DEVICE_TYPE], CosemObject) assert result[obis.DEVICE_TYPE].unit is None assert isinstance(result[obis.DEVICE_TYPE].value, int) assert result[obis.DEVICE_TYPE].value == 3 diff --git a/test/test_parse_v5.py b/test/test_parse_v5.py index e9cfbc1..67d7cd8 100644 --- a/test/test_parse_v5.py +++ b/test/test_parse_v5.py @@ -171,7 +171,7 @@ class TelegramParserV5Test(unittest.TestCase): assert result[obis.TEXT_MESSAGE].value is None # DEVICE_TYPE (0-x:24.1.0) - assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) + assert isinstance(result[obis.DEVICE_TYPE], CosemObject) assert result[obis.DEVICE_TYPE].unit is None assert isinstance(result[obis.DEVICE_TYPE].value, int) assert result[obis.DEVICE_TYPE].value == 3 diff --git a/test/test_telegram.py b/test/test_telegram.py index ea85704..b553714 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -1,21 +1,296 @@ import unittest +import datetime +import pytz from dsmr_parser import telegram_specifications +from dsmr_parser import obis_name_mapping from dsmr_parser.objects import CosemObject +from dsmr_parser.objects import MBusObject from dsmr_parser.objects import Telegram from dsmr_parser.parsers import TelegramParser from test.example_telegrams import TELEGRAM_V4_2 +from decimal import Decimal class TelegramTest(unittest.TestCase): """ Test instantiation of Telegram object """ + def __init__(self, *args, **kwargs): + self.item_names_tested = [] + super(TelegramTest, self).__init__(*args, **kwargs) + + def verify_telegram_item(self, telegram, testitem_name, object_type, unit_val, value_type, value_val): + testitem = eval("telegram.{}".format(testitem_name)) + assert isinstance(testitem, object_type) + assert testitem.unit == unit_val + assert isinstance(testitem.value, value_type) + assert testitem.value == value_val + self.item_names_tested.append(testitem_name) + def test_instantiate(self): parser = TelegramParser(telegram_specifications.V4) telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4) # P1_MESSAGE_HEADER (1-3:0.2.8) - testitem = telegram.P1_MESSAGE_HEADER - assert isinstance(testitem, CosemObject) - assert testitem.unit is None - assert testitem.value == '42' + self.verify_telegram_item(telegram, + 'P1_MESSAGE_HEADER', + object_type=CosemObject, + unit_val=None, + value_type=str, + value_val='42') + + # P1_MESSAGE_TIMESTAMP (0-0:1.0.0) + self.verify_telegram_item(telegram, + 'P1_MESSAGE_TIMESTAMP', + CosemObject, + unit_val=None, + value_type=datetime.datetime, + value_val=datetime.datetime(2016, 11, 13, 19, 57, 57, tzinfo=pytz.UTC)) + + # ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1) + self.verify_telegram_item(telegram, + 'ELECTRICITY_USED_TARIFF_1', + object_type=CosemObject, + unit_val='kWh', + value_type=Decimal, + value_val=Decimal('1581.123')) + + # ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2) + self.verify_telegram_item(telegram, + 'ELECTRICITY_USED_TARIFF_2', + object_type=CosemObject, + unit_val='kWh', + value_type=Decimal, + value_val=Decimal('1435.706')) + + # ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1) + self.verify_telegram_item(telegram, + 'ELECTRICITY_DELIVERED_TARIFF_1', + object_type=CosemObject, + unit_val='kWh', + value_type=Decimal, + value_val=Decimal('0')) + + # ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2) + self.verify_telegram_item(telegram, + 'ELECTRICITY_DELIVERED_TARIFF_2', + object_type=CosemObject, + unit_val='kWh', + value_type=Decimal, + value_val=Decimal('0')) + + # ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0) + self.verify_telegram_item(telegram, + 'ELECTRICITY_ACTIVE_TARIFF', + object_type=CosemObject, + unit_val=None, + value_type=str, + value_val='0002') + + # EQUIPMENT_IDENTIFIER (0-0:96.1.1) + self.verify_telegram_item(telegram, + 'EQUIPMENT_IDENTIFIER', + object_type=CosemObject, + unit_val=None, + value_type=str, + value_val='3960221976967177082151037881335713') + + # CURRENT_ELECTRICITY_USAGE (1-0:1.7.0) + self.verify_telegram_item(telegram, + 'CURRENT_ELECTRICITY_USAGE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('2.027')) + + # CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0) + self.verify_telegram_item(telegram, + 'CURRENT_ELECTRICITY_DELIVERY', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0')) + + # SHORT_POWER_FAILURE_COUNT (1-0:96.7.21) + self.verify_telegram_item(telegram, + 'SHORT_POWER_FAILURE_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=15) + + # LONG_POWER_FAILURE_COUNT (96.7.9) + self.verify_telegram_item(telegram, + 'LONG_POWER_FAILURE_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=7) + + # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SAG_L1_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SAG_L2_COUNT (1-0:52.32.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SAG_L2_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SAG_L3_COUNT (1-0:72.32.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SAG_L3_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SWELL_L1_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SWELL_L2_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0) + self.verify_telegram_item(telegram, + 'VOLTAGE_SWELL_L3_COUNT', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=0) + + # TEXT_MESSAGE_CODE (0-0:96.13.1) + self.verify_telegram_item(telegram, + 'TEXT_MESSAGE_CODE', + object_type=CosemObject, + unit_val=None, + value_type=type(None), + value_val=None) + + # TEXT_MESSAGE (0-0:96.13.0) + self.verify_telegram_item(telegram, + 'TEXT_MESSAGE', + object_type=CosemObject, + unit_val=None, + value_type=type(None), + value_val=None) + + # INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_CURRENT_L1', + object_type=CosemObject, + unit_val='A', + value_type=Decimal, + value_val=Decimal('0')) + + # INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_CURRENT_L2', + object_type=CosemObject, + unit_val='A', + value_type=Decimal, + value_val=Decimal('6')) + + # INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_CURRENT_L3', + object_type=CosemObject, + unit_val='A', + value_type=Decimal, + value_val=Decimal('2')) + + # DEVICE_TYPE (0-x:24.1.0) + self.verify_telegram_item(telegram, + 'DEVICE_TYPE', + object_type=CosemObject, + unit_val=None, + value_type=int, + value_val=3) + + # INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0.170')) + + # INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('1.247')) + + # INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0.209')) + + # INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0')) + + # INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0')) + + # INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0) + self.verify_telegram_item(telegram, + 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE', + object_type=CosemObject, + unit_val='kW', + value_type=Decimal, + value_val=Decimal('0')) + + # EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0) + self.verify_telegram_item(telegram, + 'EQUIPMENT_IDENTIFIER_GAS', + object_type=CosemObject, + unit_val=None, + value_type=str, + value_val='4819243993373755377509728609491464') + + # HOURLY_GAS_METER_READING (0-1:24.2.1) + self.verify_telegram_item(telegram, + 'HOURLY_GAS_METER_READING', + object_type=MBusObject, + unit_val='m3', + value_type=Decimal, + value_val=Decimal('981.443')) + + # check if all items in telegram V4 specification are covered + V4_name_list = [obis_name_mapping.EN[signature] for signature, parser in + telegram_specifications.V4['objects'].items()] + V4_name_set = set(V4_name_list) + item_names_tested_set = set(self.item_names_tested) + + assert item_names_tested_set == V4_name_set From fc4a96ebab4a9a6a536e542407541cabcc0e7d5d Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 3 May 2020 11:18:08 +0200 Subject: [PATCH 143/152] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9c47c51..beb8e57 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.18', + version='0.19', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From d98c93a57f2c9dc0e4009c999d6b1e7c56d7cac8 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 3 May 2020 11:37:53 +0200 Subject: [PATCH 144/152] modified changelog --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0fda0e3..5800449 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,14 @@ Change Log ---------- +**0.19** (2020-05-03) + +- Add following missing elements to telegram specification v4: + - SHORT_POWER_FAILURE_COUNT, + - INSTANTANEOUS_CURRENT_L1, + - INSTANTANEOUS_CURRENT_L2, + - INSTANTANEOUS_CURRENT_L3 +- Add missing tests + fix small test bugs +- Complete telegram object v4 parse test **0.18** (2020-01-28) From a44afb1a59536dc7ad28352165a231fe964e2126 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 10 May 2020 20:47:11 +0200 Subject: [PATCH 145/152] ignore .venv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4dfc343..33bb528 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.pyc .tox .cache +.venv *.egg-info /.project /.pydevproject From d1ad4fa5851946de3efa399dc49c02a4e5b39e91 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 11 May 2020 21:08:20 +0200 Subject: [PATCH 146/152] igonre venv/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 33bb528..6789bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,6 @@ /.coverage build/ dist/ +venv/ *.*~ *~ \ No newline at end of file From a0ce89054a02651c86fe8a4559a4218a5047df5c Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Tue, 12 May 2020 23:45:16 +0200 Subject: [PATCH 147/152] make all objects able to print their own values --- CHANGELOG.rst | 7 ++++++- dsmr_parser/objects.py | 14 +++++++++++++- dsmr_parser/parsers.py | 18 +++++++++++++++++- dsmr_parser/profile_generic_specifications.py | 14 ++++++++++++++ setup.py | 2 +- 5 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 dsmr_parser/profile_generic_specifications.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5800449..46a4645 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,10 @@ Change Log ---------- +**0.20** (2020-05-12) + +- All objects can now print their values +- Add parser + object for generic profile + **0.19** (2020-05-03) - Add following missing elements to telegram specification v4: @@ -45,7 +50,7 @@ Change Log **0.10** (2017-06-05) -- bugix: don't force full telegram signatures (`pull request #25 `_) +- bugfix: don't force full telegram signatures (`pull request #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 diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index e313cd5..d07cd35 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,4 +1,5 @@ import dsmr_parser.obis_name_mapping +import datetime class Telegram(object): @@ -48,7 +49,7 @@ class Telegram(object): def __str__(self): output = "" for attr, value in self: - output += "{}: \t {} \t[{}]\n".format(attr, str(value.value), str(value.unit)) + output += "{}: \t {}\n".format(attr, str(value)) return output @@ -87,6 +88,10 @@ class MBusObject(DSMRObject): else: return self.values[1]['unit'] + def __str__(self): + output = "{}\t[{}] at {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat())) + return output + class CosemObject(DSMRObject): @@ -98,6 +103,13 @@ class CosemObject(DSMRObject): def unit(self): return self.values[0]['unit'] + def __str__(self): + print_value = self.value + if isinstance(self.value, datetime.datetime): + print_value = self.value.astimezone().isoformat() + output = "{}\t[{}]".format(str(print_value), str(self.unit)) + return output + class ProfileGeneric(DSMRObject): pass # TODO implement diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index d9aeb5a..fd88798 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -183,7 +183,7 @@ class CosemParser(DSMRObjectParser): return CosemObject(self._parse(line)) -class ProfileGenericParser(DSMRObjectParser): +class ProfileGenericParser(object): """ Power failure log parser. @@ -205,6 +205,22 @@ class ProfileGenericParser(DSMRObjectParser): 9) Unit of buffer values (Unit of capture objects attribute) """ + def _parse(self, line): + # Match value groups, but exclude the parentheses. Adapted to also match OBIS code in 3rd position. + pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') + values = re.findall(pattern, line) + + # Convert empty value groups to None for clarity. + values = [None if value == '' else value for value in values] + + buffer_length = int(values[0]) + + if (not values) or (len(values) != (buffer_length * 2 + 2)): + raise ParseError("Invalid '%s' line for '%s'", line, self) + + return [self.value_formats[i].parse(value) + for i, value in enumerate(values)] + def parse(self, line): raise NotImplementedError() diff --git a/dsmr_parser/profile_generic_specifications.py b/dsmr_parser/profile_generic_specifications.py new file mode 100644 index 0000000..470d03f --- /dev/null +++ b/dsmr_parser/profile_generic_specifications.py @@ -0,0 +1,14 @@ +from dsmr_parser.parsers import ValueParser, MBusParser +from dsmr_parser.value_types import timestamp + +FAILURE_EVENT = r'0-0\:96\.7\.19' + +V4 = { + 'objects': { + FAILURE_EVENT: MBusParser( + ValueParser(timestamp), + ValueParser(int) + ) + } + +} diff --git a/setup.py b/setup.py index beb8e57..c925b4d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.19', + version='0.20', packages=find_packages(), install_requires=[ 'pyserial>=3,<4', From b6278a8991c8729c22c35cc0960fa0ff1dc72518 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sat, 16 May 2020 16:31:26 +0200 Subject: [PATCH 148/152] ProfileGeneric parser working, TODO complete ProfileGenericObject + Test --- dsmr_parser/objects.py | 10 ++-- dsmr_parser/parsers.py | 60 ++++++++++++------- dsmr_parser/profile_generic_specifications.py | 16 ++--- dsmr_parser/telegram_specifications.py | 14 +++-- test/test_telegram.py | 10 ++++ 5 files changed, 70 insertions(+), 40 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index d07cd35..877934a 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -74,7 +74,7 @@ class MBusObject(DSMRObject): # 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'] + return self.values[6]['value'] else: return self.values[1]['value'] @@ -84,7 +84,7 @@ class MBusObject(DSMRObject): # 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'] + return self.values[5]['value'] else: return self.values[1]['unit'] @@ -111,5 +111,7 @@ class CosemObject(DSMRObject): return output -class ProfileGeneric(DSMRObject): - pass # TODO implement +class ProfileGenericObject(DSMRObject): + def __str__(self): + output = "{}".format(self.values) + return output diff --git a/dsmr_parser/parsers.py b/dsmr_parser/parsers.py index fd88798..2c7c017 100644 --- a/dsmr_parser/parsers.py +++ b/dsmr_parser/parsers.py @@ -3,7 +3,7 @@ import re from ctypes import c_ushort -from dsmr_parser.objects import MBusObject, CosemObject +from dsmr_parser.objects import MBusObject, CosemObject, ProfileGenericObject from dsmr_parser.exceptions import ParseError, InvalidChecksumError logger = logging.getLogger(__name__) @@ -123,19 +123,28 @@ class DSMRObjectParser(object): def __init__(self, *value_formats): self.value_formats = value_formats + def _is_line_wellformed(self, line, values): + # allows overriding by child class + return (values and (len(values) == len(self.value_formats))) + + def _parse_values(self, values): + # allows overriding by child class + return [self.value_formats[i].parse(value) + for i, value in enumerate(values)] + def _parse(self, line): # Match value groups, but exclude the parentheses - pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*]{0,}(?=\)))+') + pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') + values = re.findall(pattern, line) + if not self._is_line_wellformed(line, values): + raise ParseError("Invalid '%s' line for '%s'", line, self) + # Convert empty value groups to None for clarity. values = [None if value == '' else value for value in values] - if not values or len(values) != len(self.value_formats): - raise ParseError("Invalid '%s' line for '%s'", line, self) - - return [self.value_formats[i].parse(value) - for i, value in enumerate(values)] + return self._parse_values(values) class MBusParser(DSMRObjectParser): @@ -183,7 +192,7 @@ class CosemParser(DSMRObjectParser): return CosemObject(self._parse(line)) -class ProfileGenericParser(object): +class ProfileGenericParser(DSMRObjectParser): """ Power failure log parser. @@ -204,25 +213,34 @@ class ProfileGenericParser(object): 8) Buffer value 2 (oldest entry of buffer attribute without unit) 9) Unit of buffer values (Unit of capture objects attribute) """ + def __init__(self, buffer_types, head_parsers, parsers_for_unidentified): + self.value_formats = head_parsers + self.buffer_types = buffer_types + self.parsers_for_unidentified = parsers_for_unidentified - def _parse(self, line): - # Match value groups, but exclude the parentheses. Adapted to also match OBIS code in 3rd position. - pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') - values = re.findall(pattern, line) - - # Convert empty value groups to None for clarity. - values = [None if value == '' else value for value in values] + def _is_line_wellformed(self, line, values): + if values and (len(values) >= 2) and (values[0].isdigit()): + buffer_length = int(values[0]) + return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2)) + else: + return False + def _parse_values(self, values): buffer_length = int(values[0]) + buffer_value_obis_ID = values[1] + if (buffer_length > 0): + if buffer_value_obis_ID in self.buffer_types: + bufferValueParsers = self.buffer_types[buffer_value_obis_ID] + else: + bufferValueParsers = self.parsers_for_unidentified + # add the parsers for the encountered value type z times + for _ in range(buffer_length): + self.value_formats.extend(bufferValueParsers) - if (not values) or (len(values) != (buffer_length * 2 + 2)): - raise ParseError("Invalid '%s' line for '%s'", line, self) - - return [self.value_formats[i].parse(value) - for i, value in enumerate(values)] + return [self.value_formats[i].parse(value) for i, value in enumerate(values)] def parse(self, line): - raise NotImplementedError() + return ProfileGenericObject(self._parse(line)) class ValueParser(object): diff --git a/dsmr_parser/profile_generic_specifications.py b/dsmr_parser/profile_generic_specifications.py index 470d03f..a52416c 100644 --- a/dsmr_parser/profile_generic_specifications.py +++ b/dsmr_parser/profile_generic_specifications.py @@ -1,14 +1,10 @@ -from dsmr_parser.parsers import ValueParser, MBusParser +from dsmr_parser.parsers import ValueParser from dsmr_parser.value_types import timestamp -FAILURE_EVENT = r'0-0\:96\.7\.19' +PG_FAILURE_EVENT = r'0-0:96.7.19' -V4 = { - 'objects': { - FAILURE_EVENT: MBusParser( - ValueParser(timestamp), - ValueParser(int) - ) +PG_HEAD_PARSERS = [ValueParser(int), ValueParser(str)] +PG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)] +BUFFER_TYPES = { + PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)] } - -} diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 2e2ff45..161ac91 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -2,9 +2,9 @@ from decimal import Decimal from copy import deepcopy from dsmr_parser import obis_references as obis -from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser +from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser from dsmr_parser.value_types import timestamp - +from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS """ dsmr_parser.telegram_specifications @@ -37,8 +37,9 @@ V2_2 = { ValueParser(int), ValueParser(int), ValueParser(int), - ValueParser(str), - ValueParser(Decimal), + ValueParser(str), # obis ref + ValueParser(str), # unit, position 5 + ValueParser(Decimal), # meter reading, position 6 ), } } @@ -60,7 +61,10 @@ V4 = { obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO + obis.POWER_EVENT_FAILURE_LOG: + ProfileGenericParser(BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), diff --git a/test/test_telegram.py b/test/test_telegram.py index b553714..a330bc4 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -7,6 +7,7 @@ from dsmr_parser import obis_name_mapping from dsmr_parser.objects import CosemObject from dsmr_parser.objects import MBusObject from dsmr_parser.objects import Telegram +from dsmr_parser.objects import ProfileGenericObject from dsmr_parser.parsers import TelegramParser from test.example_telegrams import TELEGRAM_V4_2 from decimal import Decimal @@ -286,6 +287,15 @@ class TelegramTest(unittest.TestCase): unit_val='m3', value_type=Decimal, value_val=Decimal('981.443')) + # POWER_EVENT_FAILURE_LOG (1-0:99.97.0) + testitem_name = 'POWER_EVENT_FAILURE_LOG' + object_type = ProfileGenericObject + testitem = eval("telegram.{}".format(testitem_name)) + assert isinstance(testitem, object_type) +# assert testitem.unit == unit_val +# assert isinstance(testitem.value, value_type) +# assert testitem.value == value_val + self.item_names_tested.append(testitem_name) # check if all items in telegram V4 specification are covered V4_name_list = [obis_name_mapping.EN[signature] for signature, parser in From 789871899c4617ecb6a82ae82d571ec5abc698db Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 17 May 2020 01:25:02 +0200 Subject: [PATCH 149/152] ProfileGeneric parser working, ProfileGenericObject implemented and Test for V4 telegram completed. --- dsmr_parser/objects.py | 37 +++++++++++++++++++++++++- dsmr_parser/telegram_specifications.py | 5 +++- test/test_telegram.py | 22 ++++++++++++--- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 877934a..ce48a01 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -112,6 +112,41 @@ class CosemObject(DSMRObject): class ProfileGenericObject(DSMRObject): + """ + Represents all data in a GenericProfile value. + All buffer values are returned as a list of MBusObjects, + containing the datetime (timestamp) and the value. + """ + + def __init__(self, values): + super().__init__(values) + self._buffer_list = None + + @property + def buffer_length(self): + return self.values[0]['value'] + + @property + def buffer_type(self): + return self.values[1]['value'] + + @property + def buffer(self): + if self._buffer_list is None: + self._buffer_list = [] + values_offset = 2 + for i in range(self.buffer_length): + offset = values_offset + i*2 + self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) + return self._buffer_list + def __str__(self): - output = "{}".format(self.values) + output = "\t buffer length: {}\n".format(self.buffer_length) + output += "\t buffer type: {}".format(self.buffer_type) + for buffer_value in self.buffer: + timestamp = buffer_value.datetime + if isinstance(timestamp, datetime.datetime): + timestamp = str(timestamp.astimezone().isoformat()) + output += "\n\t event occured at: {}".format(timestamp) + output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit) return output diff --git a/dsmr_parser/telegram_specifications.py b/dsmr_parser/telegram_specifications.py index 161ac91..1341ded 100644 --- a/dsmr_parser/telegram_specifications.py +++ b/dsmr_parser/telegram_specifications.py @@ -107,7 +107,10 @@ V5 = { 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.POWER_EVENT_FAILURE_LOG: + ProfileGenericParser(BUFFER_TYPES, + PG_HEAD_PARSERS, + PG_UNIDENTIFIED_BUFFERTYPE_PARSERS), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), diff --git a/test/test_telegram.py b/test/test_telegram.py index a330bc4..90b8eff 100644 --- a/test/test_telegram.py +++ b/test/test_telegram.py @@ -287,14 +287,30 @@ class TelegramTest(unittest.TestCase): unit_val='m3', value_type=Decimal, value_val=Decimal('981.443')) + # POWER_EVENT_FAILURE_LOG (1-0:99.97.0) testitem_name = 'POWER_EVENT_FAILURE_LOG' object_type = ProfileGenericObject testitem = eval("telegram.{}".format(testitem_name)) assert isinstance(testitem, object_type) -# assert testitem.unit == unit_val -# assert isinstance(testitem.value, value_type) -# assert testitem.value == value_val + assert testitem.buffer_length == 3 + assert testitem.buffer_type == '0-0:96.7.19' + buffer = testitem.buffer + assert isinstance(testitem.buffer, list) + assert len(buffer) == 3 + assert all([isinstance(item, MBusObject) for item in buffer]) + date0 = datetime.datetime(2000, 1, 4, 17, 3, 20, tzinfo=datetime.timezone.utc) + date1 = datetime.datetime(1999, 12, 31, 23, 0, 1, tzinfo=datetime.timezone.utc) + date2 = datetime.datetime(2000, 1, 1, 23, 0, 3, tzinfo=datetime.timezone.utc) + assert buffer[0].datetime == date0 + assert buffer[1].datetime == date1 + assert buffer[2].datetime == date2 + assert buffer[0].value == 237126 + assert buffer[1].value == 2147583646 + assert buffer[2].value == 2317482647 + assert all([isinstance(item.value, int) for item in buffer]) + assert all([isinstance(item.unit, str) for item in buffer]) + assert all([(item.unit == 's') for item in buffer]) self.item_names_tested.append(testitem_name) # check if all items in telegram V4 specification are covered From c2dea29c83d5818581d2821db6bef073208dad42 Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 17 May 2020 16:49:29 +0200 Subject: [PATCH 150/152] add a value property to GenericProfileObject, return a dict --- dsmr_parser/objects.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index ce48a01..3e99d53 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -140,6 +140,18 @@ class ProfileGenericObject(DSMRObject): self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) return self._buffer_list + @property + def value(self): + list = [['buffer_length', self.buffer_length]] + list.append(['buffer_type', self.buffer_type]) + buffer_repr = [ + (['datetime', buffer_item.datetime], + ['value', buffer_item.value]) + for buffer_item in self.buffer + ] + list.append(['buffer', buffer_repr]) + return dict(list) + def __str__(self): output = "\t buffer length: {}\n".format(self.buffer_length) output += "\t buffer type: {}".format(self.buffer_type) From 94447c357128b38b11c203dcfeef42652179859d Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Sun, 17 May 2020 16:58:28 +0200 Subject: [PATCH 151/152] GenericProfileObject value: make embedded buffer items also appear as dicts --- dsmr_parser/objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 3e99d53..1ad1dbc 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -145,8 +145,8 @@ class ProfileGenericObject(DSMRObject): list = [['buffer_length', self.buffer_length]] list.append(['buffer_type', self.buffer_type]) buffer_repr = [ - (['datetime', buffer_item.datetime], - ['value', buffer_item.value]) + dict([['datetime', buffer_item.datetime], + ['value', buffer_item.value]]) for buffer_item in self.buffer ] list.append(['buffer', buffer_repr]) From 837ba3b6f7645dd7bc6fc92980af12d549c8790a Mon Sep 17 00:00:00 2001 From: Hans Erik van Elburg Date: Mon, 25 May 2020 01:38:14 +0200 Subject: [PATCH 152/152] add json serialization --- CHANGELOG.rst | 4 +++ dsmr_parser/objects.py | 67 ++++++++++++++++++++++++++++++++++-------- setup.py | 2 +- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 46a4645..dae3e36 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,9 @@ Change Log ---------- +**0.21** (2020-05-25) + +- All objects can produce a json serialization of their state. + **0.20** (2020-05-12) - All objects can now print their values diff --git a/dsmr_parser/objects.py b/dsmr_parser/objects.py index 1ad1dbc..4cd987d 100644 --- a/dsmr_parser/objects.py +++ b/dsmr_parser/objects.py @@ -1,5 +1,7 @@ import dsmr_parser.obis_name_mapping import datetime +import json +from decimal import Decimal class Telegram(object): @@ -52,6 +54,9 @@ class Telegram(object): output += "{}: \t {}\n".format(attr, str(value)) return output + def to_json(self): + return json.dumps(dict([[attr, json.loads(value.to_json())] for attr, value in self])) + class DSMRObject(object): """ @@ -92,6 +97,22 @@ class MBusObject(DSMRObject): output = "{}\t[{}] at {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat())) return output + def to_json(self): + timestamp = self.datetime + if isinstance(self.datetime, datetime.datetime): + timestamp = self.datetime.astimezone().isoformat() + value = self.value + if isinstance(self.value, datetime.datetime): + value = self.value.astimezone().isoformat() + if isinstance(self.value, Decimal): + value = float(self.value) + output = { + 'datetime': timestamp, + 'value': value, + 'unit': self.unit + } + return json.dumps(output) + class CosemObject(DSMRObject): @@ -110,6 +131,18 @@ class CosemObject(DSMRObject): output = "{}\t[{}]".format(str(print_value), str(self.unit)) return output + def to_json(self): + json_value = self.value + if isinstance(self.value, datetime.datetime): + json_value = self.value.astimezone().isoformat() + if isinstance(self.value, Decimal): + json_value = float(self.value) + output = { + 'value': json_value, + 'unit': self.unit + } + return json.dumps(output) + class ProfileGenericObject(DSMRObject): """ @@ -140,18 +173,6 @@ class ProfileGenericObject(DSMRObject): self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]])) return self._buffer_list - @property - def value(self): - list = [['buffer_length', self.buffer_length]] - list.append(['buffer_type', self.buffer_type]) - buffer_repr = [ - dict([['datetime', buffer_item.datetime], - ['value', buffer_item.value]]) - for buffer_item in self.buffer - ] - list.append(['buffer', buffer_repr]) - return dict(list) - def __str__(self): output = "\t buffer length: {}\n".format(self.buffer_length) output += "\t buffer type: {}".format(self.buffer_type) @@ -162,3 +183,25 @@ class ProfileGenericObject(DSMRObject): output += "\n\t event occured at: {}".format(timestamp) output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit) return output + + def to_json(self): + """ + :return: A json of all values in the GenericProfileObject , with the following structure + {'buffer_length': n, + 'buffer_type': obis_ref, + 'buffer': [{'datetime': d1, + 'value': v1, + 'unit': u1}, + ... + {'datetime': dn, + 'value': vn, + 'unit': un} + ] + } + """ + list = [['buffer_length', self.buffer_length]] + list.append(['buffer_type', self.buffer_type]) + buffer_repr = [json.loads(buffer_item.to_json()) for buffer_item in self.buffer] + list.append(['buffer', buffer_repr]) + output = dict(list) + return json.dumps(output) diff --git a/setup.py b/setup.py index c925b4d..9072eef 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( author='Nigel Dokter', author_email='nigel@nldr.net', url='https://github.com/ndokter/dsmr_parser', - version='0.20', + version='0.21', packages=find_packages(), install_requires=[ 'pyserial>=3,<4',