From a86b7cf986c046affda470ccedf040025c5f1da6 Mon Sep 17 00:00:00 2001 From: Johan Isacsson Date: Thu, 26 Aug 2021 21:42:10 +0200 Subject: [PATCH 1/7] Add timeout param to tcp client connect Adds an optional timeout param to the client connect method, this will make it possible for example to have a short timeout for connection and a longer timeout for regular read/write. The internal modbus client already supports this so this is a very small change. --- sunspec2/modbus/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunspec2/modbus/client.py b/sunspec2/modbus/client.py index 87cc410..38dd957 100644 --- a/sunspec2/modbus/client.py +++ b/sunspec2/modbus/client.py @@ -304,8 +304,8 @@ def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx if self.client is None: raise SunSpecModbusClientError('No modbus tcp client set for device') - def connect(self): - self.client.connect() + def connect(self, timeout=None): + self.client.connect(timeout) def disconnect(self): self.client.disconnect() From b5d84c31d239627ca009bbcffc31c55c30930075 Mon Sep 17 00:00:00 2001 From: Betrand Roussel Date: Tue, 3 May 2022 09:38:21 -0700 Subject: [PATCH 2/7] Migrate setup.py from distutils to setuptools distutils is deprecated and will be removed in python 3.12 ( https://peps.python.org/pep-0632/ ) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d85fd32..9463aee 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ All Rights Reserved """ -from distutils.core import setup +from setuptools import setup setup( name='pysunspec2', From 2db848efe6ccf185c4b275b3623f3eff797f1ac4 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 1 Oct 2022 19:47:01 -0400 Subject: [PATCH 3/7] initialize FileClientGroup point_class with FileClientPoint --- sunspec2/file/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunspec2/file/client.py b/sunspec2/file/client.py index db073eb..74f75bc 100644 --- a/sunspec2/file/client.py +++ b/sunspec2/file/client.py @@ -52,7 +52,7 @@ def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, dat self.add_error(str(e)) FileClientGroup.__init__(self, gdef=gdef, model=self, model_offset=0, group_len=self.model_len, data=data, - data_offset=0, group_class=group_class) + data_offset=0, group_class=group_class, point_class=point_class) def add_error(self, error_info): self.error_info = '%s%s\n' % (self.error_info, error_info) From c0d48666924c1ab95ee59cbed7f143a66d159ceb Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 1 Oct 2022 20:06:36 -0400 Subject: [PATCH 4/7] add cache for get_model_defs to save disk io --- sunspec2/device.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sunspec2/device.py b/sunspec2/device.py index d3db78b..43c90e0 100644 --- a/sunspec2/device.py +++ b/sunspec2/device.py @@ -19,7 +19,7 @@ class ModelError(Exception): model_defs_path = ['.', models_dir] model_path_options = ['.', 'json', 'smdx'] - +model_defs_cache = {} def get_model_defs_path(): return model_defs_path @@ -72,6 +72,10 @@ def get_model_def(model_id, mapping=True): except: raise mdef.ModelDefinitionError('Invalid model id: %s' % model_id) + global model_defs_cache + if (model_id, mapping) in model_defs_cache: + return model_defs_cache[(model_id, mapping)].copy() + model_def_file_json = mdef.to_json_filename(model_id) model_def_file_smdx = smdx.to_smdx_filename(model_id) model_def = None @@ -98,6 +102,7 @@ def get_model_def(model_id, mapping=True): if model_def is not None: if mapping: add_mappings(model_def[mdef.GROUP]) + model_defs_cache[(model_id, mapping)] = model_def.copy() return model_def raise mdef.ModelDefinitionError('Model definition not found for model %s' % model_id) From f4e1c15d4b180f4af4ff891f75cd1980942e5fbb Mon Sep 17 00:00:00 2001 From: Bertrand Roussel Date: Tue, 25 Oct 2022 21:11:13 -0700 Subject: [PATCH 5/7] #71 - Populate repeating block groups from file Even though the group_class is being passed the index, it is not being used at the point level, and any update that I tried to use it at the point level resulted in the output changing, and lots of tests failing. Using the index as part of the _init_repeating_group instead, without passing it to the group_class, provides the expected results. Note that this follows the same logic as if the count is available (line 530). --- sunspec2/device.py | 5 +- sunspec2/tests/test_data/inverter_123.json | 213 +++++++++++++++++++++ sunspec2/tests/test_file_client.py | 7 + 3 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 sunspec2/tests/test_data/inverter_123.json diff --git a/sunspec2/device.py b/sunspec2/device.py index 62fd438..86c903c 100644 --- a/sunspec2/device.py +++ b/sunspec2/device.py @@ -540,7 +540,7 @@ def _init_repeating_group(self, gdef=None, model_offset=None, data=None, data_of # compute count based on model len if present, otherwise allocate when set model_len = self.model.len if model_len: - gdata = self._group_data(data=data, name=gdef[mdef.NAME]) + gdata = self._group_data(data=data, name=gdef[mdef.NAME], index=0) g = self.group_class(gdef=gdef, model=self.model, model_offset=model_offset, data=gdata, data_offset=data_offset, index=1) group_points_len = g.points_len @@ -560,7 +560,8 @@ def _init_repeating_group(self, gdef=None, model_offset=None, data=None, data_of model_offset += g.len data_offset += g.len for i in range(count - 1): - g = self.group_class(gdef=gdef, model=self.model, model_offset=model_offset, data=data, + gdata = self._group_data(data=data, index=(i+1)) + g = self.group_class(gdef=gdef, model=self.model, model_offset=model_offset, data=gdata, data_offset=data_offset, index=i+2) model_offset += g.len data_offset += g.len diff --git a/sunspec2/tests/test_data/inverter_123.json b/sunspec2/tests/test_data/inverter_123.json new file mode 100644 index 0000000..e262fcc --- /dev/null +++ b/sunspec2/tests/test_data/inverter_123.json @@ -0,0 +1,213 @@ +{ + "name": null, + "did": "fa0a9f2d-d503-470e-8ce8-ec9c51428829", + "models": [ + { + "ID": 1, + "L": 66, + "Mn": "Device Manufacturer", + "Md": "Inverter 123", + "Opt": null, + "Vr": "v0.0.1", + "SN": "9999abcd", + "DA": null, + "Pad": 32768 + }, + { + "ID": 129, + "L": 210, + "ActCrv": 1, + "ModEna": 0, + "WinTms": null, + "RvrtTms": null, + "RmpTms": null, + "NCrv": 4, + "NPt": 10, + "Tms_SF": -2, + "V_SF": -1, + "Pad": 32768, + "curve": [ + { + "ActPt": 4, + "Tms1": 200, + "V1": 880, + "Tms2": 71, + "V2": 650, + "Tms3": 20, + "V3": 450, + "Tms4": 0, + "V4": 300, + "Tms5": 0, + "V5": 0, + "Tms6": 0, + "V6": 0, + "Tms7": 0, + "V7": 0, + "Tms8": 0, + "V8": 0, + "Tms9": 0, + "V9": 0, + "Tms10": 0, + "V10": 0, + "Tms11": null, + "V11": null, + "Tms12": null, + "V12": null, + "Tms13": null, + "V13": null, + "Tms14": null, + "V14": null, + "Tms15": null, + "V15": null, + "Tms16": null, + "V16": null, + "Tms17": null, + "V17": null, + "Tms18": null, + "V18": null, + "Tms19": null, + "V19": null, + "Tms20": null, + "V20": null, + "CrvNam": null, + "ReadOnly": 1 + }, + { + "ActPt": 0, + "Tms1": 0, + "V1": 0, + "Tms2": 0, + "V2": 0, + "Tms3": 0, + "V3": 0, + "Tms4": 0, + "V4": 0, + "Tms5": 0, + "V5": 0, + "Tms6": 0, + "V6": 0, + "Tms7": 0, + "V7": 0, + "Tms8": 0, + "V8": 0, + "Tms9": 0, + "V9": 0, + "Tms10": 0, + "V10": 0, + "Tms11": null, + "V11": null, + "Tms12": null, + "V12": null, + "Tms13": null, + "V13": null, + "Tms14": null, + "V14": null, + "Tms15": null, + "V15": null, + "Tms16": null, + "V16": null, + "Tms17": null, + "V17": null, + "Tms18": null, + "V18": null, + "Tms19": null, + "V19": null, + "Tms20": null, + "V20": null, + "CrvNam": null, + "ReadOnly": 0 + }, + { + "ActPt": 0, + "Tms1": 0, + "V1": 0, + "Tms2": 0, + "V2": 0, + "Tms3": 0, + "V3": 0, + "Tms4": 0, + "V4": 0, + "Tms5": 0, + "V5": 0, + "Tms6": 0, + "V6": 0, + "Tms7": 0, + "V7": 0, + "Tms8": 0, + "V8": 0, + "Tms9": 0, + "V9": 0, + "Tms10": 0, + "V10": 0, + "Tms11": null, + "V11": null, + "Tms12": null, + "V12": null, + "Tms13": null, + "V13": null, + "Tms14": null, + "V14": null, + "Tms15": null, + "V15": null, + "Tms16": null, + "V16": null, + "Tms17": null, + "V17": null, + "Tms18": null, + "V18": null, + "Tms19": null, + "V19": null, + "Tms20": null, + "V20": null, + "CrvNam": null, + "ReadOnly": 0 + }, + { + "ActPt": 0, + "Tms1": 0, + "V1": 0, + "Tms2": 0, + "V2": 0, + "Tms3": 0, + "V3": 0, + "Tms4": 0, + "V4": 0, + "Tms5": 0, + "V5": 0, + "Tms6": 0, + "V6": 0, + "Tms7": 0, + "V7": 0, + "Tms8": 0, + "V8": 0, + "Tms9": 0, + "V9": 0, + "Tms10": 0, + "V10": 0, + "Tms11": null, + "V11": null, + "Tms12": null, + "V12": null, + "Tms13": null, + "V13": null, + "Tms14": null, + "V14": null, + "Tms15": null, + "V15": null, + "Tms16": null, + "V16": null, + "Tms17": null, + "V17": null, + "Tms18": null, + "V18": null, + "Tms19": null, + "V19": null, + "Tms20": null, + "V20": null, + "CrvNam": null, + "ReadOnly": 0 + } + ] + } + ] +} diff --git a/sunspec2/tests/test_file_client.py b/sunspec2/tests/test_file_client.py index 65f972c..943df17 100644 --- a/sunspec2/tests/test_file_client.py +++ b/sunspec2/tests/test_file_client.py @@ -2847,6 +2847,13 @@ def test_scan(self): assert d.common assert d.DERMeasureAC + def test_repeating_point(self): + d = file_client.FileClientDevice('sunspec2/tests/test_data/inverter_123.json') + d.scan() + assert d.models[129][-1].curve[0].Tms1.value == 200 + assert d.models[129][-1].curve[1].Tms1.value == 0 + assert d.models[129][-1].curve[2].Tms11.value is None + def test_get_text(self, model_705_data): d = file_client.FileClientDevice() m = file_client.FileClientModel(705, data=model_705_data) From 785c245cf9ddebc0fafb2ea5dd5e06049e43c42b Mon Sep 17 00:00:00 2001 From: Bertrand Roussel Date: Wed, 1 Feb 2023 13:06:20 -0800 Subject: [PATCH 6/7] Provide symbols info in model def The dict provided by smdx.from_smdx didn't include all the info provided in the SMDX for symbols, and instead just contained name & value. Going through all the smdx string/symbols elements to collect info, and assigning that to matching symbol definitions. This is a symbol before: ``` "symbols": [ { "name": "IEEE_1547", "value": 0 }, ``` and after: ``` "symbols": [ { "name": "IEEE_1547", "value": 0, "label": "IEEE 1547 (default)", "desc": "'IEEE 1547' is the most common inverter compliance setting." }, ``` Most SMDX files do not provide additional symbol info and are therefore not affected. --- sunspec2/smdx.py | 41 ++++++++++++++++++++++++++++--------- sunspec2/tests/test_smdx.py | 15 ++++++++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/sunspec2/smdx.py b/sunspec2/smdx.py index 8d32d9f..3e2d743 100644 --- a/sunspec2/smdx.py +++ b/sunspec2/smdx.py @@ -255,6 +255,8 @@ def from_smdx(element): fixed_def[mdef.DESCRIPTION] = a.text elif a.tag == SMDX_NOTES and a.text: fixed_def[mdef.DETAIL] = a.text + + # Assign point info to point definitions for p in e.findall(SMDX_POINT): pid = p.attrib.get(SMDX_ATTR_ID) label = desc = notes = None @@ -266,16 +268,11 @@ def from_smdx(element): elif a.tag == SMDX_NOTES and a.text: notes = a.text - point_def = fixed_points_map.get(pid) - if point_def is not None: - if label: - point_def[mdef.LABEL] = label - if desc: - point_def[mdef.DESCRIPTION] = desc - if notes: - point_def[mdef.DETAIL] = notes - point_def = repeating_points_map.get(pid) - if point_def is not None: + for points_map in [fixed_points_map, repeating_points_map]: + point_def = points_map.get(pid) + if point_def is None: + continue + if label: point_def[mdef.LABEL] = label if desc: @@ -283,6 +280,30 @@ def from_smdx(element): if notes: point_def[mdef.DETAIL] = notes + # Assign symbol info to the point's symbol definitions + for s in p.findall(SMDX_SYMBOL): + sid = s.attrib.get(SMDX_ATTR_ID) + s_label = s_desc = s_notes = None + for a in s.findall('*'): + if a.tag == SMDX_LABEL and a.text: + s_label = a.text + elif a.tag == SMDX_DESCRIPTION and a.text: + s_desc = a.text + elif a.tag == SMDX_NOTES and a.text: + s_notes = a.text + + for s in point_def.get(mdef.SYMBOLS): + if s[mdef.NAME] != sid: + continue + + if s_label: + s[mdef.LABEL] = s_label + if s_desc: + s[mdef.DESCRIPTION] = s_desc + if s_notes: + s[mdef.DETAIL] = s_notes + break + model_def = {'id': mid, 'group': fixed_def} return model_def diff --git a/sunspec2/tests/test_smdx.py b/sunspec2/tests/test_smdx.py index d74f9cb..34011a4 100644 --- a/sunspec2/tests/test_smdx.py +++ b/sunspec2/tests/test_smdx.py @@ -32,6 +32,21 @@ def test_from_smdx_file(): assert smdx.from_smdx_file('sunspec2/models/smdx/smdx_00304.xml') == smdx_304 +def test_from_smdx_file_symbols(): + mdef = smdx.from_smdx_file('sunspec2/models/smdx/smdx_00803.xml') + for point_def in mdef["group"]["groups"][0]["points"]: + if point_def["name"] != "StrSt": + continue + symbol = point_def["symbols"][1] + assert symbol["name"] == "CONTACTOR_STATUS" + assert symbol["label"] == "Contactor Status" + assert symbol["desc"].startswith("String") + assert symbol["detail"] + break + else: + pytest.fail("Point not found") + + def test_from_smdx(): tree = ET.parse('sunspec2/models/smdx/smdx_00304.xml') root = tree.getroot() From 46b4eaa668703ebcad5047ace1d57774789324f4 Mon Sep 17 00:00:00 2001 From: Kudrat9 <51246738+Kudrat9@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:43:26 -0800 Subject: [PATCH 7/7] Update test_modbus_modbus.py --- sunspec2/tests/test_modbus_modbus.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sunspec2/tests/test_modbus_modbus.py b/sunspec2/tests/test_modbus_modbus.py index a29132b..e4ac92d 100644 --- a/sunspec2/tests/test_modbus_modbus.py +++ b/sunspec2/tests/test_modbus_modbus.py @@ -204,3 +204,20 @@ def test_write(self, monkeypatch): check_req = b"\x00\x00\x00\x00\x00'\x01\x10\x9ct\x00\x10 sn-000\x00\x00\x00\x00\x00\x00\x00" \ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" assert c.socket.request[0] == check_req + + def test_write_over_max_size(self, monkeypatch): + c = modbus_client.ModbusClientTCP() + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + c.connect() + data_to_write = bytearray((c.max_write_count+1)*2) + data_to_write[:6] = b'sn-000' + + buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9ct\x00\x7b', + b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c\xef\x00\x01'] + c.socket._set_buffer(buffer) + c.write(40052, data_to_write) + + check_req0 = b"\x00\x00\x00\x00\x00\xfd\x01" + b"\x10\x9ct\x00{\xf6" + data_to_write[:(c.max_write_count*2)] + check_req1 = b"\x00\x00\x00\x00\x00\x09\x01" + b"\x10\x9c\xef\x00\x01\x02\x00\x00" + assert c.socket.request[0] == check_req0 + assert c.socket.request[1] == check_req1