commit
ba29e34cf6
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
|||||||
.idea
|
.idea
|
||||||
*.pyc
|
*.pyc
|
||||||
|
.tox
|
||||||
|
.cache
|
||||||
|
*.egg-info
|
||||||
|
10
.travis.yml
Normal file
10
.travis.yml
Normal 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
|
32
dsmr_parser/__main__.py
Normal file
32
dsmr_parser/__main__.py
Normal 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()
|
@ -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'
|
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE = r'1-0:62\.7\.0'
|
||||||
EQUIPMENT_IDENTIFIER_GAS = r'0-\d:96\.1\.0'
|
EQUIPMENT_IDENTIFIER_GAS = r'0-\d:96\.1\.0'
|
||||||
HOURLY_GAS_METER_READING = r'0-1:24\.2\.1'
|
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_ALL = (
|
||||||
ELECTRICITY_USED_TARIFF_1,
|
ELECTRICITY_USED_TARIFF_1,
|
||||||
|
@ -19,6 +19,21 @@ class MBusObject(DSMRObject):
|
|||||||
return self.values[1]['unit']
|
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):
|
class CosemObject(DSMRObject):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .objects import MBusObject, CosemObject
|
from .objects import MBusObject, MBusObjectV2_2, CosemObject
|
||||||
from .exceptions import ParseError
|
from .exceptions import ParseError
|
||||||
|
from .obis_references import GAS_METER_READING
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ class TelegramParser(object):
|
|||||||
telegram = {}
|
telegram = {}
|
||||||
|
|
||||||
for line_value in line_values:
|
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
|
telegram[obis_reference] = dsmr_object
|
||||||
|
|
||||||
@ -47,6 +47,26 @@ class TelegramParser(object):
|
|||||||
return obis_reference, parser.parse(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):
|
class DSMRObjectParser(object):
|
||||||
|
|
||||||
def __init__(self, *value_formats):
|
def __init__(self, *value_formats):
|
||||||
@ -85,7 +105,11 @@ class MBusParser(DSMRObjectParser):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def parse(self, line):
|
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):
|
class CosemParser(DSMRObjectParser):
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import serial
|
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 = {
|
SERIAL_SETTINGS_V4 = {
|
||||||
'baudrate': 115200,
|
'baudrate': 115200,
|
||||||
@ -26,7 +36,12 @@ class SerialReader(object):
|
|||||||
def __init__(self, device, serial_settings, telegram_specification):
|
def __init__(self, device, serial_settings, telegram_specification):
|
||||||
self.serial_settings = serial_settings
|
self.serial_settings = serial_settings
|
||||||
self.serial_settings['port'] = device
|
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):
|
def read(self):
|
||||||
"""
|
"""
|
||||||
@ -52,4 +67,3 @@ class SerialReader(object):
|
|||||||
if is_end_of_telegram(line):
|
if is_end_of_telegram(line):
|
||||||
yield self.telegram_parser.parse(telegram)
|
yield self.telegram_parser.parse(telegram)
|
||||||
telegram = []
|
telegram = []
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from .obis_references import *
|
from . import obis_references as obis
|
||||||
from .parsers import CosemParser, ValueParser, MBusParser
|
from .parsers import CosemParser, ValueParser, MBusParser
|
||||||
from .value_types import timestamp
|
from .value_types import timestamp
|
||||||
|
|
||||||
@ -13,36 +13,61 @@ This module contains DSMR telegram specifications. Each specifications describes
|
|||||||
how the telegram lines are parsed.
|
how the telegram lines are parsed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
V4 = {
|
V2_2 = {
|
||||||
P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
|
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
||||||
P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
|
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||||
ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||||
ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
||||||
ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
|
obis.ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
||||||
ELECTRICITY_DELIVERED_TARIFF_2: CosemParser(ValueParser(Decimal)),
|
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
||||||
ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
|
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
||||||
EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
|
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
||||||
CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
|
obis.ACTUAL_TRESHOLD_ELECTRICITY: CosemParser(ValueParser(Decimal)),
|
||||||
CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
|
obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)),
|
||||||
LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
|
obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)),
|
||||||
# POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO
|
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
|
||||||
VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)),
|
obis.EQUIPMENT_IDENTIFIER_GAS: CosemParser(ValueParser(str)),
|
||||||
VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)),
|
obis.DEVICE_TYPE: CosemParser(ValueParser(str)),
|
||||||
VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)),
|
obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)),
|
||||||
VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)),
|
obis.GAS_METER_READING: MBusParser(
|
||||||
VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)),
|
ValueParser(timestamp),
|
||||||
VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)),
|
ValueParser(int),
|
||||||
TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)),
|
ValueParser(int),
|
||||||
TEXT_MESSAGE: CosemParser(ValueParser(str)),
|
ValueParser(int),
|
||||||
DEVICE_TYPE: CosemParser(ValueParser(int)),
|
ValueParser(str),
|
||||||
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
|
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
@ -6,7 +6,10 @@ import pytz
|
|||||||
def timestamp(value):
|
def timestamp(value):
|
||||||
|
|
||||||
naive_datetime = datetime.datetime.strptime(value[:-1], '%y%m%d%H%M%S')
|
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')
|
local_tz = pytz.timezone('Europe/Amsterdam')
|
||||||
localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst)
|
localized_datetime = local_tz.localize(naive_datetime, is_dst=is_dst)
|
||||||
|
9
setup.py
9
setup.py
@ -7,7 +7,10 @@ setup(
|
|||||||
version='0.1',
|
version='0.1',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pyserial==3.0.1',
|
'pyserial>=3,<4',
|
||||||
'pytz==2016.3'
|
'pytz'
|
||||||
]
|
],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
40
test/test_parse.py
Normal file
40
test/test_parse.py
Normal 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'
|
Loading…
Reference in New Issue
Block a user