Progress on removing TelegramParserV2_2 and TelegramParserV4 in favor of a generic TelegramParser

This commit is contained in:
Nigel Dokter 2017-01-20 23:02:19 +01:00
parent e2e4bb36a2
commit 07634abed1
7 changed files with 70 additions and 128 deletions

View File

@ -9,7 +9,7 @@ from serial_asyncio import create_serial_connection
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications
from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError 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, \ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
SERIAL_SETTINGS_V4 SERIAL_SETTINGS_V4
@ -18,18 +18,16 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None):
"""Creates a DSMR asyncio protocol.""" """Creates a DSMR asyncio protocol."""
if dsmr_version == '2.2': if dsmr_version == '2.2':
specifications = telegram_specifications.V2_2 specification = telegram_specifications.V2_2
telegram_parser = TelegramParserV2_2
serial_settings = SERIAL_SETTINGS_V2_2 serial_settings = SERIAL_SETTINGS_V2_2
elif dsmr_version == '4': elif dsmr_version == '4':
specifications = telegram_specifications.V4 specification = telegram_specifications.V4
telegram_parser = TelegramParserV4
serial_settings = SERIAL_SETTINGS_V4 serial_settings = SERIAL_SETTINGS_V4
else: else:
raise NotImplementedError("No telegram parser found for version: %s", raise NotImplementedError("No telegram parser found for version: %s",
dsmr_version) dsmr_version)
protocol = partial(DSMRProtocol, loop, telegram_parser(specifications), protocol = partial(DSMRProtocol, loop, TelegramParser(specification),
telegram_callback=telegram_callback) telegram_callback=telegram_callback)
return protocol, serial_settings return protocol, serial_settings

View File

@ -5,10 +5,7 @@ import serial_asyncio
from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError from dsmr_parser.exceptions import ParseError
from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2, \ from dsmr_parser.parsers import TelegramParser
TelegramParserV4
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
SERIAL_SETTINGS_V4
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,14 +18,7 @@ class SerialReader(object):
self.serial_settings = serial_settings self.serial_settings = serial_settings
self.serial_settings[self.PORT_KEY] = device self.serial_settings[self.PORT_KEY] = device
if serial_settings is SERIAL_SETTINGS_V2_2: self.telegram_parser = TelegramParser(telegram_specification)
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_buffer = TelegramBuffer() self.telegram_buffer = TelegramBuffer()
def read(self): def read(self):

View File

@ -1,36 +1,44 @@
P1_MESSAGE_HEADER = r'1-3:0\.2\.8' """
P1_MESSAGE_TIMESTAMP = r'0-0:1\.0\.0' Contains the signatures of each telegram line.
ELECTRICITY_USED_TARIFF_1 = r'1-0:1\.8\.1'
ELECTRICITY_USED_TARIFF_2 = r'1-0:1\.8\.2' Previously contained the channel + obis reference signatures, but has been
ELECTRICITY_DELIVERED_TARIFF_1 = r'1-0:2\.8\.1' refactored to full line signatures to maintain backwards compatibility.
ELECTRICITY_DELIVERED_TARIFF_2 = r'1-0:2\.8\.2' Might be refactored in a backwards incompatible way as soon as proper telegram
ELECTRICITY_ACTIVE_TARIFF = r'0-0:96\.14\.0' objects are introduced.
EQUIPMENT_IDENTIFIER = r'0-0:96\.1\.1' """
CURRENT_ELECTRICITY_USAGE = r'1-0:1\.7\.0' P1_MESSAGE_HEADER = r'\d-\d:0\.2\.8.+?\r\n'
CURRENT_ELECTRICITY_DELIVERY = r'1-0:2\.7\.0' P1_MESSAGE_TIMESTAMP = r'\d-\d:1\.0\.0.+?\r\n'
LONG_POWER_FAILURE_COUNT = r'96\.7\.9' ELECTRICITY_USED_TARIFF_1 = r'\d-\d:1\.8\.1.+?\r\n'
POWER_EVENT_FAILURE_LOG = r'99\.97\.0' ELECTRICITY_USED_TARIFF_2 = r'\d-\d:1\.8\.2.+?\r\n'
VOLTAGE_SAG_L1_COUNT = r'1-0:32\.32\.0' ELECTRICITY_DELIVERED_TARIFF_1 = r'\d-\d:2\.8\.1.+?\r\n'
VOLTAGE_SAG_L2_COUNT = r'1-0:52\.32\.0' ELECTRICITY_DELIVERED_TARIFF_2 = r'\d-\d:2\.8\.2.+?\r\n'
VOLTAGE_SAG_L3_COUNT = r'1-0:72\.32\.0' ELECTRICITY_ACTIVE_TARIFF = r'\d-\d:96\.14\.0.+?\r\n'
VOLTAGE_SWELL_L1_COUNT = r'1-0:32\.36\.0' EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n'
VOLTAGE_SWELL_L2_COUNT = r'1-0:52\.36\.0' CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n'
VOLTAGE_SWELL_L3_COUNT = r'1-0:72\.36\.0' CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n'
TEXT_MESSAGE_CODE = r'0-0:96\.13\.1' LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n'
TEXT_MESSAGE = r'0-0:96\.13\.0' POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n'
DEVICE_TYPE = r'0-\d:24\.1\.0' VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'1-0:21\.7\.0' VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'1-0:41\.7\.0' VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'1-0:61\.7\.0' VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE = r'1-0:22\.7\.0' VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE = r'1-0:42\.7\.0' VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n'
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'1-0:62\.7\.0' TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n'
EQUIPMENT_IDENTIFIER_GAS = r'0-\d:96\.1\.0' TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n'
HOURLY_GAS_METER_READING = r'0-1:24\.2\.1' DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n'
GAS_METER_READING = r'0-\d:24\.3\.0' INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE = r'\d-\d:21\.7\.0.+?\r\n'
ACTUAL_TRESHOLD_ELECTRICITY = r'0-0:17\.0\.0' INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE = r'\d-\d:41\.7\.0.+?\r\n'
ACTUAL_SWITCH_POSITION = r'0-0:96\.3\.10' INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE = r'\d-\d:61\.7\.0.+?\r\n'
VALVE_POSITION_GAS = r'0-\d:24\.4\.0' 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_ALL = (
ELECTRICITY_USED_TARIFF_1, ELECTRICITY_USED_TARIFF_1,

View File

@ -5,28 +5,22 @@ from PyCRC.CRC16 import CRC16
from dsmr_parser.objects import MBusObject, MBusObjectV2_2, CosemObject from dsmr_parser.objects import MBusObject, MBusObjectV2_2, CosemObject
from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.obis_references import GAS_METER_READING
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TelegramParser(object): 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 :param telegram_specification: determines how the telegram is parsed
:type telegram_specification: dict :type telegram_specification: dict
""" """
self.telegram_specification = telegram_specification self.telegram_specification = telegram_specification
self.enable_checksum_validation = enable_checksum_validation
def _find_line_parser(self, line_value): def parse(self, telegram_data):
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):
""" """
Parse telegram from string to dict. 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 if self.enable_checksum_validation:
for obis_reference, dsmr_object in parsed_lines} self.validate_checksum(telegram_data)
def parse_line(self, line): telegram = {}
logger.debug("Parsing line '%s'", line)
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: if match:
logger.debug("No line class found for: '%s'", line) telegram[signature] = parser.parse(match.group(0))
return None, None
return obis_reference, parser.parse(line) return telegram
class TelegramParserV4(TelegramParser):
@staticmethod @staticmethod
def validate_telegram_checksum(telegram): def validate_checksum(telegram):
""" """
:param str telegram: :param str telegram:
:raises ParseError: :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): class DSMRObjectParser(object):

