From 53789088f2135527cb187109c52a793c9196c19b Mon Sep 17 00:00:00 2001 From: hg Date: Fri, 12 Jun 2015 00:09:58 +0200 Subject: [PATCH] frame parsing --- MeterbusLib.py | 146 +++++++++++++++++++++++++++++--------- MeterbusLibExceptions.py | 35 +++++++++ MeterbusTypeConversion.py | 53 +++++++++++--- test1.py | 42 +++++++++-- test2.py | 18 ++--- 5 files changed, 232 insertions(+), 62 deletions(-) create mode 100644 MeterbusLibExceptions.py diff --git a/MeterbusLib.py b/MeterbusLib.py index 5ef3eb0..a4f8607 100644 --- a/MeterbusLib.py +++ b/MeterbusLib.py @@ -6,32 +6,7 @@ Created on 11.06.2015 import MeterbusTypeConversion -class MeterbusLibException(Exception): - pass - -class InvalidFrameCodeException(MeterbusLibException): - pass - -class InvalidFrameException(MeterbusLibException): - pass - -class InvalidChecksumException(MeterbusLibException): - pass - -class InvalidStopCharException(MeterbusLibException): - pass - -class InvalidStartCharException(MeterbusLibException): - pass - -class InvalidLengthException(MeterbusLibException): - pass - -class InvalidSecondLengthException(MeterbusLibException): - pass - -class PayloadTooShortException(MeterbusLibException): - pass +from MeterbusLibExceptions import * class Frame(object): def __init__(self, startCharacter, frameLength, firstPayload, telegram): @@ -100,16 +75,113 @@ class LongFrame(ControlFrame): class FixedDataHeader(object): def __init__(self, data): self.data = data - print("Fixed header length: %d" % len(self.data)) def parse(self): self.identNr = MeterbusTypeConversion.bcd(self.data[:4]) - self.manufacturer = self.data[4:6] + self.manufacturer = MeterbusTypeConversion.manufCode(self.data[4:6]) self.version = self.data[6] - self.medium = self.data[7] + self.medium = MeterbusTypeConversion.mediumCode(self.data[7]) self.accessNo = self.data[8] self.status = self.data[9] self.signature = self.data[10:] + if self.signature[0] != 0 or self.signature[1] != 0: + raise InvalidFrameException("signature should be zero") + + class DataInformationElement(object): + def __init__(self, data): + self.data = data + self.consumed = 0 + + @classmethod + def create(cls, data): + if data[0] != 0x0f: + return LongFrame.DataInformationBlock(data) + else: + return LongFrame.ManufacturerSpecificDataBlock(data) + + class ManufacturerSpecificDataBlock(DataInformationElement): + def parse(self): + self.dif = self.data[0] + self.mdh = self.data[1:] + + def getJSON(self): + j = {'dif': self.dif, 'mdh': self.mdh} + return j + + class DataInformationBlock(DataInformationElement): + VALUE_LENGTH_MAP = [0, 1, 2, 3, 4, 4, 6, 8, 0, 1, 2, 3, 4, -1, 6, 0 ] + + 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: + dataLength = self.data[self.consumed] + self.consumed += 1 + self.vifData = [v for v in self.data[self.consumed:self.consumed+dataLength]] + self.consumed += dataLength + self.userData = [u for u in self.data[self.consumed:self.consumed+LongFrame.DataInformationBlock.VALUE_LENGTH_MAP[self.dif&0x0f]]] + + return self.consumed + + def value(self): + res = 0 + difCode = self.dif & 0x0f + if difCode == 0: + # 0 + pass + elif difCode == 1: + # 8bit int + res = self.userData[0] + elif difCode == 2: + # 16bit int + res = (self.userData[1] << 8) + self.userData[0] + elif difCode == 3: + # 24bit int + res = (self.userData[2] << 16) + (self.userData[1] << 8) + self.userData[0] + elif difCode == 4: + # 32bit int + res = (self.userData[3] << 24) + (self.userData[2] << 16) + (self.userData[1] << 8) + self.userData[0] + elif difCode == 5: + # 32bit float + res = 1.0 + elif difCode == 6: + # 48bit int + res = (self.userData[5] << 40) + (self.userData[4] << 32) + (self.userData[3] << 24) + (self.userData[2] << 16) + (self.userData[1] << 8) + self.userData[0] + elif difCode == 7: + # 64bit int + res = (self.userData[7] << 56) + (self.userData[6] << 48) + (self.userData[5] << 40) + (self.userData[4] << 32) + (self.userData[3] << 24) + (self.userData[2] << 16) + (self.userData[1] << 8) + self.userData[0] + elif difCode == 8: + # strange + res = 0 + elif difCode in [9, 10, 11, 12, 14]: + res = MeterbusTypeConversion.bcd(self.userData) + return res + + def getJSON(self): + j = {'dif': self.dif, 'dife': self.dife, 'vif': self.vif, 'vife': self.vife, + 'vifData': self.vifData, 'userData':self.userData, 'value':self.value()} + return j + + def parse2(self): super(LongFrame, self).parse2() @@ -117,6 +189,17 @@ class LongFrame(ControlFrame): raise PayloadTooShortException("too short for fixed data header") self.fixedDataHeader = LongFrame.FixedDataHeader(self.telegram[7:19]) self.fixedDataHeader.parse() + self.dib = [] + consumed = 0 + #print("Telegram length: %d" % len(self.telegram)) + while True: + curDib = LongFrame.DataInformationElement.create(self.telegram[(19 + consumed):]) + consumed += curDib.parse() + self.dib.append(curDib) + #print("PayloadLength: %d, Consumed: %d" % (self.payloadLength, consumed)) + print("DIB: %s" % str(curDib.getJSON())) + if (consumed + 12 + 3 - 1) >= self.payloadLength: + break class Telegram(object): @@ -128,9 +211,6 @@ class Telegram(object): octetList = [int(o, 16) for o in self.hexString.split(' ')] self.telegram = bytearray(octetList) - for i in self.telegram: - print(int(i)) - def toHexString(self): return str([hex(b) for b in self.telegram]) @@ -148,7 +228,5 @@ class Telegram(object): self.frame.parse() - print("Frame is %s" % self.frame) - diff --git a/MeterbusLibExceptions.py b/MeterbusLibExceptions.py new file mode 100644 index 0000000..08995e2 --- /dev/null +++ b/MeterbusLibExceptions.py @@ -0,0 +1,35 @@ +''' +Created on 11.06.2015 + +@author: wn +''' + +class MeterbusLibException(Exception): + pass + +class InvalidFrameCodeException(MeterbusLibException): + pass + +class InvalidFrameException(MeterbusLibException): + pass + +class InvalidChecksumException(MeterbusLibException): + pass + +class InvalidStopCharException(MeterbusLibException): + pass + +class InvalidStartCharException(MeterbusLibException): + pass + +class InvalidLengthException(MeterbusLibException): + pass + +class InvalidSecondLengthException(MeterbusLibException): + pass + +class PayloadTooShortException(MeterbusLibException): + pass + +class MediumConversionException(MeterbusLibException): + pass diff --git a/MeterbusTypeConversion.py b/MeterbusTypeConversion.py index 6fcb983..ba12f11 100644 --- a/MeterbusTypeConversion.py +++ b/MeterbusTypeConversion.py @@ -4,6 +4,9 @@ Created on 11.06.2015 @author: wn ''' +from MeterbusLibExceptions import MediumConversionException + + def bcd(data): v = "" @@ -13,13 +16,43 @@ def bcd(data): return v def manufCode(data): - print("data: %s %s" % (hex(data[1]), hex(data[0]))) - v = data[1] * 256 + data[0] - print("v: %s" % hex(v)) - l3 = chr((v & 0x20) + 64) - print("l3: %s" % l3) - l2 = chr((v >> 5) & 0x20) - print("l2: %s" % l2) - l1 = chr((v >> 10) & 0x20) - print("l1: %s" % l1) - return l1 + l2 + l3 \ No newline at end of file + v = (data[1] << 8) + data[0] + l3 = chr((v & 0x1f) + 64) + l2 = chr(((v >> 5) & 0x1f) + 64) + l1 = chr(((v >> 10) & 0x1f) + 64) + return l1 + l2 + l3 + + +def mediumCode(data): + try: + res = ('Other', + 'Oil', + 'Electrity', + 'Gas', + 'Heat (Volume measured at return temperature: outlet', + 'Steam', + 'Hot Water', + 'Water', + 'Heat Cost Allocator', + 'Compressed Air', + 'Cooling Load Meter (Volume measured at return temperature: outlet', + 'Cooling Load Meter (Volume measured at flow temperature: inlet', + 'Heat (Volume measured at flow temperature: inlet)', + 'Heat / Cooling Load Meter', + 'Bus / System', + 'Unknown Medium', + 'Reserved (10)', + 'Reserved (11)', + 'Reserved (12)', + 'Reserved (13)', + 'Reserved (14)', + 'Reserved (15)', + 'Cold Water', + 'Dual Water', + 'Pressure', + 'A/D Converter', + )[data] + return res + except IndexError: + raise MediumConversionException + \ No newline at end of file diff --git a/test1.py b/test1.py index 625ad74..ed81aa3 100644 --- a/test1.py +++ b/test1.py @@ -7,12 +7,15 @@ Created on 11.06.2015 import unittest import MeterbusLib import MeterbusTypeConversion +import MeterbusLibExceptions class TestFrameParsing(unittest.TestCase): def setUp(self): # dishwasher, electric self.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" + self.inputNOkLongFrame_1phase_electric_wrong_medium = "68 38 38 68 08 53 72 17 00 13 00 2E 19 24 FE 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 1c 16" + self.inputNOkLongFrame_1phase_electric_wrong_signature = "68 38 38 68 08 53 72 17 00 13 00 2E 19 24 02 D6 00 00 01 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 21 16" # electricity self.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" @@ -51,6 +54,22 @@ class TestFrameParsing(unittest.TestCase): self.assertEqual(telegram.frame.address, 0x53); self.assertEqual(telegram.frame.ciField, 0x72); self.assertEqual(telegram.frame.fixedDataHeader.identNr, "17001300") + self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "FIN") + self.assertEqual(telegram.frame.fixedDataHeader.version, 36) + self.assertEqual(telegram.frame.fixedDataHeader.medium, "Electrity") + + def test_OK_LongFrame_1phase_electric_wrong_medium(self): + telegram = MeterbusLib.Telegram() + telegram.fromHexString(self.inputNOkLongFrame_1phase_electric_wrong_medium) + with self.assertRaises(MeterbusLibExceptions.MediumConversionException): + telegram.parse() + + def test_OK_LongFrame_1phase_electric_wrong_signature(self): + telegram = MeterbusLib.Telegram() + telegram.fromHexString(self.inputNOkLongFrame_1phase_electric_wrong_signature) + with self.assertRaises(MeterbusLibExceptions.InvalidFrameException): + telegram.parse() + def test_OK_LongFrame_3phase_electric(self): telegram = MeterbusLib.Telegram() @@ -61,6 +80,9 @@ class TestFrameParsing(unittest.TestCase): self.assertEqual(telegram.frame.address, 0x50); self.assertEqual(telegram.frame.ciField, 0x72); self.assertEqual(telegram.frame.fixedDataHeader.identNr, "81140111") + self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "FIN") + self.assertEqual(telegram.frame.fixedDataHeader.version, 22) + self.assertEqual(telegram.frame.fixedDataHeader.medium, "Electrity") def test_OK_LongFrame_water(self): telegram = MeterbusLib.Telegram() @@ -71,6 +93,10 @@ class TestFrameParsing(unittest.TestCase): self.assertEqual(telegram.frame.address, 0x30); self.assertEqual(telegram.frame.ciField, 0x72); self.assertEqual(telegram.frame.fixedDataHeader.identNr, "45714300") + self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "HYD") + self.assertEqual(telegram.frame.fixedDataHeader.version, 37) + self.assertEqual(telegram.frame.fixedDataHeader.medium, "Water") + def test_OK_LongFrame_gas(self): telegram = MeterbusLib.Telegram() @@ -81,6 +107,9 @@ class TestFrameParsing(unittest.TestCase): self.assertEqual(telegram.frame.address, 0x40); self.assertEqual(telegram.frame.ciField, 0x72); self.assertEqual(telegram.frame.fixedDataHeader.identNr, "43605200") + self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "ACW") + self.assertEqual(telegram.frame.fixedDataHeader.version, 20) + self.assertEqual(telegram.frame.fixedDataHeader.medium, "Gas") def test_OK_LongFrame_thermometer(self): telegram = MeterbusLib.Telegram() @@ -91,6 +120,9 @@ class TestFrameParsing(unittest.TestCase): self.assertEqual(telegram.frame.address, 0x21); self.assertEqual(telegram.frame.ciField, 0x72); self.assertEqual(telegram.frame.fixedDataHeader.identNr, "00000000") + self.assertEqual(telegram.frame.fixedDataHeader.manufacturer, "@@@") + self.assertEqual(telegram.frame.fixedDataHeader.version, 1) + self.assertEqual(telegram.frame.fixedDataHeader.medium, "Other") def test_OK_SingleCharacter(self): telegram = MeterbusLib.Telegram() @@ -118,31 +150,31 @@ class TestFrameParsing(unittest.TestCase): def test_NOk_Shortframe_checksum_failure(self): telegram = MeterbusLib.Telegram() telegram.fromHexString(self.inputNOkShortframe_checksum_failure) - with self.assertRaises(MeterbusLib.InvalidChecksumException): + with self.assertRaises(MeterbusLibExceptions.InvalidChecksumException): telegram.parse() def test_NOk_LongFrame_thermometer_checksum_failure(self): telegram = MeterbusLib.Telegram() telegram.fromHexString(self.inputNOkLongFrame_thermometer_checksum_failure) - with self.assertRaises(MeterbusLib.InvalidChecksumException): + with self.assertRaises(MeterbusLibExceptions.InvalidChecksumException): telegram.parse() def test_NOk_Controlframe_wrong_stopcode(self): telegram = MeterbusLib.Telegram() telegram.fromHexString(self.inputNOKControlframe_wrong_stopcode) - with self.assertRaises(MeterbusLib.InvalidStopCharException): + with self.assertRaises(MeterbusLibExceptions.InvalidStopCharException): telegram.parse() def test_NOk_Controlframe_wrong_secondlength(self): telegram = MeterbusLib.Telegram() telegram.fromHexString(self.inputNOKControlframe_wrong_secondlength) - with self.assertRaises(MeterbusLib.InvalidSecondLengthException): + with self.assertRaises(MeterbusLibExceptions.InvalidSecondLengthException): telegram.parse() def test_NOk_wrong_startcode(self): telegram = MeterbusLib.Telegram() telegram.fromHexString(self.inputNOk_wrong_startcode) - with self.assertRaises(MeterbusLib.InvalidStartCharException): + with self.assertRaises(MeterbusLibExceptions.InvalidStartCharException): telegram.parse() def test_bcd(self): diff --git a/test2.py b/test2.py index 310f361..61b0384 100644 --- a/test2.py +++ b/test2.py @@ -4,19 +4,11 @@ Created on 11.06.2015 @author: dehottgw ''' -class A(object): - def __init__(self, x): - self.x = x - - def out(self): - print("X is %s" % self.x) -class B(A): - pass +import MeterbusLib +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" -a = A('abc') -a.out() - -b = B('xyz') -b.out() \ No newline at end of file +telegram = MeterbusLib.Telegram() +telegram.fromHexString(inputOkLongFrame_1phase_electric) +telegram.parse()