added telegram CRC verification
This commit is contained in:
@@ -1,2 +1,6 @@
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidChecksumError(ParseError):
|
||||
pass
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user