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())