Skip to content

Commit ca6af43

Browse files
author
bobfox
authored
Merge pull request #35 from sunspec/development
1.0.4 Update
2 parents 71a659f + 36197c8 commit ca6af43

File tree

14 files changed

+833
-1037
lines changed

14 files changed

+833
-1037
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
setup(
1111
name='pysunspec2',
12-
version='1.0.3',
12+
version='1.0.4',
1313
description='Python SunSpec Tools',
1414
author='SunSpec Alliance',
1515
author_email='support@sunspec.org',

sunspec2/device.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def get_model_info(model_id):
4444
for pdef in points:
4545
info = mb.point_type_info.get(pdef[mdef.TYPE])
4646
plen = pdef.get(mdef.SIZE, None)
47-
if plen is None:
47+
if plen is not None:
4848
glen += info.len
4949
except:
5050
raise
@@ -133,6 +133,11 @@ def __init__(self, pdef=None, model=None, group=None, model_offset=0, data=None,
133133
self.sf = None # scale factor point name
134134
self.sf_value = None # value of scale factor
135135
self.sf_required = False # point has a scale factor
136+
self.read_func = None # function to be called on read
137+
self.read_func_arg = None # the argument passed to the read_func
138+
self.write_func = None # function to be called on write
139+
self.write_func_arg = None # the argument passed to the write_func
140+
136141
if pdef:
137142
self.sf_required = (pdef.get(mdef.SF) is not None)
138143
if self.sf_required:
@@ -189,6 +194,10 @@ def cvalue(self, v):
189194
self.set_value(v, computed=True, dirty=True)
190195

191196
def get_value(self, computed=False):
197+
# call read function, if set
198+
if self.read_func:
199+
self.read_func(self.model, self.read_func_arg)
200+
192201
v = self._value
193202
if computed and v is not None:
194203
if self.sf_required:
@@ -227,16 +236,29 @@ def set_value(self, data=None, computed=False, dirty=None):
227236
(self.sf, self.pdef['name']))
228237
else:
229238
raise ModelError('Scale factor %s for point %s not found' % (self.sf, self.pdef['name']))
230-
if self.sf_value:
239+
if self.sf_value is not None:
231240
self._value = round(round(float(v), abs(self.sf_value)) / math.pow(10, self.sf_value))
232241
else:
233242
self._value = v
234243
else:
235244
self._value = v
236245

246+
# call write function, if set
247+
# should be used to set indication for subsequent processing rather than do detailed processing
248+
if self.write_func:
249+
self.write_func(self.model, self.write_func_arg)
250+
251+
def set_read_func(self, func, arg=None):
252+
self.read_func = func
253+
self.read_func_arg = arg
254+
255+
def set_write_func(self, func, arg=None):
256+
self.write_func = func
257+
self.write_func_arg = arg
258+
237259
def get_mb(self, computed=False):
238260
v = self._value
239-
data = None
261+
data = err = None
240262
if computed and v is not None:
241263
if self.sf_required:
242264
if self.sf_value is None:
@@ -252,17 +274,27 @@ def get_mb(self, computed=False):
252274
sfv = self.sf_value
253275
if sfv:
254276
v = int(v * math.pow(10, sfv))
255-
data = self.info.to_data(v, (int(self.len) * 2))
277+
try:
278+
data = self.info.to_data(v, (int(self.len) * 2))
279+
except Exception as e:
280+
err = 'Error getting point value %s %s: %s' % (self.pdef[mdef.NAME], v, e)
281+
if err:
282+
raise ModelError(err)
256283
elif v is None:
257284
data = mb.create_unimpl_value(self.pdef[mdef.TYPE], len=(int(self.len) * 2))
258285

259286
if data is None:
260-
data = self.info.to_data(v, (int(self.len) * 2))
287+
try:
288+
data = self.info.to_data(v, (int(self.len) * 2))
289+
except Exception as e:
290+
err = 'Error getting point value %s %s: %s' % (self.pdef[mdef.NAME], v, e)
291+
if err:
292+
raise ModelError(err)
261293
return data
262294

263295
def set_mb(self, data=None, computed=False, dirty=None):
296+
mb_len = self.len
264297
try:
265-
mb_len = self.len
266298
# if not enough data, do not set but consume the data
267299
if len(data) < mb_len * 2:
268300
return len(data)
@@ -310,12 +342,14 @@ def __init__(self, gdef=None, model=None, model_offset=0, group_len=0, data=None
310342
points = self.gdef.get(mdef.POINTS)
311343
if points:
312344
for pdef in points:
313-
p = point_class(pdef, model=self.model, group=self, model_offset=model_offset, data=data,
314-
data_offset=data_offset)
315-
self.points_len += p.len
316-
model_offset += p.len
317-
data_offset += p.len
318-
self.points[pdef[mdef.NAME]] = p
345+
# allow legacy model 1 to have an alternate length of 65 registers
346+
if self.len != 65 or model.model_id != 1 or pdef[mdef.NAME] != 'Pad':
347+
p = point_class(pdef, model=self.model, group=self, model_offset=model_offset, data=data,
348+
data_offset=data_offset)
349+
self.points_len += p.len
350+
model_offset += p.len
351+
data_offset += p.len
352+
self.points[pdef[mdef.NAME]] = p
319353
# initialize groups
320354
groups = self.gdef.get(mdef.GROUPS)
321355
if groups:
@@ -644,13 +678,14 @@ def add_model(self, model):
644678
model_list.append(model)
645679
# add by group id
646680
gname = model.gname
647-
model_list = self.models.get(gname)
648-
if model_list is None:
649-
model_list = []
650-
self.models[gname] = model_list
651-
model_list.append(model)
652-
# add to model list
653-
self.model_list.append(model)
681+
if gname is not None:
682+
model_list = self.models.get(gname)
683+
if model_list is None:
684+
model_list = []
685+
self.models[gname] = model_list
686+
model_list.append(model)
687+
# add to model list
688+
self.model_list.append(model)
654689

655690
model.device = self
656691

sunspec2/mdef.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,5 +451,4 @@ def get_group_len_points_index(group_def):
451451

452452
if __name__ == "__main__":
453453

454-
model_def = from_json_file('./models/json/model_711.json')
455-
print(get_group_len_points_index(model_def.get(GROUP)))
454+
pass

sunspec2/modbus/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ def scan(self, progress=None, delay=None, connect=True):
228228
except SunSpecModbusClientError as e:
229229
if not error:
230230
error = str(e)
231+
except modbus_client.ModbusClientTimeout as e:
232+
if not error:
233+
error = str(e)
231234
except modbus_client.ModbusClientException:
232235
pass
233236

sunspec2/smdx.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,10 @@ def from_smdx(element):
188188
mdef.TYPE: mdef.TYPE_GROUP,
189189
mdef.POINTS: [
190190
{mdef.NAME: 'ID', mdef.VALUE: mid,
191-
mdef.DESCRIPTION: 'Model identifier', mdef.LABEL: 'Model ID',
191+
mdef.DESCRIPTION: 'Model identifier', mdef.LABEL: 'Model ID', mdef.SIZE: 1,
192192
mdef.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16},
193193
{mdef.NAME: 'L',
194-
mdef.DESCRIPTION: 'Model length', mdef.LABEL: 'Model Length',
194+
mdef.DESCRIPTION: 'Model length', mdef.LABEL: 'Model Length', mdef.SIZE: 1,
195195
mdef.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16}
196196
]
197197
}
@@ -316,6 +316,8 @@ def from_smdx_point(element):
316316
if plen is None:
317317
raise mdef.ModelDefinitionError('Missing len attribute for point: %s' % pid)
318318
point_def[mdef.SIZE] = plen
319+
else:
320+
point_def[mdef.SIZE] = mdef.point_type_info.get(ptype)['len']
319321
mandatory = element.attrib.get(SMDX_ATTR_MANDATORY, SMDX_MANDATORY_FALSE)
320322
if mandatory not in smdx_mandatory_types:
321323
raise mdef.ModelDefinitionError('Unknown mandatory type: %s' % mandatory)

sunspec2/spreadsheet.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,11 +320,17 @@ def to_spreadsheet_point(ss, point, has_notes, addr_offset=None, group_offset=No
320320
row[NAME_IDX] = name
321321
else:
322322
raise Exception('Point missing name attribute')
323+
323324
ptype = point.get(mdef.TYPE, '')
324-
if ptype != '':
325-
row[TYPE_IDX] = ptype
326-
else:
325+
326+
if ptype == '':
327327
raise Exception('Point %s missing type attribute' % name)
328+
329+
if mdef.point_type_info.get(ptype) is None:
330+
raise Exception('Unknown point type %s for point %s' % (ptype, name))
331+
332+
row[TYPE_IDX] = ptype
333+
328334
if addr_offset is not None:
329335
row[ADDRESS_OFFSET_IDX] = addr_offset
330336
elif group_offset is not None:

sunspec2/tests/mock_socket.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,15 @@ def clear_buffer(self):
3535

3636
def mock_socket(AF_INET, SOCK_STREAM):
3737
return MockSocket()
38+
39+
40+
def mock_tcp_connect(self):
41+
if self.client.socket is None:
42+
self.client.socket = mock_socket('foo', 'bar')
43+
self.client.socket.settimeout(999)
44+
self.client.socket.connect((999, 999))
45+
pass
46+
47+
48+
def mock_tcp_disconnect(self):
49+
pass

sunspec2/tests/test_device.py

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,9 +1799,9 @@ def test_get_json(self):
17991799
m.groups['Crv'][0].points['DeptRef'].sf_value = 3
18001800
m.groups['Crv'][0].points['Pri'].sf_required = True
18011801
m.groups['Crv'][0].points['Pri'].sf_value = 3
1802-
assert m.groups['Crv'][0].get_json(computed=True) == '''{"ActPt": 4, "DeptRef": 1000.0,''' + \
1803-
''' "Pri": 1000.0, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null,''' + \
1804-
''' "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 92.0, "Var": 30.0}, {"V": 96.7, "Var": 0.0},''' + \
1802+
assert m.groups['Crv'][0].get_json(computed=True) == '''{"ActPt": 4, "DeptRef": 1000.0, "Pri": 1000.0,''' + \
1803+
''' "VRef": 0.01, "VRefAuto": 0.0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6,''' + \
1804+
''' "ReadOnly": 1, "Pt": [{"V": 92.0, "Var": 30.0}, {"V": 96.7, "Var": 0.0},''' + \
18051805
''' {"V": 103.0, "Var": 0.0}, {"V": 107.0, "Var": -30.0}]}'''
18061806

18071807
def test_set_json(self):
@@ -2042,9 +2042,9 @@ def test_get_mb(self):
20422042
m.groups['Crv'][0].points['DeptRef'].sf_value = 3
20432043
m.groups['Crv'][0].points['Pri'].sf_required = True
20442044
m.groups['Crv'][0].points['Pri'].sf_value = 3
2045-
assert m.groups['Crv'][0].get_mb(computed=True) == b'\x00\x04\x03\xe8\x03\xe8\x00\x01\x00\x00\xff\xff\xff' \
2046-
b'\xff\x00\x00\x00\x06\x00\x01\x00\\\x00\x1e\x00`\x00\x00' \
2047-
b'\x00g\x00\x00\x00k\xff\xe2'
2045+
assert m.groups['Crv'][0].get_mb(computed=True) == b'\x00\x04\x03\xe8\x03\xe8\x00\x00\x00\x00\xff\xff\xff' \
2046+
b'\xff\x00\x00\x00\x06\x00\x01\x00\\\x00\x1e\x00`\x00' \
2047+
b'\x00\x00g\x00\x00\x00k\xff\xe2'
20482048

20492049
def test_set_mb(self):
20502050
gdata_705 = {
@@ -2299,6 +2299,21 @@ def test__init__(self):
22992299
assert m2.mid is None
23002300
assert m2.device is None
23012301

2302+
def test_model_1(self):
2303+
2304+
mdata = {
2305+
"ID": 1,
2306+
"L": 68,
2307+
"Mn": "Test manuf",
2308+
"Md": "Test model",
2309+
"Opt": "Test options",
2310+
"Vr": "Test version",
2311+
"SN": "Test serial num",
2312+
"DA": 12,
2313+
"Pad": 0
2314+
}
2315+
m = device.Model(1, data=mdata)
2316+
23022317
def test__error(self):
23032318
m = device.Model(704)
23042319
m.add_error('test error')
@@ -2467,16 +2482,16 @@ def test_get_dict(self):
24672482
assert d.get_dict(computed=True) == {'name': None, 'did': None, 'models': [
24682483
{'ID': 705, 'L': 67, 'Ena': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, 'RvrtTms': 0,
24692484
'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'RspTms_SF': None, 'Crv': [
2470-
{'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None,
2485+
{'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None,
24712486
'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 1,
24722487
'Pt': [{'V': 92.0, 'Var': 30.0}, {'V': 96.7, 'Var': 0.0}, {'V': 103.0, 'Var': 0.0},
24732488
{'V': 107.0, 'Var': -30.0}]},
2474-
{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None,
2475-
'RspTms': 6, 'ReadOnly': 0,
2489+
{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None,
2490+
'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 0,
24762491
'Pt': [{'V': 93.0, 'Var': 30.0}, {'V': 95.7, 'Var': 0.0}, {'V': 102.0, 'Var': 0.0},
24772492
{'V': 106.0, 'Var': -40.0}]},
2478-
{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None,
2479-
'RspTms': 6, 'ReadOnly': 0,
2493+
{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None,
2494+
'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 0,
24802495
'Pt': [{'V': 94.0, 'Var': 20.0}, {'V': 95.7, 'Var': 0.0}, {'V': 105.0, 'Var': 0.0},
24812496
{'V': 108.0, 'Var': -20.0}]}], 'mid': None, 'error': '', 'model_id': 705}]}
24822497

@@ -2603,17 +2618,18 @@ def test_get_json(self):
26032618
m.groups['Crv'][0].points['DeptRef'].sf_value = 3
26042619
m.groups['Crv'][0].points['Pri'].sf_required = True
26052620
m.groups['Crv'][0].points['Pri'].sf_value = 3
2606-
assert d.get_json(computed=True) == '''{"name": null, "did": null, "models": [{"ID": 705,''' + \
2607-
''' "L": 67, "Ena": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3, "RvrtTms": 0,''' + \
2608-
''' "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2, "RspTms_SF": null, ''' + \
2609-
'''"Crv": [{"ActPt": 4, "DeptRef": 1000.0, "Pri": 1000.0, "VRef": 1, "VRefAuto": 0, ''' + \
2610-
'''"VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1, ''' + \
2611-
'''"Pt": [{"V": 92.0, "Var": 30.0}, {"V": 96.7, "Var": 0.0}, {"V": 103.0, "Var": 0.0},''' + \
2612-
''' {"V": 107.0, "Var": -30.0}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \
2613-
''' "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 0,''' + \
2614-
''' "Pt": [{"V": 93.0, "Var": 30.0}, {"V": 95.7, "Var": 0.0}, {"V": 102.0, "Var": 0.0},''' + \
2615-
''' {"V": 106.0, "Var": -40.0}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \
2621+
assert d.get_json(computed=True) == '''{"name": null, "did": null, "models":''' + \
2622+
''' [{"ID": 705, "L": 67, "Ena": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0,''' + \
2623+
''' "NPt": 4, "NCrv": 3, "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2,''' + \
2624+
''' "DeptRef_SF": -2, "RspTms_SF": null, "Crv": [{"ActPt": 4, "DeptRef": 1000.0,''' + \
2625+
''' "Pri": 1000.0, "VRef": 0.01, "VRefAuto": 0.0, "VRefAutoEna": null,''' + \
2626+
''' "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 92.0, "Var": 30.0},''' + \
2627+
''' {"V": 96.7, "Var": 0.0}, {"V": 103.0, "Var": 0.0}, {"V": 107.0, "Var": -30.0}]},''' + \
2628+
''' {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 0.01, "VRefAuto": 0.0,''' + \
26162629
''' "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 0,''' + \
2630+
''' "Pt": [{"V": 93.0, "Var": 30.0}, {"V": 95.7, "Var": 0.0}, {"V": 102.0, "Var": 0.0},''' + \
2631+
''' {"V": 106.0, "Var": -40.0}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 0.01,''' + \
2632+
''' "VRefAuto": 0.0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 0,''' + \
26172633
''' "Pt": [{"V": 94.0, "Var": 20.0}, {"V": 95.7, "Var": 0.0}, {"V": 105.0, "Var": 0.0},''' + \
26182634
''' {"V": 108.0, "Var": -20.0}]}], "mid": null, "error": "", "model_id": 705}]}'''
26192635

@@ -2733,14 +2749,15 @@ def test_get_mb(self):
27332749
m.groups['Crv'][0].points['DeptRef'].sf_value = 3
27342750
m.groups['Crv'][0].points['Pri'].sf_required = True
27352751
m.groups['Crv'][0].points['Pri'].sf_value = 3
2736-
assert d.get_mb(computed=True) == b'\x02\xc1\x00C\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00' \
2737-
b'\x00\x00\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x03' \
2738-
b'\xe8\x03\xe8\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00' \
2739-
b'\x01\x00\\\x00\x1e\x00`\x00\x00\x00g\x00\x00\x00k\x00\x1e\x00\x04' \
2740-
b'\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06' \
2741-
b'\x00\x00\x00]\x00\x1e\x00_\x00\x00\x00f\x00\x00\x00j\x00(\x00\x04' \
2742-
b'\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06' \
2743-
b'\x00\x00\x00^\x00\x14\x00_\x00\x00\x00i\x00\x00\x00l\x00\x14'
2752+
assert d.get_mb(computed=True) == b'\x02\xc1\x00C\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00' \
2753+
b'\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x03\xe8\x03\xe8\x00' \
2754+
b'\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x01\x00\\\x00\x1e\x00`' \
2755+
b'\x00\x00\x00g\x00\x00\x00k\x00\x1e\x00\x04\x00\x01\x00\x01\x00\x00\x00' \
2756+
b'\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x00\x00]\x00\x1e\x00_\x00\x00' \
2757+
b'\x00f\x00\x00\x00j\x00(\x00\x04\x00\x01\x00\x01\x00\x00\x00\x00\xff\xff' \
2758+
b'\xff\xff\x00\x00\x00\x06\x00\x00\x00^\x00\x14\x00_\x00\x00\x00i\x00\x00' \
2759+
b'\x00l\x00\x14'
2760+
27442761

27452762
def test_set_mb(self):
27462763
d = device.Device()

0 commit comments

Comments
 (0)