Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DSMR 2.2 #2

Merged
merged 8 commits into from
Nov 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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