Merge pull request #40 from lowdef/add_a_true_telegram_object

Add a true telegram object
This commit is contained in:
Nigel Dokter 2019-12-21 17:36:28 +01:00 committed by GitHub
commit 659560222a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 412 additions and 5 deletions

4
.gitignore vendored
View File

@ -6,3 +6,7 @@
/.project
/.pydevproject
/.coverage
build/
dist/
*.*~
*~

View File

@ -85,8 +85,8 @@ into a dictionary.
telegram = parser.parse(telegram_str)
print(telegram) # see 'Telegram object' docs below
Telegram object
---------------
Telegram dictionary
-------------------
A dictionary of which the key indicates the field type. These regex values
correspond to one of dsmr_parser.obis_reference constants.
@ -138,6 +138,115 @@ Example to get some of the values:
# See dsmr_reader.obis_references for all readable telegram values.
# 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
------------

View File

@ -0,0 +1,122 @@
import logging
import fileinput
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()
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 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)

View File

@ -6,6 +6,7 @@ import serial_asyncio
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__)
@ -20,6 +21,7 @@ class SerialReader(object):
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read(self):
"""
@ -41,6 +43,25 @@ class SerialReader(object):
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
"""
with serial.Serial(**self.serial_settings) as serial_handle:
while True:
data = serial_handle.readline()
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)
class AsyncSerialReader(SerialReader):
"""Serial reader using asyncio pyserial."""

View File

@ -0,0 +1,54 @@
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_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'
}
REVERSE_EN = dict([ (v,k) for k,v in EN.items()])

View File

@ -1,3 +1,56 @@
import dsmr_parser.obis_name_mapping
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 {} \t[{}]\n".format(attr,str(value.value),str(value.unit))
return output
class DSMRObject(object):
"""
Represents all data from a single telegram line.

View File

@ -3,7 +3,7 @@ import re
from PyCRC.CRC16 import CRC16
from dsmr_parser.objects import MBusObject, CosemObject
from dsmr_parser.objects import MBusObject, CosemObject, Telegram
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
logger = logging.getLogger(__name__)

View File

@ -6,7 +6,7 @@ setup(
author='Nigel Dokter',
author_email='nigel@nldr.net',
url='https://github.com/ndokter/dsmr_parser',
version='0.16',
version='0.17',
packages=find_packages(),
install_requires=[
'pyserial>=3,<4',

View File

@ -0,0 +1,14 @@
from decimal import Decimal
import datetime
import unittest
import pytz
from dsmr_parser import obis_references as obis
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 example_telegrams import TELEGRAM_V4_2
parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
print(telegram)

30
test/test_telegram.py Normal file
View File

@ -0,0 +1,30 @@
from decimal import Decimal
import datetime
import unittest
import pytz
from dsmr_parser import obis_references as obis
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
class TelegramTest(unittest.TestCase):
""" Test instantiation of Telegram object """
def test_instantiate(self):
parser = TelegramParser(telegram_specifications.V4)
#result = parser.parse(TELEGRAM_V4_2)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
# P1_MESSAGE_HEADER (1-3:0.2.8)
#assert isinstance(result[obis.P1_MESSAGE_HEADER], CosemObject)
#assert result[obis.P1_MESSAGE_HEADER].unit is None
#assert isinstance(result[obis.P1_MESSAGE_HEADER].value, str)
#assert result[obis.P1_MESSAGE_HEADER].value == '50'

View File

@ -15,7 +15,7 @@ commands=
pylama dsmr_parser test
[pylama:dsmr_parser/clients/__init__.py]
ignore = W0611
ignore = W0611,W0605
[pylama:pylint]
max_line_length = 100