Skip to content

Commit

Permalink
Merge pull request #35 from sunspec/development
Browse files Browse the repository at this point in the history
1.0.4 Update
  • Loading branch information
bobfox authored May 1, 2021
2 parents 71a659f + 36197c8 commit ca6af43
Show file tree
Hide file tree
Showing 14 changed files with 833 additions and 1,037 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

setup(
name='pysunspec2',
version='1.0.3',
version='1.0.4',
description='Python SunSpec Tools',
author='SunSpec Alliance',
author_email='support@sunspec.org',
Expand Down
73 changes: 54 additions & 19 deletions sunspec2/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def get_model_info(model_id):
for pdef in points:
info = mb.point_type_info.get(pdef[mdef.TYPE])
plen = pdef.get(mdef.SIZE, None)
if plen is None:
if plen is not None:
glen += info.len
except:
raise
Expand Down Expand Up @@ -133,6 +133,11 @@ def __init__(self, pdef=None, model=None, group=None, model_offset=0, data=None,
self.sf = None # scale factor point name
self.sf_value = None # value of scale factor
self.sf_required = False # point has a scale factor
self.read_func = None # function to be called on read
self.read_func_arg = None # the argument passed to the read_func
self.write_func = None # function to be called on write
self.write_func_arg = None # the argument passed to the write_func

if pdef:
self.sf_required = (pdef.get(mdef.SF) is not None)
if self.sf_required:
Expand Down Expand Up @@ -189,6 +194,10 @@ def cvalue(self, v):
self.set_value(v, computed=True, dirty=True)

def get_value(self, computed=False):
# call read function, if set
if self.read_func:
self.read_func(self.model, self.read_func_arg)

v = self._value
if computed and v is not None:
if self.sf_required:
Expand Down Expand Up @@ -227,16 +236,29 @@ def set_value(self, data=None, computed=False, dirty=None):
(self.sf, self.pdef['name']))
else:
raise ModelError('Scale factor %s for point %s not found' % (self.sf, self.pdef['name']))
if self.sf_value:
if self.sf_value is not None:
self._value = round(round(float(v), abs(self.sf_value)) / math.pow(10, self.sf_value))
else:
self._value = v
else:
self._value = v

# call write function, if set
# should be used to set indication for subsequent processing rather than do detailed processing
if self.write_func:
self.write_func(self.model, self.write_func_arg)

def set_read_func(self, func, arg=None):
self.read_func = func
self.read_func_arg = arg

def set_write_func(self, func, arg=None):
self.write_func = func
self.write_func_arg = arg

def get_mb(self, computed=False):
v = self._value
data = None
data = err = None
if computed and v is not None:
if self.sf_required:
if self.sf_value is None:
Expand All @@ -252,17 +274,27 @@ def get_mb(self, computed=False):
sfv = self.sf_value
if sfv:
v = int(v * math.pow(10, sfv))
data = self.info.to_data(v, (int(self.len) * 2))
try:
data = self.info.to_data(v, (int(self.len) * 2))
except Exception as e:
err = 'Error getting point value %s %s: %s' % (self.pdef[mdef.NAME], v, e)
if err:
raise ModelError(err)
elif v is None:
data = mb.create_unimpl_value(self.pdef[mdef.TYPE], len=(int(self.len) * 2))

if data is None:
data = self.info.to_data(v, (int(self.len) * 2))
try:
data = self.info.to_data(v, (int(self.len) * 2))
except Exception as e:
err = 'Error getting point value %s %s: %s' % (self.pdef[mdef.NAME], v, e)
if err:
raise ModelError(err)
return data

def set_mb(self, data=None, computed=False, dirty=None):
mb_len = self.len
try:
mb_len = self.len
# if not enough data, do not set but consume the data
if len(data) < mb_len * 2:
return len(data)
Expand Down Expand Up @@ -310,12 +342,14 @@ def __init__(self, gdef=None, model=None, model_offset=0, group_len=0, data=None
points = self.gdef.get(mdef.POINTS)
if points:
for pdef in points:
p = point_class(pdef, model=self.model, group=self, model_offset=model_offset, data=data,
data_offset=data_offset)
self.points_len += p.len
model_offset += p.len
data_offset += p.len
self.points[pdef[mdef.NAME]] = p
# allow legacy model 1 to have an alternate length of 65 registers
if self.len != 65 or model.model_id != 1 or pdef[mdef.NAME] != 'Pad':
p = point_class(pdef, model=self.model, group=self, model_offset=model_offset, data=data,
data_offset=data_offset)
self.points_len += p.len
model_offset += p.len
data_offset += p.len
self.points[pdef[mdef.NAME]] = p
# initialize groups
groups = self.gdef.get(mdef.GROUPS)
if groups:
Expand Down Expand Up @@ -644,13 +678,14 @@ def add_model(self, model):
model_list.append(model)
# add by group id
gname = model.gname
model_list = self.models.get(gname)
if model_list is None:
model_list = []
self.models[gname] = model_list
model_list.append(model)
# add to model list
self.model_list.append(model)
if gname is not None:
model_list = self.models.get(gname)
if model_list is None:
model_list = []
self.models[gname] = model_list
model_list.append(model)
# add to model list
self.model_list.append(model)

model.device = self

Expand Down
3 changes: 1 addition & 2 deletions sunspec2/mdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,5 +451,4 @@ def get_group_len_points_index(group_def):

if __name__ == "__main__":

model_def = from_json_file('./models/json/model_711.json')
print(get_group_len_points_index(model_def.get(GROUP)))
pass
3 changes: 3 additions & 0 deletions sunspec2/modbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ def scan(self, progress=None, delay=None, connect=True):
except SunSpecModbusClientError as e:
if not error:
error = str(e)
except modbus_client.ModbusClientTimeout as e:
if not error:
error = str(e)
except modbus_client.ModbusClientException:
pass

Expand Down
6 changes: 4 additions & 2 deletions sunspec2/smdx.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ def from_smdx(element):
mdef.TYPE: mdef.TYPE_GROUP,
mdef.POINTS: [
{mdef.NAME: 'ID', mdef.VALUE: mid,
mdef.DESCRIPTION: 'Model identifier', mdef.LABEL: 'Model ID',
mdef.DESCRIPTION: 'Model identifier', mdef.LABEL: 'Model ID', mdef.SIZE: 1,
mdef.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16},
{mdef.NAME: 'L',
mdef.DESCRIPTION: 'Model length', mdef.LABEL: 'Model Length',
mdef.DESCRIPTION: 'Model length', mdef.LABEL: 'Model Length', mdef.SIZE: 1,
mdef.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16}
]
}
Expand Down Expand Up @@ -316,6 +316,8 @@ def from_smdx_point(element):
if plen is None:
raise mdef.ModelDefinitionError('Missing len attribute for point: %s' % pid)
point_def[mdef.SIZE] = plen
else:
point_def[mdef.SIZE] = mdef.point_type_info.get(ptype)['len']
mandatory = element.attrib.get(SMDX_ATTR_MANDATORY, SMDX_MANDATORY_FALSE)
if mandatory not in smdx_mandatory_types:
raise mdef.ModelDefinitionError('Unknown mandatory type: %s' % mandatory)
Expand Down
12 changes: 9 additions & 3 deletions sunspec2/spreadsheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,17 @@ def to_spreadsheet_point(ss, point, has_notes, addr_offset=None, group_offset=No
row[NAME_IDX] = name
else:
raise Exception('Point missing name attribute')

ptype = point.get(mdef.TYPE, '')
if ptype != '':
row[TYPE_IDX] = ptype
else:

if ptype == '':
raise Exception('Point %s missing type attribute' % name)

if mdef.point_type_info.get(ptype) is None:
raise Exception('Unknown point type %s for point %s' % (ptype, name))

row[TYPE_IDX] = ptype

if addr_offset is not None:
row[ADDRESS_OFFSET_IDX] = addr_offset
elif group_offset is not None:
Expand Down
12 changes: 12 additions & 0 deletions sunspec2/tests/mock_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,15 @@ def clear_buffer(self):

def mock_socket(AF_INET, SOCK_STREAM):
return MockSocket()


def mock_tcp_connect(self):
if self.client.socket is None:
self.client.socket = mock_socket('foo', 'bar')
self.client.socket.settimeout(999)
self.client.socket.connect((999, 999))
pass


def mock_tcp_disconnect(self):
pass
75 changes: 46 additions & 29 deletions sunspec2/tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -1799,9 +1799,9 @@ def test_get_json(self):
m.groups['Crv'][0].points['DeptRef'].sf_value = 3
m.groups['Crv'][0].points['Pri'].sf_required = True
m.groups['Crv'][0].points['Pri'].sf_value = 3
assert m.groups['Crv'][0].get_json(computed=True) == '''{"ActPt": 4, "DeptRef": 1000.0,''' + \
''' "Pri": 1000.0, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null,''' + \
''' "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 92.0, "Var": 30.0}, {"V": 96.7, "Var": 0.0},''' + \
assert m.groups['Crv'][0].get_json(computed=True) == '''{"ActPt": 4, "DeptRef": 1000.0, "Pri": 1000.0,''' + \
''' "VRef": 0.01, "VRefAuto": 0.0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6,''' + \
''' "ReadOnly": 1, "Pt": [{"V": 92.0, "Var": 30.0}, {"V": 96.7, "Var": 0.0},''' + \
''' {"V": 103.0, "Var": 0.0}, {"V": 107.0, "Var": -30.0}]}'''

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

def test_set_mb(self):
gdata_705 = {
Expand Down Expand Up @@ -2299,6 +2299,21 @@ def test__init__(self):
assert m2.mid is None
assert m2.device is None

def test_model_1(self):

mdata = {
"ID": 1,
"L": 68,
"Mn": "Test manuf",
"Md": "Test model",
"Opt": "Test options",
"Vr": "Test version",
"SN": "Test serial num",
"DA": 12,
"Pad": 0
}
m = device.Model(1, data=mdata)

def test__error(self):
m = device.Model(704)
m.add_error('test error')
Expand Down Expand Up @@ -2467,16 +2482,16 @@ def test_get_dict(self):
assert d.get_dict(computed=True) == {'name': None, 'did': None, 'models': [
{'ID': 705, 'L': 67, 'Ena': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, 'RvrtTms': 0,
'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'RspTms_SF': None, 'Crv': [
{'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None,
{'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None,
'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 1,
'Pt': [{'V': 92.0, 'Var': 30.0}, {'V': 96.7, 'Var': 0.0}, {'V': 103.0, 'Var': 0.0},
{'V': 107.0, 'Var': -30.0}]},
{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None,
'RspTms': 6, 'ReadOnly': 0,
{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None,
'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 0,
'Pt': [{'V': 93.0, 'Var': 30.0}, {'V': 95.7, 'Var': 0.0}, {'V': 102.0, 'Var': 0.0},
{'V': 106.0, 'Var': -40.0}]},
{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None,
'RspTms': 6, 'ReadOnly': 0,
{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None,
'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 0,
'Pt': [{'V': 94.0, 'Var': 20.0}, {'V': 95.7, 'Var': 0.0}, {'V': 105.0, 'Var': 0.0},
{'V': 108.0, 'Var': -20.0}]}], 'mid': None, 'error': '', 'model_id': 705}]}

Expand Down Expand Up @@ -2603,17 +2618,18 @@ def test_get_json(self):
m.groups['Crv'][0].points['DeptRef'].sf_value = 3
m.groups['Crv'][0].points['Pri'].sf_required = True
m.groups['Crv'][0].points['Pri'].sf_value = 3
assert d.get_json(computed=True) == '''{"name": null, "did": null, "models": [{"ID": 705,''' + \
''' "L": 67, "Ena": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3, "RvrtTms": 0,''' + \
''' "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2, "RspTms_SF": null, ''' + \
'''"Crv": [{"ActPt": 4, "DeptRef": 1000.0, "Pri": 1000.0, "VRef": 1, "VRefAuto": 0, ''' + \
'''"VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1, ''' + \
'''"Pt": [{"V": 92.0, "Var": 30.0}, {"V": 96.7, "Var": 0.0}, {"V": 103.0, "Var": 0.0},''' + \
''' {"V": 107.0, "Var": -30.0}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \
''' "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 0,''' + \
''' "Pt": [{"V": 93.0, "Var": 30.0}, {"V": 95.7, "Var": 0.0}, {"V": 102.0, "Var": 0.0},''' + \
''' {"V": 106.0, "Var": -40.0}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \
assert d.get_json(computed=True) == '''{"name": null, "did": null, "models":''' + \
''' [{"ID": 705, "L": 67, "Ena": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0,''' + \
''' "NPt": 4, "NCrv": 3, "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2,''' + \
''' "DeptRef_SF": -2, "RspTms_SF": null, "Crv": [{"ActPt": 4, "DeptRef": 1000.0,''' + \
''' "Pri": 1000.0, "VRef": 0.01, "VRefAuto": 0.0, "VRefAutoEna": null,''' + \
''' "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 92.0, "Var": 30.0},''' + \
''' {"V": 96.7, "Var": 0.0}, {"V": 103.0, "Var": 0.0}, {"V": 107.0, "Var": -30.0}]},''' + \
''' {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 0.01, "VRefAuto": 0.0,''' + \
''' "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 0,''' + \
''' "Pt": [{"V": 93.0, "Var": 30.0}, {"V": 95.7, "Var": 0.0}, {"V": 102.0, "Var": 0.0},''' + \
''' {"V": 106.0, "Var": -40.0}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 0.01,''' + \
''' "VRefAuto": 0.0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 0,''' + \
''' "Pt": [{"V": 94.0, "Var": 20.0}, {"V": 95.7, "Var": 0.0}, {"V": 105.0, "Var": 0.0},''' + \
''' {"V": 108.0, "Var": -20.0}]}], "mid": null, "error": "", "model_id": 705}]}'''

Expand Down Expand Up @@ -2733,14 +2749,15 @@ def test_get_mb(self):
m.groups['Crv'][0].points['DeptRef'].sf_value = 3
m.groups['Crv'][0].points['Pri'].sf_required = True
m.groups['Crv'][0].points['Pri'].sf_value = 3
assert d.get_mb(computed=True) == b'\x02\xc1\x00C\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00' \
b'\x00\x00\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x03' \
b'\xe8\x03\xe8\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00' \
b'\x01\x00\\\x00\x1e\x00`\x00\x00\x00g\x00\x00\x00k\x00\x1e\x00\x04' \
b'\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06' \
b'\x00\x00\x00]\x00\x1e\x00_\x00\x00\x00f\x00\x00\x00j\x00(\x00\x04' \
b'\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06' \
b'\x00\x00\x00^\x00\x14\x00_\x00\x00\x00i\x00\x00\x00l\x00\x14'
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' \
b'\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x03\xe8\x03\xe8\x00' \
b'\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x01\x00\\\x00\x1e\x00`' \
b'\x00\x00\x00g\x00\x00\x00k\x00\x1e\x00\x04\x00\x01\x00\x01\x00\x00\x00' \
b'\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x00\x00]\x00\x1e\x00_\x00\x00' \
b'\x00f\x00\x00\x00j\x00(\x00\x04\x00\x01\x00\x01\x00\x00\x00\x00\xff\xff' \
b'\xff\xff\x00\x00\x00\x06\x00\x00\x00^\x00\x14\x00_\x00\x00\x00i\x00\x00' \
b'\x00l\x00\x14'


def test_set_mb(self):
d = device.Device()
Expand Down
Loading

0 comments on commit ca6af43

Please sign in to comment.