View File

@ -1,16 +1,16 @@
import unittest import unittest
from test.example_telegrams import TELEGRAM_V2_2 from dsmr_parser.parsers import TelegramParser
from dsmr_parser.parsers import TelegramParserV2_2
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications
from dsmr_parser import obis_references as obis from dsmr_parser import obis_references as obis
from test.example_telegrams import TELEGRAM_V2_2
class TelegramParserV2_2Test(unittest.TestCase): class TelegramParserV2_2Test(unittest.TestCase):
""" Test parsing of a DSMR v2.2 telegram. """ """ Test parsing of a DSMR v2.2 telegram. """
def test_parse(self): def test_parse(self):
parser = TelegramParserV2_2(telegram_specifications.V2_2) parser = TelegramParser(telegram_specifications.V2_2)
result = parser.parse(TELEGRAM_V2_2) result = parser.parse(TELEGRAM_V2_2)
assert float(result[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01 assert float(result[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01

View File

@ -4,12 +4,12 @@ import unittest
import pytz import pytz
from test.example_telegrams import TELEGRAM_V4_2
from dsmr_parser import obis_references as obis from dsmr_parser import obis_references as obis
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications
from dsmr_parser.exceptions import InvalidChecksumError, ParseError from dsmr_parser.exceptions import InvalidChecksumError, ParseError
from dsmr_parser.objects import CosemObject, MBusObject 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): class TelegramParserV4_2Test(unittest.TestCase):
@ -17,7 +17,7 @@ class TelegramParserV4_2Test(unittest.TestCase):
def test_valid(self): def test_valid(self):
# No exception is raised. # No exception is raised.
TelegramParserV4.validate_telegram_checksum(TELEGRAM_V4_2) TelegramParser.validate_checksum(TELEGRAM_V4_2)
def test_invalid(self): def test_invalid(self):
# Remove the electricty used data value. This causes the checksum to # Remove the electricty used data value. This causes the checksum to
@ -28,14 +28,14 @@ class TelegramParserV4_2Test(unittest.TestCase):
) )
with self.assertRaises(InvalidChecksumError): with self.assertRaises(InvalidChecksumError):
TelegramParserV4.validate_telegram_checksum(corrupted_telegram) TelegramParser.validate_checksum(corrupted_telegram)
def test_missing_checksum(self): def test_missing_checksum(self):
# Remove the checksum value causing a ParseError. # Remove the checksum value causing a ParseError.
corrupted_telegram = TELEGRAM_V4_2.replace('!6796\r\n', '') corrupted_telegram = TELEGRAM_V4_2.replace('!6796\r\n', '')
with self.assertRaises(ParseError): with self.assertRaises(ParseError):
TelegramParserV4.validate_telegram_checksum(corrupted_telegram) TelegramParser.validate_checksum(corrupted_telegram)
def test_parse(self): def test_parse(self):
parser = TelegramParser(telegram_specifications.V4) parser = TelegramParser(telegram_specifications.V4)

View File

@ -4,7 +4,7 @@ import unittest
from dsmr_parser import obis_references as obis from dsmr_parser import obis_references as obis
from dsmr_parser import telegram_specifications 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 from dsmr_parser.clients.protocol import DSMRProtocol
@ -35,10 +35,7 @@ TELEGRAM_V2_2 = (
class ProtocolTest(unittest.TestCase): class ProtocolTest(unittest.TestCase):
def setUp(self): def setUp(self):
parser = TelegramParserV2_2 telegram_parser = TelegramParser(telegram_specifications.V2_2)
specification = telegram_specifications.V2_2
telegram_parser = parser(specification)
self.protocol = DSMRProtocol(None, telegram_parser, self.protocol = DSMRProtocol(None, telegram_parser,
telegram_callback=Mock()) telegram_callback=Mock())