seems to work

This commit is contained in:
hg 2015-06-12 15:14:38 +02:00
parent d64e8bff98
commit 88e36b0170
5 changed files with 133 additions and 59 deletions

View File

@ -12,6 +12,12 @@ import MeterbusTypeConversion
from MeterbusLibExceptions import *
import struct
class Device(object):
def __init__(self, address, comment, dataItems):
self.address = address
self.comment = comment
self.dataItems = dataItems
class Frame(object):
def __init__(self, startCharacter, frameLength, firstPayload, telegram):
self.telegram = telegram
@ -88,6 +94,14 @@ class ControlFrame(Frame):
class LongFrame(ControlFrame):
def __init__(self, telegram, devices=[]):
super(LongFrame, self).__init__(telegram)
self.devices = devices
if self.devices != None:
for device in self.devices:
if not isinstance(device, Device):
raise InvalidDevicesStructureException()
class FixedDataHeader(object):
def __init__(self, data):
self.data = data
@ -102,6 +116,7 @@ class LongFrame(ControlFrame):
self.signature = [s for s in self.data[10:]]
if self.signature[0] != 0 or self.signature[1] != 0:
raise InvalidFrameException("signature should be zero")
#print("FixedDataHeader: " + str(self.getJSON()))
def getJSON(self):
j = {'identNr': self.identNr, 'manufacturer': self.manufacturer, 'version': self.version, 'medium': self.medium,
@ -109,24 +124,34 @@ class LongFrame(ControlFrame):
return j
class DataInformationElement(object):
def __init__(self, data):
def __init__(self, data, comment):
self.data = data
self.comment = comment
self.consumed = 0
@classmethod
def create(cls, data):
def create(cls, data, comment):
if data[0] != 0x0f:
return LongFrame.DataInformationBlock(data)
return LongFrame.DataInformationBlock(data, comment)
else:
return LongFrame.ManufacturerSpecificDataBlock(data)
return LongFrame.ManufacturerSpecificDataBlock(data, comment)
def getJSON(self):
j = {'comment': self.comment}
return j
class ManufacturerSpecificDataBlock(DataInformationElement):
def parse(self):
self.dif = self.data[0]
self.mdh = self.data[1:]
self.mdh = [ m for m in self.data[1:]]
self.consumed = len(self.data)
return self.consumed
def getJSON(self):
superJSON = super(LongFrame.ManufacturerSpecificDataBlock, self).getJSON()
j = {'dif': self.dif, 'mdh': self.mdh}
j.update(superJSON)
return j
class DataInformationBlock(DataInformationElement):
@ -136,7 +161,7 @@ class LongFrame(ControlFrame):
('16 Bit Integer', 2, lambda x: (x[1] << 8) + x[0]),
('24 Bit Integer', 3, lambda x: (x[2] << 16) + (x[1] << 8) + x[0]),
('32 Bit Integer', 4, lambda x: (x[3] << 24) + (x[2] << 16) + (x[1] << 8) + x[0]),
('32 Bit Real', 4, lambda x: struct.unpack('f', x.getAll())[0]),
('32 Bit Real', 4, lambda x: struct.unpack('f', str(bytearray(x)))[0]),
('48 Bit Integer', 6, lambda x: (x[5] << 40) + (x[4] << 32) + (x[3] << 24) + (x[2] << 16) + (x[1] << 8) + x[0]),
('64 Bit Integer', 8, lambda x: (x[7] << 56) + (x[6] << 48) + (x[5] << 40) + (x[4] << 32) + (x[3] << 24) + (x[2] << 16) + (x[1] << 8) + x[0]),
('Selection for Readout', 0, None),
@ -153,24 +178,24 @@ class LongFrame(ControlFrame):
(0b01111000, 0b00001000, 'Energy', 'J', 0b00000111, lambda d: 10**d),
(0b01111000, 0b00010000, 'Volume', 'm**3', 0b00000111, lambda d: 10**(d-6)),
(0b01111000, 0b00011000, 'Mass', 'kg', 0b00000111, lambda d: 10**(d-3)),
(0b01111100, 0b00100000, 'On Time', 'TimeCalc', 0b00000011, lambda d: d),
(0b01111100, 0b00100100, 'Operating Time', 'TimeCalc', 0b00000011, lambda d: d),
(0b01111100, 0b00100000, 'On Time', 'TimeCalc', 0b00000011, None),
(0b01111100, 0b00100100, 'Operating Time', 'TimeCalc', 0b00000011, None),
(0b01111000, 0b00101000, 'Power', 'W', 0b00000111, lambda d: 10**(d-3)),
(0b01111000, 0b00110000, 'Power', 'J/h', 0b00000111, lambda d: 10**d),
(0b01111000, 0b00111000, 'Volume Flow', 'm**3/h', 0b00000111, lambda d: 10**(d-6)),
(0b01111000, 0b01000000, 'Volume Flow ext.', 'm**3/min', 0b00000111, lambda d: 10**(d-7)),
(0b01111000, 0b01001000, 'Volume Flow ext.', 'm**3/s', 0b00000111, lambda d: 10**(d-9)),
(0b01111000, 0b01010000, 'Mass Flow', 'kg/h', 0b00000111, lambda d: 10**(d-3)),
(0b01111100, 0b01011000, 'Flow Temperature', '°C', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01011100, 'Return Temperature', '°C', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01100000, 'Temperature Diff.', 'K', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01100100, 'Extern. Temp', '°C', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01011000, 'Flow Temperature', 'Celsius', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01011100, 'Return Temperature', 'Celsius', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01100000, 'Temperature Diff.', 'Kelvin', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01100100, 'Extern. Temp', 'Celsius', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01101000, 'Pressure', 'bar', 0b00000011, lambda d: 10**(d-3)),
(0b01111110, 0b01101100, 'Time Point', 'TimePointCalc', 0b00000001,lambda d: d),
(0b01111110, 0b01101100, 'Time Point', 'TimePointCalc', 0b00000001,None),
(0b01111111, 0b01101110, 'Units for H.C.A.', '', 0, None),
(0b01111111, 0b01101111, 'Reserved', '', 0, None),
(0b01111100, 0b01110000, 'Averaging Duration', 'TimeCalc', 0b00000011, lambda d: d),
(0b01111100, 0b01110100, 'Actuality Duration', 'TimeCalc', 0b00000011, lambda d: d),
(0b01111100, 0b01110000, 'Averaging Duration', 'TimeCalc', 0b00000011, None),
(0b01111100, 0b01110100, 'Actuality Duration', 'TimeCalc', 0b00000011, None),
(0b01111111, 0b01111000, 'Fabrication Number', '', 0, None),
(0b01111111, 0b01111001, 'Enhanced Ident.', '', 0, None),
(0b01111111, 0b01111010, 'Bus Address', '', 0, None),
@ -258,13 +283,13 @@ class LongFrame(ControlFrame):
(0b01111110, 0b00101010, 'Reserved', '', 0, None),
(0b01111100, 0b00101100, 'Reserved', '', 0, None),
(0b01111110, 0b00110000, 'Power', 'GJ/h', 0b00000001, lambda d: 10**(d-1)),
(0b01111100, 0b01011000, 'Flow Temperature', '°F', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01011100, 'Return Temperature', '°F', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01100000, 'Temperature Diff.', '°F', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01100100, 'Extern. Temp.', '°F', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01011000, 'Flow Temperature', 'Fahrenheit', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01011100, 'Return Temperature', 'Fahrenheit', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01100000, 'Temperature Diff.', 'Fahrenheit', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01100100, 'Extern. Temp.', 'Fahrenheit', 0b00000011, lambda d: 10**(d-3)),
(0b01111000, 0b01101000, 'Reserved', '', 0, None),
(0b01111100, 0b01110000, 'Cold/Warm Temp.Limit', '°F', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01110100, 'Cold/Warm Temp.Limit', '°C', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01110000, 'Cold/Warm Temp.Limit', 'Fahrenheit', 0b00000011, lambda d: 10**(d-3)),
(0b01111100, 0b01110100, 'Cold/Warm Temp.Limit', 'Celsius', 0b00000011, lambda d: 10**(d-3)),
(0b01111000, 0b01111000, 'cumul. count max power', 'W', 0b00000111, lambda d: 10**(d-3))
)
@ -276,52 +301,50 @@ class LongFrame(ControlFrame):
def extVifCalc(cls, table, vife):
retDescr = 'unknown'
retUnit = 'unknown'
retRange = 'unknown'
retFactor = 1
for vifeCode in table:
# print vifeCode
(mask, value, descr, unit, rangeMask, rangeFunc) = vifeCode
(mask, value, descr, unit, factorMask, factorFunc) = vifeCode
if (vife & mask) == value:
if rangeFunc == None:
retRange = 1
if factorFunc == None:
retFactor = 1
else:
rangeHelper = vife & rangeMask
retRange = rangeFunc(rangeHelper)
factorHelper = vife & factorMask
retFactor = factorFunc(factorHelper)
retDescr = descr
retUnit = unit
break
return (retDescr, retUnit, retRange)
return (retDescr, retUnit, retFactor)
@classmethod
def vifCalc(cls, vif, vife):
retDescr = 'unknown'
retUnit = 'unknown'
retRange = 1
retFactor = 1
for vifCode in cls.VIF_CODES:
(mask, value, descr, unit, rangeMask, rangeFunc) = vifCode
(mask, value, descr, unit, factorMask, factorFunc) = vifCode
if (vif & mask) == value:
if unit == 'TimeCalc':
retDescr = descr
retUnit = cls.TIME_UNITS[vif & rangeMask]
retRange = None
elif unit == 'Table8.4.4a':
(retDescr, retUnit, retRange) = cls.extVifCalc(cls.TABLE844a, vife[0])
elif unit == 'Table8.4.4b':
(retDescr, retUnit, retRange) = cls.extVifCalc(cls.TABLE844b, vife[0])
retUnit = cls.TIME_UNITS[vif & factorMask]
elif unit == 'TimePointCalc':
retDescr = descr
retUnit = cls.TIME_POINT_UNITS[vif & rangeMask]
retRange = None
retUnit = cls.TIME_POINT_UNITS[vif & factorMask]
elif unit == 'Table8.4.4a':
(retDescr, retUnit, retFactor) = cls.extVifCalc(cls.TABLE844a, vife[0])
elif unit == 'Table8.4.4b':
(retDescr, retUnit, retFactor) = cls.extVifCalc(cls.TABLE844b, vife[0])
else:
retDescr = descr
retUnit = unit
if rangeFunc != None:
rangeHelper = vif & rangeMask
retRange = rangeFunc(rangeHelper)
if factorFunc != None:
factorHelper = vif & factorMask
retFactor = factorFunc(factorHelper)
break
# print("vifCalc: %s %s %s %s" % (retDescr, retRange, retUnit, rangeHelper))
return (retDescr, retUnit, retRange)
return (retDescr, retUnit, retFactor)
def parse(self):
@ -355,19 +378,28 @@ class LongFrame(ControlFrame):
self.userData = [u for u in self.data[self.consumed:self.consumed+userDataLength]]
self.consumed += userDataLength
self.value = LongFrame.DataInformationBlock.DATA_FIELD_CODES[self.dif & 0x0f][2](self.userData)
dataConvFunc = LongFrame.DataInformationBlock.DATA_FIELD_CODES[self.dif & 0x0f][2]
if dataConvFunc == None:
self.rawValue = 0
else:
self.rawValue = dataConvFunc(self.userData)
self.dataType = LongFrame.DataInformationBlock.DATA_FIELD_CODES[self.dif & 0x0f][0]
(self.quantity, self.unit, self.factor) = LongFrame.DataInformationBlock.vifCalc(self.vif, self.vife)
self.value = self.rawValue * self.factor
return self.consumed
def getJSON(self):
superJSON = super(LongFrame.DataInformationBlock, self).getJSON()
j = {'dif': self.dif, 'dife': self.dife, 'vif': self.vif, 'vife': self.vife,
'vifData': self.vifData, 'userData':self.userData,
'dataType': self.dataType, 'value':self.value,
'dataType': self.dataType, 'rawValue':self.rawValue,
'value': self.value,
'quantity': self.quantity, 'factor': self.factor, 'unit': self.unit,
'comment': self.comment
}
j.update(superJSON)
return j
@ -378,11 +410,27 @@ class LongFrame(ControlFrame):
raise PayloadTooShortException("too short for fixed data header")
self.fixedDataHeader = LongFrame.FixedDataHeader(self.telegram[7:19])
self.fixedDataHeader.parse()
device = None
for d in self.devices:
if d.address == self.address:
print("device found")
device = d
break
if device != None:
self.comment = device.comment
else:
self.comment = '-'
self.dib = []
consumed = 0
dibIndex = 0
#print("Telegram length: %d" % len(self.telegram))
while True:
curDib = LongFrame.DataInformationElement.create(self.telegram[(19 + consumed):-2])
if device != None:
comment = device.dataItems[dibIndex]
else:
comment = '-'
dibIndex += 1
curDib = LongFrame.DataInformationElement.create(self.telegram[(19 + consumed):-2], comment)
consumed += curDib.parse()
self.dib.append(curDib)
#print("PayloadLength: %d, Consumed: %d" % (self.payloadLength, consumed))
@ -391,15 +439,16 @@ class LongFrame(ControlFrame):
if (consumed + 3 + 12 + 2 - 1) >= self.payloadLength:
break
def getJSON(self):
superJSON = super(LongFrame, self).getJSON()
j = {'header': self.fixedDataHeader.getJSON(), 'dib': [dib.getJSON() for dib in self.dib]}
j = {'comment': self.comment, 'header': self.fixedDataHeader.getJSON(), 'dib': [dib.getJSON() for dib in self.dib]}
j.update(superJSON)
return j
class Telegram(object):
def __init__(self):
pass
def __init__(self, devices=[]):
self.devices = devices
def fromHexString(self, hexString):
self.hexString = hexString
@ -413,7 +462,7 @@ class Telegram(object):
if self.telegram[0] == 0x68 and self.telegram[1] == 0x03:
self.frame = ControlFrame(self.telegram)
elif self.telegram[0] == 0x68:
self.frame = LongFrame(self.telegram)
self.frame = LongFrame(self.telegram, self.devices)
elif self.telegram[0] == 0x10:
self.frame = ShortFrame(self.telegram)
elif self.telegram[0] == 0xe5:

View File

@ -33,3 +33,9 @@ class PayloadTooShortException(MeterbusLibException):
class MediumConversionException(MeterbusLibException):
pass
class InvalidDevicesStructureException(MeterbusLibException):
pass
class DeviceItemsMismatchException(MeterbusLibException):
pass

View File

@ -13,6 +13,7 @@ def bcd(data):
for c in reversed(data):
v += str((c & 0xf0) >> 4)
v += str(c & 0x0f)
v = int(v)
return v
def manufCode(data):

View File

@ -53,7 +53,7 @@ class TestFrameParsing(unittest.TestCase):
self.assertEqual(telegram.frame.cField, 0x08);
self.assertEqual(telegram.frame.address, 0x53);
self.assertEqual(telegram.frame.ciField, 0x72);
self.assertEqual(telegram.frame.fixedDataHeader.identNr, "17001300")
self.assertEqual(telegram.frame.fixedDataHeader.identNr, 130017)
self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "FIN")
self.assertEqual(telegram.frame.fixedDataHeader.version, 36)
self.assertEqual(telegram.frame.fixedDataHeader.medium, "Electrity")
@ -79,7 +79,7 @@ class TestFrameParsing(unittest.TestCase):
self.assertEqual(telegram.frame.cField, 0x08);
self.assertEqual(telegram.frame.address, 0x50);
self.assertEqual(telegram.frame.ciField, 0x72);
self.assertEqual(telegram.frame.fixedDataHeader.identNr, "81140111")
self.assertEqual(telegram.frame.fixedDataHeader.identNr, 11011481)
self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "FIN")
self.assertEqual(telegram.frame.fixedDataHeader.version, 22)
self.assertEqual(telegram.frame.fixedDataHeader.medium, "Electrity")
@ -92,7 +92,7 @@ class TestFrameParsing(unittest.TestCase):
self.assertEqual(telegram.frame.cField, 0x08);
self.assertEqual(telegram.frame.address, 0x30);
self.assertEqual(telegram.frame.ciField, 0x72);
self.assertEqual(telegram.frame.fixedDataHeader.identNr, "45714300")
self.assertEqual(telegram.frame.fixedDataHeader.identNr, 437145)
self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "HYD")
self.assertEqual(telegram.frame.fixedDataHeader.version, 37)
self.assertEqual(telegram.frame.fixedDataHeader.medium, "Water")
@ -106,7 +106,7 @@ class TestFrameParsing(unittest.TestCase):
self.assertEqual(telegram.frame.cField, 0x08);
self.assertEqual(telegram.frame.address, 0x40);
self.assertEqual(telegram.frame.ciField, 0x72);
self.assertEqual(telegram.frame.fixedDataHeader.identNr, "43605200")
self.assertEqual(telegram.frame.fixedDataHeader.identNr, 526043)
self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "ACW")
self.assertEqual(telegram.frame.fixedDataHeader.version, 20)
self.assertEqual(telegram.frame.fixedDataHeader.medium, "Gas")
@ -119,7 +119,7 @@ class TestFrameParsing(unittest.TestCase):
self.assertEqual(telegram.frame.cField, 0x08);
self.assertEqual(telegram.frame.address, 0x21);
self.assertEqual(telegram.frame.ciField, 0x72);
self.assertEqual(telegram.frame.fixedDataHeader.identNr, "00000000")
self.assertEqual(telegram.frame.fixedDataHeader.identNr, 0)
self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "@@@")
self.assertEqual(telegram.frame.fixedDataHeader.version, 1)
self.assertEqual(telegram.frame.fixedDataHeader.medium, "Other")
@ -178,9 +178,9 @@ class TestFrameParsing(unittest.TestCase):
telegram.parse()
def test_bcd(self):
self.assertEqual(MeterbusTypeConversion.bcd([0x99]), '99')
self.assertEqual(MeterbusTypeConversion.bcd([0x12, 0x34]), '1234')
self.assertEqual(MeterbusTypeConversion.bcd([0x12, 0x34, 0x56, 0x78]), '12345678')
self.assertEqual(MeterbusTypeConversion.bcd([0x99]), 99)
self.assertEqual(MeterbusTypeConversion.bcd([0x12, 0x34]), 3412)
self.assertEqual(MeterbusTypeConversion.bcd([0x12, 0x34, 0x56, 0x78]), 78563412)
def test_manufCode(self):
self.assertEqual(MeterbusTypeConversion.manufCode([0x77, 0x04]), 'ACW')

