Skip to content

Commit

Permalink
Merge branch 'master' of github.com:ganehag/pyMeterBus
Browse files Browse the repository at this point in the history
  • Loading branch information
ganehag committed Jul 18, 2024
2 parents 83cf02d + a7275f6 commit f0d6ae2
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 16 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ test_venv/*
mbus_ref/*
misc
venv
# Eclipse
.project
.pydevproject
.settings
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Meter-Bus for Python
====================
[![Build status](https://github.com/ganehag/pyMeterBus/actions/workflows/run-test.yml/badge.svg)](https://github.com/ganehag/pyMeterBus/actions/workflows/run-test.yml) [![codecov](https://codecov.io/gh/ganehag/pyMeterBus/branch/master/graph/badge.svg?token=gHfokXGQ70)](https://codecov.io/gh/ganehag/pyMeterBus)
[![pypi](https://img.shields.io/pypi/pyversions/pyMeterBus)](https://pypi.org/project/pyMeterBus/)
[![GitHub issues](https://img.shields.io/github/issues/ganehag/pyMeterBus.svg)](https://github.com/ganehag/pyMeterBus/issues)
[![GitHub issues](https://img.shields.io/github/issues-closed/ganehag/pyMeterBus.svg)](https://github.com/ganehag/pyMeterBus/issues/?q=is%3Aissue+is%3Aclosed)
[![PyPI Status](https://img.shields.io/pypi/v/pyMeterBus.svg)](https://pypi.python.org/pypi/pyMeterBus/)

About
-----
Expand Down
77 changes: 77 additions & 0 deletions meterbus/core_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class DataEncoding(Enum):


class VIFUnit(Enum):
# REF: M-Bus, §8.4.4a

ENERGY_WH = 0x07 # E000 0xxx
ENERGY_J = 0x0F # E000 1xxx
VOLUME = 0x17 # E001 0xxx
Expand Down Expand Up @@ -96,6 +98,8 @@ class VIFUnit(Enum):


class VIFUnitExt(Enum):
# REF: M-Bus, §8.4.4b

# Currency Units
CURRENCY_CREDIT = 0x03 # E000 00nn Credit of 10 nn-3 of the nominal ...
CURRENCY_DEBIT = 0x07 # E000 01nn Debit of 10 nn-3 of the nominal ...
Expand Down Expand Up @@ -174,6 +178,44 @@ class VIFUnitSecExt(Enum):
RELATIVE_HUMIDITY = 0x1A


class VIFUnitEnhExt(Enum):
# REF: M-Bus, §8.4.5 ("enhancement of VIF's other than $FD")

# E00x xxxx Reserved
PER_SECOND = 0x20 # E010 0000 per second
PER_MINUTE = 0x21 # E010 0001 per minute
PER_HOUR = 0x22 # E010 0010 per hour
PER_DAY = 0x23 # E010 0011 per day
PER_WEEK = 0x24 # E010 0100 per week
PER_MONTH = 0x25 # E010 0101 per month
PER_YEAR = 0x26 # E010 0110 per year
PER_REVOLUTION = 0x27 # E010 0111 per revolution / measurement
PER_INPUT_PULSE0 = 0x28 # E010 100p increment per input pulse on input channel #p
PER_INPUT_PULSE1 = 0x29
PER_OUTPUT_PULSE0 = 0x2A # E010 101p increment per output pulse on output channel #p
PER_OUTPUT_PULSE1 = 0x2B
PER_LITER = 0x2C # E010 1100 per liter
PER_M3 = 0x2D # E010 1101 per m3
PER_KG = 0x2E # E010 1110 per kg
PER_KELVIN = 0x2F # E010 1111 per K (Kelvin)
PER_KWH = 0x30 # E011 0000 per kWh
PER_GJ = 0x31 # E011 0001 per GJ
PER_KW = 0x32 # E011 0010 per kW
PER_KELVIN_LITER = 0x33 # E011 0011 per (K*l) (Kelvin*liter)
PER_VOLT = 0x34 # E011 0100 per V (Volt)
PER_AMPERE = 0x35 # E011 0101 per A (Ampere)
MULT_SEK = 0x36 # E011 0110 multiplied by sek
MULT_SEK_PER_VOLT = 0x37 # E011 0111 multiplied by sek / V
MULT_SEK_PER_AMPERE = 0x38 # E011 1000 multiplied by sek / A
START_DATE_TIME = 0x39 # E011 1001 start date(/time) of Œ 
UNCORRECTED_UNIT = 0x3A # E011 1010 VIF contains uncorrected unit instead of corrected unit
POSITIVE_ACCUMULATION = 0x3B # E011 1011 Accumulation only if positive ACCUMULATIONs
NEGATIVE_ACCUMULATION = 0x3C # E011 1100 Accumulation of abs value only if negative ACCUMULATIONs
# E011 1101 to # E011 1111 Reserved

UNKNOWN_ENHANCEMENT = 0x100


class VIFTable(object):
# Primary VIFs (main table), range 0x00 - 0xFF

Expand Down Expand Up @@ -801,6 +843,41 @@ class VIFTable(object):
0x27F: (1.0e4, MeasureUnit.W, "Cumul count max power")
}

# Additional VIFE-Code Extension table (following other primary VIF)
# See 8.4.5

enh = {
0x20: VIFUnitEnhExt.PER_SECOND,
0x21: VIFUnitEnhExt.PER_MINUTE,
0x22: VIFUnitEnhExt.PER_HOUR,
0x23: VIFUnitEnhExt.PER_DAY,
0x24: VIFUnitEnhExt.PER_WEEK,
0x25: VIFUnitEnhExt.PER_MONTH,
0x26: VIFUnitEnhExt.PER_YEAR,
0x27: VIFUnitEnhExt.PER_REVOLUTION,
0x28: VIFUnitEnhExt.PER_INPUT_PULSE0,
0x29: VIFUnitEnhExt.PER_INPUT_PULSE1,
0x2A: VIFUnitEnhExt.PER_OUTPUT_PULSE0,
0x2B: VIFUnitEnhExt.PER_OUTPUT_PULSE1,
0x2C: VIFUnitEnhExt.PER_LITER,
0x2D: VIFUnitEnhExt.PER_M3,
0x2E: VIFUnitEnhExt.PER_KG,
0x2F: VIFUnitEnhExt.PER_KELVIN,
0x30: VIFUnitEnhExt.PER_KWH,
0x31: VIFUnitEnhExt.PER_GJ,
0x32: VIFUnitEnhExt.PER_KW,
0x33: VIFUnitEnhExt.PER_KELVIN_LITER,
0x34: VIFUnitEnhExt.PER_VOLT,
0x35: VIFUnitEnhExt.PER_AMPERE,
0x36: VIFUnitEnhExt.MULT_SEK,
0x37: VIFUnitEnhExt.MULT_SEK_PER_VOLT,
0x38: VIFUnitEnhExt.MULT_SEK_PER_AMPERE,
0x39: VIFUnitEnhExt.START_DATE_TIME,
0x3A: VIFUnitEnhExt.UNCORRECTED_UNIT,
0x3B: VIFUnitEnhExt.POSITIVE_ACCUMULATION,
0x3C: VIFUnitEnhExt.NEGATIVE_ACCUMULATION,
}


class TelegramDateMasks(Enum):
DATE = 0x02 # "Auctual Date", 0010 Type G
Expand Down
31 changes: 20 additions & 11 deletions meterbus/telegram_variable_data_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .value_information_block import ValueInformationBlock
from .data_information_block import DataInformationBlock

from .core_objects import VIFTable, VIFUnit, DataEncoding, MeasureUnit
from .core_objects import VIFTable, VIFUnit, VIFUnitEnhExt, DataEncoding, MeasureUnit


class TelegramVariableDataRecord(object):
Expand All @@ -36,11 +36,11 @@ def more_records_follow(self):

def _parse_vifx(self):
if len(self.vib.parts) == 0:
return None, None, None
return None, None, None, None

vif = self.vib.parts[0]
vife = self.vib.parts[1:]
vtf_ebm = self.EXTENSION_BIT_MASK
vif_enh = None

if vif == VIFUnit.FIRST_EXT_VIF_CODES.value: # 0xFB
code = (vife[0] & self.UNIT_MULTIPLIER_MASK) | 0x200
Expand All @@ -49,7 +49,7 @@ def _parse_vifx(self):
code = (vife[0] & self.UNIT_MULTIPLIER_MASK) | 0x100

elif vif in [VIFUnit.VIF_FOLLOWING.value]: # 0x7C
return (1, self.vib.customVIF.decodeASCII, VIFUnit.VARIABLE_VIF)
return (1, self.vib.customVIF.decodeASCII, VIFUnit.VARIABLE_VIF, None)

elif vif == 0xFC:
# && (vib->vife[0] & 0x78) == 0x70
Expand All @@ -68,22 +68,29 @@ def _parse_vifx(self):
factor = 1

return (factor, self.vib.customVIF.decodeASCII,
VIFUnit.VARIABLE_VIF)
VIFUnit.VARIABLE_VIF, None)

# // custom VIF
# n = (vib->vife[0] & 0x07);
# snprintf(buff, sizeof(buff), "%s %s", mbus_unit_prefix(n-6), vib->custom_vif);
# return buff;
# return (1, "FixME", "FixMe")
# return (1, "FixME", "FixMe", None)

elif vif & self.EXTENSION_BIT_MASK:
code = (vif & self.UNIT_MULTIPLIER_MASK)
vif_enh = vife[0] & self.UNIT_MULTIPLIER_MASK

else:
code = (vif & self.UNIT_MULTIPLIER_MASK)

return VIFTable.lut[code]
return (
*VIFTable.lut[code],
VIFTable.enh.get(vif_enh, VIFUnitEnhExt.UNKNOWN_ENHANCEMENT) if vif_enh else None,
)

@property
def unit(self):
_, unit, _ = self._parse_vifx()
_, unit, _, _ = self._parse_vifx()
if isinstance(unit, MeasureUnit):
return unit.value
return unit
Expand All @@ -109,7 +116,7 @@ def function(self):

@property
def parsed_value(self):
mult, unit, typ = self._parse_vifx()
mult, unit, _, _ = self._parse_vifx()

length, enc = self.dib.length_encoding

Expand Down Expand Up @@ -159,8 +166,7 @@ def parsed_value(self):

@property
def interpreted(self):
mult, unit, typ = self._parse_vifx()
dlen, enc = self.dib.length_encoding
_, unit, typ, unit_enh = self._parse_vifx()
storage_number, tariff, device = self.dib.parse_dife()

try:
Expand All @@ -186,6 +192,9 @@ def interpreted(self):
'function': str(self.dib.function_type)
}

if unit_enh is not None:
record['unit_enh'] = str(unit_enh)

if tariff is not None:
record['tariff'] = tariff

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
# pip-compile requirements.in
#
pycryptodome==3.15.0
pycryptodome==3.19.1
# via -r requirements.in
pyserial==3.5
# via -r requirements.in
Expand Down
33 changes: 29 additions & 4 deletions tests/test_variable_data_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def setUp(self):
"\xfc\x03\x48\x52\x25\x74\xb4\x16\x02\x65\xd0\x08\x22\x65" \
"\x70\x08\x12\x65\x23\x09\x01\x72\x18\x42\x65\xe4\x08\x82" \
"\x01\x65\xdd\x08\x0c\x78\x34\x08\x00\x54\x03\xfd\x0f\x00" \
"\x00\x04\x1f\x5d\x16"
"\x00\x04\x04\x83\x3b\x87\xd6\x12\x00\x04\x83\x3c\x87\xd6" \
"\x12\x00\x1f\xc0\x16"
self.frame2 = "\x68\xD8\xD8\x68\x08\x00\x72\x92\x03\x00\x64\x96\x15" \
"\x14\x31\x04\x00\x00\x00\x0C\x78\x92\x03\x00\x64\x0D" \
"\xFD\x0F\x05\x33\x2E\x36\x2E\x31\x0D\x7C\x03\x79\x65" \
Expand Down Expand Up @@ -50,19 +51,19 @@ def test_datafield_set(self):
def test_vif_mult_oxfc_0x78(self):
t = meterbus.TelegramVariableDataRecord()
t.vib.parts = [0xFC, 0x78]
mult, _, _ = t._parse_vifx()
mult, _, _, _ = t._parse_vifx()
self.assertEqual(mult, 0.001)

def test_vif_mult_oxfc_0x7B(self):
t = meterbus.TelegramVariableDataRecord()
t.vib.parts = [0xFC, 0x7B]
mult, _, _ = t._parse_vifx()
mult, _, _, _ = t._parse_vifx()
self.assertEqual(mult, 1.0)

def test_vif_mult_oxfc_0x7D(self):
t = meterbus.TelegramVariableDataRecord()
t.vib.parts = [0xFC, 0x7D]
mult, _, _ = t._parse_vifx()
mult, _, _, _ = t._parse_vifx()
self.assertEqual(mult, 1.0)

def test_parsed_value_invalid_data_len(self):
Expand Down Expand Up @@ -228,6 +229,30 @@ def test_json_record11(self):
frame_rec_dict = json.loads(self.frame.records[11].to_JSON())
self.assertEqual(frame_rec_dict, dict_record)

def test_json_record12(self):
dict_record = {
"value": 1234567,
"unit": "MeasureUnit.WH",
"type": "VIFUnit.ENERGY_WH",
"function": "FunctionType.INSTANTANEOUS_VALUE",
"storage_number": 0,
"unit_enh": "VIFUnitEnhExt.POSITIVE_ACCUMULATION",
}
frame_rec_dict = json.loads(self.frame.records[12].to_JSON())
self.assertEqual(frame_rec_dict, dict_record)

def test_json_record13(self):
dict_record = {
"value": 1234567,
"unit": "MeasureUnit.WH",
"type": "VIFUnit.ENERGY_WH",
"function": "FunctionType.INSTANTANEOUS_VALUE",
"storage_number": 0,
"unit_enh": "VIFUnitEnhExt.NEGATIVE_ACCUMULATION",
}
frame_rec_dict = json.loads(self.frame.records[13].to_JSON())
self.assertEqual(frame_rec_dict, dict_record)

def test_json_value_str(self):
key = "0A B3 ED 14 CD 07 58 D7 BA DE 3B 38 B2 E6 96 0C"
record = json.loads(self.frame2.records[2].to_JSON())
Expand Down

0 comments on commit f0d6ae2

Please sign in to comment.