MeterbusLib/MeterbusLib.py
2015-06-15 22:28:06 +02:00

484 lines
26 KiB
Python

# -*- coding: iso-8859-15 -*-
'''
Created on 11.06.2015
@author: wn
'''
import MeterbusTypeConversion
from MeterbusLibExceptions import *
import struct
class Device(object):
def __init__(self, address, category, comment, dataItems):
self.address = address
self.category = category
self.comment = comment
self.dataItems = dataItems
class Frame(object):
def __init__(self, startCharacter, frameLength, firstPayload, telegram):
self.telegram = telegram
self.frameName = self.__class__.__name__
self.frameLength = frameLength
self.payloadLength = self.frameLength - 6
self.frameStartCharacter = startCharacter
self.firstPayload = firstPayload
def parse(self):
if self.telegram[0] != self.frameStartCharacter:
raise InvalidStartCharException()
if len(self.telegram) != self.frameLength:
raise InvalidLengthException()
self.verifyChecksumAndEnd()
self.parse2()
def verifyChecksumAndEnd(self):
if (self.firstPayload != 0):
checksum = sum(self.telegram[self.firstPayload:-2]) & 0xff
if checksum != self.telegram[-2]:
raise InvalidChecksumException()
if self.telegram[-1] != 0x16:
raise InvalidStopCharException()
def parse2(self):
raise NotImplementedError
def getJSON(self):
raise NotImplementedError
class SingleCharacter(Frame):
def __init__(self, telegram):
super(SingleCharacter, self).__init__(0xe5, 1, 0, telegram)
def parse2(self):
pass
def getJSON(self):
j = {}
return j
class ShortFrame(Frame):
def __init__(self, telegram):
super(ShortFrame, self).__init__(0x10, 5, 1, telegram)
def parse2(self):
self.cField = self.telegram[1]
self.address = self.telegram[2]
def getJSON(self):
j = {'c': self.cField, 'a': self.address}
return j
class ControlFrame(Frame):
def __init__(self, telegram):
super(ControlFrame, self).__init__(0x68, (telegram[1] + 6), 4, telegram)
def parse2(self):
if self.telegram[2] != self.payloadLength:
raise InvalidSecondLengthException
if self.payloadLength < 3:
raise PayloadTooShortException("too short for c, a, ci fields")
self.cField = self.telegram[4]
self.address = self.telegram[5]
self.ciField = self.telegram[6]
def getJSON(self):
j = {'c': self.cField, 'a': self.address, 'ci': self.ciField}
return j
class LongFrame(ControlFrame):
def __init__(self, telegram, devices=[]):
super(LongFrame, self).__init__(telegram)
self.devices = devices
if self.devices is not None:
for device in self.devices:
if not isinstance(device, Device):
raise InvalidDevicesStructureException()
class FixedDataHeader(object):
def __init__(self, data):
self.data = data
def parse(self):
self.identNr = MeterbusTypeConversion.bcd(self.data[:4])
self.manufacturer = MeterbusTypeConversion.manufCode(self.data[4:6])
self.version = self.data[6]
self.medium = MeterbusTypeConversion.mediumCode(self.data[7])
self.accessNo = self.data[8]
self.status = self.data[9]
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,
'accessNo': self.accessNo, 'status': self.status, 'signature': self.signature}
return j
class DataInformationElement(object):
def __init__(self, data, comment):
self.data = data
self.comment = comment
self.consumed = 0
@classmethod
def create(cls, data, comment):
if data[0] != 0x0f:
return LongFrame.DataInformationBlock(data, comment)
else:
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 = [ 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):
DATA_FIELD_CODES = (
('No Data', 0, None),
('8 Bit Integer', 1, lambda x: x[0]),
('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', 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),
('2 Digit BCD', 1, lambda x: MeterbusTypeConversion.bcd(x)),
('4 Digit BCD', 2, lambda x: MeterbusTypeConversion.bcd(x)),
('6 Digit BCD', 3, lambda x: MeterbusTypeConversion.bcd(x)),
('8 Digit BCD', 4, lambda x: MeterbusTypeConversion.bcd(x)),
('variable length', -1, None),
('12 Digit BCD', 6, lambda x: MeterbusTypeConversion.bcd(x)),
('Special Function', 0, None),
)
VIF_CODES = (
(0b01111000, 0b00000000, 'Energy', 'Wh', 0b00000111, lambda d: 10**(d-3)),
(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, 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', '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,None),
(0b01111111, 0b01101110, 'Units for H.C.A.', '', 0, None),
(0b01111111, 0b01101111, 'Reserved', '', 0, None),
(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),
(0b11111111, 0b11111011, 'Extensions 1', 'Table8.4.4b', 0, None),
(0b11111111, 0b11111101, 'Extensions 2', 'Table8.4.4a', 0, None),
)
TABLE844a = (
(0b01111100, 0b00000000, 'Credit', 'Currency Unit', 0b00000011, lambda d: 10*(d-3)),
(0b01111100, 0b00000100, 'Debit', 'Currency Unit', 0b00000011, lambda d: 10*(d-3)),
(0b01111111, 0b00001000, 'Access Number', '', 0, None),
(0b01111111, 0b00001001, 'Medium', '', 0, None),
(0b01111111, 0b00001010, 'Manufacturer', '', 0, None),
(0b01111111, 0b00001011, 'Parameter set ident','', 0, None),
(0b01111111, 0b00001100, 'Model / Version', '', 0, None),
(0b01111111, 0b00001101, 'Hardware Version', '', 0, None),
(0b01111111, 0b00001110, 'Firmware Version', '', 0, None),
(0b01111111, 0b00001111, 'Software Version', '', 0, None),
(0b01111111, 0b00010000, 'Customer Location', '', 0, None),
(0b01111111, 0b00010001, 'Customer', '', 0, None),
(0b01111111, 0b00010010, 'Access Code User', '', 0, None),
(0b01111111, 0b00010011, 'Access Code Operator','', 0, None),
(0b01111111, 0b00010100, 'Access Code System Operator','', 0, None),
(0b01111111, 0b00010101, 'Access Code Developer','', 0, None),
(0b01111111, 0b00010110, 'Password', '', 0, None),
(0b01111111, 0b00010111, 'Error Flags', '', 0, None),
(0b01111111, 0b00011000, 'Error Mask', '', 0, None),
(0b01111111, 0b00011001, 'Reserved', '', 0, None),
(0b01111111, 0b00011010, 'Digital Output', '', 0, None),
(0b01111111, 0b00011011, 'Digital Input', '', 0, None),
(0b01111111, 0b00011100, 'Baudrate', 'Baud', 0, None),
(0b01111111, 0b00011101, 'response delay time', '', 0, None),
(0b01111111, 0b00011110, 'retry', '', 0, None),
(0b01111111, 0b00011111, 'Reserved', '', 0, None),
(0b01111111, 0b00100000, 'First storage', '', 0, None),
(0b01111111, 0b00100001, 'Last storage', '', 0, None),
(0b01111111, 0b00100010, 'Size of storage blk', '', 0, None),
(0b01111111, 0b00100011, 'Reserved', '', 0, None),
(0b01111100, 0b00100100, 'Storage interval', '', 0b00000011, lambda d: d),
(0b01111111, 0b00101000, 'Storage interval month', '', 0, None),
(0b01111111, 0b00101001, 'Storage interval years', '', 0, None),
(0b01111111, 0b00101010, 'Reserved', '', 0, None),
(0b01111111, 0b00101011, 'Reserved', '', 0, None),
(0b01111111, 0b00101100, 'Duration since last read', '', 0b00000011, lambda d: d),
(0b01111111, 0b00110000, 'Start of tariff', '', 0, None),
(0b01111111, 0b00110000, 'Duration of tariff', '', 0b00000011, lambda d: d),
(0b01111111, 0b00110100, 'Period of tariff', '', 0b00000011, lambda d: d),
(0b01111111, 0b00111010, 'dimensionless', '', 0, None),
(0b01111111, 0b00111011, 'Reserved', '', 0, None),
(0b01111100, 0b00111100, 'Reserved', '', 0, None),
(0b01110000, 0b01000000, 'Voltage', 'V', 0b00001111, lambda d: 10**(d-9)),
(0b01110000, 0b01010000, 'Current', 'A', 0b00001111, lambda d: 10**(d-12)),
(0b01111111, 0b01100000, 'Reset Counter', '', 0, None),
(0b01111111, 0b01100001, 'Cumulation Counter', '', 0, None),
(0b01111111, 0b01100010, 'Control Signal', '', 0, None),
(0b01111111, 0b01100011, 'Day of week', '', 0, None),
(0b01111111, 0b01100100, 'Week number', '', 0, None),
(0b01111111, 0b01100101, 'Time point of day change', '', 0, None),
(0b01111111, 0b01100110, 'State of parameter activation', '', 0, None),
(0b01111111, 0b01100111, 'Special supplier info', '', 0, None),
(0b01111100, 0b01101000, 'Duration since last cumulation', '', 0b00000011, lambda d: d),
(0b01111100, 0b01101100, 'Operating time battery', '', 0b00000011, lambda d: d),
(0b01111111, 0b01110000, 'Date/time of bat. change', '', 0, None)
)
TABLE844b = (
(0b01111110, 0b00000000, 'Energy', 'MWh', 0b00000001, lambda d: 10**(d-1)),
(0b01111110, 0b00000010, 'Reserved', '', 0, None),
(0b01111100, 0b00000100, 'Reserved', '', 0, None),
(0b01111110, 0b00001000, 'Energy', 'GJ', 0b00000001, lambda d: 10**(d-1)),
(0b01111110, 0b00001010, 'Reserved', '', 0, None),
(0b01111100, 0b00001100, 'Reserved', '', 0, None),
(0b01111110, 0b00010000, 'Volume', 'm**3', 0b00000001, lambda d: 10**(d+2)),
(0b01111110, 0b00010010, 'Reserved', '', 0, None),
(0b01111100, 0b00010100, 'Reserved', '', 0, None),
(0b01111110, 0b00011000, 'Mass', 't', 0b00000001, lambda d: 10**(d+2)),
(0b01111111, 0b00100001, 'Volume', 'feet**3', 0, lambda d: 0.1),
(0b01111111, 0b00100010, 'Volume', 'am. gallon', 0, lambda d: 0.1),
(0b01111111, 0b00100011, 'Volume', 'am. gallon', 0, lambda d: 1),
(0b01111111, 0b00100100, 'Volume flow', 'am. gallon/min', 0, lambda d: 0.001),
(0b01111111, 0b00100101, 'Volume flow', 'am. gallon/min', 0, lambda d: 1),
(0b01111111, 0b00100110, 'Volume flow', 'am. gallon/h', 0, lambda d: 1),
(0b01111111, 0b00100111, 'Reserved', '', 0, None),
(0b01111110, 0b00101000, 'Power', 'MW', 0b00000001, lambda d: 10**(d-1)),
(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', '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', '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))
)
TIME_UNITS = ('seconds', 'minutes', 'hours', 'days')
TIME_POINT_UNITS = ('date', 'time & date')
@classmethod
def extVifCalc(cls, table, vife):
retDescr = 'unknown'
retUnit = 'unknown'
retFactor = 1
for vifeCode in table:
# print vifeCode
(mask, value, descr, unit, factorMask, factorFunc) = vifeCode
if (vife & mask) == value:
if factorFunc is None:
retFactor = 1
else:
factorHelper = vife & factorMask
retFactor = factorFunc(factorHelper)
retDescr = descr
retUnit = unit
break
return (retDescr, retUnit, retFactor)
@classmethod
def vifCalc(cls, vif, vife):
retDescr = 'unknown'
retUnit = 'unknown'
retFactor = 1
for vifCode in cls.VIF_CODES:
(mask, value, descr, unit, factorMask, factorFunc) = vifCode
if (vif & mask) == value:
if unit == 'TimeCalc':
retDescr = descr
retUnit = cls.TIME_UNITS[vif & factorMask]
elif unit == 'TimePointCalc':
retDescr = descr
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 factorFunc is not None:
factorHelper = vif & factorMask
retFactor = factorFunc(factorHelper)
break
# print("vifCalc: %s %s %s %s" % (retDescr, retRange, retUnit, rangeHelper))
return (retDescr, retUnit, retFactor)
def parse(self):
self.dif = self.data[self.consumed]
self.consumed += 1
self.dife = []
if self.dif & 0x80 != 0:
while True:
curDife = self.data[self.consumed]
self.consumed += 1
self.dife.append(curDife)
if curDife & 0x80 == 0:
break
self.vif = self.data[self.consumed]
self.consumed += 1
self.vife = []
if self.vif & 0x80 != 0:
while True:
curVife = self.data[self.consumed]
self.consumed += 1
self.vife.append(curVife)
if curVife & 0x80 == 0:
break
self.vifData = []
if self.vif & 0x7f == 0x7c:
vifDataLength = self.data[self.consumed]
self.consumed += 1
self.vifData = [v for v in self.data[self.consumed:self.consumed+vifDataLength]]
self.consumed += vifDataLength
userDataLength = LongFrame.DataInformationBlock.DATA_FIELD_CODES[self.dif&0x0f][1]
self.userData = [u for u in self.data[self.consumed:self.consumed+userDataLength]]
self.consumed += userDataLength
dataConvFunc = LongFrame.DataInformationBlock.DATA_FIELD_CODES[self.dif & 0x0f][2]
if dataConvFunc is 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, 'rawValue':self.rawValue,
'value': self.value,
'quantity': self.quantity, 'factor': self.factor, 'unit': self.unit,
'comment': self.comment
}
j.update(superJSON)
return j
def parse2(self):
super(LongFrame, self).parse2()
if self.payloadLength < 3 + 12:
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 is not None:
self.comment, self.category = device.comment, device.category
else:
self.comment, self.category = '-', '-'
self.dib = []
consumed = 0
dibIndex = 0
#print("Telegram length: %d" % len(self.telegram))
while True:
if device is not None:
try:
comment = device.dataItems[dibIndex]
except IndexError:
raise DeviceItemsMismatchException()
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))
#print("DIB: %s" % str(curDib.getJSON()))
# 3 for the fields, 12 for the fixed data header, 2 for checksum and stop code, 1 off since consumed is an index and no length
if (consumed + 3 + 12 + 2 - 1) >= self.payloadLength:
break
def getJSON(self):
superJSON = super(LongFrame, self).getJSON()
j = {'comment': self.comment, 'category': self.category, 'header': self.fixedDataHeader.getJSON(), 'dib': [dib.getJSON() for dib in self.dib]}
j.update(superJSON)
return j
class Telegram(object):
def __init__(self, devices=[]):
self.devices = devices
def fromHexString(self, hexString):
self.hexString = hexString
octetList = [int(o, 16) for o in self.hexString.split(' ')]
self.telegram = bytearray(octetList)
def toHexString(self):
return str([hex(b) for b in self.telegram])
def parse(self):
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.devices)
elif self.telegram[0] == 0x10:
self.frame = ShortFrame(self.telegram)
elif self.telegram[0] == 0xe5:
self.frame = SingleCharacter(self.telegram)
else:
raise InvalidStartCharException()
self.frame.parse()
def getJSON(self):
j = {'frameType': self.frame.__class__.__name__, 'frame': self.frame.getJSON()}
return j