View File

@ -8,11 +8,29 @@ Created on 11.06.2015
import MeterbusLib
import json
devices = []
device_1phase_electric = MeterbusLib.Device(0x53, "1 Phase Electric", ["Energy total", "Energy partial", "Voltage", "Current", "Power", "img. Power"])
devices.append(device_1phase_electric)
inputOkLongFrame_1phase_electric = "68 38 38 68 " + "08 53 72 " + "17 00 13 00 2E 19 24 02 D6 00 00 00 " + "8C 10 04 01 02 00 00 " + "8C 11 04 01 02 00 00 " + "02 FD C9 FF 01 E4 00 " + "02 FD DB FF 01 03 00 " + "02 AC FF 01 01 00 " + "82 40 AC FF 01 FA FF " + "20 16"
device_3phase_electric = MeterbusLib.Device(0x50, "3 Phase Electric", ["Energy T1 total", "Energy T1 partial", "Energy T2 total", "Energy T2 partial",
"Voltage phase 1", "Current phase 1", "Power phase 1", "img. Power phase 1",
"Voltage phase 2", "Current phase 2", "Power phase 2", "img. Power phase 2",
"Voltage phase 3", "Current phase 3", "Power phase 3", "img. Power phase 3",
"converter ratio", "Power total", "img. Power total", "tariff"
])
devices.append(device_3phase_electric)
inputOkLongFrame_3phase_electric = "68 92 92 68 08 50 72 81 14 01 11 2E 19 16 02 88 00 00 00 8C 10 04 58 43 86 00 8C 11 04 58 43 86 00 8C 20 04 00 00 00 00 8C 21 04 00 00 00 00 02 FD C9 FF 01 E4 00 02 FD DB FF 01 5A 00 02 AC FF 01 D2 00 82 40 AC FF 01 00 00 02 FD C9 FF 02 DF 00 02 FD DB FF 02 0F 00 02 AC FF 02 21 00 82 40 AC FF 02 FD FF 02 FD C9 FF 03 E3 00 02 FD DB FF 03 04 00 02 AC FF 03 02 00 82 40 AC FF 03 F4 FF 02 FF 68 00 00 02 AC FF 00 F5 00 82 40 AC FF 00 F1 FF 01 FF 13 00 F4 16"
device_thermometer = MeterbusLib.Device(0x21, "Thermometer", ["Uptime Seconds", "Uptime Minutes", "Uptime Hours", "Uptime Days",
"Temperature 1", "Temperature 2", "Temperature 3", "Temperature 4",
"rawdata"
])
devices.append(device_thermometer)
inputOkLongFrame_thermometer = "68 61 61 68 08 21 72 00 00 00 00 00 00 01 00 7F 00 00 00 01 24 02 01 25 30 01 26 10 02 27 07 00 05 67 43 B6 DA 3D 05 67 DC 90 50 BD 05 67 AA E8 AA 41 05 67 AF 57 BA 41 0F 77 C0 00 00 7F 27 00 00 0A 00 00 00 00 00 00 00 01 00 00 00 A2 C3 7F 3F A5 BA 7F 3F 85 A7 7F 3F E7 F9 7F 3F CD CC CC 3D E8 03 00 00 02 16"
inputOkLongFrame_water = "68 46 46 68 08 30 72 45 71 43 00 24 23 25 07 F6 00 00 00 0C 13 97 95 43 00 8C 10 13 00 00 00 00 0B 3B 00 00 00 0B 26 14 60 01 02 5A AC 00 04 6D 14 0E EB 16 4C 13 96 41 33 00 CC 10 13 00 00 00 00 42 6C DF 1C 42 EC 7E FF 1C 99 16"
inputOkLongFrame_gas = "68 56 56 68 08 40 72 43 60 52 00 77 04 14 03 FF 10 00 00 0C 78 76 03 01 10 0D 7C 08 44 49 20 2E 74 73 75 63 0A 30 30 30 30 30 30 30 30 30 30 04 6D 06 0E EB 16 02 7C 09 65 6D 69 74 20 2E 74 61 62 A3 09 04 13 98 AE 04 00 04 93 7F 4E 01 00 00 44 13 FC A5 04 00 0F 01 00 1F 51 16"
telegram = MeterbusLib.Telegram()
telegram.fromHexString(inputOkLongFrame_3phase_electric)
telegram = MeterbusLib.Telegram(devices)
telegram.fromHexString(inputOkLongFrame_thermometer)
telegram.parse()
print(json.dumps(telegram.getJSON(), indent=2))