Compare commits

..
17 Commits
Author SHA1 Message Date
Nigel Dokter f41fe0894c changelog 2016-11-12 10:56:16 +01:00
Nigel Dokter 59dd0893a4 Merge pull request #3 from aequitas/asyncio
Add asyncio reader.
2016-11-12 10:51:10 +01:00
Johan Bloemberg e3c8c92198 Linting and tpyo 2016-11-12 01:36:36 +01:00
Johan Bloemberg 9363840042 Project maintenance. 2016-11-12 01:34:32 +01:00
Johan Bloemberg 4afab70dc0 Add asyncio reader. 2016-11-12 01:25:56 +01:00
Nigel Dokter 9da4a3b0e6 typo 2016-11-08 19:34:20 +01:00
Nigel Dokter defeed7e8a updated readme 2016-11-08 19:33:43 +01:00
Nigel Dokter 503cbd0ca7 updated changelog and setup 2016-11-08 19:26:21 +01:00
Nigel Dokter ba29e34cf6 Merge pull request #2 from aequitas/master
Added DSMR 2.2 support
2016-11-08 18:19:06 +01:00
Johan Bloemberg aa8ff29028 Add travis config. 2016-11-08 10:27:46 +01:00
Johan Bloemberg 726ca507f7 Do not pin pyserial to one specific version. 2016-11-08 10:21:59 +01:00
Johan Bloemberg 9ee62f5228 Add python3.4 to the test. 2016-11-08 10:18:42 +01:00
Johan Bloemberg fe5caa9126 Test and fix parsing of gas unit. 2016-11-07 20:08:28 +01:00
Johan Bloemberg cf771776cd Fix console imports. 2016-11-07 20:04:59 +01:00
Johan Bloemberg 447f2a24fb Add test and exceptions for V2_2 implementation. 2016-11-07 20:00:10 +01:00
Johan Bloemberg 4a82066144 Add test/style suite. 2016-11-07 19:59:39 +01:00
Johan Bloemberg 90eb6fb3fe Add V2.2 telegram implementation and console read. 2016-11-06 20:40:39 +01:00
14 changed files with 294 additions and 45 deletions
+3
View File
@@ -1,2 +1,5 @@
.idea
*.pyc
.tox
.cache
*.egg-info
+10
View File
@@ -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
+13
View File
@@ -0,0 +1,13 @@
Change Log
----------
**0.3** (2016-11-12)
- Added asyncio reader for non-blocking reads. (thanks to https://github.com/aequitas)
**0.2** (2016-11-08)
- Added support for DMSR version 2.2 (thanks to https://github.com/aequitas)
**0.1** (2016-08-22)
- Initial version with a serial reader and support for DSMR version 4.x
+1 -1
View File
@@ -11,7 +11,7 @@ also includes a serial client to directly read and parse smart meter data.
Features
--------
DSMR Parser currently supports DSMR version 4 and is tested with Python 3.5
DSMR Parser currently supports DSMR versions 2.2 and 4.x. It has been tested with Python 3.5 and 3.4.
Examples
+32
View File
@@ -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()
+4
View File
@@ -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,
+15
View File
@@ -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
+28 -4
View File
@@ -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):
+61 -3
View File
@@ -1,6 +1,17 @@
import serial
import asyncio
import serial_asyncio
from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2
from dsmr_parser.parsers import TelegramParser
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,
@@ -23,10 +34,17 @@ def is_end_of_telegram(line):
class SerialReader(object):
PORT_KEY = 'port'
def __init__(self, device, serial_settings, telegram_specification):
self.serial_settings = serial_settings
self.serial_settings['port'] = device
self.telegram_parser = TelegramParser(telegram_specification)
self.serial_settings[self.PORT_KEY] = device
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):
"""
@@ -53,3 +71,43 @@ class SerialReader(object):
yield self.telegram_parser.parse(telegram)
telegram = []
class AsyncSerialReader(SerialReader):
"""Serial reader using asyncio pyserial."""
PORT_KEY = 'url'
@asyncio.coroutine
def read(self, queue):
"""
Read complete DSMR telegram's from the serial interface and parse it
into CosemObject's and MbusObject's.
Instead of being a generator, values are pushed to provided queue for
asynchronous processing.
:rtype Generator/Async
"""
# create Serial StreamReader
conn = serial_asyncio.open_serial_connection(**self.serial_settings)
reader, _ = yield from conn
telegram = []
while True:
# read line if available or give control back to loop until
# new data has arrived
line = yield from reader.readline()
line = line.decode('ascii')
# Telegrams need to be complete because the values belong to a
# particular reading and can also be related to eachother.
if not telegram and not is_start_of_telegram(line):
continue
telegram.append(line)
if is_end_of_telegram(line):
# push new parsed telegram onto queue
queue.put_nowait(self.telegram_parser.parse(telegram))
telegram = []
+57 -32
View File
@@ -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))
}
+4 -1
View File
@@ -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)
+10 -4
View File
@@ -4,10 +4,16 @@ setup(
name='dsmr-parser',
description='Library to parse Dutch Smart Meter Requirements (DSMR)',
author='Nigel Dokter',
version='0.1',
author_email='nigeldokter@gmail.com',
url='https://github.com/ndokter/dsmr_parser',
version='0.3',
packages=find_packages(),
install_requires=[
'pyserial==3.0.1',
'pytz==2016.3'
]
'pyserial>=3,<4',
'pyserial-asyncio<1',
'pytz'
],
entry_points={
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
},
)
+40
View File
@@ -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'
+16
View File
@@ -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