diff --git a/.gitignore b/.gitignore index c10666e..83f3764 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .idea *.pyc +.tox +.cache +*.egg-info 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 diff --git a/dsmr_parser/__main__.py b/dsmr_parser/__main__.py new file mode 100644 index 0000000..92c0dce --- /dev/null +++ b/dsmr_parser/__main__.py @@ -0,0 +1,32 @@ +import argparse +from dsmr_parser.serial import SERIAL_SETTINGS_V2_2, SERIAL_SETTINGS_V4, SerialReader +from dsmr_parser import telegram_specifications + + +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() + + 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/objects.py b/dsmr_parser/objects.py index 5024dba..f09fda5 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]['value'] + + 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 ee80a67..3e7aa70 100644 --- a/dsmr_parser/serial.py +++ b/dsmr_parser/serial.py @@ -1,6 +1,16 @@ import serial -from dsmr_parser.parsers import TelegramParser +from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2 + +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, @@ -26,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 = TelegramParserV2_2 + else: + telegram_parser = TelegramParser + self.telegram_parser = telegram_parser(telegram_specification) def read(self): """ @@ -52,4 +67,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 bcab475..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 @@ -13,36 +13,61 @@ This module contains DSMR telegram specifications. Each specifications describes how the telegram lines are parsed. """ -V4 = { - P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), - P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), - ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), - ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), - ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), - ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), - ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), - EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), - CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), - CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), - LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), - # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO - VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), - VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), - VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), - VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)), - VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)), - VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)), - TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), - TEXT_MESSAGE: CosemParser(ValueParser(str)), - DEVICE_TYPE: CosemParser(ValueParser(int)), - INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), - INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), - INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), - INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), - INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), - INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), - EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), - HOURLY_GAS_METER_READING: MBusParser(ValueParser(timestamp), - ValueParser(Decimal)) +V2_2 = { + obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), + obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)), + obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)), + obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), + obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), + obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), + obis.DEVICE_TYPE: CosemParser(ValueParser(str)), + obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), + obis.GAS_METER_READING: MBusParser( + ValueParser(timestamp), + ValueParser(int), + ValueParser(int), + ValueParser(int), + ValueParser(str), + ValueParser(Decimal), + ), } +V4 = { + obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), + obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), + obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)), + obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), + obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), + obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), + obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), + obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), + # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO + obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)), + obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)), + obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), + obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), + obis.DEVICE_TYPE: CosemParser(ValueParser(int)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)), + obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)), + obis.HOURLY_GAS_METER_READING: MBusParser(ValueParser(timestamp), + ValueParser(Decimal)) +} 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/setup.py b/setup.py index f95903a..51ae369 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,10 @@ setup( version='0.1', packages=find_packages(), install_requires=[ - 'pyserial==3.0.1', - 'pytz==2016.3' - ] + 'pyserial>=3,<4', + 'pytz' + ], + entry_points={ + 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] + }, ) diff --git a/test/test_parse.py b/test/test_parse.py new file mode 100644 index 0000000..433fd15 --- /dev/null +++ b/test/test_parse.py @@ -0,0 +1,40 @@ +"""Test telegram parsing.""" + +from dsmr_parser.parsers import TelegramParserV2_2 +from dsmr_parser import telegram_specifications +from dsmr_parser.obis_references import CURRENT_ELECTRICITY_USAGE, GAS_METER_READING + +TELEGRAM_V2_2 = [ + "/ISk5\2MT382-1004", + "", + "0-0:96.1.1(00000000000000)", + "1-0:1.8.1(00001.001*kWh)", + "1-0:1.8.2(00001.001*kWh)", + "1-0:2.8.1(00001.001*kWh)", + "1-0:2.8.2(00001.001*kWh)", + "0-0:96.14.0(0001)", + "1-0:1.7.0(0001.01*kW)", + "1-0:2.7.0(0000.00*kW)", + "0-0:17.0.0(0999.00*kW)", + "0-0:96.3.10(1)", + "0-0:96.13.1()", + "0-0:96.13.0()", + "0-1:24.1.0(3)", + "0-1:96.1.0(000000000000)", + "0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)", + "(00001.001)", + "0-1:24.4.0(1)", + "!", +] + + +def test_parse_v2_2(): + """Test if telegram parsing results in correct results.""" + + parser = TelegramParserV2_2(telegram_specifications.V2_2) + result = parser.parse(TELEGRAM_V2_2) + + assert float(result[CURRENT_ELECTRICITY_USAGE].value) == 1.01 + assert result[CURRENT_ELECTRICITY_USAGE].unit == 'kW' + assert float(result[GAS_METER_READING].value) == 1.001 + assert result[GAS_METER_READING].unit == 'm3' diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..9bdde36 --- /dev/null +++ b/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist = py34,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