Merge pull request #8 from aequitas/async_protocol

Async protocol
This commit is contained in:
Nigel Dokter 2016-11-21 20:39:57 +01:00 committed by GitHub
commit 6f3c74ce7c
4 changed files with 157 additions and 12 deletions

View File

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

97
dsmr_parser/protocol.py Normal file
View File

@ -0,0 +1,97 @@
"""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, is_start_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)
# 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:
parsed_telegram = self.telegram_parser.parse(self.telegram)
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."""
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)

39
test/test_protocol.py Normal file
View File

@ -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'

View File

@ -5,6 +5,9 @@ envlist = py34,py35
deps=
pytest
pylama
pytest-asyncio
pytest-catchlog
pytest-mock
commands=
py.test test {posargs}
pylama dsmr_parser test