Skip to content

Commit

Permalink
Merge pull request #2 from aequitas/master
Browse files Browse the repository at this point in the history
Added DSMR 2.2 support
  • Loading branch information
Nigel Dokter committed Nov 8, 2016
2 parents 6343bce + aa8ff29 commit ba29e34
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 42 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.idea
*.pyc
.tox
.cache
*.egg-info
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions dsmr_parser/__main__.py
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions dsmr_parser/obis_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
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,
Expand Down
15 changes: 15 additions & 0 deletions dsmr_parser/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ def unit(self):
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
Expand Down
32 changes: 28 additions & 4 deletions dsmr_parser/parsers.py
Original file line number Diff line number Diff line change
@@ -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__)

Expand All @@ -29,7 +29,7 @@ def parse(self, line_values):
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

Expand All @@ -47,6 +47,26 @@ def parse_line(self, line_value):
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):
Expand Down Expand Up @@ -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):
Expand Down
20 changes: 17 additions & 3 deletions dsmr_parser/serial.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import serial

from dsmr_parser.parsers import TelegramParser
from dsmr_parser.parsers import TelegramParser, TelegramParserV2_2

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,
Expand All @@ -26,7 +36,12 @@ class SerialReader(object):
def __init__(self, device, serial_settings, telegram_specification):
self.serial_settings = serial_settings
self.serial_settings['port'] = device
self.telegram_parser = TelegramParser(telegram_specification)

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):
"""
Expand All @@ -52,4 +67,3 @@ def read(self):
if is_end_of_telegram(line):
yield self.telegram_parser.parse(telegram)
telegram = []

87 changes: 56 additions & 31 deletions dsmr_parser/telegram_specifications.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -13,36 +13,61 @@
how the telegram lines are parsed.
"""

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 = {
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)),
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
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))
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))
}

5 changes: 4 additions & 1 deletion dsmr_parser/value_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
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)
Expand Down
9 changes: 6 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
version='0.1',
packages=find_packages(),
install_requires=[
'pyserial==3.0.1',
'pytz==2016.3'
]
'pyserial>=3,<4',
'pytz'
],
entry_points={
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
},
)
40 changes: 40 additions & 0 deletions test/test_parse.py
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit ba29e34

Please sign in to comment.