Compare commits

..
35 changed files with 207 additions and 1829 deletions
-45
View File
@@ -1,45 +0,0 @@
name: Tests
on:
push: ~
pull_request: ~
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10 # Don't run forever when stale
strategy:
matrix:
python-version:
- 3.6
- 3.7
- 3.8
- 3.9
name: Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v2
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Cached PIP dependencies
uses: actions/cache@v2
with:
path: |
~/.cache/pip
~/.tox/python/.pytest_cache
key: pip-${{ matrix.python-version }}-${{ hashFiles('setup.py', 'tox.ini') }}
restore-keys: pip-${{ matrix.python-version }}-
- name: Install dependencies
run: pip install tox
- name: Run tests
run: tox
- name: Code coverage upload
uses: codecov/codecov-action@v1
-6
View File
@@ -2,13 +2,7 @@
*.pyc *.pyc
.tox .tox
.cache .cache
.venv
*.egg-info *.egg-info
/.project /.project
/.pydevproject /.pydevproject
/.coverage /.coverage
build/
dist/
venv/
*.*~
*~
+18
View File
@@ -0,0 +1,18 @@
language: python
python:
- 2.7
- 3.4
- 3.5
- 3.6
install: pip install tox-travis codecov
script: tox
after_success:
- codecov
matrix:
allow_failures:
- python: 2.7
+2 -109
View File
@@ -1,116 +1,9 @@
Change Log Change Log
---------- ----------
**0.32** (2022-01-04) **0.9** (2017-03-02)
- Support DSMR data read via RFXtrx with integrated P1 reader (`pull request #98 <https://github.com/ndokter/dsmr_parser/pull/98>`_). - allow the telegram specification to optionally be autodetected
**0.31** (2021-11-21)
- Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway (`pull request #92 <https://github.com/ndokter/dsmr_parser/pull/92>`_).
**0.30** (2021-08-18)
- Add support for Swedish smart meters (`pull request #86 <https://github.com/ndokter/dsmr_parser/pull/86>`_).
**0.29** (2021-04-18)
- Add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break (`pull request #71 <https://github.com/ndokter/dsmr_parser/pull/71>`_).
Remove deprecated asyncio coroutine decorator (`pull request #76 <https://github.com/ndokter/dsmr_parser/pull/76>`_).
**0.28** (2021-02-21)
- Optional keep alive monitoring for TCP/IP connections (`pull request #73 <https://github.com/ndokter/dsmr_parser/pull/73>`_).
- Catch parse errors in TelegramParser, ignore lines that can not be parsed (`pull request #74 <https://github.com/ndokter/dsmr_parser/pull/74>`_).
**0.27** (2020-12-24)
- fix for empty parentheses in ProfileGenericParser (redone) (`pull request #69 <https://github.com/ndokter/dsmr_parser/pull/69>`_).
**0.26** (2020-12-15)
- reverted fix for empty parentheses in ProfileGenericParser (`pull request #68 <https://github.com/ndokter/dsmr_parser/pull/68>`_).
**0.25** (2020-12-14)
- fix for empty parentheses in ProfileGenericParser (`pull request #57 <https://github.com/ndokter/dsmr_parser/pull/57>`_).
**0.24** (2020-11-27)
- Add Luxembourg equipment identifier (`pull request #62 <https://github.com/ndokter/dsmr_parser/pull/62>`_).
**0.23** (2020-11-07)
- Resolved issue with x-x:24.3.0 where it contains non-integer character (`pull request #61 <https://github.com/ndokter/dsmr_parser/pull/61>`_).
- Tests are not installed anymore (`pull request #59 <https://github.com/ndokter/dsmr_parser/pull/59>`_).
- Example telegram improvement (`pull request #58 <https://github.com/ndokter/dsmr_parser/pull/58>`_).
**0.22** (2020-08-23)
- CRC check speed is improved
- Exception info improvement
**0.21** (2020-05-25)
- All objects can produce a json serialization of their state.
**0.20** (2020-05-12)
- All objects can now print their values
- Add parser + object for generic profile
**0.19** (2020-05-03)
- Add following missing elements to telegram specification v4:
- SHORT_POWER_FAILURE_COUNT,
- INSTANTANEOUS_CURRENT_L1,
- INSTANTANEOUS_CURRENT_L2,
- INSTANTANEOUS_CURRENT_L3
- Add missing tests + fix small test bugs
- Complete telegram object v4 parse test
**0.18** (2020-01-28)
- PyCRC replacement (`pull request #48 <https://github.com/ndokter/dsmr_parser/pull/48>`_).
**0.17** (2019-12-21)
- Add a true telegram object (`pull request #40 <https://github.com/ndokter/dsmr_parser/pull/40>`_).
**0.16** (2019-12-21)
- Add support for Belgian and Smarty meters (`pull request #44 <https://github.com/ndokter/dsmr_parser/pull/44>`_).
**0.15** (2019-12-12)
- Fixed asyncio loop issue (`pull request #43 <https://github.com/ndokter/dsmr_parser/pull/43>`_).
**0.14** (2019-10-08)
- Changed serial reading to reduce CPU usage (`pull request #37 <https://github.com/ndokter/dsmr_parser/pull/37>`_).
**0.13** (2019-03-04)
- Fix DSMR v5.0 serial settings which were not used (`pull request #33 <https://github.com/ndokter/dsmr_parser/pull/33>`_).
**0.12** (2018-09-23)
- Add serial settings for DSMR v5.0 (`pull request #31 <https://github.com/ndokter/dsmr_parser/pull/31>`_).
- Lux-creos-obis-1.8.0 (`pull request #32 <https://github.com/ndokter/dsmr_parser/pull/32>`_).
**0.11** (2017-09-18)
- NULL value fix in checksum (`pull request #26 <https://github.com/ndokter/dsmr_parser/pull/26>`_)
**0.10** (2017-06-05)
- bugfix: don't force full telegram signatures (`pull request #25 <https://github.com/ndokter/dsmr_parser/pull/25>`_)
- removed unused code for automatic telegram detection as this needs reworking after the fix mentioned above
- InvalidChecksumError's are logged as warning instead of error
**0.9** (2017-05-12)
- added DSMR v5 serial settings
**0.8** (2017-01-26) **0.8** (2017-01-26)
+4 -133
View File
@@ -14,7 +14,7 @@ also includes client implementation to directly read and parse smart meter data.
Features Features
-------- --------
DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.5, 3.6, 3.7, 3.8 and 3.9. DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.4, 3.5 and 3.6.
Client module usage Client module usage
@@ -43,25 +43,6 @@ process because the code is blocking (not asynchronous):
To be documented. To be documented.
**Socket client**
Read a remote serial port (for example using ser2net) and work with the parsed telegrams.
It should be run in a separate process because the code is blocking (not asynchronous):
.. code-block:: python
from dsmr_parser import telegram_specifications
from dsmr_parser.clients import SocketReader
socket_reader = SocketReader(
host='127.0.0.1',
port=2001,
telegram_specification=telegram_specifications.V4
)
for telegram in socket_reader.read():
print(telegram) # see 'Telegram object' docs below
Parsing module usage Parsing module usage
-------------------- --------------------
@@ -73,9 +54,8 @@ into a dictionary.
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications
from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import TelegramParser
# String is formatted in separate lines for readability.
telegram_str = ( telegram_str = (
'/ISk5\\2MT382-1000\r\n' '/ISk5\2MT382-1000\r\n'
'\r\n' '\r\n'
'0-0:96.1.1(4B384547303034303436333935353037)\r\n' '0-0:96.1.1(4B384547303034303436333935353037)\r\n'
'1-0:1.8.1(12345.678*kWh)\r\n' '1-0:1.8.1(12345.678*kWh)\r\n'
@@ -104,8 +84,8 @@ into a dictionary.
telegram = parser.parse(telegram_str) telegram = parser.parse(telegram_str)
print(telegram) # see 'Telegram object' docs below print(telegram) # see 'Telegram object' docs below
Telegram dictionary Telegram object
------------------- ---------------
A dictionary of which the key indicates the field type. These regex values A dictionary of which the key indicates the field type. These regex values
correspond to one of dsmr_parser.obis_reference constants. correspond to one of dsmr_parser.obis_reference constants.
@@ -157,115 +137,6 @@ Example to get some of the values:
# See dsmr_reader.obis_references for all readable telegram values. # See dsmr_reader.obis_references for all readable telegram values.
# Note that the avilable values differ per DSMR version. # Note that the avilable values differ per DSMR version.
Telegram as an Object
---------------------
An object version of the telegram is available as well.
.. code-block:: python
# DSMR v4.2 p1 using dsmr_parser and telegram objects
from dsmr_parser import telegram_specifications
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5
from dsmr_parser.objects import CosemObject, MBusObject, Telegram
from dsmr_parser.parsers import TelegramParser
import os
serial_reader = SerialReader(
device='/dev/ttyUSB0',
serial_settings=SERIAL_SETTINGS_V5,
telegram_specification=telegram_specifications.V4
)
# telegram = next(serial_reader.read_as_object())
# print(telegram)
for telegram in serial_reader.read_as_object():
os.system('clear')
print(telegram)
Example of output of print of the telegram object:
.. code-block:: console
P1_MESSAGE_HEADER: 42 [None]
P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None]
EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None]
ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh]
ELECTRICITY_USED_TARIFF_2: 1435.706 [kWh]
ELECTRICITY_DELIVERED_TARIFF_1: 0.000 [kWh]
ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh]
ELECTRICITY_ACTIVE_TARIFF: 0002 [None]
CURRENT_ELECTRICITY_USAGE: 2.027 [kW]
CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW]
LONG_POWER_FAILURE_COUNT: 7 [None]
VOLTAGE_SAG_L1_COUNT: 0 [None]
VOLTAGE_SAG_L2_COUNT: 0 [None]
VOLTAGE_SAG_L3_COUNT: 0 [None]
VOLTAGE_SWELL_L1_COUNT: 0 [None]
VOLTAGE_SWELL_L2_COUNT: 0 [None]
VOLTAGE_SWELL_L3_COUNT: 0 [None]
TEXT_MESSAGE_CODE: None [None]
TEXT_MESSAGE: None [None]
DEVICE_TYPE: 3 [None]
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.170 [kW]
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 1.247 [kW]
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.209 [kW]
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW]
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW]
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW]
EQUIPMENT_IDENTIFIER_GAS: 4819243993373755377509728609491464 [None]
HOURLY_GAS_METER_READING: 981.443 [m3]
Accessing the telegrams information as attributes directly:
.. code-block:: python
telegram
Out[3]: <dsmr_parser.objects.Telegram at 0x7f5e995d9898>
telegram.CURRENT_ELECTRICITY_USAGE
Out[4]: <dsmr_parser.objects.CosemObject at 0x7f5e98ae5ac8>
telegram.CURRENT_ELECTRICITY_USAGE.value
Out[5]: Decimal('2.027')
telegram.CURRENT_ELECTRICITY_USAGE.unit
Out[6]: 'kW'
The telegram object has an iterator, can be used to find all the information elements in the current telegram:
.. code-block:: python
[attr for attr, value in telegram]
Out[11]:
['P1_MESSAGE_HEADER',
'P1_MESSAGE_TIMESTAMP',
'EQUIPMENT_IDENTIFIER',
'ELECTRICITY_USED_TARIFF_1',
'ELECTRICITY_USED_TARIFF_2',
'ELECTRICITY_DELIVERED_TARIFF_1',
'ELECTRICITY_DELIVERED_TARIFF_2',
'ELECTRICITY_ACTIVE_TARIFF',
'CURRENT_ELECTRICITY_USAGE',
'CURRENT_ELECTRICITY_DELIVERY',
'LONG_POWER_FAILURE_COUNT',
'VOLTAGE_SAG_L1_COUNT',
'VOLTAGE_SAG_L2_COUNT',
'VOLTAGE_SAG_L3_COUNT',
'VOLTAGE_SWELL_L1_COUNT',
'VOLTAGE_SWELL_L2_COUNT',
'VOLTAGE_SWELL_L3_COUNT',
'TEXT_MESSAGE_CODE',
'TEXT_MESSAGE',
'DEVICE_TYPE',
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
'EQUIPMENT_IDENTIFIER_GAS',
'HOURLY_GAS_METER_READING']
Installation Installation
------------ ------------
+2 -2
View File
@@ -16,8 +16,8 @@ def console():
help='alternatively connect using TCP host.') help='alternatively connect using TCP host.')
parser.add_argument('--port', default=None, parser.add_argument('--port', default=None,
help='TCP port to use for connection') help='TCP port to use for connection')
parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L', '5S', 'Q3D'], parser.add_argument('--version', default='2.2', choices=['2.2', '4'],
help='DSMR version (2.2, 4, 5, 5B, 5L, 5S, Q3D)') help='DSMR version (2.2, 4)')
parser.add_argument('--verbose', '-v', action='count') parser.add_argument('--verbose', '-v', action='count')
args = parser.parse_args() args = parser.parse_args()
+1 -2
View File
@@ -1,6 +1,5 @@
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 SERIAL_SETTINGS_V4
from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader
from dsmr_parser.clients.socket_ import SocketReader
from dsmr_parser.clients.protocol import create_dsmr_protocol, \ from dsmr_parser.clients.protocol import create_dsmr_protocol, \
create_dsmr_reader, create_tcp_dsmr_reader create_dsmr_reader, create_tcp_dsmr_reader
-174
View File
@@ -1,174 +0,0 @@
import logging
import fileinput
import tailer
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.objects import Telegram
from dsmr_parser.parsers import TelegramParser
logger = logging.getLogger(__name__)
class FileReader(object):
"""
Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects
for each read telegram.
Usage:
from dsmr_parser import telegram_specifications
from dsmr_parser.clients.filereader import FileReader
if __name__== "__main__":
infile = '/data/smartmeter/readings.txt'
file_reader = FileReader(
file = infile,
telegram_specification = telegram_specifications.V4
)
for telegram in file_reader.read_as_object():
print(telegram)
The file can be created like:
from dsmr_parser import telegram_specifications
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5
if __name__== "__main__":
outfile = '/data/smartmeter/readings.txt'
serial_reader = SerialReader(
device='/dev/ttyUSB0',
serial_settings=SERIAL_SETTINGS_V5,
telegram_specification=telegram_specifications.V4
)
for telegram in serial_reader.read_as_object():
f=open(outfile,"ab+")
f.write(telegram._telegram_data.encode())
f.close()
"""
def __init__(self, file, telegram_specification):
self._file = file
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read_as_object(self):
"""
Read complete DSMR telegram's from a file and return a Telegram object.
:rtype: generator
"""
with open(self._file, "rb") as file_handle:
while True:
data = file_handle.readline()
if not data:
break
self.telegram_buffer.append(data.decode())
for telegram in self.telegram_buffer.get_all():
try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
class FileInputReader(object):
"""
Filereader to read and parse raw telegram strings from stdin or files specified at the commandline
and instantiate Telegram objects for each read telegram.
Usage python script "syphon_smartmeter_readings_stdin.py":
from dsmr_parser import telegram_specifications
from dsmr_parser.clients.filereader import FileInputReader
if __name__== "__main__":
fileinput_reader = FileReader(
file = infile,
telegram_specification = telegram_specifications.V4
)
for telegram in fileinput_reader.read_as_object():
print(telegram)
Command line:
tail -f /data/smartmeter/readings.txt | python3 syphon_smartmeter_readings_stdin.py
"""
def __init__(self, telegram_specification):
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read_as_object(self):
"""
Read complete DSMR telegram's from stdin of filearguments specified on teh command line
and return a Telegram object.
:rtype: generator
"""
with fileinput.input(mode='rb') as file_handle:
while True:
data = file_handle.readline()
str = data.decode()
self.telegram_buffer.append(str)
for telegram in self.telegram_buffer.get_all():
try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
class FileTailReader(object):
"""
Filereader to read and parse raw telegram strings from the tail of a
given file and instantiate Telegram objects for each read telegram.
Usage python script "syphon_smartmeter_readings_stdin.py":
from dsmr_parser import telegram_specifications
from dsmr_parser.clients.filereader import FileTailReader
if __name__== "__main__":
infile = '/data/smartmeter/readings.txt'
filetail_reader = FileTailReader(
file = infile,
telegram_specification = telegram_specifications.V5
)
for telegram in filetail_reader.read_as_object():
print(telegram)
"""
def __init__(self, file, telegram_specification):
self._file = file
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read_as_object(self):
"""
Read complete DSMR telegram's from a files tail and return a Telegram object.
:rtype: generator
"""
with open(self._file, "rb") as file_handle:
for data in tailer.follow(file_handle):
str = data.decode()
self.telegram_buffer.append(str)
for telegram in self.telegram_buffer.get_all():
try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
+13 -65
View File
@@ -8,20 +8,13 @@ from serial_asyncio import create_serial_connection
from dsmr_parser import telegram_specifications from dsmr_parser import telegram_specifications
from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.exceptions import ParseError
from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import TelegramParser
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \ from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5 SERIAL_SETTINGS_V4
def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs): def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None):
"""Creates a DSMR asyncio protocol."""
protocol = _create_dsmr_protocol(dsmr_version, telegram_callback,
DSMRProtocol, loop, **kwargs)
return protocol
def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, **kwargs):
"""Creates a DSMR asyncio protocol.""" """Creates a DSMR asyncio protocol."""
if dsmr_version == '2.2': if dsmr_version == '2.2':
@@ -30,30 +23,12 @@ def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None,
elif dsmr_version == '4': elif dsmr_version == '4':
specification = telegram_specifications.V4 specification = telegram_specifications.V4
serial_settings = SERIAL_SETTINGS_V4 serial_settings = SERIAL_SETTINGS_V4
elif dsmr_version == '4+':
specification = telegram_specifications.V5
serial_settings = SERIAL_SETTINGS_V4
elif dsmr_version == '5':
specification = telegram_specifications.V5
serial_settings = SERIAL_SETTINGS_V5
elif dsmr_version == '5B':
specification = telegram_specifications.BELGIUM_FLUVIUS
serial_settings = SERIAL_SETTINGS_V5
elif dsmr_version == "5L":
specification = telegram_specifications.LUXEMBOURG_SMARTY
serial_settings = SERIAL_SETTINGS_V5
elif dsmr_version == "5S":
specification = telegram_specifications.SWEDEN
serial_settings = SERIAL_SETTINGS_V5
elif dsmr_version == "Q3D":
specification = telegram_specifications.Q3D
serial_settings = SERIAL_SETTINGS_V5
else: else:
raise NotImplementedError("No telegram parser found for version: %s", raise NotImplementedError("No telegram parser found for version: %s",
dsmr_version) dsmr_version)
protocol = partial(protocol, loop, TelegramParser(specification), protocol = partial(DSMRProtocol, loop, TelegramParser(specification),
telegram_callback=telegram_callback, **kwargs) telegram_callback=telegram_callback)
return protocol, serial_settings return protocol, serial_settings
@@ -69,14 +44,10 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):
def create_tcp_dsmr_reader(host, port, dsmr_version, def create_tcp_dsmr_reader(host, port, dsmr_version,
telegram_callback, loop=None, telegram_callback, loop=None):
keep_alive_interval=None):
"""Creates a DSMR asyncio protocol coroutine using TCP connection.""" """Creates a DSMR asyncio protocol coroutine using TCP connection."""
if not loop:
loop = asyncio.get_event_loop()
protocol, _ = create_dsmr_protocol( protocol, _ = create_dsmr_protocol(
dsmr_version, telegram_callback, loop=loop, dsmr_version, telegram_callback, loop=None)
keep_alive_interval=keep_alive_interval)
conn = loop.create_connection(protocol, host, port) conn = loop.create_connection(protocol, host, port)
return conn return conn
@@ -87,8 +58,7 @@ class DSMRProtocol(asyncio.Protocol):
transport = None transport = None
telegram_callback = None telegram_callback = None
def __init__(self, loop, telegram_parser, def __init__(self, loop, telegram_parser, telegram_callback=None):
telegram_callback=None, keep_alive_interval=None):
"""Initialize class.""" """Initialize class."""
self.loop = loop self.loop = loop
self.log = logging.getLogger(__name__) self.log = logging.getLogger(__name__)
@@ -99,46 +69,25 @@ class DSMRProtocol(asyncio.Protocol):
self.telegram_buffer = TelegramBuffer() self.telegram_buffer = TelegramBuffer()
# keep a lock until the connection is closed # keep a lock until the connection is closed
self._closed = asyncio.Event() self._closed = asyncio.Event()
self._keep_alive_interval = keep_alive_interval
self._active = True
def connection_made(self, transport): def connection_made(self, transport):
"""Just logging for now.""" """Just logging for now."""
self.transport = transport self.transport = transport
self.log.debug('connected') self.log.debug('connected')
self._active = False
if self.loop and self._keep_alive_interval:
self.loop.call_later(self._keep_alive_interval, self.keep_alive)
def data_received(self, data): def data_received(self, data):
"""Add incoming data to buffer.""" """Add incoming data to buffer."""
data = data.decode('ascii')
# accept latin-1 (8-bit) on the line, to allow for non-ascii transport or padding
data = data.decode("latin1")
self._active = True
self.log.debug('received data: %s', data) self.log.debug('received data: %s', data)
self.telegram_buffer.append(data) self.telegram_buffer.append(data)
for telegram in self.telegram_buffer.get_all(): for telegram in self.telegram_buffer.get_all():
# ensure actual telegram is ascii (7-bit) only (ISO 646:1991 IRV required in section 5.5 of IEC 62056-21)
telegram = telegram.encode("latin1").decode("ascii")
self.handle_telegram(telegram) self.handle_telegram(telegram)
def keep_alive(self):
if self._active:
self.log.debug('keep-alive checked')
self._active = False
if self.loop:
self.loop.call_later(self._keep_alive_interval, self.keep_alive)
else:
self.log.warning('keep-alive check failed')
if self.transport:
self.transport.close()
def connection_lost(self, exc): def connection_lost(self, exc):
"""Stop when connection is lost.""" """Stop when connection is lost."""
if exc: if exc:
self.log.exception('disconnected due to exception', exc_info=exc) self.log.exception('disconnected due to exception')
else: else:
self.log.info('disconnected because of close/abort.') self.log.info('disconnected because of close/abort.')
self._closed.set() self._closed.set()
@@ -149,13 +98,12 @@ class DSMRProtocol(asyncio.Protocol):
try: try:
parsed_telegram = self.telegram_parser.parse(telegram) parsed_telegram = self.telegram_parser.parse(telegram)
except InvalidChecksumError as e:
self.log.warning(str(e))
except ParseError: except ParseError:
self.log.exception("failed to parse telegram") self.log.exception("failed to parse telegram")
else: else:
self.telegram_callback(parsed_telegram) self.telegram_callback(parsed_telegram)
async def wait_closed(self): @asyncio.coroutine
def wait_closed(self):
"""Wait until connection is closed.""" """Wait until connection is closed."""
await self._closed.wait() yield from self._closed.wait()
-62
View File
@@ -1,62 +0,0 @@
"""Asyncio protocol implementation for handling telegrams over a RFXtrx connection ."""
import asyncio
from serial_asyncio import create_serial_connection
from .protocol import DSMRProtocol, _create_dsmr_protocol
def create_rfxtrx_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs):
"""Creates a RFXtrxDSMR asyncio protocol."""
protocol = _create_dsmr_protocol(dsmr_version, telegram_callback,
RFXtrxDSMRProtocol, loop, **kwargs)
return protocol
def create_rfxtrx_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):
"""Creates a DSMR asyncio protocol coroutine using a RFXtrx serial port."""
protocol, serial_settings = create_rfxtrx_dsmr_protocol(
dsmr_version, telegram_callback, loop=None)
serial_settings['url'] = port
conn = create_serial_connection(loop, protocol, **serial_settings)
return conn
def create_rfxtrx_tcp_dsmr_reader(host, port, dsmr_version,
telegram_callback, loop=None,
keep_alive_interval=None):
"""Creates a DSMR asyncio protocol coroutine using a RFXtrx TCP connection."""
if not loop:
loop = asyncio.get_event_loop()
protocol, _ = create_rfxtrx_dsmr_protocol(
dsmr_version, telegram_callback, loop=loop,
keep_alive_interval=keep_alive_interval)
conn = loop.create_connection(protocol, host, port)
return conn
PACKETTYPE_DSMR = 0x62
SUBTYPE_P1 = 0x01
class RFXtrxDSMRProtocol(DSMRProtocol):
remaining_data = b''
def data_received(self, data):
"""Add incoming data to buffer."""
data = self.remaining_data + data
packetlength = data[0] + 1 if len(data) > 0 else 1
while packetlength <= len(data):
packettype = data[1]
subtype = data[2]
if (packettype == PACKETTYPE_DSMR and subtype == SUBTYPE_P1):
dsmr_data = data[4:packetlength]
super().data_received(dsmr_data)
data = data[packetlength:]
packetlength = data[0] + 1 if len(data) > 0 else 1
self.remaining_data = data
+7 -28
View File
@@ -1,11 +1,11 @@
import asyncio
import logging import logging
import serial import serial
import serial_asyncio import serial_asyncio
from dsmr_parser.clients.telegram_buffer import TelegramBuffer from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.exceptions import ParseError
from dsmr_parser.parsers import TelegramParser from dsmr_parser.parsers import TelegramParser
from dsmr_parser.objects import Telegram
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -20,32 +20,12 @@ class SerialReader(object):
self.telegram_parser = TelegramParser(telegram_specification) self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer() self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read(self): def read(self):
""" """
Read complete DSMR telegram's from the serial interface and parse it Read complete DSMR telegram's from the serial interface and parse it
into CosemObject's and MbusObject's into CosemObject's and MbusObject's
:rtype: generator
"""
with serial.Serial(**self.serial_settings) as serial_handle:
while True:
data = serial_handle.read(max(1, min(1024, serial_handle.in_waiting)))
self.telegram_buffer.append(data.decode('ascii'))
for telegram in self.telegram_buffer.get_all():
try:
yield self.telegram_parser.parse(telegram)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
def read_as_object(self):
"""
Read complete DSMR telegram's from the serial interface and return a Telegram object.
:rtype: generator :rtype: generator
""" """
with serial.Serial(**self.serial_settings) as serial_handle: with serial.Serial(**self.serial_settings) as serial_handle:
@@ -55,9 +35,7 @@ class SerialReader(object):
for telegram in self.telegram_buffer.get_all(): for telegram in self.telegram_buffer.get_all():
try: try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification) yield self.telegram_parser.parse(telegram)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e: except ParseError as e:
logger.error('Failed to parse telegram: %s', e) logger.error('Failed to parse telegram: %s', e)
@@ -67,7 +45,8 @@ class AsyncSerialReader(SerialReader):
PORT_KEY = 'url' PORT_KEY = 'url'
async def read(self, queue): @asyncio.coroutine
def read(self, queue):
""" """
Read complete DSMR telegram's from the serial interface and parse it Read complete DSMR telegram's from the serial interface and parse it
into CosemObject's and MbusObject's. into CosemObject's and MbusObject's.
@@ -79,12 +58,12 @@ class AsyncSerialReader(SerialReader):
""" """
# create Serial StreamReader # create Serial StreamReader
conn = serial_asyncio.open_serial_connection(**self.serial_settings) conn = serial_asyncio.open_serial_connection(**self.serial_settings)
reader, _ = await conn reader, _ = yield from conn
while True: while True:
# Read line if available or give control back to loop until new # Read line if available or give control back to loop until new
# data has arrived. # data has arrived.
data = await reader.readline() data = yield from reader.readline()
self.telegram_buffer.append(data.decode('ascii')) self.telegram_buffer.append(data.decode('ascii'))
for telegram in self.telegram_buffer.get_all(): for telegram in self.telegram_buffer.get_all():
-10
View File
@@ -20,13 +20,3 @@ SERIAL_SETTINGS_V4 = {
'rtscts': 0, 'rtscts': 0,
'timeout': 20 'timeout': 20
} }
SERIAL_SETTINGS_V5 = {
'baudrate': 115200,
'bytesize': serial.EIGHTBITS,
'parity': serial.PARITY_NONE,
'stopbits': serial.STOPBITS_ONE,
'xonxoff': 0,
'rtscts': 0,
'timeout': 20
}
-90
View File
@@ -1,90 +0,0 @@
import logging
import socket
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.parsers import TelegramParser
from dsmr_parser.objects import Telegram
logger = logging.getLogger(__name__)
class SocketReader(object):
BUFFER_SIZE = 256
def __init__(self, host, port, telegram_specification):
self.host = host
self.port = port
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read(self):
"""
Read complete DSMR telegram's from remote interface and parse it
into CosemObject's and MbusObject's
:rtype: generator
"""
buffer = b""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:
socket_handle.connect((self.host, self.port))
while True:
buffer += socket_handle.recv(self.BUFFER_SIZE)
lines = buffer.splitlines(keepends=True)
if len(lines) == 0:
continue
for data in lines:
self.telegram_buffer.append(data.decode('ascii'))
for telegram in self.telegram_buffer.get_all():
try:
yield self.telegram_parser.parse(telegram)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
buffer = b""
def read_as_object(self):
"""
Read complete DSMR telegram's from remote and return a Telegram object.
:rtype: generator
"""
buffer = b""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:
socket_handle.connect((self.host, self.port))
while True:
buffer += socket_handle.recv(self.BUFFER_SIZE)
lines = buffer.splitlines(keepends=True)
if len(lines) == 0:
continue
for data in lines:
self.telegram_buffer.append(data.decode('ascii'))
for telegram in self.telegram_buffer.get_all():
try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
buffer = b""
+1 -1
View File
@@ -51,7 +51,7 @@ class TelegramBuffer(object):
# - The checksum is optional '{0,4}' because not all telegram versions # - The checksum is optional '{0,4}' because not all telegram versions
# support it. # support it.
return re.findall( return re.findall(
r'\/[^\/]+?\![A-F0-9]{0,4}\0?\r\n', r'\/[^\/]+?\![A-F0-9]{0,4}\r\n',
self._buffer, self._buffer,
re.DOTALL re.DOTALL
) )
+4
View File
@@ -4,3 +4,7 @@ class ParseError(Exception):
class InvalidChecksumError(ParseError): class InvalidChecksumError(ParseError):
pass pass
class TelegramSpecificationMatchError(ParseError):
pass
-60
View File
@@ -1,60 +0,0 @@
from dsmr_parser import obis_references as obis
"""
dsmr_parser.obis_name_mapping
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains a mapping of obis references to names.
"""
EN = {
obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER',
obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP',
obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL',
obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1',
obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2',
obis.ELECTRICITY_EXPORTED_TOTAL: 'ELECTRICITY_EXPORTED_TOTAL',
obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1',
obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2',
obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF',
obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER',
obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE',
obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY',
obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT',
obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT',
obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG',
obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT',
obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT',
obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT',
obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT',
obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT',
obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT',
obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1',
obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2',
obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3',
obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1',
obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2',
obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3',
obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE',
obis.TEXT_MESSAGE: 'TEXT_MESSAGE',
obis.DEVICE_TYPE: 'DEVICE_TYPE',
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS',
obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING',
obis.GAS_METER_READING: 'GAS_METER_READING',
obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY',
obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION',
obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS',
obis.BELGIUM_HOURLY_GAS_METER_READING: 'BELGIUM_HOURLY_GAS_METER_READING',
obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER',
obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER',
obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE',
obis.Q3D_EQUIPMENT_SERIALNUMBER: 'Q3D_EQUIPMENT_SERIALNUMBER',
}
REVERSE_EN = dict([(v, k) for k, v in EN.items()])
-18
View File
@@ -17,7 +17,6 @@ EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n'
CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n' CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n'
CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n' CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n'
LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n' LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n'
SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n'
POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n' POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n'
VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n' VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n'
VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n' VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n'
@@ -25,12 +24,6 @@ VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n'
VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n' VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n'
VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n' VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n'
VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n' VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n'
INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n'
INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n'
INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n'
INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n'
INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n'
INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n'
TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n' TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n'
TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n' TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n'
DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n' DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n'
@@ -59,14 +52,3 @@ ELECTRICITY_DELIVERED_TARIFF_ALL = (
ELECTRICITY_DELIVERED_TARIFF_1, ELECTRICITY_DELIVERED_TARIFF_1,
ELECTRICITY_DELIVERED_TARIFF_2 ELECTRICITY_DELIVERED_TARIFF_2
) )
# International generalized additions
ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+)
ELECTRICITY_EXPORTED_TOTAL = r'\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-)
# International non generalized additions (country specific) / risk for necessary refactoring
BELGIUM_HOURLY_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n' # Different code, same format.
LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'\d-\d:42\.0\.0.+?\r\n' # Logical device name
Q3D_EQUIPMENT_IDENTIFIER = r'\d-\d:0\.0\.0.+?\r\n' # Logical device name
Q3D_EQUIPMENT_STATE = r'\d-\d:96\.5\.5.+?\r\n' # Device state (hexadecimal)
Q3D_EQUIPMENT_SERIALNUMBER = r'\d-\d:96\.1\.255.+?\r\n' # Device Serialnumber
+4 -172
View File
@@ -1,63 +1,3 @@
import dsmr_parser.obis_name_mapping
import datetime
import json
from decimal import Decimal
class Telegram(object):
"""
Container for raw and parsed telegram data.
Initializing:
from dsmr_parser import telegram_specifications
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
from dsmr_parser.objects import CosemObject, MBusObject, Telegram
from dsmr_parser.parsers import TelegramParser
from test.example_telegrams import TELEGRAM_V4_2
parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
Attributes can be accessed on a telegram object by addressing by their english name, for example:
telegram.ELECTRICITY_USED_TARIFF_1
All attributes in a telegram can be iterated over, for example:
[k for k,v in telegram]
yields:
['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...]
"""
def __init__(self, telegram_data, telegram_parser, telegram_specification):
self._telegram_data = telegram_data
self._telegram_specification = telegram_specification
self._telegram_parser = telegram_parser
self._obis_name_mapping = dsmr_parser.obis_name_mapping.EN
self._reverse_obis_name_mapping = dsmr_parser.obis_name_mapping.REVERSE_EN
self._dictionary = self._telegram_parser.parse(telegram_data)
self._item_names = self._get_item_names()
def __getattr__(self, name):
''' will only get called for undefined attributes '''
obis_reference = self._reverse_obis_name_mapping[name]
value = self._dictionary[obis_reference]
setattr(self, name, value)
return value
def _get_item_names(self):
return [self._obis_name_mapping[k] for k, v in self._dictionary.items()]
def __iter__(self):
for attr in self._item_names:
value = getattr(self, attr)
yield attr, value
def __str__(self):
output = ""
for attr, value in self:
output += "{}: \t {}\n".format(attr, str(value))
return output
def to_json(self):
return json.dumps(dict([[attr, json.loads(value.to_json())] for attr, value in self]))
class DSMRObject(object): class DSMRObject(object):
""" """
Represents all data from a single telegram line. Represents all data from a single telegram line.
@@ -79,7 +19,7 @@ class MBusObject(DSMRObject):
# TODO object, but let the parse set them differently? So don't use # TODO object, but let the parse set them differently? So don't use
# TODO hardcoded indexes here. # TODO hardcoded indexes here.
if len(self.values) != 2: # v2 if len(self.values) != 2: # v2
return self.values[6]['value'] return self.values[5]['value']
else: else:
return self.values[1]['value'] return self.values[1]['value']
@@ -89,30 +29,10 @@ class MBusObject(DSMRObject):
# TODO object, but let the parse set them differently? So don't use # TODO object, but let the parse set them differently? So don't use
# TODO hardcoded indexes here. # TODO hardcoded indexes here.
if len(self.values) != 2: # v2 if len(self.values) != 2: # v2
return self.values[5]['value'] return self.values[4]['value']
else: else:
return self.values[1]['unit'] return self.values[1]['unit']
def __str__(self):
output = "{}\t[{}] at {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat()))
return output
def to_json(self):
timestamp = self.datetime
if isinstance(self.datetime, datetime.datetime):
timestamp = self.datetime.astimezone().isoformat()
value = self.value
if isinstance(self.value, datetime.datetime):
value = self.value.astimezone().isoformat()
if isinstance(self.value, Decimal):
value = float(self.value)
output = {
'datetime': timestamp,
'value': value,
'unit': self.unit
}
return json.dumps(output)
class CosemObject(DSMRObject): class CosemObject(DSMRObject):
@@ -124,94 +44,6 @@ class CosemObject(DSMRObject):
def unit(self): def unit(self):
return self.values[0]['unit'] return self.values[0]['unit']
def __str__(self):
print_value = self.value
if isinstance(self.value, datetime.datetime):
print_value = self.value.astimezone().isoformat()
output = "{}\t[{}]".format(str(print_value), str(self.unit))
return output
def to_json(self): class ProfileGeneric(DSMRObject):
json_value = self.value pass # TODO implement
if isinstance(self.value, datetime.datetime):
json_value = self.value.astimezone().isoformat()
if isinstance(self.value, Decimal):
json_value = float(self.value)
output = {
'value': json_value,
'unit': self.unit
}
return json.dumps(output)
class ProfileGenericObject(DSMRObject):
"""
Represents all data in a GenericProfile value.
All buffer values are returned as a list of MBusObjects,
containing the datetime (timestamp) and the value.
"""
def __init__(self, values):
super().__init__(values)
self._buffer_list = None
@property
def value(self):
# value is added to make sure the telegram iterator does not break
return self.values
@property
def unit(self):
# value is added to make sure all items have a unit so code that relies on that does not break
return None
@property
def buffer_length(self):
return self.values[0]['value']
@property
def buffer_type(self):
return self.values[1]['value']
@property
def buffer(self):
if self._buffer_list is None:
self._buffer_list = []
values_offset = 2
for i in range(self.buffer_length):
offset = values_offset + i * 2
self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]]))
return self._buffer_list
def __str__(self):
output = "\t buffer length: {}\n".format(self.buffer_length)
output += "\t buffer type: {}".format(self.buffer_type)
for buffer_value in self.buffer:
timestamp = buffer_value.datetime
if isinstance(timestamp, datetime.datetime):
timestamp = str(timestamp.astimezone().isoformat())
output += "\n\t event occured at: {}".format(timestamp)
output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit)
return output
def to_json(self):
"""
:return: A json of all values in the GenericProfileObject , with the following structure
{'buffer_length': n,
'buffer_type': obis_ref,
'buffer': [{'datetime': d1,
'value': v1,
'unit': u1},
...
{'datetime': dn,
'value': vn,
'unit': un}
]
}
"""
list = [['buffer_length', self.buffer_length]]
list.append(['buffer_type', self.buffer_type])
buffer_repr = [json.loads(buffer_item.to_json()) for buffer_item in self.buffer]
list.append(['buffer', buffer_repr])
output = dict(list)
return json.dumps(output)
+53 -91
View File
@@ -1,20 +1,21 @@
import logging import logging
import re import re
from ctypes import c_ushort from PyCRC.CRC16 import CRC16
from dsmr_parser.objects import MBusObject, CosemObject, ProfileGenericObject from dsmr_parser.objects import MBusObject, CosemObject
from dsmr_parser.exceptions import ParseError, InvalidChecksumError from dsmr_parser.exceptions import ParseError, InvalidChecksumError, \
TelegramSpecificationMatchError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TelegramParser(object): class TelegramParser(object):
crc16_tab = []
def __init__(self, telegram_specification, apply_checksum_validation=True): def __init__(self, telegram_specification=None, apply_checksum_validation=True):
""" """
:param telegram_specification: determines how the telegram is parsed :param telegram_specification: determines how the telegram is parsed.
Will attempt to autodetect if omitted.
:param apply_checksum_validation: validate checksum if applicable for :param apply_checksum_validation: validate checksum if applicable for
telegram DSMR version (v4 and up). telegram DSMR version (v4 and up).
:type telegram_specification: dict :type telegram_specification: dict
@@ -40,8 +41,10 @@ class TelegramParser(object):
.. ..
} }
:raises ParseError: :raises ParseError:
:raises InvalidChecksumError:
""" """
if not self.telegram_specification:
self.telegram_specification = \
match_telegram_specification(telegram_data)
if self.apply_checksum_validation \ if self.apply_checksum_validation \
and self.telegram_specification['checksum_support']: and self.telegram_specification['checksum_support']:
@@ -52,14 +55,12 @@ class TelegramParser(object):
for signature, parser in self.telegram_specification['objects'].items(): for signature, parser in self.telegram_specification['objects'].items():
match = re.search(signature, telegram_data, re.DOTALL) match = re.search(signature, telegram_data, re.DOTALL)
# Some signatures are optional and may not be present, # All telegram specification lines/signatures are expected to be
# so only parse lines that match # present.
if match: if not match:
try: raise ParseError('Telegram specification does not match '
telegram[signature] = parser.parse(match.group(0)) 'telegram data')
except Exception: telegram[signature] = parser.parse(match.group(0))
logger.error("ignore line with signature {}, because parsing failed.".format(signature),
exc_info=True)
return telegram return telegram
@@ -84,7 +85,7 @@ class TelegramParser(object):
'incomplete. The checksum and/or content values are missing.' 'incomplete. The checksum and/or content values are missing.'
) )
calculated_crc = TelegramParser.crc16(checksum_contents.group(0)) calculated_crc = CRC16().calculate(checksum_contents.group(0))
expected_crc = int(checksum_hex.group(0), base=16) expected_crc = int(checksum_hex.group(0), base=16)
if calculated_crc != expected_crc: if calculated_crc != expected_crc:
@@ -96,32 +97,35 @@ class TelegramParser(object):
) )
) )
@staticmethod
def crc16(telegram):
"""
Calculate the CRC16 value for the given telegram
:param str telegram: def match_telegram_specification(telegram_data):
""" """
crcValue = 0x0000 Find telegram specification that matches the telegram data by trying all
specifications.
if len(TelegramParser.crc16_tab) == 0: Could be further optimized to check the actual 0.2.8 OBIS reference which
for i in range(0, 256): is available for DSMR version 4 and up.
crc = c_ushort(i).value
for j in range(0, 8):
if (crc & 0x0001):
crc = c_ushort(crc >> 1).value ^ 0xA001
else:
crc = c_ushort(crc >> 1).value
TelegramParser.crc16_tab.append(hex(crc))
for c in telegram: :param str telegram_data: full telegram from start ('/') to checksum
d = ord(c) ('!ABCD') including line endings in between the telegram's lines
tmp = crcValue ^ d :return: telegram specification
rotated = c_ushort(crcValue >> 8).value :rtype: dict
crcValue = rotated ^ int(TelegramParser.crc16_tab[(tmp & 0x00ff)], 0) """
# Prevent circular import
from dsmr_parser import telegram_specifications
return crcValue for specification in telegram_specifications.ALL:
try:
TelegramParser(specification).parse(telegram_data)
except ParseError:
pass
else:
return specification
raise TelegramSpecificationMatchError(
'Could automatically match telegram specification. Make sure the data'
'is not corrupt. Alternatively manually specify one.'
)
class DSMRObjectParser(object): class DSMRObjectParser(object):
@@ -132,28 +136,19 @@ class DSMRObjectParser(object):
def __init__(self, *value_formats): def __init__(self, *value_formats):
self.value_formats = value_formats self.value_formats = value_formats
def _is_line_wellformed(self, line, values):
# allows overriding by child class
return (values and (len(values) == len(self.value_formats)))
def _parse_values(self, values):
# allows overriding by child class
return [self.value_formats[i].parse(value)
for i, value in enumerate(values)]
def _parse(self, line): def _parse(self, line):
# Match value groups, but exclude the parentheses # Match value groups, but exclude the parentheses
pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))') pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*]{0,}(?=\)))+')
values = re.findall(pattern, line) values = re.findall(pattern, line)
if not self._is_line_wellformed(line, values):
raise ParseError("Invalid '%s' line for '%s'", line, self)
# Convert empty value groups to None for clarity. # Convert empty value groups to None for clarity.
values = [None if value == '' else value for value in values] values = [None if value == '' else value for value in values]
return self._parse_values(values) if not values or len(values) != len(self.value_formats):
raise ParseError("Invalid '%s' line for '%s'", line, self)
return [self.value_formats[i].parse(value)
for i, value in enumerate(values)]
class MBusParser(DSMRObjectParser): class MBusParser(DSMRObjectParser):
@@ -190,11 +185,10 @@ class CosemParser(DSMRObjectParser):
1 23 45 1 23 45
1) OBIS Reduced ID-code 1) OBIS Reduced ID-code
2) Separator "(", ASCII 28h 2) Separator “(“, ASCII 28h
3) COSEM object attribute value 3) COSEM object attribute value
4) Unit of measurement values (Unit of capture objects attribute) - only if 4) Unit of measurement values (Unit of capture objects attribute) – only if applicable
applicable 5) Separator “)”, ASCII 29h
5) Separator ")", ASCII 29h
""" """
def parse(self, line): def parse(self, line):
@@ -223,41 +217,8 @@ class ProfileGenericParser(DSMRObjectParser):
9) Unit of buffer values (Unit of capture objects attribute) 9) Unit of buffer values (Unit of capture objects attribute)
""" """
def __init__(self, buffer_types, head_parsers, parsers_for_unidentified):
self.value_formats = head_parsers
self.buffer_types = buffer_types
self.parsers_for_unidentified = parsers_for_unidentified
def _is_line_wellformed(self, line, values):
if values and (len(values) == 1) and (values[0] == ''):
# special case: single empty parentheses (indicated by empty string)
return True
if values and (len(values) >= 2) and (values[0].isdigit()):
buffer_length = int(values[0])
return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2))
else:
return False
def _parse_values(self, values):
if values and (len(values) == 1) and (values[0] is None):
# special case: single empty parentheses; make sure empty ProfileGenericObject is created
values = [0, None] # buffer_length=0, buffer_value_obis_ID=None
buffer_length = int(values[0])
buffer_value_obis_ID = values[1]
if (buffer_length > 0):
if buffer_value_obis_ID in self.buffer_types:
bufferValueParsers = self.buffer_types[buffer_value_obis_ID]
else:
bufferValueParsers = self.parsers_for_unidentified
# add the parsers for the encountered value type z times
for _ in range(buffer_length):
self.value_formats.extend(bufferValueParsers)
return [self.value_formats[i].parse(value) for i, value in enumerate(values)]
def parse(self, line): def parse(self, line):
return ProfileGenericObject(self._parse(line)) raise NotImplementedError()
class ValueParser(object): class ValueParser(object):
@@ -275,6 +236,7 @@ class ValueParser(object):
self.coerce_type = coerce_type self.coerce_type = coerce_type
def parse(self, value): def parse(self, value):
unit_of_measurement = None unit_of_measurement = None
if value and '*' in value: if value and '*' in value:
@@ -1,10 +0,0 @@
from dsmr_parser.parsers import ValueParser
from dsmr_parser.value_types import timestamp
PG_FAILURE_EVENT = r'0-0:96.7.19'
PG_HEAD_PARSERS = [ValueParser(int), ValueParser(str)]
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)]
BUFFER_TYPES = {
PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)]
}
+7 -84
View File
@@ -1,10 +1,9 @@
from decimal import Decimal from decimal import Decimal
from copy import deepcopy
from dsmr_parser import obis_references as obis from dsmr_parser import obis_references as obis
from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser
from dsmr_parser.value_types import timestamp from dsmr_parser.value_types import timestamp
from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS
""" """
dsmr_parser.telegram_specifications dsmr_parser.telegram_specifications
@@ -34,12 +33,11 @@ V2_2 = {
obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)), obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)),
obis.GAS_METER_READING: MBusParser( obis.GAS_METER_READING: MBusParser(
ValueParser(timestamp), ValueParser(timestamp),
ValueParser(str), # changed to str see issue60
ValueParser(int), ValueParser(int),
ValueParser(int), ValueParser(int),
ValueParser(str), # obis ref ValueParser(int),
ValueParser(str), # unit, position 5 ValueParser(str),
ValueParser(Decimal), # meter reading, position 6 ValueParser(Decimal),
), ),
} }
} }
@@ -59,12 +57,8 @@ V4 = {
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)), obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
obis.POWER_EVENT_FAILURE_LOG: # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO
ProfileGenericParser(BUFFER_TYPES,
PG_HEAD_PARSERS,
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS),
obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)),
@@ -74,9 +68,6 @@ V4 = {
obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)), obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)),
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
obis.DEVICE_TYPE: CosemParser(ValueParser(int)), obis.DEVICE_TYPE: CosemParser(ValueParser(int)),
obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L2_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_L3_POSITIVE: CosemParser(ValueParser(Decimal)),
@@ -97,7 +88,6 @@ V5 = {
obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)), obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)), obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)), obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)), obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
@@ -106,23 +96,13 @@ V5 = {
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)), obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)), obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)), # POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO
obis.POWER_EVENT_FAILURE_LOG:
ProfileGenericParser(BUFFER_TYPES,
PG_HEAD_PARSERS,
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS),
obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)), obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)),
obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)), obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
obis.DEVICE_TYPE: CosemParser(ValueParser(int)), obis.DEVICE_TYPE: CosemParser(ValueParser(int)),
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)), obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
@@ -140,60 +120,3 @@ V5 = {
} }
ALL = (V2_2, V3, V4, V5) ALL = (V2_2, V3, V4, V5)
BELGIUM_FLUVIUS = deepcopy(V5)
BELGIUM_FLUVIUS['objects'].update({
obis.BELGIUM_HOURLY_GAS_METER_READING: MBusParser(
ValueParser(timestamp),
ValueParser(Decimal)
)
})
LUXEMBOURG_SMARTY = deepcopy(V5)
LUXEMBOURG_SMARTY['objects'].update({
obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
})
# Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/
# branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf
SWEDEN = {
'checksum_support': True,
'objects': {
obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
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.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
}
}
Q3D = {
"checksum_support": False,
"objects": {
obis.Q3D_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
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.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
obis.Q3D_EQUIPMENT_STATE: CosemParser(ValueParser(str)),
obis.Q3D_EQUIPMENT_SERIALNUMBER: CosemParser(ValueParser(str)),
},
}
+5 -6
View File
@@ -3,17 +3,16 @@ from setuptools import setup, find_packages
setup( setup(
name='dsmr-parser', name='dsmr-parser',
description='Library to parse Dutch Smart Meter Requirements (DSMR)', description='Library to parse Dutch Smart Meter Requirements (DSMR)',
author='Nigel Dokter and many others', author='Nigel Dokter',
author_email='nigel@nldr.net', author_email='nigeldokter@gmail.com',
license='MIT',
url='https://github.com/ndokter/dsmr_parser', url='https://github.com/ndokter/dsmr_parser',
version='0.32', version='0.9',
packages=find_packages(exclude=('test', 'test.*')), packages=find_packages(),
install_requires=[ install_requires=[
'pyserial>=3,<4', 'pyserial>=3,<4',
'pyserial-asyncio<1', 'pyserial-asyncio<1',
'pytz', 'pytz',
'Tailer==0.4.1' 'PyCRC>=1.2,<2'
], ],
entry_points={ entry_points={
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console'] 'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
+4 -45
View File
@@ -1,5 +1,5 @@
TELEGRAM_V2_2 = ( TELEGRAM_V2_2 = (
'/ISk5\\2MT382-1004\r\n' '/ISk5\2MT382-1004\r\n'
'\r\n' '\r\n'
'0-0:96.1.1(00000000000000)\r\n' '0-0:96.1.1(00000000000000)\r\n'
'1-0:1.8.1(00001.001*kWh)\r\n' '1-0:1.8.1(00001.001*kWh)\r\n'
@@ -22,7 +22,7 @@ TELEGRAM_V2_2 = (
) )
TELEGRAM_V3 = ( TELEGRAM_V3 = (
'/ISk5\\2MT382-1000\r\n' '/ISk5\2MT382-1000\r\n'
'\r\n' '\r\n'
'0-0:96.1.1(4B384547303034303436333935353037)\r\n' '0-0:96.1.1(4B384547303034303436333935353037)\r\n'
'1-0:1.8.1(12345.678*kWh)\r\n' '1-0:1.8.1(12345.678*kWh)\r\n'
@@ -87,7 +87,7 @@ TELEGRAM_V4_2 = (
) )
TELEGRAM_V5 = ( TELEGRAM_V5 = (
'/ISk5\\2MT382-1000\r\n' '/ISk5\2MT382-1000\r\n'
'\r\n' '\r\n'
'1-3:0.2.8(50)\r\n' '1-3:0.2.8(50)\r\n'
'0-0:1.0.0(170102192002W)\r\n' '0-0:1.0.0(170102192002W)\r\n'
@@ -126,46 +126,5 @@ TELEGRAM_V5 = (
'0-1:24.2.1(170102161005W)(00000.107*m3)\r\n' '0-1:24.2.1(170102161005W)(00000.107*m3)\r\n'
'0-2:24.1.0(003)\r\n' '0-2:24.1.0(003)\r\n'
'0-2:96.1.0()\r\n' '0-2:96.1.0()\r\n'
'!6EEE\r\n' '!87B3\r\n'
)
# EasyMeter via COM-1 Ethernet Gateway
# Q3D Manual (german) https://www.easymeter.com/downloads/products/zaehler/Q3D/Easymeter_Q3D_DE_2016-06-15.pdf
# - type code on page 8
# - D0-Specs on page 20
#
# last two lines are added by the COM-1 Ethernet Gateway
TELEGRAM_ESY5Q3DB1024_V304 = (
'/ESY5Q3DB1024 V3.04\r\n'
'\r\n'
'1-0:0.0.0*255(0272031312565)\r\n'
'1-0:1.8.0*255(00052185.7825309*kWh)\r\n'
'1-0:2.8.0*255(00019949.3221493*kWh)\r\n'
'1-0:21.7.0*255(000747.85*W)\r\n'
'1-0:41.7.0*255(000737.28*W)\r\n'
'1-0:61.7.0*255(000639.73*W)\r\n'
'1-0:1.7.0*255(002124.86*W)\r\n'
'1-0:96.5.5*255(80)\r\n'
'0-0:96.1.255*255(1ESY1313002565)\r\n'
'!\r\n'
' 25803103\r\n'
'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
'\xff\xff\xff\xff\xff\r\n'
)
TELEGRAM_ESY5Q3DA1004_V304 = (
'/ESY5Q3DA1004 V3.04\r\n'
'\r\n'
'1-0:0.0.0*255(1336001560)\r\n'
'1-0:1.8.0*255(00032549.5061662*kWh)\r\n'
'1-0:21.7.0*255(000557.29*W)\r\n'
'1-0:41.7.0*255(000521.62*W)\r\n'
'1-0:61.7.0*255(000609.30*W)\r\n'
'1-0:1.7.0*255(001688.21*W)\r\n'
'1-0:96.5.5*255(80)\r\n'
'0-0:96.1.255*255(1ESY1336001560)\r\n'
'!\r\n'
' 25818685\r\n'
'DE0000000000000000000000000000003\r\n'
) )
-8
View File
@@ -1,8 +0,0 @@
from dsmr_parser import telegram_specifications
from dsmr_parser.objects import Telegram
from dsmr_parser.parsers import TelegramParser
from example_telegrams import TELEGRAM_V4_2
parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
print(telegram)
-21
View File
@@ -1,21 +0,0 @@
import unittest
import tempfile
from dsmr_parser.clients.filereader import FileReader
from dsmr_parser.telegram_specifications import V5
from test.example_telegrams import TELEGRAM_V5
class FileReaderTest(unittest.TestCase):
def test_read_as_object(self):
with tempfile.NamedTemporaryFile() as file:
with open(file.name, "w") as f:
f.write(TELEGRAM_V5)
telegrams = []
reader = FileReader(file=file.name, telegram_specification=V5)
# Call
for telegram in reader.read_as_object():
telegrams.append(telegram)
self.assertEqual(len(telegrams), 1)
+29
View File
@@ -0,0 +1,29 @@
import unittest
from dsmr_parser.exceptions import TelegramSpecificationMatchError
from dsmr_parser.parsers import match_telegram_specification
from dsmr_parser import telegram_specifications
from test import example_telegrams
class MatchTelegramSpecificationTest(unittest.TestCase):
def test_v2_2(self):
assert match_telegram_specification(example_telegrams.TELEGRAM_V2_2) \
== telegram_specifications.V2_2
def test_v3(self):
assert match_telegram_specification(example_telegrams.TELEGRAM_V3) \
== telegram_specifications.V3
def test_v4_2(self):
assert match_telegram_specification(example_telegrams.TELEGRAM_V4_2) \
== telegram_specifications.V4
def test_v5(self):
assert match_telegram_specification(example_telegrams.TELEGRAM_V5) \
== telegram_specifications.V5
def test_malformed_telegram(self):
with self.assertRaises(TelegramSpecificationMatchError):
match_telegram_specification(example_telegrams.TELEGRAM_V5[:-4])
+9
View File
@@ -12,6 +12,15 @@ from test.example_telegrams import TELEGRAM_V2_2
class TelegramParserV2_2Test(unittest.TestCase): class TelegramParserV2_2Test(unittest.TestCase):
""" Test parsing of a DSMR v2.2 telegram. """ """ Test parsing of a DSMR v2.2 telegram. """
def test_telegram_specification_matching(self):
parser = TelegramParser()
parser.parse(TELEGRAM_V2_2)
self.assertEqual(
parser.telegram_specification,
telegram_specifications.V2_2
)
def test_parse(self): def test_parse(self):
parser = TelegramParser(telegram_specifications.V2_2) parser = TelegramParser(telegram_specifications.V2_2)
result = parser.parse(TELEGRAM_V2_2) result = parser.parse(TELEGRAM_V2_2)
+9
View File
@@ -12,6 +12,15 @@ from test.example_telegrams import TELEGRAM_V3
class TelegramParserV3Test(unittest.TestCase): class TelegramParserV3Test(unittest.TestCase):
""" Test parsing of a DSMR v3 telegram. """ """ Test parsing of a DSMR v3 telegram. """
def test_telegram_specification_matching(self):
parser = TelegramParser()
parser.parse(TELEGRAM_V3)
self.assertEqual(
parser.telegram_specification,
telegram_specifications.V3
)
def test_parse(self): def test_parse(self):
parser = TelegramParser(telegram_specifications.V3) parser = TelegramParser(telegram_specifications.V3)
result = parser.parse(TELEGRAM_V3) result = parser.parse(TELEGRAM_V3)
+10 -25
View File
@@ -15,6 +15,15 @@ from test.example_telegrams import TELEGRAM_V4_2
class TelegramParserV4_2Test(unittest.TestCase): class TelegramParserV4_2Test(unittest.TestCase):
""" Test parsing of a DSMR v4.2 telegram. """ """ Test parsing of a DSMR v4.2 telegram. """
def test_telegram_specification_matching(self):
parser = TelegramParser()
parser.parse(TELEGRAM_V4_2)
self.assertEqual(
parser.telegram_specification,
telegram_specifications.V4
)
def test_parse(self): def test_parse(self):
parser = TelegramParser(telegram_specifications.V4) parser = TelegramParser(telegram_specifications.V4)
result = parser.parse(TELEGRAM_V4_2) result = parser.parse(TELEGRAM_V4_2)
@@ -80,12 +89,6 @@ class TelegramParserV4_2Test(unittest.TestCase):
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal) assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0') assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0')
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject)
assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int)
assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 15
# LONG_POWER_FAILURE_COUNT (96.7.9) # LONG_POWER_FAILURE_COUNT (96.7.9)
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject) assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject)
assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None
@@ -138,26 +141,8 @@ class TelegramParserV4_2Test(unittest.TestCase):
assert result[obis.TEXT_MESSAGE].unit is None assert result[obis.TEXT_MESSAGE].unit is None
assert result[obis.TEXT_MESSAGE].value is None assert result[obis.TEXT_MESSAGE].value is None
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0')
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('6')
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('2')
# DEVICE_TYPE (0-x:24.1.0) # DEVICE_TYPE (0-x:24.1.0)
assert isinstance(result[obis.DEVICE_TYPE], CosemObject) assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
assert result[obis.DEVICE_TYPE].unit is None assert result[obis.DEVICE_TYPE].unit is None
assert isinstance(result[obis.DEVICE_TYPE].value, int) assert isinstance(result[obis.DEVICE_TYPE].value, int)
assert result[obis.DEVICE_TYPE].value == 3 assert result[obis.DEVICE_TYPE].value == 3
+12 -44
View File
@@ -16,6 +16,15 @@ from test.example_telegrams import TELEGRAM_V5
class TelegramParserV5Test(unittest.TestCase): class TelegramParserV5Test(unittest.TestCase):
""" Test parsing of a DSMR v5.x telegram. """ """ Test parsing of a DSMR v5.x telegram. """
def test_telegram_specification_matching(self):
parser = TelegramParser()
parser.parse(TELEGRAM_V5)
self.assertEqual(
parser.telegram_specification,
telegram_specifications.V5
)
def test_parse(self): def test_parse(self):
parser = TelegramParser(telegram_specifications.V5) parser = TelegramParser(telegram_specifications.V5)
result = parser.parse(TELEGRAM_V5) result = parser.parse(TELEGRAM_V5)
@@ -87,12 +96,6 @@ class TelegramParserV5Test(unittest.TestCase):
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int) assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int)
assert result[obis.LONG_POWER_FAILURE_COUNT].value == 0 assert result[obis.LONG_POWER_FAILURE_COUNT].value == 0
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject)
assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int)
assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 13
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0) # VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject) assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject)
assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None
@@ -129,49 +132,13 @@ class TelegramParserV5Test(unittest.TestCase):
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int) assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int)
assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0 assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0
# INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject)
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V'
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal)
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('230.0')
# INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject)
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V'
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal)
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('230.0')
# INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject)
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V'
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal)
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('229.0')
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.48')
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.44')
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.86')
# TEXT_MESSAGE (0-0:96.13.0) # TEXT_MESSAGE (0-0:96.13.0)
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject) assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
assert result[obis.TEXT_MESSAGE].unit is None assert result[obis.TEXT_MESSAGE].unit is None
assert result[obis.TEXT_MESSAGE].value is None assert result[obis.TEXT_MESSAGE].value is None
# DEVICE_TYPE (0-x:24.1.0) # DEVICE_TYPE (0-x:24.1.0)
assert isinstance(result[obis.DEVICE_TYPE], CosemObject) assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
assert result[obis.DEVICE_TYPE].unit is None assert result[obis.DEVICE_TYPE].unit is None
assert isinstance(result[obis.DEVICE_TYPE].value, int) assert isinstance(result[obis.DEVICE_TYPE].value, int)
assert result[obis.DEVICE_TYPE].value == 3 assert result[obis.DEVICE_TYPE].value == 3
@@ -241,6 +208,7 @@ class TelegramParserV5Test(unittest.TestCase):
def test_checksum_missing(self): def test_checksum_missing(self):
# Remove the checksum value causing a ParseError. # Remove the checksum value causing a ParseError.
corrupted_telegram = TELEGRAM_V5.replace('!6EEE\r\n', '') corrupted_telegram = TELEGRAM_V5.replace('!87B3\r\n', '')
with self.assertRaises(ParseError): with self.assertRaises(ParseError):
TelegramParser.validate_checksum(corrupted_telegram) TelegramParser.validate_checksum(corrupted_telegram)
-89
View File
@@ -1,89 +0,0 @@
import unittest
from dsmr_parser import telegram_specifications
from dsmr_parser.objects import Telegram
from dsmr_parser.objects import ProfileGenericObject
from dsmr_parser.parsers import TelegramParser
from dsmr_parser.parsers import ProfileGenericParser
from dsmr_parser.profile_generic_specifications import BUFFER_TYPES
from dsmr_parser.profile_generic_specifications import PG_HEAD_PARSERS
from dsmr_parser.profile_generic_specifications import PG_UNIDENTIFIED_BUFFERTYPE_PARSERS
from test.example_telegrams import TELEGRAM_V5
class TestParserCornerCases(unittest.TestCase):
""" Test instantiation of Telegram object """
def test_power_event_log_empty_1(self):
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
parser = TelegramParser(telegram_specifications.V5)
telegram = Telegram(TELEGRAM_V5, parser, telegram_specifications.V5)
object_type = ProfileGenericObject
testitem = telegram.POWER_EVENT_FAILURE_LOG
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 0
assert testitem.buffer_type == '0-0:96.7.19'
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 0
def test_power_event_log_empty_2(self):
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
object_type = ProfileGenericObject
# Power Event Log with 0 items and no object type
pefl_line = r'1-0:99.97.0(0)()\r\n'
testitem = pef_parser.parse(pefl_line)
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 0
assert testitem.buffer_type is None
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 0
assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]
json = testitem.to_json()
assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}'
def test_power_event_log_null_values(self):
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
object_type = ProfileGenericObject
# Power Event Log with 1 item and no object type and nno values for the item
pefl_line = r'1-0:99.97.0(1)()()()\r\n'
testitem = pef_parser.parse(pefl_line)
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 1
assert testitem.buffer_type is None
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 1
assert testitem.values == [{'value': 1, 'unit': None}, {'value': None, 'unit': None},
{'value': None, 'unit': None}, {'value': None, 'unit': None}]
json = testitem.to_json()
assert json == \
'{"buffer_length": 1, "buffer_type": null, "buffer": [{"datetime": null, "value": null, "unit": null}]}'
def test_power_event_log_brackets_only(self):
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
# Issue 57
# Test of an ill formatted empty POWER_EVENT_FAILURE_LOG, observed on some smartmeters
# The idea is that instead of failing, the parser converts it to an empty POWER_EVENT_FAILURE_LOG
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
object_type = ProfileGenericObject
pefl_line = r'1-0:99.97.0()\r\n'
testitem = pef_parser.parse(pefl_line)
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 0
assert testitem.buffer_type is None
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 0
assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]
json = testitem.to_json()
assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}'
+6 -25
View File
@@ -3,7 +3,9 @@ from unittest.mock import Mock
import unittest import unittest
from dsmr_parser import obis_references as obis from dsmr_parser import obis_references as obis
from dsmr_parser.clients.protocol import create_dsmr_protocol from dsmr_parser import telegram_specifications
from dsmr_parser.parsers import TelegramParser
from dsmr_parser.clients.protocol import DSMRProtocol
TELEGRAM_V2_2 = ( TELEGRAM_V2_2 = (
@@ -33,10 +35,9 @@ TELEGRAM_V2_2 = (
class ProtocolTest(unittest.TestCase): class ProtocolTest(unittest.TestCase):
def setUp(self): def setUp(self):
new_protocol, _ = create_dsmr_protocol('2.2', telegram_parser = TelegramParser(telegram_specifications.V2_2)
telegram_callback=Mock(), self.protocol = DSMRProtocol(None, telegram_parser,
keep_alive_interval=1) telegram_callback=Mock())
self.protocol = new_protocol()
def test_complete_packet(self): def test_complete_packet(self):
"""Protocol should assemble incoming lines into complete packet.""" """Protocol should assemble incoming lines into complete packet."""
@@ -51,23 +52,3 @@ class ProtocolTest(unittest.TestCase):
assert float(telegram[obis.GAS_METER_READING].value) == 1.001 assert float(telegram[obis.GAS_METER_READING].value) == 1.001
assert telegram[obis.GAS_METER_READING].unit == 'm3' assert telegram[obis.GAS_METER_READING].unit == 'm3'
def test_receive_packet(self):
"""Protocol packet reception."""
mock_transport = Mock()
self.protocol.connection_made(mock_transport)
assert not self.protocol._active
self.protocol.data_received(TELEGRAM_V2_2.encode('ascii'))
assert self.protocol._active
# 1st call of keep_alive resets 'active' flag
self.protocol.keep_alive()
assert not self.protocol._active
# 2nd call of keep_alive should close the transport
self.protocol.keep_alive()
assert mock_transport.close.called_once()
self.protocol.connection_lost(None)
-77
View File
@@ -1,77 +0,0 @@
from unittest.mock import Mock
import unittest
from dsmr_parser import obis_references as obis
from dsmr_parser.clients.rfxtrx_protocol import create_rfxtrx_dsmr_protocol, PACKETTYPE_DSMR, SUBTYPE_P1
TELEGRAM_V2_2 = (
'/ISk5\2MT382-1004\r\n'
'\r\n'
'0-0:96.1.1(00000000000000)\r\n'
'1-0:1.8.1(00001.001*kWh)\r\n'
'1-0:1.8.2(00001.001*kWh)\r\n'
'1-0:2.8.1(00001.001*kWh)\r\n'
'1-0:2.8.2(00001.001*kWh)\r\n'
'0-0:96.14.0(0001)\r\n'
'1-0:1.7.0(0001.01*kW)\r\n'
'1-0:2.7.0(0000.00*kW)\r\n'
'0-0:17.0.0(0999.00*kW)\r\n'
'0-0:96.3.10(1)\r\n'
'0-0:96.13.1()\r\n'
'0-0:96.13.0()\r\n'
'0-1:24.1.0(3)\r\n'
'0-1:96.1.0(000000000000)\r\n'
'0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n'
'(00001.001)\r\n'
'0-1:24.4.0(1)\r\n'
'!\r\n'
)
OTHER_RF_PACKET = b'\x03\x01\x02\x03'
def encode_telegram_as_RF_packets(telegram):
data = b''
for line in telegram.split('\n'):
packet_data = (line + '\n').encode('ascii')
packet_header = bytes(bytearray([
len(packet_data) + 3, # excluding length byte
PACKETTYPE_DSMR,
SUBTYPE_P1,
0 # seq num (ignored)
]))
data += packet_header + packet_data
# other RF packets can pass by on the line
data += OTHER_RF_PACKET
return data
class RFXtrxProtocolTest(unittest.TestCase):
def setUp(self):
new_protocol, _ = create_rfxtrx_dsmr_protocol('2.2',
telegram_callback=Mock(),
keep_alive_interval=1)
self.protocol = new_protocol()
def test_complete_packet(self):
"""Protocol should assemble incoming lines into complete packet."""
data = encode_telegram_as_RF_packets(TELEGRAM_V2_2)
# send data broken up in two parts
self.protocol.data_received(data[0:200])
self.protocol.data_received(data[200:])
telegram = self.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'
-322
View File
@@ -1,322 +0,0 @@
import unittest
import datetime
import pytz
from dsmr_parser import telegram_specifications
from dsmr_parser import obis_name_mapping
from dsmr_parser.objects import CosemObject
from dsmr_parser.objects import MBusObject
from dsmr_parser.objects import Telegram
from dsmr_parser.objects import ProfileGenericObject
from dsmr_parser.parsers import TelegramParser
from test.example_telegrams import TELEGRAM_V4_2
from decimal import Decimal
class TelegramTest(unittest.TestCase):
""" Test instantiation of Telegram object """
def __init__(self, *args, **kwargs):
self.item_names_tested = []
super(TelegramTest, self).__init__(*args, **kwargs)
def verify_telegram_item(self, telegram, testitem_name, object_type, unit_val, value_type, value_val):
testitem = eval("telegram.{}".format(testitem_name))
assert isinstance(testitem, object_type)
assert testitem.unit == unit_val
assert isinstance(testitem.value, value_type)
assert testitem.value == value_val
self.item_names_tested.append(testitem_name)
def test_instantiate(self):
parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
# P1_MESSAGE_HEADER (1-3:0.2.8)
self.verify_telegram_item(telegram,
'P1_MESSAGE_HEADER',
object_type=CosemObject,
unit_val=None,
value_type=str,
value_val='42')
# P1_MESSAGE_TIMESTAMP (0-0:1.0.0)
self.verify_telegram_item(telegram,
'P1_MESSAGE_TIMESTAMP',
CosemObject,
unit_val=None,
value_type=datetime.datetime,
value_val=datetime.datetime(2016, 11, 13, 19, 57, 57, tzinfo=pytz.UTC))
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
self.verify_telegram_item(telegram,
'ELECTRICITY_USED_TARIFF_1',
object_type=CosemObject,
unit_val='kWh',
value_type=Decimal,
value_val=Decimal('1581.123'))
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
self.verify_telegram_item(telegram,
'ELECTRICITY_USED_TARIFF_2',
object_type=CosemObject,
unit_val='kWh',
value_type=Decimal,
value_val=Decimal('1435.706'))
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
self.verify_telegram_item(telegram,
'ELECTRICITY_DELIVERED_TARIFF_1',
object_type=CosemObject,
unit_val='kWh',
value_type=Decimal,
value_val=Decimal('0'))
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
self.verify_telegram_item(telegram,
'ELECTRICITY_DELIVERED_TARIFF_2',
object_type=CosemObject,
unit_val='kWh',
value_type=Decimal,
value_val=Decimal('0'))
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
self.verify_telegram_item(telegram,
'ELECTRICITY_ACTIVE_TARIFF',
object_type=CosemObject,
unit_val=None,
value_type=str,
value_val='0002')
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
self.verify_telegram_item(telegram,
'EQUIPMENT_IDENTIFIER',
object_type=CosemObject,
unit_val=None,
value_type=str,
value_val='3960221976967177082151037881335713')
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
self.verify_telegram_item(telegram,
'CURRENT_ELECTRICITY_USAGE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('2.027'))
# CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)
self.verify_telegram_item(telegram,
'CURRENT_ELECTRICITY_DELIVERY',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0'))
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
self.verify_telegram_item(telegram,
'SHORT_POWER_FAILURE_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=15)
# LONG_POWER_FAILURE_COUNT (96.7.9)
self.verify_telegram_item(telegram,
'LONG_POWER_FAILURE_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=7)
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SAG_L1_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SAG_L2_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SAG_L3_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SWELL_L1_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SWELL_L2_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SWELL_L3_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# TEXT_MESSAGE_CODE (0-0:96.13.1)
self.verify_telegram_item(telegram,
'TEXT_MESSAGE_CODE',
object_type=CosemObject,
unit_val=None,
value_type=type(None),
value_val=None)
# TEXT_MESSAGE (0-0:96.13.0)
self.verify_telegram_item(telegram,
'TEXT_MESSAGE',
object_type=CosemObject,
unit_val=None,
value_type=type(None),
value_val=None)
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_CURRENT_L1',
object_type=CosemObject,
unit_val='A',
value_type=Decimal,
value_val=Decimal('0'))
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_CURRENT_L2',
object_type=CosemObject,
unit_val='A',
value_type=Decimal,
value_val=Decimal('6'))
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_CURRENT_L3',
object_type=CosemObject,
unit_val='A',
value_type=Decimal,
value_val=Decimal('2'))
# DEVICE_TYPE (0-x:24.1.0)
self.verify_telegram_item(telegram,
'DEVICE_TYPE',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=3)
# INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0.170'))
# INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('1.247'))
# INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0.209'))
# INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0'))
# INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0'))
# INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0'))
# EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)
self.verify_telegram_item(telegram,
'EQUIPMENT_IDENTIFIER_GAS',
object_type=CosemObject,
unit_val=None,
value_type=str,
value_val='4819243993373755377509728609491464')
# HOURLY_GAS_METER_READING (0-1:24.2.1)
self.verify_telegram_item(telegram,
'HOURLY_GAS_METER_READING',
object_type=MBusObject,
unit_val='m3',
value_type=Decimal,
value_val=Decimal('981.443'))
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
testitem_name = 'POWER_EVENT_FAILURE_LOG'
object_type = ProfileGenericObject
testitem = eval("telegram.{}".format(testitem_name))
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 3
assert testitem.buffer_type == '0-0:96.7.19'
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 3
assert all([isinstance(item, MBusObject) for item in buffer])
date0 = datetime.datetime(2000, 1, 4, 17, 3, 20, tzinfo=datetime.timezone.utc)
date1 = datetime.datetime(1999, 12, 31, 23, 0, 1, tzinfo=datetime.timezone.utc)
date2 = datetime.datetime(2000, 1, 1, 23, 0, 3, tzinfo=datetime.timezone.utc)
assert buffer[0].datetime == date0
assert buffer[1].datetime == date1
assert buffer[2].datetime == date2
assert buffer[0].value == 237126
assert buffer[1].value == 2147583646
assert buffer[2].value == 2317482647
assert all([isinstance(item.value, int) for item in buffer])
assert all([isinstance(item.unit, str) for item in buffer])
assert all([(item.unit == 's') for item in buffer])
self.item_names_tested.append(testitem_name)
# check if all items in telegram V4 specification are covered
V4_name_list = [obis_name_mapping.EN[signature] for signature, parser in
telegram_specifications.V4['objects'].items()]
V4_name_set = set(V4_name_list)
item_names_tested_set = set(self.item_names_tested)
assert item_names_tested_set == V4_name_set
+7 -5
View File
@@ -1,10 +1,15 @@
[tox]
envlist = py34,py35,p36
[testenv] [testenv]
deps= deps=
pytest pytest
pytest-cov pytest-cov
pylama pylama
pytest-asyncio pytest-asyncio
pytest-catchlog
pytest-mock pytest-mock
PyCRC
commands= commands=
py.test --cov=dsmr_parser test {posargs} py.test --cov=dsmr_parser test {posargs}
pylama dsmr_parser test pylama dsmr_parser test
@@ -12,11 +17,8 @@ commands=
[pylama:dsmr_parser/clients/__init__.py] [pylama:dsmr_parser/clients/__init__.py]
ignore = W0611 ignore = W0611
[pylama:dsmr_parser/parsers.py]
ignore = W605
[pylama:pylint] [pylama:pylint]
max_line_length = 120 max_line_length = 100
[pylama:pycodestyle] [pylama:pycodestyle]
max_line_length = 120 max_line_length = 100