From ff876b884a010467cfddebc09849cf0f1d25d9ad Mon Sep 17 00:00:00 2001 From: Johnson Date: Thu, 1 Oct 2020 19:21:57 -0600 Subject: [PATCH 1/2] TLS support --- build/lib/sunspec2/__init__.py | 2 + build/lib/sunspec2/device.py | 668 ++++ build/lib/sunspec2/file/__init__.py | 0 build/lib/sunspec2/file/client.py | 97 + build/lib/sunspec2/mb.py | 341 ++ build/lib/sunspec2/mdef.py | 399 +++ build/lib/sunspec2/modbus/__init__.py | 0 build/lib/sunspec2/modbus/client.py | 379 +++ build/lib/sunspec2/modbus/modbus.py | 717 +++++ build/lib/sunspec2/smdx.py | 372 +++ build/lib/sunspec2/spreadsheet.py | 457 +++ build/lib/sunspec2/tests/__init__.py | 0 build/lib/sunspec2/tests/mock_port.py | 43 + build/lib/sunspec2/tests/mock_socket.py | 37 + .../lib/sunspec2/tests/test_data/__init__.py | 0 .../sunspec2/tests/test_data/device_1547.json | 612 ++++ .../lib/sunspec2/tests/test_data/smdx_304.csv | 8 + .../sunspec2/tests/test_data/wb_701-705.xlsx | Bin 0 -> 34344 bytes build/lib/sunspec2/tests/test_device.py | 2845 +++++++++++++++++ build/lib/sunspec2/tests/test_file_client.py | 2668 ++++++++++++++++ build/lib/sunspec2/tests/test_mb.py | 221 ++ build/lib/sunspec2/tests/test_mdef.py | 307 ++ .../lib/sunspec2/tests/test_modbus_client.py | 731 +++++ .../lib/sunspec2/tests/test_modbus_modbus.py | 206 ++ build/lib/sunspec2/tests/test_smdx.py | 250 ++ build/lib/sunspec2/tests/test_spreadsheet.py | 591 ++++ build/lib/sunspec2/tests/test_xlsx.py | 910 ++++++ build/lib/sunspec2/xlsx.py | 387 +++ sunspec2/modbus/client.py | 13 + sunspec2/modbus/modbus.py | 51 + 30 files changed, 13312 insertions(+) create mode 100644 build/lib/sunspec2/__init__.py create mode 100644 build/lib/sunspec2/device.py create mode 100644 build/lib/sunspec2/file/__init__.py create mode 100644 build/lib/sunspec2/file/client.py create mode 100644 build/lib/sunspec2/mb.py create mode 100644 build/lib/sunspec2/mdef.py create mode 100644 build/lib/sunspec2/modbus/__init__.py create mode 100644 build/lib/sunspec2/modbus/client.py create mode 100644 build/lib/sunspec2/modbus/modbus.py create mode 100644 build/lib/sunspec2/smdx.py create mode 100644 build/lib/sunspec2/spreadsheet.py create mode 100644 build/lib/sunspec2/tests/__init__.py create mode 100644 build/lib/sunspec2/tests/mock_port.py create mode 100644 build/lib/sunspec2/tests/mock_socket.py create mode 100644 build/lib/sunspec2/tests/test_data/__init__.py create mode 100644 build/lib/sunspec2/tests/test_data/device_1547.json create mode 100644 build/lib/sunspec2/tests/test_data/smdx_304.csv create mode 100644 build/lib/sunspec2/tests/test_data/wb_701-705.xlsx create mode 100644 build/lib/sunspec2/tests/test_device.py create mode 100644 build/lib/sunspec2/tests/test_file_client.py create mode 100644 build/lib/sunspec2/tests/test_mb.py create mode 100644 build/lib/sunspec2/tests/test_mdef.py create mode 100644 build/lib/sunspec2/tests/test_modbus_client.py create mode 100644 build/lib/sunspec2/tests/test_modbus_modbus.py create mode 100644 build/lib/sunspec2/tests/test_smdx.py create mode 100644 build/lib/sunspec2/tests/test_spreadsheet.py create mode 100644 build/lib/sunspec2/tests/test_xlsx.py create mode 100644 build/lib/sunspec2/xlsx.py diff --git a/build/lib/sunspec2/__init__.py b/build/lib/sunspec2/__init__.py new file mode 100644 index 0000000..b60550d --- /dev/null +++ b/build/lib/sunspec2/__init__.py @@ -0,0 +1,2 @@ +# pySunSpec version +VERSION = '1.0.0' diff --git a/build/lib/sunspec2/device.py b/build/lib/sunspec2/device.py new file mode 100644 index 0000000..6661909 --- /dev/null +++ b/build/lib/sunspec2/device.py @@ -0,0 +1,668 @@ +import json +import math +from collections import OrderedDict +import os +import sunspec2.mdef as mdef +import sunspec2.smdx as smdx +import sunspec2.mb as mb + + +class ModelError(Exception): + pass + + +this_dir, this_filename = os.path.split(__file__) +models_dir = os.path.join(this_dir, 'models') + +model_defs_path = ['.', models_dir] +model_path_options = ['.', 'json', 'smdx'] + + +def get_model_defs_path(): + return model_defs_path + + +def set_model_defs_path(path_list): + if not isinstance(path_list, list): + raise mdef.ModelDefinitionError('Invalid path list type, path list is not a list') + global model_defs_path + model_defs_path = path_list + + +def get_model_info(model_id): + try: + glen = 0 + model_def = get_model_def(model_id) + gdef = model_def.get(mdef.GROUP) + # check if groups have a count point + has_group_count = check_group_count(gdef) + # if group has count point, compute the length of top-level points + if has_group_count: + points = gdef.get(mdef.POINTS) + if points: + for pdef in points: + info = mb.point_type_info.get(pdef[mdef.TYPE]) + plen = pdef.get(mdef.SIZE, None) + if plen is None: + glen += info.len + except: + raise + + return (model_def, has_group_count, glen) + + +def check_group_count(gdef): + has_group_count = (gdef.get(mdef.COUNT) is not None) + if not has_group_count: + groups = gdef.get(mdef.GROUPS) + if groups: + for g in groups: + has_group_count = check_group_count(g) + if has_group_count: + break + return has_group_count + + +def get_model_def(model_id, mapping=True): + try: + model_id = int(model_id) + except: + raise mdef.ModelDefinitionError('Invalid model id: %s' % model_id) + + model_def_file_json = mdef.to_json_filename(model_id) + model_def_file_smdx = smdx.to_smdx_filename(model_id) + model_def = None + for path in model_defs_path: + # look in directory, then json/, then smdx/ + for path_option in model_path_options: + try: + model_def = mdef.from_json_file(os.path.join(path, path_option, model_def_file_json)) + except FileNotFoundError: + pass + except Exception as e: + raise mdef.ModelDefinitionError('Error loading model definition for model %s: %s' % + (model_id, str(e))) + + if model_def is None: + try: + model_def = smdx.from_smdx_file(os.path.join(path, path_option, model_def_file_smdx)) + except FileNotFoundError: + pass + except Exception as e: + raise mdef.ModelDefinitionError('Error loading model definition for model %s: %s' % + (model_id, str(e))) + + if model_def is not None: + if mapping: + add_mappings(model_def[mdef.GROUP]) + return model_def + raise mdef.ModelDefinitionError('Model definition not found for model %s\nLooking in: %s' % + (model_id, os.path.join(path, path_option, model_def_file_json))) + + +# add id mapping for points and groups for more efficient lookup by id +def add_mappings(group_def): + point_defs = {} + group_defs = {} + + points = group_def.get(mdef.POINTS, None) + if points: + for p in group_def[mdef.POINTS]: + point_defs[p[mdef.NAME]] = p + + groups = group_def.get(mdef.GROUPS, None) + if groups: + for g in group_def[mdef.GROUPS]: + group_defs[g[mdef.NAME]] = g + add_mappings(g) + + group_def['point_defs'] = point_defs + group_def['group_defs'] = group_defs + + +class Point(object): + def __init__(self, pdef=None, model=None, group=None, model_offset=0, data=None, data_offset=0): + self.model = model # model object containing the point + self.group = group + self.pdef = pdef # point definition + self.len = 0 # mb register len of point + self.info = None # point def info + self.offset = model_offset # mb register offset from beginning of the model + self._value = None # value + self.dirty = False # value has been changed without being written + self.sf = None # scale factor point name + self.sf_value = None # value of scale factor + self.sf_required = False # point has a scale factor + if pdef: + self.sf_required = (pdef.get(mdef.SF) is not None) + if self.sf_required: + sf = self.pdef.get(mdef.SF) + try: + self.sf_value = int(sf) + except ValueError: + self.sf = sf + + self.info = mb.point_type_info.get(pdef[mdef.TYPE]) + plen = pdef.get(mdef.SIZE, None) + self.len = self.info.len + if plen is not None: + self.len = int(plen) + + if data is not None: + self._set_data(data=data, offset=data_offset) + + def __str__(self): + return self.disp() + + def disp(self, indent=None): + if indent is None: + indent = '' + return '%s%s: %s\n' % (indent, self.pdef[mdef.NAME], self.value) + + def _set_data(self, data=None, offset=0): + if isinstance(data, (bytes, bytearray)): + byte_offset = offset * 2 + if byte_offset < len(data): + self.set_mb(data=data[byte_offset:], dirty=False) + elif isinstance(data, dict): + value = data.get(self.pdef[mdef.NAME]) + if value is not None: + self.set_value(data=value) + + def resolve_sf(self): + pass + + @property + def value(self): + return self.get_value() + + @value.setter + def value(self, v): + self.set_value(v, dirty=True) + + @property + def cvalue(self): + return self.get_value(computed=True) + + @cvalue.setter + def cvalue(self, v): + self.set_value(v, computed=True, dirty=True) + + def get_value(self, computed=False): + v = self._value + if computed and v is not None: + if self.sf_required: + if self.sf_value is None: + if self.sf: + sf = self.group.points.get(self.sf) + if sf is None: + sf = self.model.points.get(self.sf) + if sf is not None: + self.sf_value = sf.value + else: + raise ModelError('Scale factor %s for point %s not found' % (self.sf, self.pdef['name'])) + if self.sf_value: + sfv = self.sf_value + if sfv: + v = v * math.pow(10, sfv) + return v + + def set_value(self, data=None, computed=False, dirty=None): + v = data + if dirty is not None: + self.dirty = dirty + if computed: + if self.sf_required: + if self.sf_value is None: + if self.sf: + sf = self.group.points.get(self.sf) + if sf is None: + sf = self.model.points.get(self.sf) + if sf is not None: + self.sf_value = sf.value + if sf.value is not None: + self.sf_value = sf.value + else: + raise ModelError('SF field %s value not initialized for point %s' % + (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: + self._value = int(round(float(v), abs(self.sf_value)) / math.pow(10, self.sf_value)) + else: + self._value = v + else: + self._value = v + + def get_mb(self, computed=False): + v = self._value + data = None + if computed and v is not None: + if self.sf_required: + if self.sf_value is None: + if self.sf: + sf = self.group.points.get(self.sf) + if sf is None: + sf = self.model.points.get(self.sf) + if sf is not None: + self.sf_value = sf.value + else: + raise ModelError('Scale factor %s for point %s not found' % (self.sf, self.pdef['name'])) + if self.sf_value: + sfv = self.sf_value + if sfv: + v = int(v * math.pow(10, sfv)) + data = self.info.to_data(v, (int(self.len) * 2)) + 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)) + return data + + def set_mb(self, data=None, computed=False, dirty=None): + try: + mb_len = self.len + # if not enough data, do not set but consume the data + if len(data) < mb_len: + return len(data) + self.set_value(self.info.data_to(data[:mb_len * 2]), computed=computed, dirty=dirty) + if not self.info.is_impl(self.value): + self.set_value(None) + self.sf_value = None + except Exception as e: + self.model.add_error('Error setting value for %s: %s' % (self.pdef[mdef.NAME], str(e))) + return mb_len + + +class Group(object): + def __init__(self, gdef=None, model=None, model_offset=0, group_len=0, data=None, data_offset=0, group_class=None, + point_class=Point, index=None): + self.gdef = gdef + self.model = model + self.gname = None + self.offset = model_offset + self.len = group_len + self.points = OrderedDict() + self.groups = OrderedDict() + self.points_len = 0 + self.group_class = group_class + self.index = index + + if group_class is None: + self.group_class = self.__class__ + + if gdef is not None: + self.gname = gdef[mdef.NAME] + self.gdef = gdef + + # initialize points and point values, if present + 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 + # initialize groups + groups = self.gdef.get(mdef.GROUPS) + if groups: + for gdef in groups: + gdata = self._group_data(data=data, name=gdef[mdef.NAME]) + if gdef.get(mdef.COUNT) is not None: + g = self._init_repeating_group(gdef=gdef, model_offset=model_offset, data=gdata, + data_offset=data_offset) + group_count = len(g) + if group_count: + self.groups[gdef[mdef.NAME]] = g + glen = g[0].len * group_count + model_offset += glen + data_offset += glen + else: + g = self.group_class(gdef, model=self.model, model_offset=model_offset, data=gdata, + data_offset=data_offset) + self.groups[gdef[mdef.NAME]] = g + model_offset += g.len + data_offset += g.len + mlen = model_offset - self.offset + if self.len: + if self.len + 2 != mlen: + self.model.add_error('Model length %s not equal to calculated model length %s for model %s' % + (self.len + 2, mlen, self.model.model_id )) + self.len = mlen + + len_point = self.points.get('L') + if len_point: + len_point.set_value(self.len - 2) + + id_point = self.points.get('ID') + if id_point: + id_val = id_point.pdef.get('value') + if id_val: + id_point.set_value(id_point.pdef['value']) + + def __getattr__(self, attr): + v = self.points.get(attr) + if v is None: + v = self.groups.get(attr) + if v is None: + raise AttributeError("%s object has no attribute %s" % (self.group_class.__name__, attr)) + return v + + def __str__(self): + return self.disp() + + def disp(self, indent=None): + if indent is None: + indent = '' + if self.index is not None: + index = '(%s)' % self.index + else: + index = '' + s = '%s%s%s:\n' % (indent, self.gdef[mdef.NAME], index) + + indent += ' ' + for k, p in self.points.items(): + s += p.disp(indent) + + for k, g in self.groups.items(): + if isinstance(g, list): + for i in range(len(g)): + s += g[i].disp(indent=indent) + else: + s += g.disp(indent) + + return s + + def _group_data(self, data=None, name=None, index=None): + if isinstance(data, dict): + data = data.get(name) + elif isinstance(data, list): + if index is not None and len(data) > index: + data = data[index] + else: + data = None + return data + + # check group count in dict data + def _get_data_group_count(self, data=None): + if isinstance(data, list): + return len(data) + + def _init_repeating_group(self, gdef=None, model_offset=None, data=None, data_offset=0): + groups = [] + # get group count as a constant + count = None + try: + count = int(gdef[mdef.COUNT]) + except ValueError: + pass + except AttributeError: + self.model.add_error('Count definition %s missing for group %s' % (gdef[mdef.COUNT], gdef[mdef.NAME])) + if count is None: + # get count as model point + count_attr = getattr(self.model, gdef[mdef.COUNT], None) + if count_attr is None: + raise ModelError('Count field %s undefined for group %s' % (gdef[mdef.COUNT], gdef[mdef.NAME])) + count = count_attr.value + if count is None: + raise ModelError('Count field %s value not initialized for group %s ' % + (gdef[mdef.COUNT], gdef[mdef.NAME])) + + data_group_count = self._get_data_group_count(data=data) + model_len = self.model.len + if model_len <= self.model.points_len: + # if legacy model definition but it is defined in format that number of groups are known, use that count + # to avoid having to figure out the length in the model data + if count == 0 and data_group_count: + count = data_group_count + + # allocate the group entries if the count is available + if count > 0: + for i in range(count): + gdata = self._group_data(data=data, index=i) + g = self.group_class(gdef=gdef, model=self.model, model_offset=model_offset, data=gdata, + data_offset=data_offset, index=i+1) + model_offset += g.len + data_offset += g.len + groups.append(g) + elif count == 0: + data_group_count = self._get_data_group_count(data=data) + # legacy model definition - need to calculate repeating count by model length + # 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]) + 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 + # count is model.len-model.points_len/group_points_len + # (ID and L points are not included in model length) + repeating_len = model_len - (self.model.points_len - 2) + if repeating_len > 0: + remaining = repeating_len % group_points_len + if remaining != 0: + raise ModelError('Repeating group count not consistent with model length for model %s,' + 'model repeating len = %s, model repeating group len = %s' % + (self.model._id, repeating_len, group_points_len)) + + count = int(repeating_len / group_points_len) + if count > 0: + groups.append(g) + 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, + data_offset=data_offset, index=i+2) + model_offset += g.len + data_offset += g.len + groups.append(g) + return groups + + def get_dict(self, computed=False): + d = {} + for pid, p in self.points.items(): + d[pid] = p.get_value(computed=computed) + for gid, group in self.groups.items(): + if isinstance(group, list): + glist = [] + for g in group: + glist.append(g.get_dict(computed=computed)) + d[gid] = glist + else: + d[gid] = group.get_dict(computed=computed) + return d + + def set_dict(self, data=None, computed=False, dirty=None): + groups = [] + group_def = self.gdef + for k, v in data.items(): + if k in group_def['point_defs']: + self.points[k].set_value(data=v, computed=computed, dirty=dirty) + elif k in group_def['group_defs']: + # write points first as group initialization may depend on point value for group counts + groups.append(k) + for k in groups: + if isinstance(self.groups[k], list): + i = 0 + for rg in self.groups[k]: + rg.set_dict(data[k][i], computed=computed, dirty=dirty) + i += 1 + else: + self.groups[k].set_dict(data[k], computed=computed, dirty=dirty) + + def get_json(self, computed=False): + return json.dumps(self.get_dict(computed=computed)) + + def set_json(self, data=None, computed=False, dirty=None): + if data is not None: + d = json.loads(data) + self.set_dict(d, computed=computed, dirty=dirty) + + def get_mb(self, computed=False): + data = bytearray() + for pid, point in self.points.items(): + data.extend(point.get_mb(computed=computed)) + for gid, group in self.groups.items(): + if isinstance(group, list): + for g in group: + data.extend(g.get_mb(computed=computed)) + else: + data.extend(group.get_mb(computed=computed)) + return bytes(data) + + def set_mb(self, data=None, computed=False, dirty=None): + if data: + data_len = len(data) + else: + data_len = 0 + offset = 0 + for pid, point in self.points.items(): + if data_len > offset: + mb_len = point.set_mb(data[offset:], computed=computed, dirty=dirty) + if mb_len is not None: + offset += mb_len * 2 + + for gid, group in self.groups.items(): + if isinstance(group, list): + for g in group: + if data_len > offset: + mb_len = g.set_mb(data[offset:], computed=computed, dirty=dirty) + if mb_len is not None: + offset += mb_len * 2 + else: + return None + else: + if data_len > offset: + mb_len = group.set_mb(data[offset:], computed=computed, dirty=dirty) + if mb_len is not None: + offset += mb_len * 2 + else: + return None + return int(offset/2) + + +class Model(Group): + def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, data=None, group_class=Group): + self.model_id = model_id + self.model_addr = model_addr + self.model_len = model_len + self.model_def = model_def + self.error_info = '' + self.mid = None + self.device = None + self.model = self + + gdef = None + try: + if self.model_def is None and model_id is not None: + self.model_def = get_model_def(model_id) + if self.model_def is not None: + gdef = self.model_def.get(mdef.GROUP) + except Exception as e: + self.add_error(str(e)) + + Group.__init__(self, gdef=gdef, model=self.model, model_offset=0, group_len=self.model_len, data=data, + data_offset=0, group_class=group_class) + + def add_error(self, error_info): + self.error_info = '%s%s\n' % (self.error_info, error_info) + + def get_dict(self, computed=False): + d = Group.get_dict(self, computed=computed) + d['mid'] = self.mid + d['error'] = self.error_info + d['model_id'] = self.model_id + return d + + +class Device(object): + def __init__(self, model_class=Model): + self.name = None + self.did = None + self.models = {} + self.model_list = [] + self.model_class = model_class + + def __getattr__(self, attr): + v = self.models.get(attr) + if v is None: + raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)) + return v + + def scan(self, data=None): + pass + + def add_model(self, model): + # add by model id + model_id = model.model_id + model_list = self.models.get(model_id) + if model_list is None: + model_list = [] + self.models[model_id] = model_list + 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) + + model.device = self + + def get_dict(self, computed=False): + d = {'name': self.name, 'did': self.did, 'models': []} + for m in self.model_list: + d['models'].append(m.get_dict(computed=computed)) + return d + + def get_json(self, computed=False): + return json.dumps(self.get_dict(computed=computed)) + + def get_mb(self, computed=False): + data = bytearray() + for m in self.model_list: + data.extend(m.get_mb(computed=computed)) + return bytes(data) + + def set_mb(self, data=None, computed=False, dirty=None): + if data: + data_len = len(data) + else: + data_len = 0 + offset = 0 + for m in self.model_list: + if data_len > offset: + mb_len = m.set_mb(data[offset:], dirty=dirty, computed=computed) + if mb_len is not None: + offset += mb_len * 2 + else: + return None + return int(offset/2) + + def find_mid(self, mid=None): + if mid is not None: + for m in self.model_list: + if m.mid == mid: + return m + + # assumes data should be used to create and initialize the models, does not currently update an initialized device + def _set_dict(self, data, computed=False, detail=False): + if self.model_list: + raise ModelError('Device already initialized') + self.name = data.get('name') + models = data.get('models') + for m in models: + if detail: + model_id = m['ID']['value'] + else: + model_id = m['ID'] + if model_id != mdef.END_MODEL_ID: + model_def = get_model_def(model_id) + model = Model(model_def=model_def, data=m, model_id=m['ID']) + self.add_model(model=model) diff --git a/build/lib/sunspec2/file/__init__.py b/build/lib/sunspec2/file/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/sunspec2/file/client.py b/build/lib/sunspec2/file/client.py new file mode 100644 index 0000000..398d5a2 --- /dev/null +++ b/build/lib/sunspec2/file/client.py @@ -0,0 +1,97 @@ +import json +import uuid +import sunspec2.mdef as mdef +import sunspec2.device as device +import sunspec2.mb as mb + + +class FileClientError(Exception): + pass + + +class FileClientPoint(device.Point): + + def read(self): + pass + + def write(self): + pass + + +class FileClientGroup(device.Group): + + def read(self): + pass + + def write(self): + pass + + +class FileClientModel(FileClientGroup): + def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, data=None, + group_class=FileClientGroup, point_class=FileClientPoint): + self.model_id = model_id + self.model_addr = model_addr + if model_len is None: + self.model_len = 0 + else: + self.model_len = model_len + self.model_def = model_def + self.error_info = '' + self.mid = None + self.device = None + self.model = self + + gdef = None + try: + if self.model_def is None and model_id is not None: + self.model_def = device.get_model_def(model_id) + if self.model_def is not None: + gdef = self.model_def.get(mdef.GROUP) + except Exception as e: + 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) + + def add_error(self, error_info): + self.error_info = '%s%s\n' % (self.error_info, error_info) + + +class FileClientDevice(device.Device): + def __init__(self, filename=None, addr=40002, model_class=FileClientModel): + device.Device.__init__(self, model_class=model_class) + self.did = str(uuid.uuid4()) + self.filename = filename + self.addr = addr + + def scan(self, data=None): + try: + if self.filename: + f = open(self.filename) + data = json.load(f) + + mid = 0 + addr = self.addr + for m in data.get('models'): + model_id = m.get('ID') + model_len = m.get('L') + if model_id != mb.SUNS_END_MODEL_ID: + model = self.model_class(model_id=model_id, model_addr=addr, model_len=model_len, model_def=None, + data=m) + model.mid = '%s_%s' % (self.did, mid) + mid += 1 + self.add_model(model) + addr += model.len + except Exception as e: + raise FileClientError(str(e)) + + def read(self): + return '' + + def write(self): + return + + +class FileClient(FileClientDevice): + pass diff --git a/build/lib/sunspec2/mb.py b/build/lib/sunspec2/mb.py new file mode 100644 index 0000000..93ce8c2 --- /dev/null +++ b/build/lib/sunspec2/mb.py @@ -0,0 +1,341 @@ +""" + Copyright (C) 2020 SunSpec Alliance + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +""" + +import struct +import base64 +import collections + +import sunspec2.mdef as mdef + +SUNS_BASE_ADDR_DEFAULT = 40000 +SUNS_SUNS_LEN = 2 + +SUNS_UNIMPL_INT16 = -32768 +SUNS_UNIMPL_UINT16 = 0xffff +SUNS_UNIMPL_ACC16 = 0 +SUNS_UNIMPL_ENUM16 = 0xffff +SUNS_UNIMPL_BITFIELD16 = 0xffff +SUNS_UNIMPL_INT32 = -2147483648 +SUNS_UNIMPL_UINT32 = 0xffffffff +SUNS_UNIMPL_ACC32 = 0 +SUNS_UNIMPL_ENUM32 = 0xffffffff +SUNS_UNIMPL_BITFIELD32 = 0xffffffff +SUNS_UNIMPL_IPADDR = 0 +SUNS_UNIMPL_INT64 = -9223372036854775808 +SUNS_UNIMPL_UINT64 = 0xffffffffffffffff +SUNS_UNIMPL_ACC64 = 0 +SUNS_UNIMPL_IPV6ADDR = 0 +SUNS_UNIMPL_FLOAT32 = 0x7fc00000 +SUNS_UNIMPL_FLOAT64 = 0x7ff8000000000000 +SUNS_UNIMPL_STRING = '\0' +SUNS_UNIMPL_SUNSSF = -32768 +SUNS_UNIMPL_EUI48 = 'FF:FF:FF:FF:FF:FF' +SUNS_UNIMPL_PAD = 0 + +SUNS_BLOCK_FIXED = 'fixed' +SUNS_BLOCK_REPEATING = 'repeating' + +SUNS_END_MODEL_ID = 0xffff + +unimpl_value = { + mdef.TYPE_INT16: SUNS_UNIMPL_INT16, + mdef.TYPE_UINT16: SUNS_UNIMPL_UINT16, + mdef.TYPE_ACC16: SUNS_UNIMPL_ACC16, + mdef.TYPE_ENUM16: SUNS_UNIMPL_ENUM16, + mdef.TYPE_BITFIELD16: SUNS_UNIMPL_BITFIELD16, + mdef.TYPE_INT32: SUNS_UNIMPL_INT32, + mdef.TYPE_UINT32: SUNS_UNIMPL_UINT32, + mdef.TYPE_ACC32: SUNS_UNIMPL_ACC32, + mdef.TYPE_ENUM32: SUNS_UNIMPL_ENUM32, + mdef.TYPE_BITFIELD32: SUNS_UNIMPL_BITFIELD32, + mdef.TYPE_IPADDR: SUNS_UNIMPL_IPADDR, + mdef.TYPE_INT64: SUNS_UNIMPL_INT64, + mdef.TYPE_UINT64: SUNS_UNIMPL_UINT64, + mdef.TYPE_ACC64: SUNS_UNIMPL_ACC64, + mdef.TYPE_IPV6ADDR: SUNS_UNIMPL_IPV6ADDR, + mdef.TYPE_FLOAT32: SUNS_UNIMPL_FLOAT32, + mdef.TYPE_STRING: SUNS_UNIMPL_STRING, + mdef.TYPE_SUNSSF: SUNS_UNIMPL_SUNSSF, + mdef.TYPE_EUI48: SUNS_UNIMPL_EUI48, + mdef.TYPE_PAD: SUNS_UNIMPL_PAD +} + + +def create_unimpl_value(vtype, len=None): + value = unimpl_value.get(vtype) + if vtype is None: + raise ValueError('Unknown SunSpec value type: %s' % vtype) + if vtype == mdef.TYPE_STRING: + if len is not None: + return b'\0' * len + else: + raise ValueError('Unimplemented value creation for string requires a length') + elif vtype == mdef.TYPE_IPV6ADDR: + return b'\0' * 16 + return point_type_info[vtype][3](value) + + +class SunSpecError(Exception): + pass + + +""" +Functions to pack and unpack data string values +""" + + +def data_to_s16(data): + s16 = struct.unpack('>h', data[:2]) + return s16[0] + + +def data_to_u16(data): + u16 = struct.unpack('>H', data[:2]) + return u16[0] + + +def data_to_s32(data): + s32 = struct.unpack('>l', data[:4]) + return s32[0] + + +def data_to_u32(data): + u32 = struct.unpack('>L', data[:4]) + return u32[0] + + +def data_to_s64(data): + s64 = struct.unpack('>q', data[:8]) + return s64[0] + + +def data_to_u64(data): + u64 = struct.unpack('>Q', data[:8]) + return u64[0] + + +def data_to_ipv6addr(data): + value = False + for i in data: + if i != 0: + value = True + break + if value and len(data) == 16: + return '%02X%02X%02X%02X:%02X%02X%02X%02X:%02X%02X%02X%02X:%02X%02X%02X%02X' % ( + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]) + + +def data_to_eui48(data): + value = False + for i in data: + if i != 0: + value = True + break + if value and len(data) == 8: + return '%02X:%02X:%02X:%02X:%02X:%02X' % ( + data[2], data[3], data[4], data[5], data[6], data[7]) + + +def data_to_f32(data): + f = struct.unpack('>f', data[:4]) + if str(f[0]) != str(float('nan')): + return f[0] + + +def data_to_f64(data): + d = struct.unpack('>d', data[:8]) + if str(d[0]) != str(float('nan')): + return d[0] + + +def data_to_str(data): + data = str(data, 'utf-8') + + if len(data) > 1: + data = data[0] + data[1:].rstrip('\0') + return data + + +def s16_to_data(s16, len=None): + return struct.pack('>h', s16) + + +def u16_to_data(u16, len=None): + return struct.pack('>H', u16) + + +def s32_to_data(s32, len=None): + return struct.pack('>l', s32) + + +def u32_to_data(u32, len=None): + return struct.pack('>L', u32) + + +def s64_to_data(s64, len=None): + return struct.pack('>q', s64) + + +def u64_to_data(u64, len=None): + return struct.pack('>Q', u64) + + +def ipv6addr_to_data(addr, slen=None): + s = base64.b16decode(addr.replace(':', '')) + if slen is None: + slen = len(s) + return struct.pack(str(slen) + 's', s) + + +def f32_to_data(f, len=None): + return struct.pack('>f', f) + + +def f64_to_data(f, len=None): + return struct.pack('>d', f) + + +def str_to_data(s, slen=None): + if slen is None: + slen = len(s) + s = bytes(s, 'utf-8') + return struct.pack(str(slen) + 's', s) + + +def eui48_to_data(eui48): + return (b'\x00\x00' + base64.b16decode(eui48.replace(':', ''))) + + +def is_impl_int16(value): + return not value == SUNS_UNIMPL_INT16 + + +def is_impl_uint16(value): + return not value == SUNS_UNIMPL_UINT16 + + +def is_impl_acc16(value): + return not value == SUNS_UNIMPL_ACC16 + + +def is_impl_enum16(value): + return not value == SUNS_UNIMPL_ENUM16 + + +def is_impl_bitfield16(value): + return not value == SUNS_UNIMPL_BITFIELD16 + + +def is_impl_int32(value): + return not value == SUNS_UNIMPL_INT32 + + +def is_impl_uint32(value): + return not value == SUNS_UNIMPL_UINT32 + + +def is_impl_acc32(value): + return not value == SUNS_UNIMPL_ACC32 + + +def is_impl_enum32(value): + return not value == SUNS_UNIMPL_ENUM32 + + +def is_impl_bitfield32(value): + return not value == SUNS_UNIMPL_BITFIELD32 + + +def is_impl_ipaddr(value): + return not value == SUNS_UNIMPL_IPADDR + + +def is_impl_int64(value): + return not value == SUNS_UNIMPL_INT64 + + +def is_impl_uint64(value): + return not value == SUNS_UNIMPL_UINT64 + + +def is_impl_acc64(value): + return not value == SUNS_UNIMPL_ACC64 + + +def is_impl_ipv6addr(value): + if value: + return not value[0] == '\0' + return False + + +def is_impl_float32(value): + return (value == value) and (value != None) + + +def is_impl_float64(value): + return (value == value) and (value != None) + + +def is_impl_string(value): + if value: + return not value[0] == '\0' + return False + + +def is_impl_sunssf(value): + return not value == SUNS_UNIMPL_SUNSSF + + +def is_impl_eui48(value): + return not value == SUNS_UNIMPL_EUI48 + + +def is_impl_pad(value): + return True + + +PointInfo = collections.namedtuple('PointInfo', 'len is_impl data_to to_data to_type default') +point_type_info = { + mdef.TYPE_INT16: PointInfo(1, is_impl_int16, data_to_s16, s16_to_data, mdef.to_int, 0), + mdef.TYPE_UINT16: PointInfo(1, is_impl_uint16, data_to_u16, u16_to_data, mdef.to_int, 0), + mdef.TYPE_COUNT: PointInfo(1, is_impl_uint16, data_to_u16, u16_to_data, mdef.to_int, 0), + mdef.TYPE_ACC16: PointInfo(1, is_impl_acc16, data_to_u16, u16_to_data, mdef.to_int, 0), + mdef.TYPE_ENUM16: PointInfo(1, is_impl_enum16, data_to_u16, u16_to_data, mdef.to_int, 0), + mdef.TYPE_BITFIELD16: PointInfo(1, is_impl_bitfield16, data_to_u16, u16_to_data, mdef.to_int, 0), + mdef.TYPE_PAD: PointInfo(1, is_impl_pad, data_to_u16, u16_to_data, mdef.to_int, 0), + mdef.TYPE_INT32: PointInfo(2, is_impl_int32, data_to_s32, s32_to_data, mdef.to_int, 0), + mdef.TYPE_UINT32: PointInfo(2, is_impl_uint32, data_to_u32, u32_to_data, mdef.to_int, 0), + mdef.TYPE_ACC32: PointInfo(2, is_impl_acc32, data_to_u32, u32_to_data, mdef.to_int, 0), + mdef.TYPE_ENUM32: PointInfo(2, is_impl_enum32, data_to_u32, u32_to_data, mdef.to_int, 0), + mdef.TYPE_BITFIELD32: PointInfo(2, is_impl_bitfield32, data_to_u32, u32_to_data, mdef.to_int, 0), + mdef.TYPE_IPADDR: PointInfo(2, is_impl_ipaddr, data_to_u32, u32_to_data, mdef.to_int, 0), + mdef.TYPE_INT64: PointInfo(4, is_impl_int64, data_to_s64, s64_to_data, mdef.to_int, 0), + mdef.TYPE_UINT64: PointInfo(4, is_impl_uint64, data_to_u64, u64_to_data, mdef.to_int, 0), + mdef.TYPE_ACC64: PointInfo(4, is_impl_acc64, data_to_u64, u64_to_data, mdef.to_int, 0), + mdef.TYPE_IPV6ADDR: PointInfo(8, is_impl_ipv6addr, data_to_ipv6addr, ipv6addr_to_data, mdef.to_str, 0), + mdef.TYPE_FLOAT32: PointInfo(2, is_impl_float32, data_to_f32, f32_to_data, mdef.to_float, 0), + mdef.TYPE_FLOAT64: PointInfo(4, is_impl_float64, data_to_f64, f64_to_data, mdef.to_float, 0), + mdef.TYPE_STRING: PointInfo(None, is_impl_string, data_to_str, str_to_data, mdef.to_str, ''), + mdef.TYPE_SUNSSF: PointInfo(1, is_impl_sunssf, data_to_s16, s16_to_data, mdef.to_int, 0), + mdef.TYPE_EUI48: PointInfo(4, is_impl_eui48, data_to_eui48, eui48_to_data, mdef.to_str, 0) +} diff --git a/build/lib/sunspec2/mdef.py b/build/lib/sunspec2/mdef.py new file mode 100644 index 0000000..bd9a561 --- /dev/null +++ b/build/lib/sunspec2/mdef.py @@ -0,0 +1,399 @@ +import json +import os + +''' +1. JSON is used for the native encoding of information model definitions. +2. JSON can be used to represent the values associated with information model points at a specific point in time. + +Python support for information models: +- Python model support is based on dictionaries and their afinity with JSON objects. + +Model instance notes: + +- If a model contains repeating groups, the group counts must be known to fully initialized the model. If fields are + accessed that depend on group counts that have not been initialized, a ModelError exception is generated. +- Points that have not been read or written contain a value of None. +- If a point that can not be changed from the initialized value is changed, a ModelError exception is generated. + +A model definition is represented as a dictionary using the constants defined in this file as the entry keys. + +A model definition is required to have a single top level group. + +A model dict + - must contain: 'id' and 'group' + +A group dict + - must contain: 'name', 'type', 'points' + - may contain: 'count', 'groups', 'label', 'description', 'notes', 'comments' + +A point dict + - must contain: 'name', 'type' + - may contain: 'count', 'size', 'sf', 'units', 'mandatory', 'access', 'symbols', 'label', 'description', 'notes', + 'comments' + +A symbol dict + - must contain: 'name', 'value' + - may contain: 'label', 'description', 'notes', 'comments' + +Example: + model_def = { + 'id': 123, + 'group': { + 'id': model_name, + 'groups': [], + 'points': [] + } +''' + +DEVICE = 'device' # device (device dict) ### currently not in the spec +MODEL = 'model' # model (model dict) ### currently not in the spec +GROUP = 'group' # top level model group (group dict) +GROUPS = 'groups' # groups in group (list of group dicts) +POINTS = 'points' # points in group (list of point dicts) + +ID = 'id' # id (int or str) +NAME = 'name' # name (str) +VALUE = 'value' # value (int, float, str) +COUNT = 'count' # instance count (int or str) + +TYPE = 'type' # point type (str of TYPE_XXX) +MANDATORY = 'mandatory' # point mandatory (str of MANDATORY_XXX) +ACCESS = 'access' # point access (str of ACCESS_XXX) +STATIC = 'static' # point value is static (str of STATIC_XXX) +SF = 'sf' # point scale factor (int) +UNITS = 'units' # point units (str) +SIZE = 'size' # point string length (int) + +LABEL = 'label' # label (str) +DESCRIPTION = 'desc' # description (str) +NOTES = 'notes' # notes (str) +DETAIL = 'detail' # detailed description (str) +SYMBOLS = 'symbols' # symbols (list of symbol dicts) +COMMENTS = 'comments' # comments (list of str) + +TYPE_GROUP = 'group' +TYPE_SYNC_GROUP = 'sync' + +TYPE_INT16 = 'int16' +TYPE_UINT16 = 'uint16' +TYPE_COUNT = 'count' +TYPE_ACC16 = 'acc16' +TYPE_ENUM16 = 'enum16' +TYPE_BITFIELD16 = 'bitfield16' +TYPE_PAD = 'pad' +TYPE_INT32 = 'int32' +TYPE_UINT32 = 'uint32' +TYPE_ACC32 = 'acc32' +TYPE_ENUM32 = 'enum32' +TYPE_BITFIELD32 = 'bitfield32' +TYPE_IPADDR = 'ipaddr' +TYPE_INT64 = 'int64' +TYPE_UINT64 = 'uint64' +TYPE_ACC64 = 'acc64' +TYPE_IPV6ADDR = 'ipv6addr' +TYPE_FLOAT32 = 'float32' +TYPE_FLOAT64 = 'float64' +TYPE_STRING = 'string' +TYPE_SUNSSF = 'sunssf' +TYPE_EUI48 = 'eui48' + +ACCESS_R = 'R' +ACCESS_RW = 'RW' + +MANDATORY_FALSE = 'O' +MANDATORY_TRUE = 'M' + +STATIC_FALSE = 'D' +STATIC_TRUE = 'S' + +MODEL_ID_POINT_NAME = 'ID' +MODEL_LEN_POINT_NAME = 'L' + +END_MODEL_ID = 65535 + +MODEL_DEF_EXT = '.json' + + +def to_int(x): + try: + return int(x, 0) + except TypeError: + return int(x) + + +def to_str(s): + return str(s) + + +def to_float(f): + try: + return float(f) + except ValueError: + return None + + +# valid model attributes +model_attr = {ID: {'type': int, 'mand': True}, GROUP: {'mand': True}, COMMENTS: {}} + +# valid group attributes +group_attr = {NAME: {'type': str, 'mand': True}, COUNT: {'type': [int, str]}, TYPE: {'mand': True}, + GROUPS: {}, POINTS: {'mand': True}, LABEL: {'type': str}, + DESCRIPTION: {'type': str}, NOTES: {'type': str}, COMMENTS: {}, DETAIL: {'type': str}} + +# valid point attributes +point_attr = {NAME: {'type': str, 'mand': True}, COUNT: {'type': int}, VALUE: {}, TYPE: {'mand': True}, + SIZE: {'type': int}, SF: {}, UNITS: {'type': str}, + ACCESS: {'type': str, 'values': ['R', 'RW'], 'default': 'R'}, + MANDATORY: {'type': str, 'values': ['O', 'M'], 'default': 'O'}, + STATIC: {'type': str, 'values': ['D', 'S'], 'default': 'D'}, + LABEL: {'type': str}, DESCRIPTION: {'type': str}, NOTES: {'type': str}, SYMBOLS: {}, COMMENTS: {}, + DETAIL: {'type': str}} + +# valid symbol attributes +symbol_attr = {NAME: {'type': str, 'mand': True}, VALUE: {'mand': True}, LABEL: {'type': str}, + DESCRIPTION: {'type': str}, NOTES: {'type': str}, COMMENTS: {}, DETAIL: {'type': str}} + +group_types = [TYPE_GROUP, TYPE_SYNC_GROUP] + +point_type_info = { + TYPE_INT16: {'len': 1, 'to_type': to_int, 'default': 0}, + TYPE_UINT16: {'len': 1, 'to_type': to_int, 'default': 0}, + TYPE_COUNT: {'len': 1, 'to_type': to_int, 'default': 0}, + TYPE_ACC16: {'len': 1, 'to_type': to_int, 'default': 0}, + TYPE_ENUM16: {'len': 1, 'to_type': to_int, 'default': 0}, + TYPE_BITFIELD16: {'len': 1, 'to_type': to_int, 'default': 0}, + TYPE_PAD: {'len': 1, 'to_type': to_int, 'default': 0}, + TYPE_INT32: {'len': 2, 'to_type': to_int, 'default': 0}, + TYPE_UINT32: {'len': 2, 'to_type': to_int, 'default': 0}, + TYPE_ACC32: {'len': 2, 'to_type': to_int, 'default': 0}, + TYPE_ENUM32: {'len': 2, 'to_type': to_int, 'default': 0}, + TYPE_BITFIELD32: {'len': 2, 'to_type': to_int, 'default': 0}, + TYPE_IPADDR: {'len': 2, 'to_type': to_int, 'default': 0}, + TYPE_INT64: {'len': 4, 'to_type': to_int, 'default': 0}, + TYPE_UINT64: {'len': 4, 'to_type': to_int, 'default': 0}, + TYPE_ACC64: {'len': 4, 'to_type': to_int, 'default': 0}, + TYPE_IPV6ADDR: {'len': 8, 'to_type': to_str, 'default': 0}, + TYPE_FLOAT32: {'len': 2, 'to_type': to_float, 'default': 0}, + TYPE_FLOAT64: {'len': 4, 'to_type': to_float, 'default': 0}, + TYPE_STRING: {'len': None, 'to_type': to_str, 'default': ''}, + TYPE_SUNSSF: {'len': 1, 'to_type': to_int, 'default': 0}, + TYPE_EUI48: {'len': 4, 'to_type': to_str, 'default': 0} +} + + +class ModelDefinitionError(Exception): + pass + + +def to_number_type(n): + if isinstance(n, str): + try: + n = int(n) + except ValueError: + try: + n = float(n) + except ValueError: + pass + return n + + +def validate_find_point(group, pname): + points = group.get(POINTS, list()) + for p in points: + pxname = p.get(NAME) + if pxname: + if p[NAME] == pname: + return p + + +def validate_attrs(element, attrs, result=''): + # check for unexpected attributes + for k in element: + if k not in attrs: + result += 'Unexpected model definition attribute: %s in %s\n' % (k, element.get(NAME)) + # check for missing attributes + for k, a in attrs.items(): + if k in element and element[k] is not None: + # check type if specified + t = a.get('type') + if isinstance(t, list): + if t and type(element[k]) not in t: + result += 'Unexpected type for model attribute %s, expected %s, found %s\n' % \ + (k, t, type(element[k])) + else: + if t and type(element[k]) != t: + result += 'Unexpected type for model attribute %s, expected %s, found %s\n' % (k, t, type(element[k])) + values = a.get('values') + if values and element[k] not in values: + result += 'Unexpected value for model attribute %s: %s\n' % (k, element[k]) + elif a.get('mand', False): + result += 'Mandatory attribute missing from model definition: %s\n' % k + return result + + +def validate_group_point_dup(group, result=''): + groups = group.get(GROUPS, list()) + for g in groups: + gname = g.get(NAME) + if gname: + count = 0 + for gx in groups: + gxname = gx.get(NAME) + if gxname: + if gx[NAME] == gname: + count += 1 + if count > 1: + result += 'Duplicate group id %s in group %s' % (gname, group[NAME]) + if validate_find_point(group, gname): + result += 'Duplicate group and point id %s in group %s' % (gname, group[NAME]) + else: + result += 'Mandatory %s attribute missing in group definition element\n' % (NAME) + points = group.get(POINTS, list()) + for p in points: + pname = p.get(NAME) + if pname: + count = 0 + for px in points: + pxname = px.get(NAME) + if pxname: + if px[NAME] == pname: + count += 1 + if count > 1: + result += 'Duplicate point id %s in group %s' % (pname, group[NAME]) + else: + result += 'Mandatory attribute missing in point definition element: %s\n' % (NAME) + return result + + +def validate_symbols(symbols, model_group, result=''): + for symbol in symbols: + result = validate_attrs(symbol, model_group, result) + return result + + +def validate_sf(point, sf, sf_groups, result=''): + found = False + if type(sf) == str: + for group in sf_groups: + p = validate_find_point(group, sf) + if p: + found = True + if p[TYPE] != TYPE_SUNSSF: + result += 'Scale factor %s for point %s is not scale factor type: %s\n' % (sf, point[NAME], p[TYPE]) + break + if not found: + result += 'Scale factor %s for point %s not found\n' % (sf, point[NAME]) + elif type(sf) == int: + if sf < - 10 or sf > 10: + result += 'Scale factor %s for point %s out of range\n' % (sf, point[NAME]) + else: + result += 'Scale factor %s for point %s has invalid type %s\n' % (sf, point[NAME], type(sf)) + return result + + +def validate_point_def(point, model_group, group, result=''): + # validate general point attributes + result = validate_attrs(point, point_attr, result) + # validate point type + ptype = point.get(TYPE) + if ptype not in point_type_info: + result += 'Unknown point type %s for point %s\n' % (ptype, point[NAME]) + # validate scale foctor, if present + sf = point.get(SF) + if sf: + result = validate_sf(point, sf, [model_group, group], result) + # validate symbols + symbols = point.get(SYMBOLS, list()) + result = validate_symbols(symbols, symbol_attr, result) + # check for duplicate symbols + for s in symbols: + sname = s.get(NAME) + if sname: + count = 0 + for sx in symbols: + if sx[NAME] == sname: + count += 1 + if count > 1: + result += 'Duplicate symbol id %s in point %s\n' % (sname, point[NAME]) + else: + result += 'Mandatory attribute missing in symbol definition element: %s\n' % (NAME) + return result + + +def validate_group_def(group, model_group, result=''): + # validate general group attributes + result = validate_attrs(group, group_attr, result) + # validate points + points = group.get(POINTS, list()) + for p in points: + result = validate_point_def(p, model_group, group, result) + # validate groups + groups = group.get(GROUPS, list()) + for g in groups: + result = validate_group_def(g, model_group, result) + # check for group and point duplicates + result = validate_group_point_dup(group, result) + return result + + +def validate_model_group_def(model_def, group, result=''): + # must contain ID and length points + points = group.get(POINTS) + if points: + if len(points) >= 2: + pname = points[0].get(NAME) + if pname != MODEL_ID_POINT_NAME: + result += "First point in top-level group must be %s, found: %s\n" % (MODEL_ID_POINT_NAME, pname) + if points[0].get(VALUE) != model_def.get(ID): + result += 'Model ID does not match top-level group ID: %s %s %s %s\n' % ( + model_def.get(ID), type(model_def.get(ID)), points[0].get(VALUE), type(points[0].get(VALUE))) + pname = points[1].get(NAME) + if pname != MODEL_LEN_POINT_NAME: + result += "Second point in top-level group must be %s, found: %s\n" % (MODEL_LEN_POINT_NAME, pname) + else: + result += "Top-level group must contain at least two points: %s and %s\n" % (MODEL_ID_POINT_NAME, + MODEL_LEN_POINT_NAME) + else: + result += 'Top-level group missing point definitions\n' + # perform normal group validation + result = validate_group_def(group, group, result) + return result + + +def validate_model_def(model_def, result=''): + result = validate_attrs(model_def, model_attr, result) + group = model_def.get(GROUP) + result = validate_model_group_def(model_def, group, result) + return result + + +def from_json_str(s): + return json.loads(s) + + +def from_json_file(filename): + f = open(filename) + model_def = json.load(f) + f.close() + return(model_def) + + +def to_json_str(model_def, indent=4): + return json.dumps(model_def, indent=indent, sort_keys=True) + + +def to_json_filename(model_id): + return 'model_%s%s' % (model_id, MODEL_DEF_EXT) + + +def to_json_file(model_def, filename=None, filedir=None, indent=4): + if filename is None: + filename = to_json_filename(model_def[ID]) + if filedir is not None: + filename = os.path.join(filedir, filename) + f = open(filename, 'w') + json.dump(model_def, f, indent=indent, sort_keys=True) + + +if __name__ == "__main__": + pass diff --git a/build/lib/sunspec2/modbus/__init__.py b/build/lib/sunspec2/modbus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/sunspec2/modbus/client.py b/build/lib/sunspec2/modbus/client.py new file mode 100644 index 0000000..b294a9c --- /dev/null +++ b/build/lib/sunspec2/modbus/client.py @@ -0,0 +1,379 @@ +""" + Copyright (C) 2020 SunSpec Alliance + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +""" + +import time +import uuid +import sunspec2.mdef as mdef +import sunspec2.device as device +import sunspec2.mb as mb +import sunspec2.modbus.modbus as modbus_client + +TEST_NAME = 'test_name' + +modbus_rtu_clients = {} + +# Reference for SVP driver +MAPPED = 'Mapped SunSpec Device' +RTU = 'Modbus RTU' +TCP = 'Modbus TCP' + +class SunSpecModbusClientError(Exception): + pass + + +class SunSpecModbusClientTimeout(SunSpecModbusClientError): + pass + + +class SunSpecModbusClientException(SunSpecModbusClientError): + pass + + +class SunSpecModbusClientPoint(device.Point): + + def read(self): + data = self.model.device.read(self.model.model_addr + self.offset, self.len) + self.set_mb(data=data, dirty=False) + + def write(self): + """Write the point to the physical device""" + + data = self.info.to_data(self.value, int(self.len) * 2) + model_addr = self.model.model_addr + point_offset = self.offset + addr = model_addr + point_offset + self.model.device.write(addr, data) + self.dirty = False + + +class SunSpecModbusClientGroup(device.Group): + + def read(self): + + data = self.model.device.read(self.model.model_addr + self.offset, self.len) + self.set_mb(data=data, dirty=False) + + def write(self): + + start_addr = next_addr = self.model.model_addr + self.offset + data = b'' + start_addr, next_addr, data = self.write_points(start_addr, next_addr, data) + if data: + self.model.device.write(start_addr, data) + + def write_points(self, start_addr=None, next_addr=None, data=None): + """ + Write all points that have been modified since the last write operation to the physical device + """ + + for name, point in self.points.items(): + model_addr = self.model.model_addr + point_offset = point.offset + point_addr = model_addr + point_offset + if data and (not point.dirty or point_addr != next_addr): + self.model.device.write(start_addr, data) + data = b'' + if point.dirty: + point_len = point.len + point_data = point.info.to_data(point.value, int(point_len) * 2) + if not data: + start_addr = point_addr + next_addr = point_addr + point_len + data += point_data + point.dirty = False + + for name, group in self.groups.items(): + if isinstance(group, list): + for g in group: + start_addr, next_addr, data = g.write_points(start_addr, next_addr, data) + else: + start_addr, next_addr, data = group.write_points(start_addr, next_addr, data) + + return start_addr, next_addr, data + + +class SunSpecModbusClientModel(SunSpecModbusClientGroup): + def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, data=None, mb_device=None, + group_class=SunSpecModbusClientGroup, point_class=SunSpecModbusClientPoint): + self.model_id = model_id + self.model_addr = model_addr + self.model_len = model_len + self.model_def = model_def + self.error_info = '' + self.mid = None + self.device = mb_device + self.model = self + + gdef = None + try: + if self.model_def is None: + self.model_def = device.get_model_def(model_id) + if self.model_def is not None: + gdef = self.model_def.get(mdef.GROUP) + except Exception as e: + self.add_error(str(e)) + + SunSpecModbusClientGroup.__init__(self, gdef=gdef, model=self.model, model_offset=0, group_len=self.model_len, + data=data, data_offset=0, group_class=group_class, point_class=point_class) + + if self.model_len is not None: + self.len = self.model_len + + if self.model_len and self.len: + if self.model_len != self.len: + self.add_error('Model error: Discovered length %s does not match computed length %s' % + (self.model_len, self.len)) + + def add_error(self, error_info): + self.error_info = '%s%s\n' % (self.error_info, error_info) + + +class SunSpecModbusClientDevice(device.Device): + def __init__(self, model_class=SunSpecModbusClientModel): + device.Device.__init__(self, model_class=model_class) + self.did = str(uuid.uuid4()) + self.retry_count = 2 + self.base_addr_list = [40000, 0, 50000] + self.base_addr = None + + def connect(self): + pass + + def disconnect(self): + pass + + def close(self): + pass + + # must be overridden by Modbus protocol implementation + def read(self, addr, count): + return '' + + # must be overridden by Modbus protocol implementation + def write(self, addr, data): + return + + def scan(self, progress=None, delay=None, connect=False): + """Scan all the models of the physical device and create the + corresponding model objects within the device object based on the + SunSpec model definitions. + """ + + data = error = '' + connected = False + + if connect: + self.connect() + connected = True + + if delay is not None: + time.sleep(delay) + + if self.base_addr is None: + for addr in self.base_addr_list: + try: + data = self.read(addr, 3) + if data[:4] == b'SunS': + self.base_addr = addr + break + else: + error = 'Device responded - not SunSpec register map' + except SunSpecModbusClientError as e: + if not error: + error = str(e) + + if delay is not None: + time.sleep(delay) + + if self.base_addr is not None: + model_id = mb.data_to_u16(data[4:6]) + addr = self.base_addr + 2 + + mid = 0 + while model_id != mb.SUNS_END_MODEL_ID: + # read model and model len separately due to some devices not supplying + # count for the end model id + data = self.read(addr + 1, 1) + if data and len(data) == 2: + if progress is not None: + cont = progress('Scanning model %s' % (model_id)) + if not cont: + raise SunSpecModbusClientError('Device scan terminated') + model_len = mb.data_to_u16(data) + + # read model data + model_data = self.read(addr, model_len + 2) + model = self.model_class(model_id=model_id, model_addr=addr, model_len=model_len, data=model_data, + mb_device=self) + model.mid = '%s_%s' % (self.did, mid) + mid += 1 + self.add_model(model) + + addr += model_len + 2 + data = self.read(addr, 1) + if data and len(data) == 2: + model_id = mb.data_to_u16(data) + else: + break + else: + break + + if delay is not None: + time.sleep(delay) + + else: + if not error: + error = 'Unknown error' + raise SunSpecModbusClientError(error) + + if connected: + self.disconnect() + + +class SunSpecModbusClientDeviceTCP(SunSpecModbusClientDevice): + def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None, + tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False, + max_count=modbus_client.REQ_COUNT_MAX, test=False, model_class=SunSpecModbusClientModel): + SunSpecModbusClientDevice.__init__(self, model_class=model_class) + + self.slave_id = slave_id + self.ipaddr = ipaddr + self.ipport = ipport + self.timeout = timeout + self.ctx = ctx + self.socket = None + self.trace_func = trace_func + self.max_count = max_count + self.tls = tls + self.cafile = cafile + self.certfile = certfile + self.keyfile = keyfile + self.insecure_skip_tls_verify = insecure_skip_tls_verify + + self.client = modbus_client.ModbusClientTCP(slave_id=slave_id, ipaddr=ipaddr, ipport=ipport, timeout=timeout, + ctx=ctx, trace_func=trace_func, + tls=tls, cafile=cafile, certfile=certfile, keyfile=keyfile, + insecure_skip_tls_verify=insecure_skip_tls_verify, + max_count=modbus_client.REQ_COUNT_MAX, test=test) + if self.client is None: + raise SunSpecModbusClientError('No modbus tcp client set for device') + + def connect(self): + self.client.connect() + + def disconnect(self): + self.client.disconnect() + + def read(self, addr, count, op=modbus_client.FUNC_READ_HOLDING): + return self.client.read(addr, count, op) + + def write(self, addr, data): + return self.client.write(addr, data) + + +class SunSpecModbusClientDeviceRTU(SunSpecModbusClientDevice): + """Provides access to a Modbus RTU device. + Parameters: + slave_id : + Modbus slave id. + name : + Name of the serial port such as 'com4' or '/dev/ttyUSB0'. + baudrate : + Baud rate such as 9600 or 19200. Default is 9600 if not specified. + parity : + Parity. Possible values: + :const:`sunspec.core.modbus.client.PARITY_NONE`, + :const:`sunspec.core.modbus.client.PARITY_EVEN` Defaulted to + :const:`PARITY_NONE`. + timeout : + Modbus request timeout in seconds. Fractional seconds are permitted + such as .5. + ctx : + Context variable to be used by the object creator. Not used by the + modbus module. + trace_func : + Trace function to use for detailed logging. No detailed logging is + perform is a trace function is not supplied. + max_count : + Maximum register count for a single Modbus request. + Raises: + SunSpecModbusClientError: Raised for any general modbus client error. + SunSpecModbusClientTimeoutError: Raised for a modbus client request timeout. + SunSpecModbusClientException: Raised for an exception response to a modbus + client request. + """ + + def __init__(self, slave_id, name, baudrate=None, parity=None, timeout=None, ctx=None, trace_func=None, + max_count=modbus_client.REQ_COUNT_MAX, model_class=SunSpecModbusClientModel): + # test if this super class init is needed + SunSpecModbusClientDevice.__init__(self, model_class=model_class) + self.slave_id = slave_id + self.name = name + self.client = None + self.ctx = ctx + self.trace_func = trace_func + self.max_count = max_count + + self.client = modbus_client.modbus_rtu_client(name, baudrate, parity) + if self.client is None: + raise SunSpecModbusClientError('No modbus rtu client set for device') + self.client.add_device(self.slave_id, self) + + if timeout is not None and self.client.serial is not None: + self.client.serial.timeout = timeout + self.client.serial.writeTimeout = timeout + + def open(self): + self.client.open() + + def close(self): + """Close the device. Called when device is not longer in use. + """ + + if self.client: + self.client.remove_device(self.slave_id) + + def read(self, addr, count, op=modbus_client.FUNC_READ_HOLDING): + """Read Modbus device registers. + Parameters: + addr : + Starting Modbus address. + count : + Read length in Modbus registers. + op : + Modbus function code for request. + Returns: + Byte string containing register contents. + """ + + return self.client.read(self.slave_id, addr, count, op=op, trace_func=self.trace_func, max_count=self.max_count) + + def write(self, addr, data): + """Write Modbus device registers. + Parameters: + addr : + Starting Modbus address. + count : + Byte string containing register contents. + """ + + return self.client.write(self.slave_id, addr, data, trace_func=self.trace_func, max_count=self.max_count) diff --git a/build/lib/sunspec2/modbus/modbus.py b/build/lib/sunspec2/modbus/modbus.py new file mode 100644 index 0000000..887ed5d --- /dev/null +++ b/build/lib/sunspec2/modbus/modbus.py @@ -0,0 +1,717 @@ +""" + Copyright (C) 2018 SunSpec Alliance + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +""" + +import socket +import struct +import serial +try: + import ssl +except Exception as e: + print('Missing ssl python package: %s' % e) + +PARITY_NONE = 'N' +PARITY_EVEN = 'E' + +REQ_COUNT_MAX = 125 + +FUNC_READ_HOLDING = 3 +FUNC_READ_INPUT = 4 +FUNC_WRITE_MULTIPLE = 16 + +TEST_NAME = 'test_name' + +modbus_rtu_clients = {} + +TCP_HDR_LEN = 6 +TCP_RESP_MIN_LEN = 3 +TCP_HDR_O_LEN = 4 +TCP_READ_REQ_LEN = 6 +TCP_WRITE_MULT_REQ_LEN = 7 + +TCP_DEFAULT_PORT = 502 +TCP_DEFAULT_TIMEOUT = 2 + + +class ModbusClientError(Exception): + pass + + +class ModbusClientTimeout(ModbusClientError): + pass + + +class ModbusClientException(ModbusClientError): + pass + + +def modbus_rtu_client(name=None, baudrate=None, parity=None): + global modbus_rtu_clients + + client = modbus_rtu_clients.get(name) + if client is not None: + if baudrate is not None and client.baudrate != baudrate: + raise ModbusClientError('Modbus client baudrate mismatch') + if parity is not None and client.parity != parity: + raise ModbusClientError('Modbus client parity mismatch') + else: + if baudrate is None: + baudrate = 9600 + if parity is None: + parity = PARITY_NONE + client = ModbusClientRTU(name, baudrate, parity) + modbus_rtu_clients[name] = client + return client + + +def modbus_rtu_client_remove(name=None): + + global modbus_rtu_clients + + if modbus_rtu_clients.get(name): + del modbus_rtu_clients[name] + + +def __generate_crc16_table(): + ''' Generates a crc16 lookup table + .. note:: This will only be generated once + ''' + result = [] + for byte in range(256): + crc = 0x0000 + for bit in range(8): + if (byte ^ crc) & 0x0001: + crc = (crc >> 1) ^ 0xa001 + else: crc >>= 1 + byte >>= 1 + result.append(crc) + return result + + +__crc16_table = __generate_crc16_table() + + +def computeCRC(data): + ''' Computes a crc16 on the passed in string. For modbus, + this is only used on the binary serial protocols (in this + case RTU). + The difference between modbus's crc16 and a normal crc16 + is that modbus starts the crc value out at 0xffff. + :param data: The data to create a crc16 of + :returns: The calculated CRC + ''' + crc = 0xffff + + for a in data: + idx = __crc16_table[(crc ^ a) & 0xff]; + crc = ((crc >> 8) & 0xff) ^ idx + swapped = ((crc << 8) & 0xff00) | ((crc >> 8) & 0x00ff) + return swapped + + +def checkCRC(data, check): + ''' Checks if the data matches the passed in CRC + :param data: The data to create a crc16 of + :param check: The CRC to validate + :returns: True if matched, False otherwise + ''' + return computeCRC(data) == check + + +class ModbusClientRTU(object): + """A Modbus RTU client that multiple devices can use to access devices over + the same serial interface. Currently, the implementation does not support + concurent device requests so the support of multiple devices must be single + threaded. + Parameters: + name : + Name of the serial port such as 'com4' or '/dev/ttyUSB0'. + baudrate : + Baud rate such as 9600 or 19200. Default is 9600 if not specified. + parity : + Parity. Possible values: + :const:`sunspec.core.modbus.client.PARITY_NONE`, + :const:`sunspec.core.modbus.client.PARITY_EVEN`. Defaults to + :const:`PARITY_NONE`. + Raises: + ModbusClientError: Raised for any general modbus client error. + ModbusClientTimeoutError: Raised for a modbus client request timeout. + ModbusClientException: Raised for an exception response to a modbus + client request. + Attributes: + name + Name of the serial port such as 'com4' or '/dev/ttyUSB0'. + baudrate + Baud rate. + parity + Parity. Possible values: + :const:`sunspec.core.modbus.client.PARITY_NONE`, + :const:`sunspec.core.modbus.client.PARITY_EVEN` + serial + The pyserial.Serial object used for serial communications. + timeout + Read timeout in seconds. Fractional values are permitted. + write_timeout + Write timeout in seconds. Fractional values are permitted. + devices + List of :const:`sunspec.core.modbus.client.ModbusClientDeviceRTU` + devices currently using the client. + """ + + def __init__(self, name='/dev/ttyUSB0', baudrate=9600, parity=None): + self.name = name + self.baudrate = baudrate + self.parity = parity + self.serial = None + self.timeout = .5 + self.write_timeout = .5 + self.devices = {} + + self.open() + + def open(self): + """Open the RTU client serial interface. + """ + + try: + if self.parity == PARITY_EVEN: + parity = serial.PARITY_EVEN + else: + parity = serial.PARITY_NONE + + self.serial = serial.Serial(port=self.name, baudrate=self.baudrate, + bytesize=8, parity=parity, + stopbits=1, xonxoff=0, + timeout=self.timeout, writeTimeout=self.write_timeout) + + except Exception as e: + if self.serial is not None: + self.serial.close() + self.serial = None + raise ModbusClientError('Serial init error: %s' % str(e)) + + def close(self): + """Close the RTU client serial interface. + """ + + try: + if self.serial is not None: + self.serial.close() + except Exception as e: + raise ModbusClientError('Serial close error: %s' % str(e)) + + def add_device(self, slave_id, device): + """Add a device to the RTU client. + Parameters: + slave_id : + Modbus slave id. + device : + Device to add to the client. + """ + + self.devices[slave_id] = device + + def remove_device(self, slave_id): + """Remove a device from the RTU client. + Parameters: + slave_id : + Modbus slave id. + """ + + if self.devices.get(slave_id): + del self.devices[slave_id] + + # if no more devices using the client interface, close and remove the client + if len(self.devices) == 0: + self.close() + modbus_rtu_client_remove(self.name) + + def _read(self, slave_id, addr, count, op=FUNC_READ_HOLDING, trace_func=None): + resp = bytearray() + len_remaining = 5 + len_found = False + except_code = None + + req = struct.pack('>BBHH', int(slave_id), op, int(addr), int(count)) + req += struct.pack('>H', computeCRC(req)) + + if trace_func: + s = '{}:{}[addr={}] ->'.format(self.name, str(slave_id), addr) + for c in req: + s += '%02X' % (ord(c)) + trace_func(s) + + self.serial.flushInput() + try: + self.serial.write(req) + except Exception as e: + raise ModbusClientError('Serial write error: %s' % str(e)) + + while len_remaining > 0: + c = self.serial.read(len_remaining) + + len_read = len(c) + if len_read > 0: + resp += c + len_remaining -= len_read + if len_found is False and len(resp) >= 5: + if not (resp[1] & 0x80): + len_remaining = (resp[2] + 5) - len(resp) + len_found = True + else: + except_code = resp[2] + else: + raise ModbusClientTimeout('Response timeout') + + if trace_func: + s = '{}:{}[addr={}] <--'.format(self.name, str(slave_id), addr) + for c in resp: + s += '%02X' % (ord(c)) + trace_func(s) + + crc = (resp[-2] << 8) | resp[-1] + if not checkCRC(resp[:-2], crc): + raise ModbusClientError('CRC error') + + if except_code: + raise ModbusClientException('Modbus exception %d' % (except_code)) + + return resp[3:-2] + + def read(self, slave_id, addr, count, op=FUNC_READ_HOLDING, trace_func=None, max_count=REQ_COUNT_MAX): + """ + Parameters: + slave_id : + Modbus slave id. + addr : + Starting Modbus address. + count : + Read length in Modbus registers. + op : + Modbus function code for request. Possible values: + :const:`FUNC_READ_HOLDING`, :const:`FUNC_READ_INPUT`. + trace_func : + Trace function to use for detailed logging. No detailed logging + is perform is a trace function is not supplied. + max_count : + Maximum register count for a single Modbus request. + Returns: + Byte string containing register contents. + """ + + resp = bytearray() + read_count = 0 + read_offset = 0 + + if self.serial is not None: + while count > 0: + if count > max_count: + read_count = max_count + else: + read_count = count + data = self._read(slave_id, addr + read_offset, read_count, op=op, trace_func=trace_func) + if data: + resp += data + count -= read_count + read_offset += read_count + else: + return + else: + raise ModbusClientError('Client serial port not open: %s' % self.name) + + return bytes(resp) + + def _write(self, slave_id, addr, data, trace_func=None): + resp = bytearray() + len_remaining = 5 + len_found = False + except_code = None + func = FUNC_WRITE_MULTIPLE + len_data = len(data) + count = int(len_data/2) + + req = struct.pack('>BBHHB', int(slave_id), func, int(addr), count, len_data) + + req += data + req += struct.pack('>H', computeCRC(req)) + + if trace_func: + s = '{}:{}[addr={}] ->'.format(self.name, str(slave_id), addr) + for c in req: + s += '%02X' % (ord(c)) + trace_func(s) + + self.serial.flushInput() + + try: + self.serial.write(bytes(req)) + except Exception as e: + raise ModbusClientError('Serial write error: %s' % str(e)) + + while len_remaining > 0: + c = self.serial.read(len_remaining) + + len_read = len(c) + if len_read > 0: + resp += c + len_remaining -= len_read + if len_found is False and len(resp) >= 5: + if not (resp[1] & 0x80): + len_remaining = 8 - len(resp) + len_found = True + else: + except_code = resp[2] + else: + raise ModbusClientTimeout('Response timeout') + + if trace_func: + s = '{}:{}[addr={}] <--'.format(self.name, str(slave_id), addr) + for c in resp: + s += '%02X' % (ord(c)) + trace_func(s) + + crc = (resp[-2] << 8) | resp[-1] + if not checkCRC(resp[:-2], crc): + raise ModbusClientError('CRC error') + + if except_code: + raise ModbusClientException('Modbus exception: %d' % (except_code)) + else: + resp_slave_id, resp_func, resp_addr, resp_count, resp_crc = struct.unpack('>BBHHH', bytes(resp)) + if resp_slave_id != slave_id or resp_func != func or resp_addr != addr or resp_count != count: + raise ModbusClientError('Mobus response format error') + + def write(self, slave_id, addr, data, trace_func=None, max_count=REQ_COUNT_MAX): + """ + Parameters: + slave_id : + Modbus slave id. + addr : + Starting Modbus address. + data : + Byte string containing register contents. + trace_func : + Trace function to use for detailed logging. No detailed logging + is perform is a trace function is not supplied. + max_count : + Maximum register count for a single Modbus request. + """ + + write_offset = 0 + count = len(data)/2 + + if self.serial is not None: + while (count > 0): + if count > max_count: + write_count = max_count + else: + write_count = count + start = int(write_offset * 2) + end = int((write_offset + write_count) * 2) + self._write(slave_id, addr + write_offset, data[start:end], + trace_func=trace_func) + count -= write_count + write_offset += write_count + else: + raise ModbusClientError('Client serial port not open: %s' % self.name) + + +class ModbusClientTCP(object): + """Provides access to a Modbus TCP device. + + Parameters: + slave_id : + Modbus slave id. + ipaddr : + IP address string. + ipport : + IP port. + timeout : + Modbus request timeout in seconds. Fractional seconds are permitted such as .5. + ctx : + Context variable to be used by the object creator. Not used by the modbus module. + trace_func : + Trace function to use for detailed logging. No detailed logging is perform is a trace function is + not supplied. + tls : + Use TLS (Modbus/TCP Security). Defaults to `tls=False`. + cafile : + Path to certificate authority (CA) certificate to use for validating server certificates. + Only used if `tls=True`. + certfile : + Path to client TLS certificate to use for client authentication. Only used if `tls=True`. + keyfile : + Path to client TLS key to use for client authentication. Only used if `tls=True`. + insecure_skip_tls_verify : + Skip verification of server TLS certificate. Only used if `tls=True`. + max_count : + Maximum register count for a single Modbus request. + test : + Use test socket. If True use the fake socket module for network communications. + """ + + def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None, + tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False, + max_count=REQ_COUNT_MAX, test=False): + self.slave_id = slave_id + self.ipaddr = ipaddr + self.ipport = ipport + self.timeout = timeout + self.ctx = ctx + self.socket = None + self.trace_func = trace_func + self.tls = tls + self.cafile = cafile + self.certfile = certfile + self.keyfile = keyfile + self.tls_verify = not insecure_skip_tls_verify + self.max_count = max_count + + if ipport is None: + self.ipport = TCP_DEFAULT_PORT + if timeout is None: + self.timeout = TCP_DEFAULT_TIMEOUT + + def close(self): + + self.disconnect() + + def connect(self, timeout=None): + """Connect to TCP destination. + + Parameters: + + timeout : + Connection timeout in seconds. + """ + if self.socket: + self.disconnect() + + if timeout is None: + timeout = self.timeout + + try: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.settimeout(timeout) + + if self.tls: + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=self.cafile) + context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile) + context.check_hostname = self.tls_verify + + self.socket = context.wrap_socket(self.socket, server_side=False, server_hostname=self.ipaddr) + + self.socket.connect((self.ipaddr, self.ipport)) + except Exception as e: + raise ModbusClientError('Connection error: %s' % str(e)) + + def disconnect(self): + """Disconnect from TCP destination. + """ + + try: + if self.socket: + self.socket.close() + self.socket = None + except Exception: + pass + + def _read(self, addr, count, op=FUNC_READ_HOLDING): + resp = bytearray() + len_remaining = TCP_HDR_LEN + TCP_RESP_MIN_LEN + len_found = False + except_code = None + + req = struct.pack('>HHHBBHH', 0, 0, TCP_READ_REQ_LEN, int(self.slave_id), op, int(addr), int(count)) + + if self.trace_func: + s = '%s:%s:%s[addr=%s] ->' % (self.ipaddr, str(self.ipport), str(self.slave_id), addr) + for c in req: + s += '%02X' % (ord(c)) + self.trace_func(s) + + try: + self.socket.sendall(req) + except Exception as e: + raise ModbusClientError('Socket write error: %s' % str(e)) + + while len_remaining > 0: + c = self.socket.recv(len_remaining) + len_read = len(c) + if len_read > 0: + resp += c + len_remaining -= len_read + if len_found is False and len(resp) >= TCP_HDR_LEN + TCP_RESP_MIN_LEN: + data_len = struct.unpack('>H', resp[TCP_HDR_O_LEN:TCP_HDR_O_LEN + 2]) + len_remaining = data_len[0] - (len(resp) - TCP_HDR_LEN) + else: + raise ModbusClientError('Response timeout') + + if not (resp[TCP_HDR_LEN + 1] & 0x80): + len_remaining = (resp[TCP_HDR_LEN + 2] + TCP_HDR_LEN) - len(resp) + len_found = True + else: + except_code = resp[TCP_HDR_LEN + 2] + + if self.trace_func: + s = '%s:%s:%s[addr=%s] <--' % (self.ipaddr, str(self.ipport), str(self.slave_id), addr) + for c in resp: + s += '%02X' % (ord(c)) + self.trace_func(s) + + if except_code: + raise ModbusClientException('Modbus exception %d: addr: %s count: %s' % (except_code, addr, count)) + + return resp[(TCP_HDR_LEN + 3):] + + def read(self, addr, count, op=FUNC_READ_HOLDING): + """ Read Modbus device registers. If no connection exists to the + destination, one is created and disconnected at the end of the request. + + Parameters: + + addr : + Starting Modbus address. + + count : + Read length in Modbus registers. + + op : + Modbus function code for request. + + Returns: + + Byte string containing register contents. + """ + + resp = bytearray() + read_count = 0 + read_offset = 0 + local_connect = False + + if self.socket is None: + local_connect = True + self.connect(self.timeout) + + try: + while (count > 0): + if count > self.max_count: + read_count = self.max_count + else: + read_count = count + data = self._read(addr + read_offset, read_count, op=op) + + if data: + resp += data + count -= read_count + read_offset += read_count + else: + break + finally: + if local_connect: + self.disconnect() + + return bytes(resp) + + def _write(self, addr, data): + resp = bytearray() + len_remaining = TCP_HDR_LEN + TCP_RESP_MIN_LEN + len_found = False + except_code = None + func = FUNC_WRITE_MULTIPLE + + write_len = len(data) + write_count = int(write_len/2) + tmp = TCP_WRITE_MULT_REQ_LEN + write_len + req = struct.pack('>HHHBBHHB', 0, 0, TCP_WRITE_MULT_REQ_LEN + write_len, int(self.slave_id), func, int(addr), + write_count, write_len) + req += data + + if self.trace_func: + s = '%s:%s:%s[addr=%s] ->' % (self.ipaddr, str(self.ipport), str(self.slave_id), addr) + for c in req: + s += '%02X' % (ord(c)) + self.trace_func(s) + + try: + self.socket.sendall(req) + except Exception as e: + raise ModbusClientError('Socket write error: %s' % str(e)) + + while len_remaining > 0: + c = self.socket.recv(len_remaining) + len_read = len(c) + if len_read > 0: + resp += c + len_remaining -= len_read + if len_found is False and len(resp) >= TCP_HDR_LEN + TCP_RESP_MIN_LEN: + data_len = struct.unpack('>H', resp[TCP_HDR_O_LEN:TCP_HDR_O_LEN + 2]) + len_remaining = data_len[0] - (len(resp) - TCP_HDR_LEN) + else: + raise ModbusClientTimeout('Response timeout') + + if not (resp[TCP_HDR_LEN + 1]) & 0x80: + len_remaining = (resp[TCP_HDR_LEN + 2] + TCP_HDR_LEN) - len(resp) + len_found = True + else: + except_code = resp[TCP_HDR_LEN + 2] + + if self.trace_func: + s = '%s:%s:%s[addr=%s] <--' % (self.ipaddr, str(self.ipport), str(self.slave_id), addr) + for c in resp: + s += '%02X' % (ord(c)) + self.trace_func(s) + + if except_code: + raise ModbusClientException('Modbus exception: %d' % (except_code)) + + def write(self, addr, data): + """ Write Modbus device registers. If no connection exists to the + destination, one is created and disconnected at the end of the request. + + Parameters: + + addr : + Starting Modbus address. + + count : + Byte string containing register contents. + """ + write_count = 0 + write_offset = 0 + local_connect = False + count = len(data)/2 + + if self.socket is None: + local_connect = True + self.connect(self.timeout) + + try: + while (count > 0): + if count > self.max_count: + write_count = self.max_count + else: + write_count = count + start = (write_offset * 2) + end = int((write_offset + write_count) * 2) + self._write(addr + write_offset, data[start:end]) + count -= write_count + write_offset += write_count + finally: + if local_connect: + self.disconnect() diff --git a/build/lib/sunspec2/smdx.py b/build/lib/sunspec2/smdx.py new file mode 100644 index 0000000..37916ce --- /dev/null +++ b/build/lib/sunspec2/smdx.py @@ -0,0 +1,372 @@ + +""" + Copyright (C) 2020 SunSpec Alliance + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +""" + +import os +import xml.etree.ElementTree as ET + +import sunspec2.mdef as mdef + +SMDX_ROOT = 'sunSpecModels' +SMDX_MODEL = mdef.MODEL +SMDX_BLOCK = 'block' +SMDX_POINT = 'point' +SMDX_ATTR_VERS = 'v' +SMDX_ATTR_ID = 'id' +SMDX_ATTR_LEN = 'len' +SMDX_ATTR_NAME = mdef.NAME +SMDX_ATTR_TYPE = mdef.TYPE +SMDX_ATTR_COUNT = mdef.COUNT +SMDX_ATTR_VALUE = mdef.VALUE +SMDX_ATTR_TYPE_FIXED = 'fixed' +SMDX_ATTR_TYPE_REPEATING = 'repeating' +SMDX_ATTR_OFFSET = 'offset' +SMDX_ATTR_MANDATORY = mdef.MANDATORY +SMDX_ATTR_ACCESS = mdef.ACCESS +SMDX_ATTR_SF = mdef.SF +SMDX_ATTR_UNITS = mdef.UNITS + +SMDX_SYMBOL = 'symbol' +SMDX_COMMENT = 'comment' + +SMDX_STRINGS = 'strings' +SMDX_ATTR_LOCALE = 'locale' +SMDX_LABEL = mdef.LABEL +SMDX_DESCRIPTION = 'description' +SMDX_NOTES = 'notes' +SMDX_DETAIL = mdef.DETAIL + +SMDX_TYPE_INT16 = mdef.TYPE_INT16 +SMDX_TYPE_UINT16 = mdef.TYPE_UINT16 +SMDX_TYPE_COUNT = mdef.TYPE_COUNT +SMDX_TYPE_ACC16 = mdef.TYPE_ACC16 +SMDX_TYPE_ENUM16 = mdef.TYPE_ENUM16 +SMDX_TYPE_BITFIELD16 = mdef.TYPE_BITFIELD16 +SMDX_TYPE_PAD = mdef.TYPE_PAD +SMDX_TYPE_INT32 = mdef.TYPE_INT32 +SMDX_TYPE_UINT32 = mdef.TYPE_UINT32 +SMDX_TYPE_ACC32 = mdef.TYPE_ACC32 +SMDX_TYPE_ENUM32 = mdef.TYPE_ENUM32 +SMDX_TYPE_BITFIELD32 = mdef.TYPE_BITFIELD32 +SMDX_TYPE_IPADDR = mdef.TYPE_IPADDR +SMDX_TYPE_INT64 = mdef.TYPE_INT64 +SMDX_TYPE_UINT64 = mdef.TYPE_UINT64 +SMDX_TYPE_ACC64 = mdef.TYPE_ACC64 +SMDX_TYPE_IPV6ADDR = mdef.TYPE_IPV6ADDR +SMDX_TYPE_FLOAT32 = mdef.TYPE_FLOAT32 +SMDX_TYPE_STRING = mdef.TYPE_STRING +SMDX_TYPE_SUNSSF = mdef.TYPE_SUNSSF +SMDX_TYPE_EUI48 = mdef.TYPE_EUI48 + +SMDX_ACCESS_R = 'r' +SMDX_ACCESS_RW = 'rw' + +SMDX_MANDATORY_FALSE = 'false' +SMDX_MANDATORY_TRUE = 'true' + +smdx_access_types = {SMDX_ACCESS_R: mdef.ACCESS_R, SMDX_ACCESS_RW: mdef.ACCESS_RW} + +smdx_mandatory_types = {SMDX_MANDATORY_FALSE: mdef.MANDATORY_FALSE, SMDX_MANDATORY_TRUE: mdef.MANDATORY_TRUE} + +smdx_type_types = [ + SMDX_TYPE_INT16, + SMDX_TYPE_UINT16, + SMDX_TYPE_COUNT, + SMDX_TYPE_ACC16, + SMDX_TYPE_ENUM16, + SMDX_TYPE_BITFIELD16, + SMDX_TYPE_PAD, + SMDX_TYPE_INT32, + SMDX_TYPE_UINT32, + SMDX_TYPE_ACC32, + SMDX_TYPE_ENUM32, + SMDX_TYPE_BITFIELD32, + SMDX_TYPE_IPADDR, + SMDX_TYPE_INT64, + SMDX_TYPE_UINT64, + SMDX_TYPE_ACC64, + SMDX_TYPE_IPV6ADDR, + SMDX_TYPE_FLOAT32, + SMDX_TYPE_STRING, + SMDX_TYPE_SUNSSF, + SMDX_TYPE_EUI48 +] + +SMDX_PREFIX = 'smdx_' +SMDX_EXT = '.xml' + + +def to_smdx_filename(model_id): + + return '%s%05d%s' % (SMDX_PREFIX, int(model_id), SMDX_EXT) + + +def model_filename_to_id(filename): + + model_id = None + + if filename[0:5] == SMDX_PREFIX and filename[-4:] == SMDX_EXT: + try: + model_id = int(filename[5:-4]) + except Exception as e: + pass + + return model_id + +''' + smdx to json mapping: + + fixed block -> top level group + model 'name' attribute -> group 'name' + ID point is created for model ID and 'value' is the model ID value as a number + L point is created for model len - model len has no value specified in the model definition + fixed block points are placed in top level group + repeating block -> group with count = 0 (indicates model len shoud be used to determine number of groups) + repeating block 'name' -> group 'name', if no 'name' is defined 'name' = 'repeating' + + points: + all type, access, and mandatory attributes are preserved + point symbol map to the symbol object and placed in the symbols list for the point + symbol 'name' attribute -> symbol object 'name' + symbol element content -> symbol object 'value' + strings 'label', 'description', 'notes' elements map to point attributes 'label', 'desc', 'detail' +''' + + +def from_smdx_file(filename): + tree = ET.parse(filename) + root = tree.getroot() + return(from_smdx(root)) + + +def from_smdx(element): + """ Sets the model type attributes based on an element tree model type + element contained in an SMDX model definition. + + Parameters: + + element : + Element Tree model type element. + """ + + model_def = {} + + m = element.find(SMDX_MODEL) + if m is None: + raise mdef.ModelDefinitionError('Model definition not found') + try: + mid = mdef.to_number_type(m.attrib.get(SMDX_ATTR_ID)) + except ValueError: + raise mdef.ModelDefinitionError('Invalid model id: %s' % m.attrib.get(SMDX_ATTR_ID)) + + name = m.attrib.get(SMDX_ATTR_NAME) + if name is None: + name = 'model_' + str(mid) + model_def[mdef.NAME] = name + + strings = element.find(SMDX_STRINGS) + + # create top level group with ID and L points + fixed_def = {mdef.NAME: name, + mdef.TYPE: mdef.TYPE_GROUP, + mdef.POINTS: [ + {mdef.NAME: 'ID', mdef.VALUE: mid, + mdef.DESCRIPTION: 'Model identifier', mdef.LABEL: 'Model ID', + 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.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16} + ] + } + + repeating_def = None + + fixed = None + repeating = None + for b in m.findall(SMDX_BLOCK): + btype = b.attrib.get(SMDX_ATTR_TYPE, SMDX_ATTR_TYPE_FIXED) + if btype == SMDX_ATTR_TYPE_FIXED: + if fixed is not None: + raise mdef.ModelDefinitionError('Duplicate fixed block type definition') + fixed = b + elif btype == SMDX_ATTR_TYPE_REPEATING: + if repeating is not None: + raise mdef.ModelDefinitionError('Duplicate repeating block type definition') + repeating = b + else: + raise mdef.ModelDefinitionError('Invalid block type: %s' % btype) + + fixed_points_map = {} + if fixed is not None: + points = [] + for e in fixed.findall(SMDX_POINT): + point_def = from_smdx_point(e) + if point_def[mdef.NAME] not in fixed_points_map: + fixed_points_map[point_def[mdef.NAME]] = point_def + points.append(point_def) + else: + raise mdef.ModelDefinitionError('Duplicate point definition: %s' % point_def[mdef.NAME]) + if points: + fixed_def[mdef.POINTS].extend(points) + + repeating_points_map = {} + if repeating is not None: + name = repeating.attrib.get(SMDX_ATTR_NAME) + if name is None: + name = 'repeating' + repeating_def = {mdef.NAME: name, mdef.TYPE: mdef.TYPE_GROUP, mdef.COUNT: 0} + points = [] + for e in repeating.findall(SMDX_POINT): + point_def = from_smdx_point(e) + if point_def[mdef.NAME] not in repeating_points_map: + repeating_points_map[point_def[mdef.NAME]] = point_def + points.append(point_def) + else: + raise mdef.ModelDefinitionError('Duplicate point definition: %s' % point_def[mdef.NAME]) + if points: + repeating_def[mdef.POINTS] = points + fixed_def[mdef.GROUPS] = [repeating_def] + + e = element.find(SMDX_STRINGS) + if e.attrib.get(SMDX_ATTR_ID) == str(mid): + m = e.find(SMDX_MODEL) + if m is not None: + for a in m.findall('*'): + if a.tag == SMDX_LABEL and a.text: + fixed_def[mdef.LABEL] = a.text + elif a.tag == SMDX_DESCRIPTION and a.text: + fixed_def[mdef.DESCRIPTION] = a.text + elif a.tag == SMDX_NOTES and a.text: + fixed_def[mdef.DETAIL] = a.text + for p in e.findall(SMDX_POINT): + pid = p.attrib.get(SMDX_ATTR_ID) + label = desc = notes = None + for a in p.findall('*'): + if a.tag == SMDX_LABEL and a.text: + label = a.text + elif a.tag == SMDX_DESCRIPTION and a.text: + desc = a.text + 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: + if label: + point_def[mdef.LABEL] = label + if desc: + point_def[mdef.DESCRIPTION] = desc + if notes: + point_def[mdef.DETAIL] = notes + + model_def = {'id': mid, 'group': fixed_def} + return model_def + + +def from_smdx_point(element): + """ Sets the point attributes based on an element tree point element + contained in an SMDX model definition. + + Parameters: + + element : + Element Tree point type element. + + strings : + Indicates if *element* is a subelement of the 'strings' + definintion within the model definition. + """ + point_def = {} + pid = element.attrib.get(SMDX_ATTR_ID) + if pid is None: + raise mdef.ModelDefinitionError('Missing point id attribute') + point_def[mdef.NAME] = pid + ptype = element.attrib.get(SMDX_ATTR_TYPE) + if ptype is None: + raise mdef.ModelDefinitionError('Missing type attribute for point: %s' % pid) + elif ptype not in smdx_type_types: + raise mdef.ModelDefinitionError('Unknown point type %s for point %s' % (ptype, pid)) + point_def[mdef.TYPE] = ptype + plen = mdef.to_number_type(element.attrib.get(SMDX_ATTR_LEN)) + if ptype == SMDX_TYPE_STRING: + if plen is None: + raise mdef.ModelDefinitionError('Missing len attribute for point: %s' % pid) + point_def[mdef.SIZE] = plen + 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) + if mandatory == SMDX_MANDATORY_TRUE: + point_def[mdef.MANDATORY] = smdx_mandatory_types.get(mandatory) + access = element.attrib.get(SMDX_ATTR_ACCESS, SMDX_ACCESS_R) + if access not in smdx_access_types: + raise mdef.ModelDefinitionError('Unknown access type: %s' % access) + if access == SMDX_ACCESS_RW: + point_def[mdef.ACCESS] = smdx_access_types.get(access) + units = element.attrib.get(SMDX_ATTR_UNITS) + if units: + point_def[mdef.UNITS] = units + # if scale factor is an number, convert to correct type + sf = mdef.to_number_type(element.attrib.get(SMDX_ATTR_SF)) + if sf is not None: + point_def[mdef.SF] = sf + # if scale factor is an number, convert to correct type + value = mdef.to_number_type(element.attrib.get(SMDX_ATTR_VALUE)) + if value is not None: + point_def[mdef.VALUE] = value + + symbols = [] + for e in element.findall('*'): + if e.tag == SMDX_SYMBOL: + sid = e.attrib.get(SMDX_ATTR_ID) + value = e.text + try: + value = int(value) + except ValueError: + pass + symbols.append({mdef.NAME: sid, mdef.VALUE: value}) + if symbols: + point_def[mdef.SYMBOLS] = symbols + + return point_def + + +def indent(elem, level=0): + i = os.linesep + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i diff --git a/build/lib/sunspec2/spreadsheet.py b/build/lib/sunspec2/spreadsheet.py new file mode 100644 index 0000000..384de52 --- /dev/null +++ b/build/lib/sunspec2/spreadsheet.py @@ -0,0 +1,457 @@ + +""" + Copyright (C) 2020 SunSpec Alliance + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +""" + +import csv +import json +import copy + +import sunspec2.mdef as mdef + +ADDRESS_OFFSET = 'Address Offset' +GROUP_OFFSET = 'Group Offset' +NAME = 'Name' +VALUE = 'Value' +COUNT = 'Count' +TYPE = 'Type' +SIZE = 'Size' +SCALE_FACTOR = 'Scale Factor' +UNITS = 'Units' +ACCESS = 'RW Access (RW)' +MANDATORY = 'Mandatory (M)' +STATIC = 'Static (S)' +LABEL = 'Label' +DESCRIPTION = 'Description' +NOTES = 'Notes' + +columns = [ADDRESS_OFFSET, GROUP_OFFSET, NAME, VALUE, COUNT, TYPE, SIZE, SCALE_FACTOR, + UNITS, ACCESS, MANDATORY, STATIC, LABEL, DESCRIPTION, NOTES] + +empty_row = [''] * len(columns) + +ADDRESS_OFFSET_IDX = columns.index(ADDRESS_OFFSET) +GROUP_OFFSET_IDX = columns.index(GROUP_OFFSET) +NAME_IDX = columns.index(NAME) +VALUE_IDX = columns.index(VALUE) +COUNT_IDX = columns.index(COUNT) +TYPE_IDX = columns.index(TYPE) +SIZE_IDX = columns.index(SIZE) +SCALE_FACTOR_IDX = columns.index(SCALE_FACTOR) +UNITS_IDX = columns.index(UNITS) +ACCESS_IDX = columns.index(ACCESS) +MANDATORY_IDX = columns.index(MANDATORY) +STATIC_IDX = columns.index(STATIC) +LABEL_IDX = columns.index(LABEL) +DESCRIPTION_IDX = columns.index(DESCRIPTION) +NOTES_IDX = columns.index(NOTES) + + +def idx(row, attr, mandatory=False): + try: + return row.index(attr) + except: + if mandatory: + raise ValueError('Missing required attribute column: %s' % (attr)) + + +def row_is_empty(row, idx): + for e in row[idx:]: + if e is not None and e != '': + return False + return True + + +def find_name(entities, name): + for e in entities: + if e[mdef.NAME] == name: + return e + + +def element_type(row): + type_idx = idx(row, TYPE, mandatory=True) + + +def from_spreadsheet(spreadsheet): + model_def = {} + row = spreadsheet[0] + address_offset_idx = idx(row, ADDRESS_OFFSET) + group_offset_idx = idx(row, GROUP_OFFSET) + name_idx = idx(row, NAME, mandatory=True) + value_idx = mdef.to_number_type(idx(row, VALUE, mandatory=True)) + count_idx = mdef.to_number_type(idx(row, COUNT, mandatory=True)) + type_idx = idx(row, TYPE, mandatory=True) + size_idx = mdef.to_number_type(idx(row, SIZE, mandatory=True)) + scale_factor_idx = mdef.to_number_type(idx(row, SCALE_FACTOR, mandatory=True)) + units_idx = idx(row, UNITS, mandatory=True) + access_idx = idx(row, ACCESS, mandatory=True) + mandatory_idx = idx(row, MANDATORY, mandatory=True) + static_idx = idx(row, STATIC, mandatory=True) + label_idx = idx(row, LABEL) + description_idx = idx(row, DESCRIPTION) + has_notes = False + # if notes col not present, notes_idx will be None + notes_idx = idx(row, NOTES) + if notes_idx and row[notes_idx] == 'Notes': + has_notes = True + row_num = 1 + + group = None + point = None + comments = [] + parent = '' + + for row in spreadsheet[1:]: + row_num += 1 + name = row[name_idx] + value = mdef.to_number_type(row[value_idx]) + etype = row[type_idx] + + label = description = notes = '' + if len(row) > label_idx: + label = row[label_idx] + if len(row) > description_idx: + description = row[description_idx] + if has_notes: + notes = row[notes_idx] + if notes is None: + notes = '' + + # point + if etype in mdef.point_type_info: + # point + if group: + if not group.get(mdef.POINTS): + group[mdef.POINTS] = [] + if find_name(group[mdef.POINTS], name) is not None: + raise Exception('Duplicate point definition in group %s: %s' % (group[mdef.NAME], name)) + else: + raise Exception('Point %s defined outside of group' % name) + + size = mdef.to_number_type(row[size_idx]) + sf = mdef.to_number_type(row[scale_factor_idx]) + units = row[units_idx] + access = row[access_idx] + mandatory = row[mandatory_idx] + static = row[static_idx] + point = {mdef.NAME: name} + if etype: + point[mdef.TYPE] = etype + if size is not None and size != '': + point[mdef.SIZE] = size + if sf: + point[mdef.SF] = sf + if units: + point[mdef.UNITS] = units + if access: + point[mdef.ACCESS] = access + if mandatory: + point[mdef.MANDATORY] = mandatory + if static: + point[mdef.STATIC] = static + if label: + point[mdef.LABEL] = label + if description: + point[mdef.DESCRIPTION] = description + if has_notes: + point[mdef.NOTES] = notes + if value is not None and value != '': + point[mdef.VALUE] = value + if comments: + point[mdef.COMMENTS] = list(comments) + group[mdef.POINTS].append(point) + + # set the model id + if not parent and name == mdef.MODEL_ID_POINT_NAME: + model_def[mdef.ID] = value + comments = [] + # group + elif etype in mdef.group_types: + path = name.split('.') + group = model_def.get(mdef.GROUP) + parent = '' + if len(path) > 1: + parent = group[mdef.NAME] + for g in path[1:-1]: + group = find_name(group[mdef.GROUPS], g) + if group is None: + raise Exception('Unknown parent group id %s in group id %s' % (g, group)) + parent += '.%s' % group[mdef.NAME] + else: + if group is not None: + raise Exception('Redefintion of top-level group %s with %s' % (group[mdef.ID], name)) + if parent: + name = '%s.%s' % (parent, path[-1]) + else: + name = path[-1] + new_group = {mdef.NAME: path[-1], mdef.TYPE: etype} + if label: + new_group[mdef.LABEL] = label + if description: + new_group[mdef.DESCRIPTION] = description + if has_notes: + new_group[mdef.NOTES] = notes + if comments: + new_group[mdef.COMMENTS] = list(comments) + comments = [] + count = mdef.to_number_type(row[count_idx]) + if count is not None and count != '': + new_group[mdef.COUNT] = count + if group is None: + model_def[mdef.GROUP] = new_group + else: + if not group.get(mdef.GROUPS): + group[mdef.GROUPS] = [] + group[mdef.GROUPS].append(new_group) + group = new_group + # symbol - has name and value with no type + elif name and value is not None and value != '': + if point is None: + raise Exception('Unknown point for symbol %s' % name) + if not point.get(mdef.SYMBOLS): + point[mdef.SYMBOLS] = [] + if find_name(point[mdef.SYMBOLS], name) is not None: + raise Exception('Duplicate symbol definition in point %s: %s' % (point[mdef.ID], name)) + symbol = {mdef.NAME: name, mdef.VALUE: value} + point[mdef.SYMBOLS].append(symbol) + if label: + symbol[mdef.LABEL] = label + if description: + symbol[mdef.DESCRIPTION] = description + if has_notes: + symbol[mdef.NOTES] = notes + if comments: + symbol[mdef.COMMENTS] = list(comments) + comments = [] + elif not row_is_empty(row, 1): + raise ValueError('Invalid spreadsheet entry row %s: %s' % (row_num, row)) + # comment - no name, value, or type + elif row[0]: + comments.append(row[0]) + # blank line - comment with nothing in column 1 + return model_def + + +def to_spreadsheet(model_def): + # check if model_def has notes attr by searching string + mdef_str = json.dumps(model_def) + has_notes = '\"notes\"' in mdef_str + c_columns = copy.deepcopy(columns) + if has_notes: + spreadsheet = [columns] + else: + c_columns.remove('Notes') + spreadsheet = [c_columns] + to_spreadsheet_group(spreadsheet, model_def[mdef.GROUP], has_notes, addr_offset=0) + return(spreadsheet) + + +def to_spreadsheet_group(ss, group, has_notes, parent='', addr_offset=None): + # process comments + for c in group.get(mdef.COMMENTS, []): + to_spreadsheet_comment(ss, c, has_notes=has_notes) + # add group info + row = None + if has_notes: + row = [''] * len(columns) + else: + row = [''] * (len(columns) - 1) + + name = group.get(mdef.NAME, '') + if name: + if parent: + name = '%s.%s' % (parent, name) + row[NAME_IDX] = name + else: + raise Exception('Group missing name attribute') + row[TYPE_IDX] = group.get(mdef.TYPE, '') + row[COUNT_IDX] = group.get(mdef.COUNT, '') + row[LABEL_IDX] = group.get(mdef.LABEL, '') + row[DESCRIPTION_IDX] = group.get(mdef.DESCRIPTION, '') + if has_notes: + row[NOTES_IDX] = group.get(mdef.NOTES, '') + ss.append(row) + # process points + group_offset = 0 + for p in group.get(mdef.POINTS, []): + plen = to_spreadsheet_point(ss, p, has_notes=has_notes, addr_offset=addr_offset, group_offset=group_offset) + if addr_offset is not None: + addr_offset += plen + if group_offset is not None: + group_offset += plen + # process groups + addr_offset = None + for g in group.get(mdef.GROUPS, []): + to_spreadsheet_group(ss, g, has_notes=has_notes, parent=name, addr_offset=addr_offset) + + +def to_spreadsheet_point(ss, point, has_notes, addr_offset=None, group_offset=None): + # process comments + for c in point.get(mdef.COMMENTS, []): + to_spreadsheet_comment(ss, c, has_notes=has_notes) + # add point info + row = None + if has_notes: + row = [''] * len(columns) + else: + row = [''] * (len(columns) - 1) + name = point.get(mdef.NAME, '') + if name: + row[NAME_IDX] = name + else: + raise Exception('Point missing name attribute') + ptype = point.get(mdef.TYPE, '') + if ptype != '': + row[TYPE_IDX] = ptype + else: + raise Exception('Point %s missing type attribute' % name) + if addr_offset is not None: + row[ADDRESS_OFFSET_IDX] = addr_offset + elif group_offset is not None: + row[GROUP_OFFSET_IDX] = group_offset + access = point.get(mdef.ACCESS, '') + if access != mdef.ACCESS_RW: + access = '' + row[ACCESS_IDX] = access + mandatory = point.get(mdef.MANDATORY, '') + if mandatory != mdef.MANDATORY_TRUE: + mandatory = '' + row[MANDATORY_IDX] = mandatory + static = point.get(mdef.STATIC, '') + if static != mdef.STATIC_TRUE: + static = '' + row[STATIC_IDX] = static + row[UNITS_IDX] = point.get(mdef.UNITS, '') + row[SCALE_FACTOR_IDX] = mdef.to_number_type(point.get(mdef.SF, '')) + row[SIZE_IDX] = mdef.to_number_type(point.get(mdef.SIZE, '')) + row[VALUE_IDX] = mdef.to_number_type(point.get(mdef.VALUE, '')) + row[LABEL_IDX] = point.get(mdef.LABEL, '') + row[DESCRIPTION_IDX] = point.get(mdef.DESCRIPTION, '') + if has_notes: + row[NOTES_IDX] = point.get(mdef.NOTES, '') + ss.append(row) + # process symbols + for s in point.get(mdef.SYMBOLS, []): + to_spreadsheet_symbol(ss, s, has_notes=has_notes) + # return point length + try: + plen = mdef.point_type_info[ptype]['len'] + except KeyError: + raise Exception('Unknown point type %s for point %s' % (ptype, name)) + if not plen: + try: + plen = int(row[SIZE_IDX]) + except ValueError: + raise Exception('Point size is for point %s not an iteger value: %s' % (name, row[SIZE_IDX])) + return plen + + +def to_spreadsheet_symbol(ss, symbol, has_notes): + # process comments + for c in symbol.get(mdef.COMMENTS, []): + to_spreadsheet_comment(ss, c, has_notes=has_notes) + # add symbol info + row = None + if has_notes: + row = [''] * len(columns) + else: + row = [''] * (len(columns) - 1) + name = symbol.get(mdef.NAME, '') + if name: + row[NAME_IDX] = name + else: + raise Exception('Symbol missing name attribute') + value = symbol.get(mdef.VALUE, '') + if value != '': + row[VALUE_IDX] = value + else: + raise Exception('Symbol %s missing value' % name) + row[LABEL_IDX] = symbol.get(mdef.LABEL, '') + row[DESCRIPTION_IDX] = symbol.get(mdef.DESCRIPTION, '') + if has_notes: + row[NOTES_IDX] = symbol.get(mdef.NOTES, '') + ss.append(row) + + +def to_spreadsheet_comment(ss, comment, has_notes): + # add comment info + row = None + if has_notes: + row = [''] * len(columns) + else: + row = [''] * (len(columns) - 1) + row[0] = comment + ss.append(row) + + +def spreadsheet_equal(ss1, ss2): + count = len(ss1) + if count != len(ss2): + raise Exception('Different length: %s %s' % (count, len(ss2))) + for i in range(count): + if ss1[i] != ss2[i]: + raise Exception('Line %s different: %s %s' % (i + 1, ss1[i], ss2[i])) + return True + + +def from_csv(filename=None, csv_str=None): + return from_spreadsheet(spreadsheet_from_csv(filename=filename, csv_str=csv_str)) + + +def to_csv(model_def, filename=None, csv_str=None): + spreadsheet_to_csv(to_spreadsheet(model_def), filename=filename, csv_str=csv_str) + + +def spreadsheet_from_csv(filename=None, csv_str=None): + spreadsheet = [] + file = '' + + if filename: + import sys + file = open(filename) + if file: + for row in csv.reader(file): + if len(row) > 0: + # filter out informative offset information from the normative model definition + if row[TYPE_IDX] and row[TYPE_IDX] != TYPE: + row[ADDRESS_OFFSET_IDX] = '' + row[GROUP_OFFSET_IDX] = '' + if row[VALUE_IDX]: + row[VALUE_IDX] = mdef.to_number_type(row[VALUE_IDX]) + if row[COUNT_IDX]: + row[COUNT_IDX] = mdef.to_number_type(row[COUNT_IDX]) + if row[SIZE_IDX]: + row[SIZE_IDX] = mdef.to_number_type(row[SIZE_IDX]) + if row[SCALE_FACTOR_IDX]: + row[SCALE_FACTOR_IDX] = mdef.to_number_type(row[SCALE_FACTOR_IDX]) + spreadsheet.append(row) + + return spreadsheet + + +def spreadsheet_to_csv(spreadsheet, filename=None, csv_str=None): + file = None + if filename: + file = open(filename, 'w') + writer = csv.writer(file, lineterminator='\n') + for row in spreadsheet: + writer.writerow(row) + file.close() diff --git a/build/lib/sunspec2/tests/__init__.py b/build/lib/sunspec2/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/sunspec2/tests/mock_port.py b/build/lib/sunspec2/tests/mock_port.py new file mode 100644 index 0000000..04f9e2b --- /dev/null +++ b/build/lib/sunspec2/tests/mock_port.py @@ -0,0 +1,43 @@ +class MockPort(object): + PARITY_NONE = 'N' + PARITY_EVEN = 'E' + + def __init__(self, port, baudrate, bytesize, parity, stopbits, xonxoff, timeout, writeTimeout): + self.connected = True + self.port = port + self.baudrate = baudrate + self.bytesize = bytesize + self.parity = parity + self.stopbits = stopbits + self.xonxoff = xonxoff + self.timeout = timeout + self.writeTimeout = writeTimeout + + self.buffer = [] + self.request = [] + + def open(self): + pass + + def close(self): + self.connected = False + + def read(self, count): + return self.buffer.pop(0) + + def write(self, data): + self.request.append(data) + + def flushInput(self): + pass + + def _set_buffer(self, resp_list): + for bs in resp_list: + self.buffer.append(bs) + + def clear_buffer(self): + self.buffer = [] + + +def mock_port(port, baudrate, bytesize, parity, stopbits, xonxoff, timeout, writeTimeout): + return MockPort(port, baudrate, bytesize, parity, stopbits, xonxoff, timeout, writeTimeout) diff --git a/build/lib/sunspec2/tests/mock_socket.py b/build/lib/sunspec2/tests/mock_socket.py new file mode 100644 index 0000000..d881601 --- /dev/null +++ b/build/lib/sunspec2/tests/mock_socket.py @@ -0,0 +1,37 @@ +class MockSocket(object): + def __init__(self): + self.connected = False + self.timeout = 0 + self.ipaddr = None + self.ipport = None + self.buffer = [] + + self.request = [] + + def settimeout(self, timeout): + self.timeout = timeout + + def connect(self, ipaddrAndipportTup): + self.connected = True + self.ipaddr = ipaddrAndipportTup[0] + self.ipport = ipaddrAndipportTup[1] + + def close(self): + self.connected = False + + def recv(self, size): + return self.buffer.pop(0) + + def sendall(self, data): + self.request.append(data) + + def _set_buffer(self, resp_list): + for bs in resp_list: + self.buffer.append(bs) + + def clear_buffer(self): + self.buffer = [] + + +def mock_socket(AF_INET, SOCK_STREAM): + return MockSocket() diff --git a/build/lib/sunspec2/tests/test_data/__init__.py b/build/lib/sunspec2/tests/test_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/sunspec2/tests/test_data/device_1547.json b/build/lib/sunspec2/tests/test_data/device_1547.json new file mode 100644 index 0000000..29c4859 --- /dev/null +++ b/build/lib/sunspec2/tests/test_data/device_1547.json @@ -0,0 +1,612 @@ +{ + "name": "device_1547", + "models": [ + { + "ID": 1, + "Mn": "SunSpecTest", + "Md": "Test-1547-1", + "Opt": "opt_a_b_c", + "Vr": "1.2.3", + "SN": "sn-123456789", + "DA": 1, + "Pad": 0 + }, + { + "ID": 701, + "L": null, + "ACType": 3, + "St": 2, + "Alrm": 0, + "W": 9800, + "VA": 10000, + "Var": 200, + "PF": 985, + "A": 411, + "LLV": 2400, + "LNV": 2400, + "Hz": 60010, + "TotWhInj": 150, + "TotWhAbs": 0, + "TotVarhInj": 9, + "TotVarhAbs": 0, + "TmpAmb": 450, + "TmpCab": 550, + "TmpSnk": 650, + "TmpTrns": 500, + "TmpSw": 400, + "TmpOt": 420, + "WL1": 3200, + "VAL1": 3333, + "VarL1": 80, + "PFL1": 984, + "AL1": 137, + "VL1L2": 120, + "VL1": 120, + "TotWhInjL1": 49, + "TotWhAbsL1": 0, + "TotVarhInjL1": 2, + "TotVarhAbsL1": 0, + "WL2": 3300, + "VAL2": 3333, + "VarL2": 80, + "PFL2": 986, + "AL2": 136, + "VL2L3": 120, + "VL2": 120, + "TotWhInjL2": 50, + "TotWhAbsL2": 0, + "TotVarhInjL2": 3, + "TotVarhAbsL2": 0, + "WL3": 3500, + "VAL3": 3333, + "VarL3": 40, + "PFL3": 987, + "AL3": 138, + "VL3L1": 120, + "VL3N": 120, + "TotWhInjL3": 51, + "TotWhAbsL3": 0, + "TotVarhInjL3": 4, + "TotVarhAbsL3": 0, + "A_SF": -1, + "V_SF": -1, + "Hz_SF": -3, + "W_SF": 0, + "PF_SF": -3, + "VA_SF": 0, + "Var_SF": 0, + "TotWh_SF": 3, + "TotVarh_SF": 3, + "Tmp_SF": -1 + }, + { + "ID": 702, + "L": null, + "WMaxRtg": 10000, + "WOvrExtRtg": 10000, + "WOvrExtRtgPF": 1000, + "WUndExtRtg": 10000, + "WUndExtRtgPF": 1000, + "VAMaxRtg": 11000, + "VarMaxInjRtg": 2500, + "VarMaxAbsRtg": 0, + "WChaRteMaxRtg": 0, + "WDisChaRteMaxRtg": 0, + "VAChaRteMaxRtg": 0, + "VADisChaRteMaxRtg": 0, + "VNomRtg": 240, + "VMaxRtg": 270, + "VMinRtg": 210, + "AMaxRtg": 50, + "PFOvrExtRtg": 850, + "PFUndExtRtg": 850, + "ReactSusceptRtg": null, + "NorOpCatRtg": 2, + "AbnOpCatRtg": 3, + "CtrlModes": null, + "IntIslandCatRtg": null, + "WMax": 10000, + "WMaxOvrExt": null, + "WOvrExtPF": null, + "WMaxUndExt": null, + "WUndExtPF": null, + "VAMax": 10000, + "AMax": null, + "Vnom": null, + "VRefOfs": null, + "VMax": null, + "VMin": null, + "VarMaxInj": null, + "VarMaxAbs": null, + "WChaRteMax": null, + "WDisChaRteMax": null, + "VAChaRteMax": null, + "VADisChaRteMax": null, + "IntIslandCat": null, + "W_SF": 0, + "PF_SF": -3, + "VA_SF": 0, + "Var_SF": 0, + "V_SF": 0, + "A_SF": 0, + "S_SF": 0 + }, + { + "ID": 703, + "ES": 1, + "ESVHi": 1050, + "ESVLo": 917, + "ESHzHi": 6010, + "ESHzLo": 5950, + "ESDlyTms": 300, + "ESRndTms": 100, + "ESRmpTms": 60, + "V_SF": -3, + "Hz_SF": -2 + }, + { + "ID": 704, + "L": null, + "PFWInjEna": 0, + "PFWInjEnaRvrt": null, + "PFWInjRvrtTms": null, + "PFWInjRvrtRem": null, + "PFWAbsEna": 0, + "PFWAbsEnaRvrt": null, + "PFWAbsRvrtTms": null, + "PFWAbsRvrtRem": null, + "WMaxLimEna": 0, + "WMaxLim": 1000, + "WMaxLimRvrt": null, + "WMaxLimEnaRvrt": null, + "WMaxLimRvrtTms": null, + "WMaxLimRvrtRem": null, + "WSetEna": null, + "WSetMod": null, + "WSet": null, + "WSetRvrt": null, + "WSetPct": null, + "WSetPctRvrt": null, + "WSetEnaRvrt": null, + "WSetRvrtTms": null, + "WSetRvrtRem": null, + "VarSetEna": null, + "VarSetMod": null, + "VarSetPri": null, + "VarSet": null, + "VarSetRvrt": null, + "VarSetPct": null, + "VarSetPctRvrt": null, + "VarSetRvrtTms": null, + "VarSetRvrtRem": null, + "RGra": null, + "PF_SF": -3, + "WMaxLim_SF": -1, + "WSet_SF": null, + "WSetPct_SF": null, + "VarSet_SF": null, + "VarSetPct_SF": null, + "PFWInj": { + "PF": 950, + "Ext": 1 + }, + "PFWInjRvrt": { + "PF": null, + "Ext": null + }, + "PFWAbs": { + "PF": null, + "Ext": null + }, + "PFWAbsRvrt": { + "PF": null, + "Ext": null + } + }, + { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + }, + { + "ID": 706, + "Ena": 0, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 2, + "NCrv": 2, + "RvrtTms": null, + "RvrtRem": null, + "RvrtCrv": null, + "V_SF": 0, + "DeptRef_SF": 0, + "Crv": [ + { + "ActPt": 2, + "DeptRef": 1, + "RspTms": 10, + "ReadOnly": 1, + "Pt": [ + { + "V": 106, + "W": 100 + }, + { + "V": 110, + "W": 0 + } + ] + }, + { + "ActPt": 2, + "DeptRef": 1, + "RspTms": 5, + "ReadOnly": 0, + "Pt": [ + { + "V": 105, + "W": 100 + }, + { + "V": 109, + "W": 0 + } + ] + } + ] + }, + { + "ID": 707, + "L": null, + "Ena": 1, + "CrvSt": null, + "AdptCrvReq": null, + "AdptCrvRslt": null, + "NPt": 1, + "NCrvSet": 1, + "V_SF": -2, + "Tms_SF": 0, + "Crv": [ + { + "MustTrip": { + "ActPt": 1, + "Pt": [ + { + "V": 5000, + "Tms": 5 + } + ] + }, + "MayTrip": { + "ActPt": 1, + "Pt": [ + { + "V": 7000, + "Tms": 5 + } + ] + }, + "MomCess": { + "ActPt": 1, + "Pt": [ + { + "V": 6000, + "Tms": 5 + } + ] + } + } + ] + }, + { + "ID": 708, + "L": null, + "Ena": 1, + "CrvSt": null, + "AdptCrvReq": null, + "AdptCrvRslt": null, + "NPt": 1, + "NCrvSet": 1, + "V_SF": -2, + "Tms_SF": 0, + "Crv": [ + { + "MustTrip": { + "ActPt": 1, + "Pt": [ + { + "V": 12000, + "Tms": 5 + } + ] + }, + "MayTrip": { + "ActPt": 1, + "Pt": [ + { + "V": 10000, + "Tms": 5 + } + ] + }, + "MomCess": { + "ActPt": 1, + "Pt": [ + { + "V": 10000, + "Tms": 5 + } + ] + } + } + ] + }, + { + "ID": 709, + "L": null, + "Ena": 1, + "CrvSt": null, + "AdptCrvReq": null, + "AdptCrvRslt": null, + "NPt": 1, + "NCrvSet": 1, + "Freq_SF": null, + "Tms_SF": -2, + "Crv": [ + { + "MustTrip": { + "ActPt": 1, + "Pt": [ + { + "Freq": 5300, + "Tms": 5 + } + ] + }, + "MayTrip": { + "ActPt": 1, + "Pt": [ + { + "Freq": 5850, + "Tms": 5 + } + ] + }, + "MomCess": { + "ActPt": 1, + "Pt": [ + { + "Freq": 5850, + "Tms": 5 + } + ] + } + } + ] + }, + { + "ID": 710, + "L": null, + "Ena": null, + "CrvSt": null, + "AdptCrvReq": null, + "AdptCrvRslt": null, + "NPt": 1, + "NCrvSet": 1, + "Freq_SF": null, + "Tms_SF": -2, + "Crv": [ + { + "MustTrip": { + "ActPt": 1, + "Pt": [ + { + "Freq": 6500, + "Tms": 5 + } + ] + }, + "MayTrip": { + "ActPt": 1, + "Pt": [ + { + "Freq": 6050, + "Tms": 5 + } + ] + }, + "MomCess": { + "ActPt": 1, + "Pt": [ + { + "Freq": 6050, + "Tms": 5 + } + ] + } + } + ] + }, + { + "ID": 711, + "L": null, + "Ena": null, + "CrvSt": null, + "AdptCrvReq": null, + "AdptCrvRslt": null, + "NCtl": 1, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "Db_SF": -2, + "K_SF": -2, + "RspTms_SF": 0, + "Ctl": [ + { + "DbOf": 60030, + "DbUf": 59970, + "KOf": 40, + "KUf": 40, + "RspTms": 600 + } + ] + }, + { + "ID": 712, + "L": null, + "Ena": null, + "CrvSt": null, + "AdptCrvReq": null, + "AdptCrvRslt": null, + "NPt": 1, + "NCrv": 1, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 1, + "DeptRef": null, + "Pri": null, + "ReadOnly": null, + "Pt": [ + { + "W": null, + "Var": null + } + ] + } + ] + }, + { + "ID": 713, + "L": null, + "PrtAlrms": null, + "NPrt": 1, + "DCA": null, + "DCW": null, + "DCWhInj": null, + "DCWhAbs": null, + "DCA_SF": null, + "DCV_SF": null, + "DCW_SF": null, + "DCWH_SF": null, + "Prt": [ + { + "PrtTyp": null, + "ID": null, + "IDStr": null, + "DCA": null, + "DCV": null, + "DCW": null, + "DCWhInj": null, + "DCWhAbs": null, + "Tmp": null, + "DCSt": null, + "DCAlrm": null + } + ] + }, + { + "ID": 65535, + "L": 0 + } + ] +} \ No newline at end of file diff --git a/build/lib/sunspec2/tests/test_data/smdx_304.csv b/build/lib/sunspec2/tests/test_data/smdx_304.csv new file mode 100644 index 0000000..0580e49 --- /dev/null +++ b/build/lib/sunspec2/tests/test_data/smdx_304.csv @@ -0,0 +1,8 @@ +Address Offset,Group Offset,Name,Value,Count,Type,Size,Scale Factor,Units,RW Access (RW),Mandatory (M),Static (S),Label,Description,Detailed Description +,,inclinometer,,,group,,,,,,,Inclinometer Model,Include to support orientation measurements, +0,,ID,304,,uint16,,,,,M,S,Model ID,Model identifier, +1,,L,,,uint16,,,,,M,S,Model Length,Model length, +,,inclinometer.incl,,0,group,,,,,,,,, +,0,Inclx,,,int32,,-2,Degrees,,M,,X,X-Axis inclination, +,2,Incly,,,int32,,-2,Degrees,,,,Y,Y-Axis inclination, +,4,Inclz,,,int32,,-2,Degrees,,,,Z,Z-Axis inclination, diff --git a/build/lib/sunspec2/tests/test_data/wb_701-705.xlsx b/build/lib/sunspec2/tests/test_data/wb_701-705.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3edd65ed83dbaa2a42e5a0615a9f6cc833864dc3 GIT binary patch literal 34344 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 literal 0 HcmV?d00001 diff --git a/build/lib/sunspec2/tests/test_device.py b/build/lib/sunspec2/tests/test_device.py new file mode 100644 index 0000000..110946f --- /dev/null +++ b/build/lib/sunspec2/tests/test_device.py @@ -0,0 +1,2845 @@ +import sunspec2.device as device +import sunspec2.mdef as mdef +import sunspec2.mb as mb +import pytest + + +def test_get_model_info(): + model_info = device.get_model_info(705) + assert model_info[0]['id'] == 705 + assert model_info[1] + assert model_info[2] == 15 + + +def test_check_group_count(): + gdef = {'count': 'NPt'} + gdef2 = {'groups': [{'test': 'test'}, {'count': 'NPt'}]} + gdef3 = {'test1': 'test2', 'groups': [{'test3': 'test3'}, {'test4': 'test4'}]} + assert device.check_group_count(gdef) is True + assert device.check_group_count(gdef2) is True + assert device.check_group_count(gdef3) is False + + +def test_get_model_def(): + + with pytest.raises(mdef.ModelDefinitionError) as exc1: + device.get_model_def('z') + assert 'Invalid model id' in str(exc1.value) + + with pytest.raises(Exception) as exc2: + device.get_model_def('000') + assert 'Model definition not found for model' in str(exc2.value) + + assert device.get_model_def(704)['id'] == 704 + + +def test_add_mappings(): + group_def = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync" + } + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "static": "S", + "type": "uint16", + "value": 704 + } + ], + "type": "group" + }, + "id": 704 + } + + group_def_w_mappings = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync", + "point_defs": { + "PF": { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + }, + "group_defs": {} + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync", + "point_defs": { + "PF": { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + }, + "group_defs": {} + } + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "static": "S", + "type": "uint16", + "value": 704 + } + ], + "type": "group", + "point_defs": { + "ID": { + "name": "ID", + "static": "S", + "type": "uint16", + "value": 704 + } + }, + "group_defs": { + "PFWInj": { + "name": "PFWInj", + "points": [ + { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync", + "point_defs": { + "PF": { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + }, + "group_defs": {} + }, + "PFWInjRvrt": { + "name": "PFWInjRvrt", + "points": [ + { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync", + "point_defs": { + "PF": { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + }, + "group_defs": {} + } + } + }, + "id": 704 + } + device.add_mappings(group_def['group']) + assert group_def == group_def_w_mappings + + +class TestPoint: + def test___init__(self): + p_def = { + "name": "Ena", + "type": "enum16", + "sf": 'test sf' + } + + p = device.Point(p_def) + assert p.model is None + assert p.pdef == p_def + assert p.info == mb.point_type_info[mdef.TYPE_ENUM16] + assert p.len == 1 + assert p.offset == 0 + assert p.value is None + assert p.dirty is False + assert p.sf == 'test sf' + assert p.sf_required is True + assert p.sf_value is None + + def test__set_data(self): + p_def = { + "name": 'TestPoint', + "type": "uint16" + } + + # bytes + p = device.Point(p_def) + p._set_data(b'\x00\x03') + assert p.value == 3 + assert not p.dirty + + # dict + data = {"TestPoint": 3} + p2 = device.Point(p_def) + p2._set_data(data) + assert p2.value == 3 + + def test_value_getter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = device.Point(p_def) + p.value = 4 + assert p.value == 4 + + def test_value_setter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = device.Point(p_def) + p.value = 4 + assert p.value == 4 + + def test_cvalue_getter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = device.Point(p_def) + p.sf_required = True + p.sf_value = 3 + p.value = 4 + assert p.cvalue == 4000.0 + + def test_cvalue_setter(self): + p_def = { + "name": "TestPoint", + "type": "uint16" + } + p = device.Point(p_def) + p.sf_required = True + p.sf_value = 3 + p.cvalue = 3000 + assert p.value == 3 + + def test_get_value(self): + p_def = { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "name": "PF", + "type": "uint16" + } + p = device.Point(p_def) + p.value = 3 + assert p.get_value() == 3 + + p2 = device.Point(p_def) + assert p2.get_value() is None + + # pdef w/ sf + pdef_sf = { + "name": "TestPoint", + "type": "uint16", + "sf": "TestSF" + } + # sf point + sf_p = { + "name": "TestSF", + "value": 3, + "type": "sunssf" + } + + # computed + p_sf = device.Point(sf_p) + p_sf.value = 3 + points = {} + points['TestSF'] = p_sf + m2 = device.Model() + setattr(m2, 'points', points) + + g = device.Group() + setattr(g, 'points', points) + + p9 = device.Point(pdef_sf, model=m2) + p9.group = g + p9.value = 2020 + assert p9.get_value(computed=True) == 2020000.0 + + # computed exception + m3 = device.Model() + points2 = {} + setattr(m3, 'points', points2) + + p10 = device.Point(pdef_sf, model=m3) + g2 = device.Group() + setattr(g2, 'points', {}) + p10.group = g2 + p10.value = 2020 + with pytest.raises(device.ModelError) as exc: + p10.get_value(computed=True) + assert 'Scale factor TestSF for point TestPoint not found' in str(exc.value) + + def test_set_value(self): + p_def = { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "name": "PF", + "type": "uint16" + } + p = device.Point(p_def) + p.set_value(3) + assert p.value == 3 + + # test computed + pdef_computed = { + "name": "TestingComputed", + "type": "uint16", + "sf": "TestSF" + } + p_SF = device.Point() + p_SF.value = 2 + + points = {} + points['TestSF'] = p_SF + m = device.Model() + setattr(m, 'points', points) + + p3 = device.Point(pdef_computed, model=m) + g = device.Group + setattr(g, 'points', {}) + p3.group = g + p3.set_value(1000, computed=True, dirty=True) + assert p3.value == 10 + assert p3.dirty + + # test exceptions + p2_sf = device.Point() + m2 = device.Model() + points2 = {} + points2['TestSF'] = p2_sf + setattr(m2, 'points', points2) + + p4 = device.Point(pdef_computed, model=m2) + p4.group = g + with pytest.raises(device.ModelError) as exc: + p4.set_value(1000, computed=True) + assert 'SF field TestSF value not initialized for point TestingComputed' in str(exc.value) + + def test_get_mb(self): + p_def = { + "name": "ESVLo", + "type": "uint16", + } + p = device.Point(p_def) + p.value = 3 + assert p.get_mb() == b'\x00\x03' + p.value = None + assert p.get_mb() == b'\xff\xff' + assert p.get_mb(computed=True) == b'\xff\xff' + + # computed + p.value = 3 + p.sf_required = True + p.sf_value = 4 + assert p.get_mb(computed=True) == b'\x75\x30' + + def test_set_mb(self): + p_def = { + "name": "ESVLo", + "type": "uint16", + } + m = device.Model() + g = device.Group() + g.points = {} + p3 = device.Point(p_def, m) + p3.group = g + p3.set_mb(None) + assert p3.model.error_info == "Error setting value for ESVLo: object of type 'NoneType' has no len()\n" + + # exceptions + p_def2 = { + "name": "ESVLo", + "type": "uint16", + "sf": "TestSF" + } + p_sf = device.Point() + points = {} + points['TestSF'] = p_sf + setattr(m, 'points', points) + + m.error_info = '' + p4 = device.Point(p_def2, model=m) + p4.group = g + p4.set_mb(b'\x00\x03', computed=True) + assert m.error_info == 'Error setting value for ESVLo: SF field TestSF value not initialized for point ESVLo\n' + + del m.points['TestSF'] + m.error_info = '' + p5 = device.Point(p_def2, model=m) + p5.group = g + p5.set_mb(b'\x00\x04', computed=True) + assert m.error_info == 'Error setting value for ESVLo: Scale factor TestSF for point ESVLo not found\n' + + # test computed + pdef_computed = { + "name": "TestingComputed", + "type": "uint16", + "sf": "TestSF" + } + p_SF = device.Point() + p_SF.value = 2 + + points = {} + points['TestSF'] = p_SF + m = device.Model() + setattr(m, 'points', points) + + p6 = device.Point(pdef_computed, model=m) + p6.group = g + p6.set_mb(b'\x0b\xb8', computed=True, dirty=True) + assert p6.value == 30 + assert p6.dirty + + +class TestGroup: + def test___init__(self): + g_704 = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "name": "Ext", + "type": "enum16" + } + ], + "type": "sync" + }, + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 704 + }, + { + "name": "L", + "static": "S", + "type": "uint16" + }, + + { + "name": "PFWInjRvrtTms", + "type": "uint32", + }, + { + "name": "PFWInjRvrtRem", + "type": "uint32", + }, + { + "name": "PFWAbsEna", + "type": "enum16" + }, + { + "name": "PF_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 704 + } + g = device.Group(g_704['group']) + + assert g.gdef == g_704['group'] + assert g.model is None + assert g.gname == 'DERCtlAC' + assert g.offset == 0 + assert g.len == 10 + assert len(g.points) == 6 + assert len(g.groups) == 2 + assert g.points_len == 8 + assert g.group_class == device.Group + + def test___getattr__(self): + g_704 = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "name": "Ext", + "type": "enum16" + } + ], + "type": "sync" + }, + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 704 + }, + { + "name": "L", + "static": "S", + "type": "uint16" + }, + + { + "name": "PFWInjRvrtTms", + "type": "uint32", + }, + { + "name": "PFWInjRvrtRem", + "type": "uint32", + }, + { + "name": "PFWAbsEna", + "type": "enum16" + }, + { + "name": "PF_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 704 + } + g = device.Group(g_704['group']) + with pytest.raises(AttributeError) as exc: + g.qwerty + assert "Group object has no attribute qwerty" in str(exc.value) + assert g.ID + assert g.PFWAbsEna + + def test__group_data(self): + gdef_705 = { + "group": { + "groups": [ + { + "count": "NCrv", + "groups": [ + { + "count": "NPt", + "name": "Pt", + "points": [ + { + "name": "V", + "sf": "V_SF", + "type": "uint16", + }, + { + "name": "Var", + "sf": "DeptRef_SF", + "type": "int16", + "units": "VarPct" + } + ], + "type": "group" + } + ], + "name": "Crv", + "points": [ + { + "name": "ActPt", + "type": "uint16" + }, + { + "name": "DeptRef", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1 + }, + { + "name": "VAR_MAX_PCT", + "value": 2 + }, + { + "name": "VAR_AVAL_PCT", + "value": 3 + } + ], + "type": "enum16" + }, + { + "name": "Pri", + "symbols": [ + { + "name": "ACTIVE", + "value": 1 + }, + { + "name": "REACTIVE", + "value": 2 + }, + { + "name": "IEEE_1547", + "value": 3 + }, + { + "name": "PF", + "value": 4 + }, + { + "name": "VENDOR", + "value": 5 + } + ], + "type": "enum16" + }, + { + "name": "VRef", + "type": "uint16" + }, + { + "name": "VRefAuto", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "VRefTms", + "type": "uint16" + }, + { + "name": "RspTms", + "type": "uint16" + }, + { + "name": "ReadOnly", + "symbols": [ + { + "name": "RW", + "value": 0 + }, + { + "name": "R", + "value": 1 + } + ], + "type": "enum16" + } + ], + "type": "group" + } + ], + "name": "DERVoltVar", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 705 + }, + { + "name": "L", + "type": "uint16" + }, + { + "name": "Ena", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "CrvSt", + "symbols": [ + { + "name": "INACTIVE", + "value": 0 + }, + { + "name": "ACTIVE", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "AdptCrvReq", + "type": "uint16" + }, + { + "name": "AdptCrvRslt", + "symbols": [ + { + "name": "IN_PROGRESS", + "value": 0 + }, + { + "name": "COMPLETED", + "value": 1 + }, + { + "name": "FAILED", + "value": 2 + } + ], + "type": "enum16" + }, + { + "name": "NPt", + "type": "uint16" + }, + { + "name": "NCrv", + "type": "uint16" + }, + { + "name": "RvrtTms", + "type": "uint32" + }, + { + "name": "RvrtRem", + "type": "uint32" + }, + { + "name": "RvrtCrv", + "type": "uint16" + }, + { + "name": "V_SF", + "type": "sunssf" + }, + { + "name": "DeptRef_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 705 + } + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = device.Group() + assert g._group_data(gdata_705, 'Crv') == [{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, + {'V': 10200, 'Var': 0}, {'V': 10600, 'Var': -4000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, + {'V': 10500, 'Var': 0}, {'V': 10800, 'Var': -2000}]}] + + assert g._group_data(gdata_705['Crv'], index=0) == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, + 'VRefAuto': 0, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + def test__get_data_group_count(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = device.Group() + assert g._get_data_group_count(gdata_705['Crv']) == 3 + + def test__init_repeating_group(self): + gdef_705 = { + "group": { + "groups": [ + { + "count": "NCrv", + "groups": [ + { + "count": "NPt", + "name": "Pt", + "points": [ + { + "name": "V", + "sf": "V_SF", + "type": "uint16", + }, + { + "name": "Var", + "sf": "DeptRef_SF", + "type": "int16", + "units": "VarPct" + } + ], + "type": "group" + } + ], + "name": "Crv", + "points": [ + { + "name": "ActPt", + "type": "uint16" + }, + { + "name": "DeptRef", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1 + }, + { + "name": "VAR_MAX_PCT", + "value": 2 + }, + { + "name": "VAR_AVAL_PCT", + "value": 3 + } + ], + "type": "enum16" + }, + { + "name": "Pri", + "symbols": [ + { + "name": "ACTIVE", + "value": 1 + }, + { + "name": "REACTIVE", + "value": 2 + }, + { + "name": "IEEE_1547", + "value": 3 + }, + { + "name": "PF", + "value": 4 + }, + { + "name": "VENDOR", + "value": 5 + } + ], + "type": "enum16" + }, + { + "name": "VRef", + "type": "uint16" + }, + { + "name": "VRefAuto", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "VRefTms", + "type": "uint16" + }, + { + "name": "RspTms", + "type": "uint16" + }, + { + "name": "ReadOnly", + "symbols": [ + { + "name": "RW", + "value": 0 + }, + { + "name": "R", + "value": 1 + } + ], + "type": "enum16" + } + ], + "type": "group" + } + ], + "name": "DERVoltVar", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 705 + }, + { + "name": "L", + "type": "uint16" + }, + { + "name": "Ena", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "CrvSt", + "symbols": [ + { + "name": "INACTIVE", + "value": 0 + }, + { + "name": "ACTIVE", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "AdptCrvReq", + "type": "uint16" + }, + { + "name": "AdptCrvRslt", + "symbols": [ + { + "name": "IN_PROGRESS", + "value": 0 + }, + { + "name": "COMPLETED", + "value": 1 + }, + { + "name": "FAILED", + "value": 2 + } + ], + "type": "enum16" + }, + { + "name": "NPt", + "type": "uint16" + }, + { + "name": "NCrv", + "type": "uint16" + }, + { + "name": "RvrtTms", + "type": "uint32" + }, + { + "name": "RvrtRem", + "type": "uint32" + }, + { + "name": "RvrtCrv", + "type": "uint16" + }, + { + "name": "V_SF", + "type": "sunssf" + }, + { + "name": "DeptRef_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 705 + } + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + + m = device.Model(705, data=gdata_705) + + pdef_NPt = {"name": "NPt", "type": "uint16"} + p_NPt = device.Point(pdef_NPt) + p_NPt.value = 4 + + pdef_NCrv = {"name": "NCrv", "type": "uint16"} + p_NCrv = device.Point(pdef_NCrv) + points = {'NPt': p_NPt, 'NCrv': p_NCrv} + setattr(m, 'points', points) + + g2 = device.Group(gdef_705['group']['groups'][0], m) + + with pytest.raises(device.ModelError) as exc: + g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert 'Count field NCrv value not initialized for group Crv' in str(exc.value) + + # set value for NCrv count and reset the points attribute on model + p_NCrv.value = 3 + setattr(m, 'points', points) + groups = g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert len(groups) == 3 + assert len(groups[0].groups['Pt']) == 4 + + def test_get_dict(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m2 = device.Model(705, data=gdata_705) + assert m2.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefAutoEna': None, 'VRefTms': 5, 'RspTms': 6, + 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + # test computed + m2.groups['Crv'][0].points['DeptRef'].sf_required = True + m2.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m2.groups['Crv'][0].points['Pri'].sf_required = True + m2.groups['Crv'][0].points['Pri'].sf_value = 3 + computed_dict = m2.groups['Crv'][0].get_dict(computed=True) + assert computed_dict['DeptRef'] == 1000.0 + assert computed_dict['Pri'] == 1000.0 + + def test_set_dict(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefAutoEna': None, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + new_dict = {'ActPt': 4, 'DeptRef': 4000, 'Pri': 5000, 'VRef': 3, 'VRefAuto': 2, 'VRefAutoEna': None, + 'VRefTms': 2, 'RspTms': 2, 'ReadOnly': 2, + 'Pt': [{'V': 111, 'Var': 111}, {'V': 123, 'Var': 1112}, {'V': 111, 'Var': 111}, + {'V': 123, 'Var': -1112}]} + + m.groups['Crv'][0].set_dict(new_dict, dirty=True) + assert m.groups['Crv'][0].get_dict() == new_dict + assert m.groups['Crv'][0].VRef.value == 3 + assert m.groups['Crv'][0].VRef.dirty + assert m.groups['Crv'][0].Pri.dirty + + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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 + m.groups['Crv'][0].set_dict(new_dict, computed=True) + computed_dict = m.groups['Crv'][0].get_dict() + assert computed_dict['DeptRef'] == 4.0 + assert computed_dict['Pri'] == 5.0 + + def test_get_json(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \ + ''' "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000},''' + \ + ''' {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0}, {"V": 10700, "Var": -3000}]}''' + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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,''' + \ + ''' "VRefTms": 5, "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): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ + ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5,''' + \ + ''' "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000},''' + \ + ''' {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0},''' + \ + ''' {"V": 10700, "Var": -3000}]}''' + + json_to_set = '''{"ActPt": 4, "DeptRef": 9999, "Pri": 9999, "VRef": 99, "VRefAuto": 88,''' + \ + ''' "VRefAutoEna": null, "VRefTms": 88, "RspTms": 88, "ReadOnly": 77, "Pt":''' + \ + ''' [{"V": 77, "Var": 66}, {"V": 55, "Var": 44}, {"V": 33, "Var": 22},''' + \ + ''' {"V": 111, "Var": -2222}]}''' + + m.groups['Crv'][0].set_json(json_to_set) + assert m.groups['Crv'][0].get_json() == json_to_set + assert m.groups['Crv'][0].DeptRef.value == 9999 + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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 + m.groups['Crv'][0].set_json(json_to_set, computed=True, dirty=True) + assert m.groups['Crv'][0].points['DeptRef'].value == 9 + assert m.groups['Crv'][0].points['DeptRef'].dirty + assert m.groups['Crv'][0].points['Pri'].value == 9 + assert m.groups['Crv'][0].points['Pri'].dirty + + def test_get_mb(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00' \ + b'\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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' \ + b'\x00\x05\x00\x06\x00\x01\x00\\\x00\x1e\x00`\x00' \ + b'\x00\x00g\x00\x00\x00k\xff\xe2' + + def test_set_mb(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00' \ + b'\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' + bs = b'\x00\x04\x03\xe7\x03x\x03\t\x02\x9a\x02+\x01\xbc\x01M' \ + b'\x00\xde\x00o\x00\xde\x01M\x01\xbc\x02+\x02\x9a\xfc\xf7\xf4H' + + m.groups['Crv'][0].set_mb(bs, dirty=True) + assert m.groups['Crv'][0].get_mb() == bs + assert m.groups['Crv'][0].DeptRef.value == 999 + assert m.groups['Crv'][0].DeptRef.dirty + + # test computed + # set points DeptRef and Pri to 3000 w/ byte string + computed_bs = b'\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<' \ + b'\x00\x00)\xcc\xf4H' + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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 + m.groups['Crv'][0].set_mb(computed_bs, computed=True) + assert m.groups['Crv'][0].points['DeptRef'].value == 3 + assert m.groups['Crv'][0].points['Pri'].value == 3 + + +class TestModel: + def test__init__(self): + m = device.Model(704) + assert m.model_id == 704 + assert m.model_addr == 0 + assert m.model_len == 0 + assert m.model_def['id'] == 704 + assert m.error_info == '' + assert m.gdef['name'] == 'DERCtlAC' + assert m.mid is None + assert m.device is None + + assert m.model == m + m2 = device.Model('abc') + assert m2.error_info == 'Invalid model id: abc\n' + + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + # test repeating group model + m2 = device.Model(705, data=gdata_705) + assert m2.model_id == 705 + assert m2.model_addr == 0 + assert m2.model_len == 0 + assert m2.model_def['id'] == 705 + assert m2.error_info == '' + assert m2.gdef['name'] == 'DERVoltVar' + assert m2.mid is None + assert m2.device is None + + def test__error(self): + m = device.Model(704) + m.add_error('test error') + assert m.error_info == 'test error\n' + + +class TestDevice: + def test__init__(self): + d = device.Device() + assert d.name is None + assert d.did is None + assert d.models == {} + assert d.model_list == [] + assert d.model_class == device.Model + + def test__get_attr__(self): + d = device.Device() + m = device.Model() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + d.add_model(m) + assert d.mid_test + + with pytest.raises(AttributeError) as exc: + d.foo + assert "\'Device\' object has no attribute \'foo\'" in str(exc.value) + + def test_scan(self): + pass + + def test_add_model(self): + d = device.Device() + m = device.Model() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + d.add_model(m) + assert d.models['mid_test'] + assert d.models['group_test'] + assert m.device == d + + def test_get_dict(self): + d = device.Device() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + d.add_model(m) + assert d.get_dict() == {'name': None, 'did': None, 'models': [ + {'ID': 705, 'L': 64, 'Ena': 1, 'CrvSt': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, + 'RvrtTms': 0, 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'Crv': [ + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, + 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, {'V': 10300, 'Var': 0}, + {'V': 10700, 'Var': -3000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, + 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, {'V': 10200, 'Var': 0}, + {'V': 10600, 'Var': -4000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, + 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, {'V': 10500, 'Var': 0}, + {'V': 10800, 'Var': -2000}]}], 'mid': None, 'error': '', 'model_id': 705}]} + + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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_dict(computed=True) == {'name': None, 'did': None, 'models': [ + {'ID': 705, 'L': 64, 'Ena': 1, 'CrvSt': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, + 'RvrtTms': 0, 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'Crv': [ + {'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, + 'VRefTms': 5, '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, 'VRefTms': 5, + '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, 'VRefTms': 5, + '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}]} + + def test_get_json(self): + d = device.Device() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + d.add_model(m) + assert d.get_json() == '''{"name": null, "did": null, "models": [{"ID": 705, "L": 64, "Ena": 1,''' + \ + ''' "CrvSt": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3,''' + \ + ''' "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2,''' + \ + ''' "Crv": [{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \ + ''' "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 1, ''' + \ + '''"Pt": [{"V": 9200, "Var": 3000}, {"V": 9670, "Var": 0}, {"V": 10300,''' + \ + ''' "Var": 0}, {"V": 10700, "Var": -3000}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1,''' + \ + ''' "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6,''' + \ + ''' "ReadOnly": 0, "Pt": [{"V": 9300, "Var": 3000}, {"V": 9570, "Var": 0},''' + \ + ''' {"V": 10200, "Var": 0}, {"V": 10600, "Var": -4000}]}, {"ActPt": 4,''' + \ + ''' "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null,''' + \ + ''' "VRefTms": 5, "RspTms": 6, "ReadOnly": 0, "Pt": [{"V": 9400, "Var": 2000},''' + \ + ''' {"V": 9570, "Var": 0}, {"V": 10500, "Var": 0}, {"V": 10800, "Var": -2000}]}],''' + \ + ''' "mid": null, "error": "", "model_id": 705}]}''' + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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": 64,''' + \ + ''' "Ena": 1, "CrvSt": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0,''' + \ + ''' "NPt": 4, "NCrv": 3, "RvrtTms": 0, "RvrtRem": 0,''' + \ + ''' "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2, "Crv": [{"ActPt": 4,''' + \ + ''' "DeptRef": 1000.0, "Pri": 1000.0, "VRef": 1, "VRefAuto": 0,''' + \ + ''' "VRefAutoEna": null, "VRefTms": 5, "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, "VRefTms": 5, "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": null, "VRefTms": 5, "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}]}''' + + def test_get_mb(self): + d = device.Device() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + d.add_model(m) + assert d.get_mb() == b"\x02\xc1\x00@\x00\x01\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\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff" \ + b"\xff\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04" \ + b"\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00" \ + b"\x00'\xd8\x00\x00)h\xf0`\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00" \ + b"\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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\x00@\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x03' \ + b'\xe8\x03\xe8\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x01\x00' \ + b'\\\x00\x1e\x00`\x00\x00\x00g\x00\x00\x00k\xff\xe2\x00\x04\x00\x01' \ + b'\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00\x00]\x00' \ + b'\x1e\x00_\x00\x00\x00f\x00\x00\x00j\xff\xd8\x00\x04\x00\x01\x00' \ + b'\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00\x00^\x00\x14' \ + b'\x00_\x00\x00\x00i\x00\x00\x00l\xff\xec' + + def test_set_mb(self): + d = device.Device() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + d.add_model(m) + assert d.get_mb() == b"\x02\xc1\x00@\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00" \ + b"\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01" \ + b"\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8" \ + b"\x00\x00)h\xf0`\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00" \ + b"\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" + + # DeptRef and Pri set to 3000 in byte string + bs = b"\x02\xc1\x00?\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00" \ + b"\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01\x00\x01\x00" \ + b"\x01\x00\x00\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8\x00\x00)h\xf0`\x00\x04" \ + b"\x00\x01\x00\x01\x00\x01\x00\x00\x00\x05\x00\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" + d.set_mb(bs, dirty=True) + assert m.groups['Crv'][0].DeptRef.value == 3000 + assert m.groups['Crv'][0].DeptRef.dirty + assert m.groups['Crv'][0].Pri.value == 3000 + assert m.groups['Crv'][0].Pri.dirty + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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 + d.set_mb(bs, computed=True, dirty=False) + assert m.groups['Crv'][0].DeptRef.value == 3 + assert not m.groups['Crv'][0].DeptRef.dirty + assert m.groups['Crv'][0].Pri.value == 3 + assert not m.groups['Crv'][0].Pri.dirty + + def test_find_mid(self): + d = device.Device() + m = device.Model() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + setattr(m, 'mid', 'mid_test') + d.add_model(m) + assert d.find_mid('mid_test') == m diff --git a/build/lib/sunspec2/tests/test_file_client.py b/build/lib/sunspec2/tests/test_file_client.py new file mode 100644 index 0000000..4bf9192 --- /dev/null +++ b/build/lib/sunspec2/tests/test_file_client.py @@ -0,0 +1,2668 @@ +import sunspec2.file.client as file_client +import sunspec2.mdef as mdef +import sunspec2.mb as mb +import sunspec2.device as device +import pytest + + +class TestFileClientPoint: + def test___init__(self): + p_def = { + "name": "Ena", + "type": "enum16", + "sf": 'test sf' + } + + p = file_client.FileClientPoint(p_def) + assert p.model is None + assert p.pdef == p_def + assert p.info == mb.point_type_info[mdef.TYPE_ENUM16] + assert p.len == 1 + assert p.offset == 0 + assert p.value is None + assert p.dirty is False + assert p.sf == 'test sf' + assert p.sf_required is True + assert p.sf_value is None + + def test__set_data(self): + p_def = { + "name": 'TestPoint', + "type": "uint16" + } + + # bytes + p = file_client.FileClientPoint(p_def) + p._set_data(b'\x00\x03') + assert p.value == 3 + assert not p.dirty + + # dict + data = {"TestPoint": 3} + p2 = file_client.FileClientPoint(p_def) + p2._set_data(data) + assert p2.value == 3 + + def test_value_getter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + p.value = 4 + assert p.value == 4 + + def test_value_setter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + p.value = 4 + assert p.value == 4 + + def test_cvalue_getter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + p.sf_required = True + p.sf_value = 3 + p.value = 4 + assert p.cvalue == 4000.0 + + def test_cvalue_setter(self): + p_def = { + "name": "TestPoint", + "type": "uint16" + } + p = file_client.FileClientPoint(p_def) + p.sf_required = True + p.sf_value = 3 + p.cvalue = 3000 + assert p.value == 3 + + def test_get_value(self): + p_def = { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "name": "PF", + "type": "uint16" + } + p = file_client.FileClientPoint(p_def) + p.value = 3 + assert p.get_value() == 3 + + p2 = file_client.FileClientPoint(p_def) + assert p2.get_value() is None + + # pdef w/ sf + pdef_sf = { + "name": "TestPoint", + "type": "uint16", + "sf": "TestSF" + } + # sf point + sf_p = { + "name": "TestSF", + "value": 3, + "type": "sunssf" + } + + # computed + p_sf = file_client.FileClientPoint(sf_p) + p_sf.value = 3 + points = {} + points['TestSF'] = p_sf + m2 = file_client.FileClientModel() + setattr(m2, 'points', points) + + p9 = file_client.FileClientPoint(pdef_sf, model=m2) + g = file_client.FileClientGroup() + g.points = {} + p9.group = g + p9.value = 2020 + assert p9.get_value(computed=True) == 2020000.0 + + # computed exception + m3 = file_client.FileClientModel() + points2 = {} + setattr(m3, 'points', points2) + + p10 = file_client.FileClientPoint(pdef_sf, model=m3) + p10.value = 2020 + p10.group = g + with pytest.raises(device.ModelError) as exc: + p10.get_value(computed=True) + assert 'Scale factor TestSF for point TestPoint not found' in str(exc.value) + + def test_set_value(self): + p_def = { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "name": "PF", + "type": "uint16" + } + p = file_client.FileClientPoint(p_def) + p.set_value(3) + assert p.value == 3 + + # test computed + pdef_computed = { + "name": "TestingComputed", + "type": "uint16", + "sf": "TestSF" + } + p_SF = file_client.FileClientPoint() + p_SF.value = 2 + + points = {} + points['TestSF'] = p_SF + m = file_client.FileClientModel() + setattr(m, 'points', points) + + g = file_client.FileClientGroup() + g.points = {} + + p3 = file_client.FileClientPoint(pdef_computed, model=m, group=g) + p3.set_value(1000, computed=True, dirty=True) + assert p3.value == 10 + assert p3.dirty + + # test exceptions + p2_sf = file_client.FileClientPoint() + m2 = file_client.FileClientModel() + points2 = {} + points2['TestSF'] = p2_sf + setattr(m2, 'points', points2) + + p4 = file_client.FileClientPoint(pdef_computed, model=m2, group=g) + with pytest.raises(device.ModelError) as exc: + p4.set_value(1000, computed=True) + assert 'SF field TestSF value not initialized for point TestingComputed' in str(exc.value) + + del m2.points['TestSF'] + with pytest.raises(device.ModelError) as exc: + p4.set_value(1000, computed=True) + assert 'Scale factor TestSF for point TestingComputed not found' in str(exc.value) + + def test_get_mb(self): + p_def = { + "name": "ESVLo", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + p.value = 3 + assert p.get_mb() == b'\x00\x03' + p.value = None + assert p.get_mb() == b'\xff\xff' + assert p.get_mb(computed=True) == b'\xff\xff' + + # computed + p.value = 3 + p.sf_required = True + p.sf_value = 4 + assert p.get_mb(computed=True) == b'\x75\x30' + + def test_set_mb(self): + p_def = { + "name": "ESVLo", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + + p.set_mb(b'\x00\x03', dirty=True) + assert p.value == 3 + assert p.dirty is True + + # unimplemented + p.set_mb(b'\xff\xff') + assert p.value is None + assert p.sf_value is None + + p2 = file_client.FileClientPoint(p_def) + p2.len = 100 + assert p2.set_mb(b'\x00\x03') == 2 + assert p2.value is None + + m = file_client.FileClientModel() + p3 = file_client.FileClientPoint(p_def, m) + p3.set_mb(None) + assert p3.model.error_info == '''Error setting value for ESVLo: object of type 'NoneType' has no len()\n''' + + # exceptions + p_def2 = { + "name": "ESVLo", + "type": "uint16", + "sf": "TestSF" + } + p_sf = file_client.FileClientPoint() + points = {} + points['TestSF'] = p_sf + setattr(m, 'points', points) + + g = file_client.FileClientGroup() + g.points = {} + + m.error_info = '' + p4 = file_client.FileClientPoint(p_def2, model=m, group=g) + p4.set_mb(b'\x00\x03', computed=True) + assert p4.model.error_info == "Error setting value for ESVLo: SF field TestSF value not initialized for point ESVLo\n" + + m.error_info = '' + del m.points['TestSF'] + p5 = file_client.FileClientPoint(p_def2, model=m, group=g) + p5.set_mb(b'\x00\x04', computed=True) + assert p5.model.error_info == '''Error setting value for ESVLo: Scale factor TestSF for point ESVLo not found\n''' + + # test computed + pdef_computed = { + "name": "TestingComputed", + "type": "uint16", + "sf": "TestSF" + } + p_SF = file_client.FileClientPoint() + p_SF.value = 2 + + points = {} + points['TestSF'] = p_SF + m = file_client.FileClientModel() + setattr(m, 'points', points) + + p3 = file_client.FileClientPoint(pdef_computed, model=m, group=g) + p3.set_mb(b'\x0b\xb8', computed=True, dirty=True) + assert p3.value == 30 + assert p3.dirty + + +class TestFileClientGroup: + def test___init__(self): + g_704 = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "name": "Ext", + "type": "enum16" + } + ], + "type": "sync" + }, + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 704 + }, + { + "name": "L", + "static": "S", + "type": "uint16" + }, + + { + "name": "PFWInjRvrtTms", + "type": "uint32", + }, + { + "name": "PFWInjRvrtRem", + "type": "uint32", + }, + { + "name": "PFWAbsEna", + "type": "enum16" + }, + { + "name": "PF_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 704 + } + g = file_client.FileClientGroup(g_704['group']) + + assert g.gdef == g_704['group'] + assert g.model is None + assert g.gname == 'DERCtlAC' + assert g.offset == 0 + assert g.len == 10 + assert len(g.points) == 6 + assert len(g.groups) == 2 + assert g.points_len == 8 + assert g.group_class == file_client.FileClientGroup + + def test___getattr__(self): + g_704 = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "name": "Ext", + "type": "enum16" + } + ], + "type": "sync" + }, + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 704 + }, + { + "name": "L", + "static": "S", + "type": "uint16" + }, + + { + "name": "PFWInjRvrtTms", + "type": "uint32", + }, + { + "name": "PFWInjRvrtRem", + "type": "uint32", + }, + { + "name": "PFWAbsEna", + "type": "enum16" + }, + { + "name": "PF_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 704 + } + g = file_client.FileClientGroup(g_704['group']) + with pytest.raises(AttributeError) as exc: + g.qwerty + assert "Group object has no attribute qwerty" in str(exc.value) + assert g.ID + assert g.PFWAbsEna + + def test__group_data(self): + gdef_705 = { + "group": { + "groups": [ + { + "count": "NCrv", + "groups": [ + { + "count": "NPt", + "name": "Pt", + "points": [ + { + "name": "V", + "sf": "V_SF", + "type": "uint16", + }, + { + "name": "Var", + "sf": "DeptRef_SF", + "type": "int16", + "units": "VarPct" + } + ], + "type": "group" + } + ], + "name": "Crv", + "points": [ + { + "name": "ActPt", + "type": "uint16" + }, + { + "name": "DeptRef", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1 + }, + { + "name": "VAR_MAX_PCT", + "value": 2 + }, + { + "name": "VAR_AVAL_PCT", + "value": 3 + } + ], + "type": "enum16" + }, + { + "name": "Pri", + "symbols": [ + { + "name": "ACTIVE", + "value": 1 + }, + { + "name": "REACTIVE", + "value": 2 + }, + { + "name": "IEEE_1547", + "value": 3 + }, + { + "name": "PF", + "value": 4 + }, + { + "name": "VENDOR", + "value": 5 + } + ], + "type": "enum16" + }, + { + "name": "VRef", + "type": "uint16" + }, + { + "name": "VRefAuto", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "VRefTms", + "type": "uint16" + }, + { + "name": "RspTms", + "type": "uint16" + }, + { + "name": "ReadOnly", + "symbols": [ + { + "name": "RW", + "value": 0 + }, + { + "name": "R", + "value": 1 + } + ], + "type": "enum16" + } + ], + "type": "group" + } + ], + "name": "DERVoltVar", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 705 + }, + { + "name": "L", + "type": "uint16" + }, + { + "name": "Ena", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "CrvSt", + "symbols": [ + { + "name": "INACTIVE", + "value": 0 + }, + { + "name": "ACTIVE", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "AdptCrvReq", + "type": "uint16" + }, + { + "name": "AdptCrvRslt", + "symbols": [ + { + "name": "IN_PROGRESS", + "value": 0 + }, + { + "name": "COMPLETED", + "value": 1 + }, + { + "name": "FAILED", + "value": 2 + } + ], + "type": "enum16" + }, + { + "name": "NPt", + "type": "uint16" + }, + { + "name": "NCrv", + "type": "uint16" + }, + { + "name": "RvrtTms", + "type": "uint32" + }, + { + "name": "RvrtRem", + "type": "uint32" + }, + { + "name": "RvrtCrv", + "type": "uint16" + }, + { + "name": "V_SF", + "type": "sunssf" + }, + { + "name": "DeptRef_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 705 + } + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = file_client.FileClientGroup() + assert g._group_data(gdata_705, 'Crv') == [{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, + {'V': 10200, 'Var': 0}, {'V': 10600, 'Var': -4000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, + {'V': 10500, 'Var': 0}, {'V': 10800, 'Var': -2000}]}] + + assert g._group_data(gdata_705['Crv'], index=0) == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, + 'VRefAuto': 0, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + def test__get_data_group_count(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = file_client.FileClientGroup() + assert g._get_data_group_count(gdata_705['Crv']) == 3 + + def test__init_repeating_group(self): + gdef_705 = { + "group": { + "groups": [ + { + "count": "NCrv", + "groups": [ + { + "count": "NPt", + "name": "Pt", + "points": [ + { + "name": "V", + "sf": "V_SF", + "type": "uint16", + }, + { + "name": "Var", + "sf": "DeptRef_SF", + "type": "int16", + "units": "VarPct" + } + ], + "type": "group" + } + ], + "name": "Crv", + "points": [ + { + "name": "ActPt", + "type": "uint16" + }, + { + "name": "DeptRef", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1 + }, + { + "name": "VAR_MAX_PCT", + "value": 2 + }, + { + "name": "VAR_AVAL_PCT", + "value": 3 + } + ], + "type": "enum16" + }, + { + "name": "Pri", + "symbols": [ + { + "name": "ACTIVE", + "value": 1 + }, + { + "name": "REACTIVE", + "value": 2 + }, + { + "name": "IEEE_1547", + "value": 3 + }, + { + "name": "PF", + "value": 4 + }, + { + "name": "VENDOR", + "value": 5 + } + ], + "type": "enum16" + }, + { + "name": "VRef", + "type": "uint16" + }, + { + "name": "VRefAuto", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "VRefTms", + "type": "uint16" + }, + { + "name": "RspTms", + "type": "uint16" + }, + { + "name": "ReadOnly", + "symbols": [ + { + "name": "RW", + "value": 0 + }, + { + "name": "R", + "value": 1 + } + ], + "type": "enum16" + } + ], + "type": "group" + } + ], + "name": "DERVoltVar", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 705 + }, + { + "name": "L", + "type": "uint16" + }, + { + "name": "Ena", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "CrvSt", + "symbols": [ + { + "name": "INACTIVE", + "value": 0 + }, + { + "name": "ACTIVE", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "AdptCrvReq", + "type": "uint16" + }, + { + "name": "AdptCrvRslt", + "symbols": [ + { + "name": "IN_PROGRESS", + "value": 0 + }, + { + "name": "COMPLETED", + "value": 1 + }, + { + "name": "FAILED", + "value": 2 + } + ], + "type": "enum16" + }, + { + "name": "NPt", + "type": "uint16" + }, + { + "name": "NCrv", + "type": "uint16" + }, + { + "name": "RvrtTms", + "type": "uint32" + }, + { + "name": "RvrtRem", + "type": "uint32" + }, + { + "name": "RvrtCrv", + "type": "uint16" + }, + { + "name": "V_SF", + "type": "sunssf" + }, + { + "name": "DeptRef_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 705 + } + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = file_client.FileClientGroup() + + with pytest.raises(device.ModelError) as exc: + g._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert 'Count field NCrv undefined for group Crv' in str(exc.value) + + m = file_client.FileClientModel() + pdef_NPt = {"name": "NPt", "type": "uint16"} + p_NPt = file_client.FileClientPoint(pdef_NPt) + p_NPt.value = 4 + + pdef_NCrv = {"name": "NCrv", "type": "uint16"} + p_NCrv = file_client.FileClientPoint(pdef_NCrv) + points = {'NPt': p_NPt, 'NCrv': p_NCrv} + setattr(m, 'points', points) + + g2 = file_client.FileClientGroup(gdef_705['group']['groups'][0], m) + + with pytest.raises(device.ModelError) as exc: + g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert 'Count field NCrv value not initialized for group Crv' in str(exc.value) + + # set value for NCrv count and reset the points attribute on model + p_NCrv.value = 3 + setattr(m, 'points', points) + groups = g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert len(groups) == 3 + assert len(groups[0].groups['Pt']) == 4 + + def test_get_dict(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m2 = file_client.FileClientModel(705, data=gdata_705) + assert m2.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefAutoEna': None, 'VRefTms': 5, 'RspTms': 6, + 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + # test computed + m2.groups['Crv'][0].points['DeptRef'].sf_required = True + m2.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m2.groups['Crv'][0].points['Pri'].sf_required = True + m2.groups['Crv'][0].points['Pri'].sf_value = 3 + computed_dict = m2.groups['Crv'][0].get_dict(computed=True) + assert computed_dict['DeptRef'] == 1000.0 + assert computed_dict['Pri'] == 1000.0 + + def test_set_dict(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefAutoEna': None, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + new_dict = {'ActPt': 4, 'DeptRef': 4000, 'Pri': 5000, 'VRef': 3, 'VRefAuto': 2, 'VRefAutoEna': None, + 'VRefTms': 2, 'RspTms': 2, 'ReadOnly': 2, + 'Pt': [{'V': 111, 'Var': 111}, {'V': 123, 'Var': 1112}, {'V': 111, 'Var': 111}, + {'V': 123, 'Var': -1112}]} + + m.groups['Crv'][0].set_dict(new_dict, dirty=True) + assert m.groups['Crv'][0].get_dict() == new_dict + assert m.groups['Crv'][0].VRef.value == 3 + assert m.groups['Crv'][0].VRef.dirty + assert m.groups['Crv'][0].Pri.dirty + + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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 + m.groups['Crv'][0].set_dict(new_dict, computed=True) + computed_dict = m.groups['Crv'][0].get_dict() + assert computed_dict['DeptRef'] == 4.0 + assert computed_dict['Pri'] == 5.0 + + def test_get_json(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \ + ''' "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000},''' + \ + ''' {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0}, {"V": 10700, "Var": -3000}]}''' + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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,''' + \ + ''' "VRefTms": 5, "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): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ + ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5,''' + \ + ''' "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000},''' + \ + ''' {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0},''' + \ + ''' {"V": 10700, "Var": -3000}]}''' + + json_to_set = '''{"ActPt": 4, "DeptRef": 9999, "Pri": 9999, "VRef": 99, "VRefAuto": 88,''' + \ + ''' "VRefAutoEna": null, "VRefTms": 88, "RspTms": 88, "ReadOnly": 77, "Pt":''' + \ + ''' [{"V": 77, "Var": 66}, {"V": 55, "Var": 44}, {"V": 33, "Var": 22},''' + \ + ''' {"V": 111, "Var": -2222}]}''' + + m.groups['Crv'][0].set_json(json_to_set) + assert m.groups['Crv'][0].get_json() == json_to_set + assert m.groups['Crv'][0].DeptRef.value == 9999 + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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 + m.groups['Crv'][0].set_json(json_to_set, computed=True, dirty=True) + assert m.groups['Crv'][0].points['DeptRef'].value == 9 + assert m.groups['Crv'][0].points['DeptRef'].dirty + assert m.groups['Crv'][0].points['Pri'].value == 9 + assert m.groups['Crv'][0].points['Pri'].dirty + + def test_get_mb(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00' \ + b'\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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' \ + b'\x00\x05\x00\x06\x00\x01\x00\\\x00\x1e\x00`\x00' \ + b'\x00\x00g\x00\x00\x00k\xff\xe2' + + def test_set_mb(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00' \ + b'\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' + bs = b'\x00\x04\x03\xe7\x03x\x03\t\x02\x9a\x02+\x01\xbc\x01M' \ + b'\x00\xde\x00o\x00\xde\x01M\x01\xbc\x02+\x02\x9a\xfc\xf7\xf4H' + + m.groups['Crv'][0].set_mb(bs, dirty=True) + assert m.groups['Crv'][0].get_mb() == bs + assert m.groups['Crv'][0].DeptRef.value == 999 + assert m.groups['Crv'][0].DeptRef.dirty + + # test computed + # set points DeptRef and Pri to 3000 w/ byte string + computed_bs = b'\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<' \ + b'\x00\x00)\xcc\xf4H' + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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 + m.groups['Crv'][0].set_mb(computed_bs, computed=True) + assert m.groups['Crv'][0].points['DeptRef'].value == 3 + assert m.groups['Crv'][0].points['Pri'].value == 3 + + +class TestFileClientModel: + def test__init__(self): + m = file_client.FileClientModel(704) + assert m.model_id == 704 + assert m.model_addr == 0 + assert m.model_len == 0 + assert m.model_def['id'] == 704 + assert m.error_info == '' + assert m.gdef['name'] == 'DERCtlAC' + assert m.mid is None + assert m.device is None + + assert m.model == m + m2 = file_client.FileClientModel('abc') + assert m2.error_info == 'Invalid model id: abc\n' + + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + # test repeating group model + m2 = file_client.FileClientModel(705, data=gdata_705) + assert m2.model_id == 705 + assert m2.model_addr == 0 + assert m2.model_len == 0 + assert m2.model_def['id'] == 705 + assert m2.error_info == '' + assert m2.gdef['name'] == 'DERVoltVar' + assert m2.mid is None + assert m2.device is None + + def test__error(self): + m = file_client.FileClientModel(704) + m.add_error('test error') + assert m.error_info == 'test error\n' + + +class TestFileClientDevice: + def test__init__(self): + d = file_client.FileClientDevice() + assert d.name is None + assert d.did + assert d.models == {} + assert d.model_list == [] + assert d.model_class == file_client.FileClientModel + + def test__get_attr__(self): + d = file_client.FileClientDevice() + m = file_client.FileClientModel() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + d.add_model(m) + assert d.mid_test + + with pytest.raises(AttributeError) as exc: + d.foo + assert "\'FileClientDevice\' object has no attribute \'foo\'" in str(exc.value) + + def test_add_model(self): + d = file_client.FileClientDevice() + m = file_client.FileClientModel() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + d.add_model(m) + assert d.models['mid_test'] + assert d.models['group_test'] + assert m.device == d + + def test_get_dict(self): + d = file_client.FileClientDevice() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + d.add_model(m) + assert d.get_dict()['models'] == [ + {'ID': 705, 'L': 64, 'Ena': 1, 'CrvSt': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, + 'RvrtTms': 0, 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'Crv': [ + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, + 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, {'V': 10300, 'Var': 0}, + {'V': 10700, 'Var': -3000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, + 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, {'V': 10200, 'Var': 0}, + {'V': 10600, 'Var': -4000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, + 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, {'V': 10500, 'Var': 0}, + {'V': 10800, 'Var': -2000}]}]}] + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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_dict(computed=True)['models'] == [ + {'ID': 705, 'L': 64, 'Ena': 1, 'CrvSt': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, + 'RvrtTms': 0, 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'Crv': [ + {'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, + 'VRefTms': 5, '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, 'VRefTms': 5, + '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, 'VRefTms': 5, + '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}]}]}] + + def test_get_json(self): + d = file_client.FileClientDevice() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + d.add_model(m) + + assert d.get_json() == '''{"name": null, "did": "''' + str(d.did) + '''", "models": [{"ID": 705,''' + \ + ''' "L": 64, "Ena": 1, "CrvSt": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4,''' + \ + ''' "NCrv": 3, "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2,''' + \ + ''' "DeptRef_SF": -2, "Crv": [{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ + ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 1,''' + \ + ''' "Pt": [{"V": 9200, "Var": 3000}, {"V": 9670, "Var": 0}, {"V": 10300,''' + \ + ''' "Var": 0}, {"V": 10700, "Var": -3000}]}, {"ActPt": 4, "DeptRef": 1,''' + \ + ''' "Pri": 1, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5,''' + \ + ''' "RspTms": 6, "ReadOnly": 0, "Pt": [{"V": 9300, "Var": 3000},''' + \ + ''' {"V": 9570, "Var": 0}, {"V": 10200, "Var": 0}, {"V": 10600, "Var": -4000}]},''' + \ + ''' {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \ + ''' "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 0, "Pt":''' + \ + ''' [{"V": 9400, "Var": 2000}, {"V": 9570, "Var": 0}, {"V": 10500, "Var": 0},''' + \ + ''' {"V": 10800, "Var": -2000}]}]}]}''' + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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 + + get_json_output2 = '''{"name": null, "did": "''' + str(d.did) + '''", "models": [{"ID": 705, "L": 63,''' + \ + ''' "Ena": 1, "CrvSt": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3,''' + \ + ''' "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2, "Crv":''' + \ + ''' [{"ActPt": 4, "DeptRef": 1000.0, "Pri": 1000.0, "VRef": 1, "VRefAuto": 0,''' + \ + ''' "VRefTms": 5, "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, "VRefTms": 5,''' + \ + ''' "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, "VRefTms": 5, "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}]}]}]}''' + assert d.get_json(computed=True) == '''{"name": null, "did": "''' + str(d.did) + '''", "models":''' + \ + ''' [{"ID": 705, "L": 64, "Ena": 1, "CrvSt": 1, "AdptCrvReq": 0,''' + \ + ''' "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3, "RvrtTms": 0,''' + \ + ''' "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2,''' + \ + ''' "Crv": [{"ActPt": 4, "DeptRef": 1000.0, "Pri": 1000.0,''' + \ + ''' "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5,''' + \ + ''' "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, "VRefTms": 5, "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": null, "VRefTms": 5, "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}]}]}]}''' + + def test_get_mb(self): + d = file_client.FileClientDevice() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + d.add_model(m) + assert d.get_mb() == b"\x02\xc1\x00@\x00\x01\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\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff" \ + b"\xff\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04" \ + b"\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00" \ + b"\x00'\xd8\x00\x00)h\xf0`\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00" \ + b"\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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\x00@\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x03' \ + b'\xe8\x03\xe8\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x01\x00' \ + b'\\\x00\x1e\x00`\x00\x00\x00g\x00\x00\x00k\xff\xe2\x00\x04\x00\x01' \ + b'\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00\x00]\x00' \ + b'\x1e\x00_\x00\x00\x00f\x00\x00\x00j\xff\xd8\x00\x04\x00\x01\x00' \ + b'\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00\x00^\x00\x14' \ + b'\x00_\x00\x00\x00i\x00\x00\x00l\xff\xec' + + def test_set_mb(self): + d = file_client.FileClientDevice() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + d.add_model(m) + assert d.get_mb() == b"\x02\xc1\x00@\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00" \ + b"\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01" \ + b"\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8" \ + b"\x00\x00)h\xf0`\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00" \ + b"\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" + + # DeptRef and Pri set to 3000 in byte string + bs = b"\x02\xc1\x00?\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00\x06" \ + b"\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01\x00\x01\x00\x01" \ + b"\x00\x00\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8\x00\x00)h\xf0`\x00\x04\x00" \ + b"\x01\x00\x01\x00\x01\x00\x00\x00\x05\x00\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" + + d.set_mb(bs, dirty=True) + assert m.groups['Crv'][0].DeptRef.value == 3000 + assert m.groups['Crv'][0].DeptRef.dirty + assert m.groups['Crv'][0].Pri.value == 3000 + assert m.groups['Crv'][0].Pri.dirty + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + 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 + d.set_mb(bs, computed=True, dirty=False) + assert m.groups['Crv'][0].DeptRef.value == 3 + assert not m.groups['Crv'][0].DeptRef.dirty + assert m.groups['Crv'][0].Pri.value == 3 + assert not m.groups['Crv'][0].Pri.dirty + + def test_find_mid(self): + d = file_client.FileClientDevice() + m = file_client.FileClientModel() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + setattr(m, 'mid', 'mid_test') + d.add_model(m) + assert d.find_mid('mid_test') == m + + def test_scan(self): + d = file_client.FileClientDevice('sunspec2/tests/test_data/device_1547.json') + d.scan() + assert d.common + assert d.DERMeasureAC + + +class FileClient: + pass diff --git a/build/lib/sunspec2/tests/test_mb.py b/build/lib/sunspec2/tests/test_mb.py new file mode 100644 index 0000000..04fdc2e --- /dev/null +++ b/build/lib/sunspec2/tests/test_mb.py @@ -0,0 +1,221 @@ +import sunspec2.mb as mb +import pytest + + +def test_create_unimpl_value(): + with pytest.raises(ValueError): + mb.create_unimpl_value(None) + + with pytest.raises(ValueError): + mb.create_unimpl_value('string') + + assert mb.create_unimpl_value('string', len=8) == b'\x00\x00\x00\x00\x00\x00\x00\x00' + assert mb.create_unimpl_value('ipv6addr') == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + assert mb.create_unimpl_value('int16') == b'\x80\x00' + assert mb.create_unimpl_value('uint16') == b'\xff\xff' + assert mb.create_unimpl_value('acc16') == b'\x00\x00' + assert mb.create_unimpl_value('enum16') == b'\xff\xff' + assert mb.create_unimpl_value('bitfield16') == b'\xff\xff' + assert mb.create_unimpl_value('int32') == b'\x80\x00\x00\x00' + assert mb.create_unimpl_value('uint32') == b'\xff\xff\xff\xff' + assert mb.create_unimpl_value('acc32') == b'\x00\x00\x00\x00' + assert mb.create_unimpl_value('enum32') == b'\xff\xff\xff\xff' + assert mb.create_unimpl_value('bitfield32') == b'\xff\xff\xff\xff' + assert mb.create_unimpl_value('ipaddr') == b'\x00\x00\x00\x00' + assert mb.create_unimpl_value('int64') == b'\x80\x00\x00\x00\x00\x00\x00\x00' + assert mb.create_unimpl_value('uint64') == b'\xff\xff\xff\xff\xff\xff\xff\xff' + assert mb.create_unimpl_value('acc64') == b'\x00\x00\x00\x00\x00\x00\x00\x00' + assert mb.create_unimpl_value('float32') == b'N\xff\x80\x00' + assert mb.create_unimpl_value('sunssf') == b'\x80\x00' + assert mb.create_unimpl_value('eui48') == b'\x00\x00\xff\xff\xff\xff\xff\xff' + assert mb.create_unimpl_value('pad') == b'\x00\x00' + + +def test_data_to_s16(): + assert mb.data_to_s16(b'\x13\x88') == 5000 + + +def test_data_to_u16(): + assert mb.data_to_u16(b'\x27\x10') == 10000 + + +def test_data_to_s32(): + assert mb.data_to_s32(b'\x12\x34\x56\x78') == 305419896 + assert mb.data_to_s32(b'\xED\xCB\xA9\x88') == -305419896 + + +def test_data_to_u32(): + assert mb.data_to_u32(b'\x12\x34\x56\x78') == 305419896 + + +def test_data_to_s64(): + assert mb.data_to_s64(b'\x12\x34\x56\x78\x12\x34\x56\x78') == 1311768465173141112 + assert mb.data_to_s64(b'\xED\xCB\xA9\x87\xED\xCB\xA9\x88') == -1311768465173141112 + + +def test_data_to_u64(): + assert mb.data_to_u64(b'\xff\xff\xff\xff\xff\xff\xff\xff') == 18446744073709551615 + + +def test_data_to_ipv6addr(): + assert mb.data_to_ipv6addr(b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34') == '20010DB8:85A30000:00008A2E:03707334' + + +def test_data_to_eui48(): + # need test to test for python 2 + assert mb.data_to_eui48(b'\x00\x00\x12\x34\x56\x78\x90\xAB') == '12:34:56:78:90:AB' + + +def test_data_to_f64(): + assert mb.data_to_f64(b'\x44\x9a\x43\xf3\x00\x00\x00\x00') == 3.1008742600725133e+22 + + +def test_data_to_str(): + assert mb.data_to_str(b'test') == 'test' + assert mb.data_to_str(b'444444') == '444444' + + +def test_s16_to_data(): + assert mb.s16_to_data(5000) == b'\x13\x88' + + +def test_u16_to_data(): + assert mb.u16_to_data(10000) == b'\x27\x10' + + +def test_s32_to_data(): + assert mb.s32_to_data(305419896) == b'\x12\x34\x56\x78' + assert mb.s32_to_data(-305419896) == b'\xED\xCB\xA9\x88' + + +def test_u32_to_data(): + assert mb.u32_to_data(305419896) == b'\x12\x34\x56\x78' + + +def test_s64_to_data(): + assert mb.s64_to_data(1311768465173141112) == b'\x12\x34\x56\x78\x12\x34\x56\x78' + assert mb.s64_to_data(-1311768465173141112) == b'\xED\xCB\xA9\x87\xED\xCB\xA9\x88' + + +def test_u64_to_data(): + assert mb.u64_to_data(18446744073709551615) == b'\xff\xff\xff\xff\xff\xff\xff\xff' + + +def test_ipv6addr_to_data(): + assert mb.ipv6addr_to_data('20010DB8:85A30000:00008A2E:03707334') == \ + b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34' + # need additional test to test for python 2 + + +def test_f32_to_data(): + assert mb.f32_to_data(32500.43359375) == b'F\xfd\xe8\xde' + + +def test_f64_to_data(): + assert mb.f64_to_data(3.1008742600725133e+22) == b'\x44\x9a\x43\xf3\x00\x00\x00\x00' + + +def test_str_to_data(): + assert mb.str_to_data('test') == b'test' + assert mb.str_to_data('444444') == b'444444' + assert mb.str_to_data('test', 5) == b'test\x00' + + +def test_eui48_to_data(): + assert mb.eui48_to_data('12:34:56:78:90:AB') == b'\x00\x00\x12\x34\x56\x78\x90\xAB' + + +def test_is_impl_int16(): + assert not mb.is_impl_int16(-32768) + assert mb.is_impl_int16(1111) + assert mb.is_impl_int16(None) + + +def test_is_impl_uint16(): + assert not mb.is_impl_uint16(0xffff) + assert mb.is_impl_uint16(0x1111) + + +def test_is_impl_acc16(): + assert not mb.is_impl_acc16(0) + assert mb.is_impl_acc16(1111) + + +def test_is_impl_enum16(): + assert not mb.is_impl_enum16(0xffff) + assert mb.is_impl_enum16(0x1111) + + +def test_is_impl_bitfield16(): + assert not mb.is_impl_bitfield16(0xffff) + assert mb.is_impl_bitfield16(0x1111) + + +def test_is_impl_int32(): + assert not mb.is_impl_int32(-2147483648) + assert mb.is_impl_int32(1111111) + + +def test_is_impl_uint32(): + assert not mb.is_impl_uint32(0xffffffff) + assert mb.is_impl_uint32(0x11111111) + + +def test_is_impl_acc32(): + assert not mb.is_impl_acc32(0) + assert mb.is_impl_acc32(1) + + +def test_is_impl_enum32(): + assert not mb.is_impl_enum32(0xffffffff) + assert mb.is_impl_enum32(0x11111111) + + +def test_is_impl_bitfield32(): + assert not mb.is_impl_bitfield32(0xffffffff) + assert mb.is_impl_bitfield32(0x11111111) + + +def test_is_impl_ipaddr(): + assert not mb.is_impl_ipaddr(0) + assert mb.is_impl_ipaddr('192.168.0.1') + + +def test_is_impl_int64(): + assert not mb.is_impl_int64(-9223372036854775808) + assert mb.is_impl_int64(111111111111111) + + +def test_is_impl_uint64(): + assert not mb.is_impl_uint64(0xffffffffffffffff) + assert mb.is_impl_uint64(0x1111111111111111) + + +def test_is_impl_acc64(): + assert not mb.is_impl_acc64(0) + assert mb.is_impl_acc64(1) + + +def test_is_impl_ipv6addr(): + assert not mb.is_impl_ipv6addr('\0') + assert mb.is_impl_ipv6addr(b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34') + + +def test_is_impl_float32(): + assert not mb.is_impl_float32(None) + assert mb.is_impl_float32(0x123456) + + +def test_is_impl_string(): + assert not mb.is_impl_string('\0') + assert mb.is_impl_string(b'\x74\x65\x73\x74') + + +def test_is_impl_sunssf(): + assert not mb.is_impl_sunssf(-32768) + assert mb.is_impl_sunssf(30000) + + +def test_is_impl_eui48(): + assert not mb.is_impl_eui48('FF:FF:FF:FF:FF:FF') + assert mb.is_impl_eui48('00:00:00:00:00:00') diff --git a/build/lib/sunspec2/tests/test_mdef.py b/build/lib/sunspec2/tests/test_mdef.py new file mode 100644 index 0000000..709b9ac --- /dev/null +++ b/build/lib/sunspec2/tests/test_mdef.py @@ -0,0 +1,307 @@ +import sunspec2.mdef as mdef +import json +import copy + + +def test_to_int(): + assert mdef.to_int('4') == 4 + assert isinstance(mdef.to_int('4'), int) + assert isinstance(mdef.to_int(4.0), int) + + +def test_to_str(): + assert mdef.to_str(4) == '4' + assert isinstance(mdef.to_str('4'), str) + + +def test_to_float(): + assert mdef.to_float('4') == 4.0 + assert isinstance(mdef.to_float('4'), float) + assert mdef.to_float('z') is None + + +def test_to_number_type(): + assert mdef.to_number_type('4') == 4 + assert mdef.to_number_type('4.0') == 4.0 + assert mdef.to_number_type('z') == 'z' + + +def test_validate_find_point(): + with open('sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + assert mdef.validate_find_point(model_json['group'], 'ID') == model_json['group']['points'][0] + assert mdef.validate_find_point(model_json['group'], 'abc') is None + + +def test_validate_attrs(): + with open('sunspec2/models/json/model_701.json') as f: + model_json = json.load(f) + + # model + assert mdef.validate_attrs(model_json, mdef.model_attr) == '' + + model_unexp_attr_err = copy.deepcopy(model_json) + model_unexp_attr_err['abc'] = 'def' + assert mdef.validate_attrs(model_unexp_attr_err, mdef.model_attr)[0:37] == 'Unexpected model definition attribute' + + model_unexp_type_err = copy.deepcopy(model_json) + model_unexp_type_err['id'] = '701' + assert mdef.validate_attrs(model_unexp_type_err, mdef.model_attr)[0:15] == 'Unexpected type' + + model_attr_missing = copy.deepcopy(model_json) + del model_attr_missing['id'] + assert mdef.validate_attrs(model_attr_missing, mdef.model_attr)[0:27] == 'Mandatory attribute missing' + + # group + assert mdef.validate_attrs(model_json['group'], mdef.group_attr) == '' + group_unexp_attr_err = copy.deepcopy(model_json)['group'] + group_unexp_attr_err['abc'] = 'def' + assert mdef.validate_attrs(group_unexp_attr_err, mdef.group_attr)[0:37] == 'Unexpected model definition attribute' + + group_unexp_type_err = copy.deepcopy(model_json)['group'] + group_unexp_type_err['name'] = 1 + assert mdef.validate_attrs(group_unexp_type_err, mdef.group_attr)[0:15] == 'Unexpected type' + + group_attr_missing = copy.deepcopy(model_json)['group'] + del group_attr_missing['name'] + assert mdef.validate_attrs(group_attr_missing, mdef.group_attr)[0:27] == 'Mandatory attribute missing' + + # point + assert mdef.validate_attrs(model_json['group']['points'][0], mdef.point_attr) == '' + + point_unexp_attr_err = copy.deepcopy(model_json)['group']['points'][0] + point_unexp_attr_err['abc'] = 'def' + assert mdef.validate_attrs(point_unexp_attr_err, mdef.point_attr)[0:37] == 'Unexpected model definition attribute' + + point_unexp_type_err = copy.deepcopy(model_json)['group']['points'][0] + point_unexp_type_err['name'] = 1 + assert mdef.validate_attrs(point_unexp_type_err, mdef.point_attr)[0:15] == 'Unexpected type' + + point_unexp_value_err = copy.deepcopy(model_json)['group']['points'][1] + point_unexp_value_err['access'] = 'z' + assert mdef.validate_attrs(point_unexp_value_err, mdef.point_attr)[0:16] == 'Unexpected value' + + point_attr_missing = copy.deepcopy(model_json)['group']['points'][0] + del point_attr_missing['name'] + assert mdef.validate_attrs(point_attr_missing, mdef.point_attr)[0:27] == 'Mandatory attribute missing' + + # symbol + assert mdef.validate_attrs(model_json['group']['points'][2]['symbols'][0], mdef.symbol_attr) == '' + + symbol_unexp_attr_err = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0] + symbol_unexp_attr_err['abc'] = 'def' + assert mdef.validate_attrs(symbol_unexp_attr_err, mdef.symbol_attr)[0:37] == 'Unexpected model definition attribute' + + symbol_unexp_type_err = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0] + symbol_unexp_type_err['name'] = 1 + assert mdef.validate_attrs(symbol_unexp_type_err, mdef.symbol_attr)[0:15] == 'Unexpected type' + + symbol_attr_missing = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0] + del symbol_attr_missing['name'] + assert mdef.validate_attrs(symbol_attr_missing, mdef.symbol_attr)[0:27] == 'Mandatory attribute missing' + + +def test_validate_group_point_dup(): + with open('sunspec2/models/json/model_704.json') as f: + model_json = json.load(f) + + assert mdef.validate_group_point_dup(model_json['group']) == '' + + dup_group_id_model = copy.deepcopy(model_json) + dup_group_id_group = dup_group_id_model['group'] + dup_group_id_group['groups'][0]['name'] = 'PFWInjRvrt' + assert mdef.validate_group_point_dup(dup_group_id_group)[0:18] == 'Duplicate group id' + + dup_group_point_id_model = copy.deepcopy(model_json) + dup_group_point_id_group = dup_group_point_id_model['group'] + dup_group_point_id_group['groups'][0]['name'] = 'PFWInjEna' + assert mdef.validate_group_point_dup(dup_group_point_id_group)[0:28] == 'Duplicate group and point id' + + mand_attr_miss_model = copy.deepcopy(model_json) + mand_attr_miss_group = mand_attr_miss_model['group'] + del mand_attr_miss_group['groups'][0]['name'] + assert mdef.validate_group_point_dup(mand_attr_miss_group)[0:32] == 'Mandatory name attribute missing' + + dup_point_id_model = copy.deepcopy(model_json) + dup_point_id_group = dup_point_id_model['group'] + dup_point_id_group['points'][1]['name'] = 'ID' + assert mdef.validate_group_point_dup(dup_point_id_group)[0:30] == 'Duplicate point id ID in group' + + mand_attr_miss_point_model = copy.deepcopy(model_json) + mand_attr_miss_point_group = mand_attr_miss_point_model['group'] + del mand_attr_miss_point_group['points'][1]['name'] + assert mdef.validate_group_point_dup(mand_attr_miss_point_group)[0:55] == 'Mandatory attribute missing in point ' \ + 'definition element' + + +def test_validate_symbols(): + symbols = [ + {'name': 'CAT_A', 'value': 1}, + {'name': 'CAT_B', 'value': 2} + ] + assert mdef.validate_symbols(symbols, mdef.symbol_attr) == '' + + +def test_validate_sf(): + with open('sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + model_point = model_json['group']['points'][2] + model_group = model_json['group'] + model_group_arr = [model_group, model_group] + assert mdef.validate_sf(model_point, 'W_SF', model_group_arr) == '' + + not_sf_type_model = copy.deepcopy(model_json) + not_sf_type_point = not_sf_type_model['group']['points'][2] + not_sf_type_group = not_sf_type_model['group'] + not_sf_type_group_arr = [not_sf_type_group, not_sf_type_group] + for point in not_sf_type_model['group']['points']: + if point['name'] == 'W_SF': + point['type'] = 'abc' + assert mdef.validate_sf(not_sf_type_point, 'W_SF', not_sf_type_group_arr)[0:60] == 'Scale factor W_SF for point ' \ + 'WMaxRtg is not scale factor ' \ + 'type' + + sf_not_found_model = copy.deepcopy(model_json) + sf_not_found_point = sf_not_found_model['group']['points'][2] + sf_not_found_group = sf_not_found_model['group'] + sf_not_found_group_arr = [sf_not_found_group, sf_not_found_group] + assert mdef.validate_sf(sf_not_found_point, 'ABC', sf_not_found_group_arr)[0:44] == 'Scale factor ABC for point ' \ + 'WMaxRtg not found' + + sf_out_range_model = copy.deepcopy(model_json) + sf_out_range_point = sf_out_range_model['group']['points'][2] + sf_out_range_group = sf_out_range_model['group'] + sf_out_range_group_arr = [sf_out_range_group, sf_out_range_group] + assert mdef.validate_sf(sf_out_range_point, 11, sf_out_range_group_arr)[0:46] == 'Scale factor 11 for point ' \ + 'WMaxRtg out of range' + + sf_invalid_type_model = copy.deepcopy(model_json) + sf_invalid_type_point = sf_invalid_type_model['group']['points'][2] + sf_invalid_type_group = sf_invalid_type_model['group'] + sf_invalid_type_group_arr = [sf_invalid_type_group, sf_invalid_type_group] + assert mdef.validate_sf(sf_invalid_type_point, 4.0, sf_invalid_type_group_arr)[0:51] == 'Scale factor 4.0 for' \ + ' point WMaxRtg has ' \ + 'invalid type' + + +def test_validate_point_def(): + with open('sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + model_group = model_json['group'] + group = model_json['group'] + point = model_json['group']['points'][0] + assert mdef.validate_point_def(point, model_group, group) == '' + + unk_point_type_model = copy.deepcopy(model_json) + unk_point_type_model_group = unk_point_type_model['group'] + unk_point_type_group = unk_point_type_model['group'] + unk_point_type_point = unk_point_type_model['group']['points'][0] + unk_point_type_point['type'] = 'abc' + assert mdef.validate_point_def(unk_point_type_point, unk_point_type_model_group, + unk_point_type_group)[0:35] == 'Unknown point type abc for point ID' + + dup_symbol_model = copy.deepcopy(model_json) + dup_symbol_model_group = dup_symbol_model['group'] + dup_symbol_group = dup_symbol_model['group'] + dup_symbol_point = dup_symbol_model['group']['points'][21] + dup_symbol_point['symbols'][0]['name'] = 'CAT_B' + assert mdef.validate_point_def(dup_symbol_point, dup_symbol_model_group, + dup_symbol_group)[0:19] == 'Duplicate symbol id' + + mand_attr_missing = copy.deepcopy(model_json) + mand_attr_missing_model_group = mand_attr_missing['group'] + mand_attr_missing_group = mand_attr_missing['group'] + mand_attr_missing_point = mand_attr_missing['group']['points'][0] + del mand_attr_missing_point['name'] + assert mdef.validate_point_def(mand_attr_missing_point, mand_attr_missing_model_group, + mand_attr_missing_group)[0:27] == 'Mandatory attribute missing' + + +def test_validate_group_def(): + with open('sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + assert mdef.validate_group_def(model_json['group'], model_json['group']) == '' + + +def test_validate_model_group_def(): + with open('sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + assert mdef.validate_model_group_def(model_json, model_json['group']) == '' + + missing_id_model = copy.deepcopy(model_json) + missing_id_group = missing_id_model['group'] + missing_id_group['points'][0]['name'] = 'abc' + assert mdef.validate_model_group_def(missing_id_model, missing_id_group)[0:41] == 'First point in top-level' \ + ' group must be ID' + + wrong_model_id_model = copy.deepcopy(model_json) + wrong_model_id_group = wrong_model_id_model['group'] + wrong_model_id_group['points'][0]['value'] = 0 + assert mdef.validate_model_group_def(wrong_model_id_model, wrong_model_id_group)[0:42] == 'Model ID does not ' \ + 'match top-level group ID' + + missing_len_model = copy.deepcopy(model_json) + missing_len_group = missing_len_model['group'] + missing_len_group['points'][1]['name'] = 'abc' + assert mdef.validate_model_group_def(missing_len_model, missing_len_group)[0:41] == 'Second point in top-level ' \ + 'group must be L' + + missing_two_p_model = copy.deepcopy(model_json) + missing_two_p_group = missing_two_p_model['group'] + missing_two_p_point = missing_two_p_group['points'][0] + del missing_two_p_group['points'] + missing_two_p_group['points'] = [missing_two_p_point] + assert mdef.validate_model_group_def(missing_two_p_model, missing_two_p_group)[0:48] == 'Top-level group must' \ + ' contain at least two ' \ + 'points' + + missing_p_def_model = copy.deepcopy(model_json) + missing_p_def_group = missing_p_def_model['group'] + del missing_p_def_group['points'] + assert mdef.validate_model_group_def(missing_p_def_model, missing_p_def_group)[0:41] == 'Top-level group' \ + ' missing point definitions' + + +def test_validate_model_def(): + with open('sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + assert mdef.validate_model_def(model_json) == '' + + +def test_from_json_str(): + with open('sunspec2/models/json/model_63001.json') as f: + model_json = json.load(f) + model_json_str = json.dumps(model_json) + assert isinstance(mdef.from_json_str(model_json_str), dict) + + +def test_from_json_file(): + assert isinstance(mdef.from_json_file('sunspec2/models/json/model_63001.json'), dict) + + +def test_to_json_str(): + with open('sunspec2/models/json/model_63001.json') as f: + model_json = json.load(f) + assert isinstance(mdef.to_json_str(model_json), str) + + +def test_to_json_filename(): + assert mdef.to_json_filename('63001') == 'model_63001.json' + + +def test_to_json_file(tmp_path): + with open('sunspec2/models/json/model_63001.json') as f: + model_json = json.load(f) + mdef.to_json_file(model_json, filedir=tmp_path) + + with open(tmp_path / 'model_63001.json') as f: + model_json = json.load(f) + assert isinstance(model_json, dict) + + diff --git a/build/lib/sunspec2/tests/test_modbus_client.py b/build/lib/sunspec2/tests/test_modbus_client.py new file mode 100644 index 0000000..daa49ff --- /dev/null +++ b/build/lib/sunspec2/tests/test_modbus_client.py @@ -0,0 +1,731 @@ +import sunspec2.modbus.client as client +import pytest +import socket +import sunspec2.tests.mock_socket as MockSocket +import serial +import sunspec2.tests.mock_port as MockPort + + +class TestSunSpecModbusClientPoint: + def test_read(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.client.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + assert d_tcp.common[0].SN.value == 'sn-123456789' + assert not d_tcp.common[0].SN.dirty + + d_tcp.common[0].SN.value = 'will be overwritten by read' + assert d_tcp.common[0].SN.value == 'will be overwritten by read' + assert d_tcp.common[0].SN.dirty + + d_tcp.client.socket.clear_buffer() + tcp_p_buffer = [b'\x00\x00\x00\x00\x00#\x01\x03 ', + b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] + d_tcp.client.socket._set_buffer(tcp_p_buffer) + d_tcp.common[0].SN.read() + assert d_tcp.common[0].SN.value == 'sn-123456789' + assert not d_tcp.common[0].SN.dirty + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].SN.value == 'sn-123456789' + assert not d_rtu.common[0].SN.dirty + + d_rtu.common[0].SN.value = 'will be overwritten by read' + assert d_rtu.common[0].SN.value == 'will be overwritten by read' + assert d_rtu.common[0].SN.dirty + + d_rtu.client.serial.clear_buffer() + tcp_p_buffer = [b'\x01\x03 sn', + b'-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd5\xb8'] + d_rtu.client.serial._set_buffer(tcp_p_buffer) + d_rtu.common[0].SN.read() + assert d_rtu.common[0].SN.value == 'sn-123456789' + assert not d_rtu.common[0].SN.dirty + + def test_write(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.client.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + + assert d_tcp.common[0].SN.value == 'sn-123456789' + assert not d_tcp.common[0].SN.dirty + + d_tcp.common[0].SN.value = 'sn-000' + assert d_tcp.common[0].SN.value == 'sn-000' + assert d_tcp.common[0].SN.dirty + + tcp_write_buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', + b't\x00\x10'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_write_buffer) + d_tcp.common[0].write() + + d_tcp.common[0].SN.value = 'will be overwritten by read' + assert d_tcp.common[0].SN.value == 'will be overwritten by read' + assert d_tcp.common[0].SN.dirty + + tcp_read_buffer = [b'\x00\x00\x00\x00\x00#\x01\x03 ', + b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_read_buffer) + d_tcp.common[0].SN.read() + assert d_tcp.common[0].SN.value == 'sn-000' + assert not d_tcp.common[0].SN.dirty + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].SN.value == 'sn-123456789' + assert not d_rtu.common[0].SN.dirty + + d_rtu.common[0].SN.value = 'sn-000' + assert d_rtu.common[0].SN.value == 'sn-000' + assert d_rtu.common[0].SN.dirty + + rtu_write_buffer = [b'\x01\x10\x9ct\x00', + b'\x10\xaf\x8f'] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_write_buffer) + d_rtu.common[0].write() + + d_rtu.common[0].SN.value = 'will be overwritten by read' + assert d_rtu.common[0].SN.value == 'will be overwritten by read' + assert d_rtu.common[0].SN.dirty + + rtu_read_buffer = [b'\x01\x03 sn', + b'-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\xfb'] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_read_buffer) + d_rtu.common[0].SN.read() + assert d_rtu.common[0].SN.value == 'sn-000' + assert not d_rtu.common[0].SN.dirty + + +class TestSunSpecModbusClientGroup: + def test_read(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.client.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + assert d_tcp.common[0].SN.value == "sn-123456789" + assert d_tcp.common[0].Vr.value == "1.2.3" + assert not d_tcp.common[0].SN.dirty + assert not d_tcp.common[0].Vr.dirty + + d_tcp.common[0].SN.value = 'this will overwrite from read' + d_tcp.common[0].Vr.value = 'this will overwrite from read' + assert d_tcp.common[0].SN.value == 'this will overwrite from read' + assert d_tcp.common[0].Vr.value == 'this will overwrite from read' + assert d_tcp.common[0].SN.dirty + assert d_tcp.common[0].Vr.dirty + + tcp_read_buffer = [b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_read_buffer) + d_tcp.common[0].read() + + assert d_tcp.common[0].SN.value == "sn-123456789" + assert d_tcp.common[0].Vr.value == "1.2.3" + assert not d_tcp.common[0].SN.dirty + assert not d_tcp.common[0].Vr.dirty + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].SN.value == "sn-123456789" + assert d_rtu.common[0].Vr.value == "1.2.3" + assert not d_rtu.common[0].SN.dirty + assert not d_rtu.common[0].Vr.dirty + + d_rtu.common[0].SN.value = 'this will overwrite from read' + d_rtu.common[0].Vr.value = 'this will overwrite from read' + assert d_rtu.common[0].SN.value == 'this will overwrite from read' + assert d_rtu.common[0].Vr.value == 'this will overwrite from read' + assert d_rtu.common[0].SN.dirty + assert d_rtu.common[0].Vr.dirty + + rtu_read_buffer = [b'\x01\x03\x84\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00H\xef'] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_read_buffer) + d_rtu.common[0].read() + assert d_rtu.common[0].SN.value == "sn-123456789" + assert d_rtu.common[0].Vr.value == "1.2.3" + assert not d_rtu.common[0].SN.dirty + assert not d_rtu.common[0].Vr.dirty + + def test_write(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.client.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + assert d_tcp.common[0].SN.value == "sn-123456789" + assert d_tcp.common[0].Vr.value == "1.2.3" + assert not d_tcp.common[0].SN.dirty + assert not d_tcp.common[0].Vr.dirty + + d_tcp.common[0].SN.value = 'sn-000' + d_tcp.common[0].Vr.value = 'v0.0.0' + assert d_tcp.common[0].SN.value == "sn-000" + assert d_tcp.common[0].Vr.value == "v0.0.0" + assert d_tcp.common[0].SN.dirty + assert d_tcp.common[0].Vr.dirty + + tcp_write_buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', + b'l\x00\x18'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_write_buffer) + d_tcp.common[0].write() + + d_tcp.common[0].SN.value = 'this will overwrite from read' + d_tcp.common[0].Vr.value = 'this will overwrite from read' + assert d_tcp.common[0].SN.value == 'this will overwrite from read' + assert d_tcp.common[0].Vr.value == 'this will overwrite from read' + assert d_tcp.common[0].SN.dirty + assert d_tcp.common[0].Vr.dirty + + tcp_read_buffer = [b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' + b'\x00\x00\x00\x00\x00\x00\x00v0.0.0\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00sn-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'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_read_buffer) + d_tcp.common[0].read() + + assert d_tcp.common[0].SN.value == "sn-000" + assert d_tcp.common[0].Vr.value == "v0.0.0" + assert not d_tcp.common[0].SN.dirty + assert not d_tcp.common[0].Vr.dirty + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].SN.value == "sn-123456789" + assert d_rtu.common[0].Vr.value == "1.2.3" + assert not d_rtu.common[0].SN.dirty + assert not d_rtu.common[0].Vr.dirty + + d_rtu.common[0].SN.value = 'sn-000' + d_rtu.common[0].Vr.value = 'v0.0.0' + assert d_rtu.common[0].SN.value == "sn-000" + assert d_rtu.common[0].Vr.value == "v0.0.0" + assert d_rtu.common[0].SN.dirty + assert d_rtu.common[0].Vr.dirty + + rtu_write_buffer = [b'\x01\x10\x9cl\x00', + b'\x18.N'] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_write_buffer) + d_rtu.common[0].write() + + d_rtu.common[0].SN.value = 'this will overwrite from read' + d_rtu.common[0].Vr.value = 'this will overwrite from read' + assert d_rtu.common[0].SN.value == 'this will overwrite from read' + assert d_rtu.common[0].Vr.value == 'this will overwrite from read' + assert d_rtu.common[0].SN.dirty + assert d_rtu.common[0].Vr.dirty + + rtu_read_buffer = [b'\x01\x03\x84\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00' + b'\x00\x00\x00\x00\x00\x00v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4h'] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_read_buffer) + d_rtu.common[0].read() + assert d_rtu.common[0].SN.value == "sn-000" + assert d_rtu.common[0].Vr.value == "v0.0.0" + assert not d_rtu.common[0].SN.dirty + assert not d_rtu.common[0].Vr.dirty + + def test_write_points(self): + pass + + +class TestSunSpecModbusClientModel: + def test___init__(self): + c = client.SunSpecModbusClientModel(704) + assert c.model_id == 704 + assert c.model_addr == 0 + assert c.model_len == 0 + assert c.model_def['id'] == 704 + assert c.error_info == '' + assert c.gdef['name'] == 'DERCtlAC' + assert c.mid is None + assert c.device is None + assert c.model == c + + def test_error(self): + c = client.SunSpecModbusClientModel(704) + c.add_error('test error') + assert c.error_info == 'test error\n' + + +class TestSunSpecModbusClientDevice: + def test___init__(self): + d = client.SunSpecModbusClientDevice() + assert d.did + assert d.retry_count == 2 + assert d.base_addr_list == [40000, 0, 50000] + assert d.base_addr is None + + def test_connect(self): + pass + + def test_disconnect(self): + pass + + def test_close(self): + pass + + def test_read(self): + pass + + def test_write(self): + pass + + def test_scan(self, monkeypatch): + # tcp scan + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + c_tcp = client.SunSpecModbusClientDeviceTCP() + tcp_req_check = [b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00\x03', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9cC\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9cB\x00D', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\x86\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\x87\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\x86\x00B', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\xc8\x00\x01'] + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + c_tcp.client.connect() + c_tcp.client.socket._set_buffer(tcp_buffer) + c_tcp.scan() + assert c_tcp.common + assert c_tcp.volt_var + for req in range(len(tcp_req_check)): + assert tcp_req_check[req] == c_tcp.client.socket.request[req] + + # rtu scan + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c_rtu = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + + rtu_req_check = [b'\x01\x03\x9c@\x00\x03*O', + b'\x01\x03\x9cC\x00\x01[\x8e', + b'\x01\x03\x9cB\x00D\xcb\xbd', + b'\x01\x03\x9c\x86\x00\x01K\xb3', + b'\x01\x03\x9c\x87\x00\x01\x1as', + b'\x01\x03\x9c\x86\x00B\nB', + b'\x01\x03\x9c\xc8\x00\x01+\xa4'] + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + c_rtu.open() + c_rtu.client.serial._set_buffer(rtu_buffer) + c_rtu.scan() + assert c_rtu.common + assert c_rtu.volt_var + for req in range(len(rtu_req_check)): + assert rtu_req_check[req] == c_rtu.client.serial.request[req] + + +class TestSunSpecModbusClientDeviceTCP: + def test___init__(self): + d = client.SunSpecModbusClientDeviceTCP() + assert d.slave_id == 1 + assert d.ipaddr == '127.0.0.1' + assert d.ipport == 502 + assert d.timeout is None + assert d.ctx is None + assert d.trace_func is None + assert d.max_count == 125 + assert d.client.__class__.__name__ == 'ModbusClientTCP' + + def test_connect(self, monkeypatch): + d = client.SunSpecModbusClientDeviceTCP() + with pytest.raises(Exception) as exc: + d.connect() + + assert 'Connection error' in str(exc.value) + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + + d.connect() + assert d.client.socket is not None + assert d.client.socket.connected is True + assert d.client.socket.ipaddr == '127.0.0.1' + assert d.client.socket.ipport == 502 + assert d.client.socket.timeout == 2 + + def test_disconnect(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + + d = client.SunSpecModbusClientDeviceTCP() + d.client.connect() + assert d.client.socket + d.client.disconnect() + assert d.client.socket is None + + def test_read(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + d = client.SunSpecModbusClientDeviceTCP() + buffer = [b'\x00\x00\x00\x00\x00\x8f\x01\x03\x8c', b'SunS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' + b'\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00'] + check_req = b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00F' + d.client.connect() + d.client.socket._set_buffer(buffer) + assert d.read(40000, 70) == buffer[1] + assert d.client.socket.request[0] == check_req + + def test_write(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + d = client.SunSpecModbusClientDeviceTCP() + d.client.connect() + + data_to_write = b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', b't\x00\x10'] + d.client.socket._set_buffer(buffer) + d.client.write(40052, data_to_write) + + 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 d.client.socket.request[0] == check_req + + +class TestSunSpecModbusClientDeviceRTU: + def test___init__(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + assert d.slave_id == 1 + assert d.name == "COMM2" + assert d.client.__class__.__name__ == "ModbusClientRTU" + assert d.ctx is None + assert d.trace_func is None + assert d.max_count == 125 + + def test_open(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + d.open() + assert d.client.serial.connected + + def test_close(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + d.open() + d.close() + assert not d.client.serial.connected + + def test_read(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + d.open() + in_buff = [b'\x01\x03\x8cSu', b'nS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00' + b'\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x01\x00\x00\xb7d'] + check_req = b'\x01\x03\x9c@\x00F\xeb\xbc' + d.client.serial._set_buffer(in_buff) + check_read = in_buff[0] + in_buff[1] + assert d.read(40000, 70) == check_read[3:-2] + assert d.client.serial.request[0] == check_req + + def test_write(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + d.open() + data_to_write = b'v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-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' + + buffer = [b'\x01\x10\x9cl\x00', b'\x18.N'] + d.client.serial._set_buffer(buffer) + d.write(40044, data_to_write) + + check_req = b'\x01\x10\x9cl\x00\x180v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\xad\xff' + assert d.client.serial.request[0] == check_req + + +if __name__ == "__main__": + pass + diff --git a/build/lib/sunspec2/tests/test_modbus_modbus.py b/build/lib/sunspec2/tests/test_modbus_modbus.py new file mode 100644 index 0000000..a29132b --- /dev/null +++ b/build/lib/sunspec2/tests/test_modbus_modbus.py @@ -0,0 +1,206 @@ +import sunspec2.modbus.modbus as modbus_client +import pytest +import socket +import serial +import sunspec2.tests.mock_socket as MockSocket +import sunspec2.tests.mock_port as MockPort + + +def test_modbus_rtu_client(monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.modbus_rtu_client('COMM2') + assert c.baudrate == 9600 + assert c.parity == "N" + assert modbus_client.modbus_rtu_clients['COMM2'] + + with pytest.raises(modbus_client.ModbusClientError) as exc1: + c2 = modbus_client.modbus_rtu_client('COMM2', baudrate=99) + assert 'Modbus client baudrate mismatch' in str(exc1.value) + + with pytest.raises(modbus_client.ModbusClientError) as exc2: + c2 = modbus_client.modbus_rtu_client('COMM2', parity='E') + assert 'Modbus client parity mismatch' in str(exc2.value) + + +def test_modbus_rtu_client_remove(monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.modbus_rtu_client('COMM2') + assert modbus_client.modbus_rtu_clients['COMM2'] + modbus_client.modbus_rtu_client_remove('COMM2') + assert modbus_client.modbus_rtu_clients.get('COMM2') is None + + +def test___generate_crc16_table(): + pass + + +def test_computeCRC(): + pass + + +def test_checkCRC(): + pass + + +class TestModbusClientRTU: + def test___init__(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + assert c.name == "COM2" + assert c.baudrate == 9600 + assert c.parity is None + assert c.serial is not None + assert c.timeout == .5 + assert c.write_timeout == .5 + assert not c.devices + + def test_open(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.open() + assert c.serial.connected + + def test_close(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.open() + c.close() + assert not c.serial.connected + + def test_add_device(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.add_device(1, "1") + assert c.devices.get(1) is not None + assert c.devices[1] == "1" + + def test_remove_device(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.add_device(1, "1") + assert c.devices.get(1) is not None + assert c.devices[1] == "1" + c.remove_device(1) + assert c.devices.get(1) is None + + def test__read(self): + pass + + def test_read(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + in_buff = [b'\x01\x03\x8cSu', b'nS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00' + b'\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x01\x00\x00\xb7d'] + check_req = b'\x01\x03\x9c@\x00F\xeb\xbc' + c.open() + c.serial._set_buffer(in_buff) + + check_read = in_buff[0] + in_buff[1] + assert c.read(1, 40000, 70) == check_read[3:-2] + assert c.serial.request[0] == check_req + + def test__write(self): + pass + + def test_write(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.open() + data_to_write = b'v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-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' + + buffer = [b'\x01\x10\x9cl\x00', b'\x18.N'] + c.serial._set_buffer(buffer) + c.write(1, 40044, data_to_write) + + check_req = b'\x01\x10\x9cl\x00\x180v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\xad\xff' + assert c.serial.request[0] == check_req + + +class TestModbusClientTCP: + def test___init__(self): + c = modbus_client.ModbusClientTCP() + assert c.slave_id == 1 + assert c.ipaddr == '127.0.0.1' + assert c.ipport == 502 + assert c.timeout == 2 + assert c.ctx is None + assert c.trace_func is None + assert c.max_count == 125 + + def test_close(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + + c = modbus_client.ModbusClientTCP() + c.connect() + assert c.socket + c.disconnect() + assert c.socket is None + + def test_connect(self, monkeypatch): + c = modbus_client.ModbusClientTCP() + + with pytest.raises(Exception) as exc: + c.connect() + assert 'Connection error' in str(exc.value) + + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + c.connect() + assert c.socket is not None + assert c.socket.connected is True + assert c.socket.ipaddr == '127.0.0.1' + assert c.socket.ipport == 502 + assert c.socket.timeout == 2 + + def test_disconnect(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + + c = modbus_client.ModbusClientTCP() + c.connect() + assert c.socket + c.disconnect() + assert c.socket is None + + def test__read(self, monkeypatch): + pass + + def test_read(self, monkeypatch): + c = modbus_client.ModbusClientTCP() + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + in_buff = [b'\x00\x00\x00\x00\x00\x8f\x01\x03\x8c', b'SunS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' + b'\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00'] + check_req = b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00F' + c.connect() + c.socket._set_buffer(in_buff) + assert c.read(40000, 70) == in_buff[1] + assert c.socket.request[0] == check_req + + def test__write(self, monkeypatch): + pass + + def test_write(self, monkeypatch): + c = modbus_client.ModbusClientTCP() + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + c.connect() + data_to_write = b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', b't\x00\x10'] + c.socket._set_buffer(buffer) + c.write(40052, data_to_write) + + 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 diff --git a/build/lib/sunspec2/tests/test_smdx.py b/build/lib/sunspec2/tests/test_smdx.py new file mode 100644 index 0000000..0276d14 --- /dev/null +++ b/build/lib/sunspec2/tests/test_smdx.py @@ -0,0 +1,250 @@ +import sunspec2.smdx as smdx +import sunspec2.mdef as mdef +import xml.etree.ElementTree as ET +import pytest +import copy + + +def test_to_smdx_filename(): + assert smdx.to_smdx_filename(77) == 'smdx_00077.xml' + + +def test_model_filename_to_id(): + assert smdx.model_filename_to_id('smdx_00077.xml') == 77 + assert smdx.model_filename_to_id('smdx_abc.xml') is None + + +def test_from_smdx_file(): + smdx_304 = { + "id": 304, + "group": { + "name": "inclinometer", + "type": "group", + "points": [ + { + "name": "ID", + "value": 304, + "desc": "Model identifier", + "label": "Model ID", + "mandatory": "M", + "static": "S", + "type": "uint16" + }, + { + "name": "L", + "desc": "Model length", + "label": "Model Length", + "mandatory": "M", + "static": "S", + "type": "uint16" + } + ], + "groups": [ + { + "name": "incl", + "type": "group", + "count": 0, + "points": [ + { + "name": "Inclx", + "type": "int32", + "mandatory": "M", + "units": "Degrees", + "sf": -2, + "label": "X", + "desc": "X-Axis inclination" + }, + { + "name": "Incly", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Y", + "desc": "Y-Axis inclination" + }, + { + "name": "Inclz", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Z", + "desc": "Z-Axis inclination" + } + ] + } + ], + "label": "Inclinometer Model", + "desc": "Include to support orientation measurements" + } + } + assert smdx.from_smdx_file('sunspec2/models/smdx/smdx_00304.xml') == smdx_304 + + +def test_from_smdx(): + tree = ET.parse('sunspec2/models/smdx/smdx_00304.xml') + root = tree.getroot() + + mdef_not_found = copy.deepcopy(root) + mdef_not_found.remove(mdef_not_found.find('model')) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(mdef_not_found) + + duplicate_fixed_btype_str = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + duplicate_fixed_btype_xml = ET.fromstring(duplicate_fixed_btype_str) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(duplicate_fixed_btype_xml) + + dup_repeating_btype_str = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + dup_repeating_btype_xml = ET.fromstring(dup_repeating_btype_str) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(dup_repeating_btype_xml) + + invalid_btype_root = copy.deepcopy(root) + invalid_btype_root.find('model').find('block').set('type', 'abc') + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(invalid_btype_root) + + dup_fixed_p_def_str = ''' + + + + + + + + + + + + + + + + ''' + dup_fixed_p_def_xml = ET.fromstring(dup_fixed_p_def_str) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(dup_fixed_p_def_xml) + + dup_repeating_p_def_str = ''' + + + + + + + + + + + + + + + + + + + + + ''' + dup_repeating_p_def_xml = ET.fromstring(dup_repeating_p_def_str) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(dup_repeating_p_def_xml) + + +def test_from_smdx_point(): + smdx_point_str = """""" + smdx_point_xml = ET.fromstring(smdx_point_str) + assert smdx.from_smdx_point(smdx_point_xml) == {'name': 'Mn', 'type': 'string', 'size': 16, 'mandatory': 'M'} + + missing_pid_xml = copy.deepcopy(smdx_point_xml) + del missing_pid_xml.attrib['id'] + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(missing_pid_xml) + + missing_ptype = copy.deepcopy(smdx_point_xml) + del missing_ptype.attrib['type'] + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(missing_ptype) + + unk_ptype = copy.deepcopy(smdx_point_xml) + unk_ptype.attrib['type'] = 'abc' + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(unk_ptype) + + missing_len = copy.deepcopy(smdx_point_xml) + del missing_len.attrib['len'] + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(missing_len) + + unk_mand_type = copy.deepcopy(smdx_point_xml) + unk_mand_type.attrib['mandatory'] = 'abc' + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(unk_mand_type) + + unk_access_type = copy.deepcopy(smdx_point_xml) + unk_access_type.attrib['access'] = 'abc' + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(unk_access_type) + + +def test_indent(): + pass diff --git a/build/lib/sunspec2/tests/test_spreadsheet.py b/build/lib/sunspec2/tests/test_spreadsheet.py new file mode 100644 index 0000000..973711b --- /dev/null +++ b/build/lib/sunspec2/tests/test_spreadsheet.py @@ -0,0 +1,591 @@ +import sunspec2.spreadsheet as spreadsheet +import pytest +import csv +import copy +import json + + +def test_idx(): + row = ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', + 'Units', 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'] + + assert spreadsheet.idx(row, 'Address Offset') == 0 + with pytest.raises(ValueError): + del row[0] + assert spreadsheet.idx(row, 'Address Offset', mandatory=True) + + +def test_row_is_empty(): + row = [''] * 10 + assert spreadsheet.row_is_empty(row, 0) + row[0] = 'abc' + assert not spreadsheet.row_is_empty(row, 0) + + +def test_find_name(): + points = [ + { + "name": "Inclx", + "type": "int32", + "mandatory": "M", + "units": "Degrees", + "sf": -2, + "label": "X", + "desc": "X-Axis inclination" + }, + { + "name": "Incly", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Y", + "desc": "Y-Axis inclination" + }, + { + "name": "Inclz", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Z", + "desc": "Z-Axis inclination" + } + ] + assert spreadsheet.find_name(points, 'abc') is None + assert spreadsheet.find_name(points, 'Incly') == points[1] + + +def test_element_type(): + pass + + +def test_from_spreadsheet(): + model_spreadsheet = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', 'Include to support orientation measurements', ''], + [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], + [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], + ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], + ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], + ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] + ] + model_def = { + "id": 304, + "group": { + "name": "inclinometer", + "type": "group", + "points": [ + { + "name": "ID", + "value": 304, + "desc": "Model identifier", + "label": "Model ID", + "mandatory": "M", + "static": "S", + "type": "uint16" + }, + { + "name": "L", + "desc": "Model length", + "label": "Model Length", + "mandatory": "M", + "static": "S", + "type": "uint16" + } + ], + "groups": [ + { + "name": "incl", + "type": "group", + "count": 0, + "points": [ + { + "name": "Inclx", + "type": "int32", + "mandatory": "M", + "units": "Degrees", + "sf": -2, + "label": "X", + "desc": "X-Axis inclination" + }, + { + "name": "Incly", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Y", + "desc": "Y-Axis inclination" + }, + { + "name": "Inclz", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Z", + "desc": "Z-Axis inclination" + } + ] + } + ], + "label": "Inclinometer Model", + "desc": "Include to support orientation measurements" + } + } + + assert spreadsheet.from_spreadsheet(model_spreadsheet) == model_def + + +def test_to_spreadsheet(): + model_spreadsheet = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', 'Include to support orientation measurements'], + [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier'], + [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length'], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', ''], + ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination'], + ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination'], + ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination'] + ] + model_def = { + "id": 304, + "group": { + "name": "inclinometer", + "type": "group", + "points": [ + { + "name": "ID", + "value": 304, + "desc": "Model identifier", + "label": "Model ID", + "mandatory": "M", + "static": "S", + "type": "uint16" + }, + { + "name": "L", + "desc": "Model length", + "label": "Model Length", + "mandatory": "M", + "static": "S", + "type": "uint16" + } + ], + "groups": [ + { + "name": "incl", + "type": "group", + "count": 0, + "points": [ + { + "name": "Inclx", + "type": "int32", + "mandatory": "M", + "units": "Degrees", + "sf": -2, + "label": "X", + "desc": "X-Axis inclination" + }, + { + "name": "Incly", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Y", + "desc": "Y-Axis inclination" + }, + { + "name": "Inclz", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Z", + "desc": "Z-Axis inclination" + } + ] + } + ], + "label": "Inclinometer Model", + "desc": "Include to support orientation measurements" + } + } + assert spreadsheet.to_spreadsheet(model_def) == model_spreadsheet + + +def test_to_spreadsheet_group(): + model_def = { + "group": { + "desc": "DER capacity model.", + "label": "DER Capacity", + "name": "DERCapacity", + "points": [ + { + "access": "R", + "desc": "DER capacity model id.", + "label": "DER Capacity Model ID", + "mandatory": "M", + "name": "ID", + "static": "S", + "type": "uint16", + "value": 702 + }, + { + "access": "R", + "desc": "DER capacity name model length.", + "label": "DER Capacity Model Length", + "mandatory": "M", + "name": "L", + "static": "S", + "type": "uint16" + }, + { + "access": "R", + "comments": [ + "Nameplate Ratings - Specifies capacity ratings" + ], + "desc": "Maximum active power rating at unity power factor in watts.", + "label": "Active Power Max Rating", + "mandatory": "O", + "name": "WMaxRtg", + "sf": "W_SF", + "type": "uint16", + "units": "W", + "symbols": [ + { + "name": "CAT_A", + "value": 1 + }, + { + "name": "CAT_B", + "value": 2 + } + ] + } + ], + "type": "group" + }, + "id": 702 + } + ss = [] + spreadsheet.to_spreadsheet_group(ss, model_def['group'], has_notes=False) + assert ss == [ + ['', '', 'DERCapacity', '', '', 'group', '', '', '', '', '', '', 'DER Capacity', 'DER capacity model.'], + ['', 0, 'ID', 702, '', 'uint16', '', '', '', '', 'M', 'S', 'DER Capacity Model ID', + 'DER capacity model id.'], + ['', 1, 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'DER Capacity Model Length', + 'DER capacity name model length.'], + ['Nameplate Ratings - Specifies capacity ratings', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', 2, 'WMaxRtg', '', '', 'uint16', '', 'W_SF', 'W', '', '', '', 'Active Power Max Rating', + 'Maximum active power rating at unity power factor in watts.'], + ['', '', 'CAT_A', 1, '', '', '', '', '', '', '', '', '', ''], + ['', '', 'CAT_B', 2, '', '', '', '', '', '', '', '', '', '']] + +def test_to_spreadsheet_point(): + point = { + "access": "R", + "desc": "Abnormal operating performance category as specified in IEEE 1547-2018.", + "label": "Abnormal Operating Category", + "mandatory": "O", + "name": "AbnOpCatRtg", + "symbols": [ + { + "name": "CAT_1", + "value": 1 + }, + { + "name": "CAT_2", + "value": 2 + }, + { + "name": "CAT_3", + "value": 3 + } + ], + "type": "enum16" + } + ss = [] + assert spreadsheet.to_spreadsheet_point(ss, point, has_notes=False) == 1 + + missing_name_p = copy.deepcopy(point) + del missing_name_p['name'] + with pytest.raises(Exception) as exc1: + spreadsheet.to_spreadsheet_point(ss, missing_name_p, has_notes=False) + assert 'Point missing name attribute' in str(exc1.value) + + missing_type_p = copy.deepcopy(point) + del missing_type_p['type'] + with pytest.raises(Exception) as exc2: + spreadsheet.to_spreadsheet_point(ss, missing_type_p, has_notes=False) + assert 'Point AbnOpCatRtg missing type' in str(exc2.value) + + unk_p_type = copy.deepcopy(point) + unk_p_type['type'] = 'abc' + with pytest.raises(Exception) as exc3: + spreadsheet.to_spreadsheet_point(ss, unk_p_type, has_notes=False) + assert 'Unknown point type' in str(exc3.value) + + p_size_not_int = copy.deepcopy(point) + p_size_not_int['type'] = 'string' + p_size_not_int['size'] = 'abc' + with pytest.raises(Exception) as exc4: + spreadsheet.to_spreadsheet_point(ss, p_size_not_int, has_notes=False) + assert 'Point size is for point AbnOpCatRtg not an iteger value' in str(exc4.value) + + +def test_to_spreadsheet_symbol(): + symbol = {"name": "MAX_W", "value": 0} + ss = [] + spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False) + assert ss[0][2] == 'MAX_W' and ss[0][3] == 0 + + ss = [] + del symbol['value'] + with pytest.raises(Exception) as exc1: + spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False) + assert 'Symbol MAX_W missing value' in str(exc1.value) + + ss = [] + del symbol['name'] + with pytest.raises(Exception) as exc2: + spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False) + assert 'Symbol missing name attribute' in str(exc2.value) + + +def test_to_spreadsheet_comment(): + ss = [] + spreadsheet.to_spreadsheet_comment(ss, 'Scaling Factors', has_notes=False) + assert ss[0][0] == 'Scaling Factors' + + +def test_spreadsheet_equal(): + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', ''], + ['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], + ['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], + ['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], + ['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], + ['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] + ] + ss_copy = copy.deepcopy(spreadsheet_smdx_304) + + assert spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy) + + with pytest.raises(Exception) as exc1: + ss_copy[0][0] = 'abc' + spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy) + assert 'Line 1 different' in str(exc1.value) + + with pytest.raises(Exception) as exc2: + del ss_copy[0] + spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy) + assert 'Different length' in str(exc2.value) + + +def test_from_csv(): + model_def = { + "id": 304, + "group": { + "name": "inclinometer", + "type": "group", + "points": [ + { + "name": "ID", + "value": 304, + "desc": "Model identifier", + "label": "Model ID", + "mandatory": "M", + "static": "S", + "type": "uint16" + }, + { + "name": "L", + "desc": "Model length", + "label": "Model Length", + "mandatory": "M", + "static": "S", + "type": "uint16" + } + ], + "groups": [ + { + "name": "incl", + "type": "group", + "count": 0, + "points": [ + { + "name": "Inclx", + "type": "int32", + "mandatory": "M", + "units": "Degrees", + "sf": -2, + "label": "X", + "desc": "X-Axis inclination" + }, + { + "name": "Incly", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Y", + "desc": "Y-Axis inclination" + }, + { + "name": "Inclz", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Z", + "desc": "Z-Axis inclination" + } + ] + } + ], + "label": "Inclinometer Model", + "desc": "Include to support orientation measurements" + } + } + assert model_def == spreadsheet.from_csv('sunspec2/tests/test_data/smdx_304.csv') + + +def test_to_csv(tmp_path): + model_def = { + "id": 304, + "group": { + "name": "inclinometer", + "type": "group", + "points": [ + { + "name": "ID", + "value": 304, + "desc": "Model identifier", + "label": "Model ID", + "mandatory": "M", + "static": "S", + "type": "uint16" + }, + { + "name": "L", + "desc": "Model length", + "label": "Model Length", + "mandatory": "M", + "static": "S", + "type": "uint16" + } + ], + "groups": [ + { + "name": "incl", + "type": "group", + "count": 0, + "points": [ + { + "name": "Inclx", + "type": "int32", + "mandatory": "M", + "units": "Degrees", + "sf": -2, + "label": "X", + "desc": "X-Axis inclination" + }, + { + "name": "Incly", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Y", + "desc": "Y-Axis inclination" + }, + { + "name": "Inclz", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Z", + "desc": "Z-Axis inclination" + } + ] + } + ], + "label": "Inclinometer Model", + "desc": "Include to support orientation measurements" + } + } + ss = spreadsheet.to_spreadsheet(model_def) + spreadsheet.to_csv(model_def, filename=tmp_path / 'smdx_304.csv') + + same_data = True + row_num = 0 + idx = 0 + with open(tmp_path / 'smdx_304.csv') as csvfile: + csvreader = csv.reader(csvfile) + for row in csvreader: + idx = 0 + for i in row: + if str(ss[row_num][idx]) != str(i): + same_data = False + idx += 1 + row_num += 1 + assert same_data + + +def test_spreadsheet_from_csv(): + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', ''], + ['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], + ['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], + ['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], + ['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], + ['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] + ] + + counter = 0 + for row in spreadsheet.spreadsheet_from_csv('sunspec2/tests/test_data/smdx_304.csv'): + same = True + counter2 = 0 + for i in row: + if i != spreadsheet_smdx_304[counter][counter2]: + same = False + counter2 += 1 + counter += 1 + assert same + + +def test_spreadsheet_to_csv(tmp_path): + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', ''], + [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], + [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], + ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], + ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], + ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] + ] + spreadsheet.spreadsheet_to_csv(spreadsheet_smdx_304, filename=tmp_path / 'smdx_304.csv') + + same_data = True + rowNum = 0 + idx = 0 + with open(tmp_path / 'smdx_304.csv') as csvfile: + csvreader = csv.reader(csvfile) + for row in csvreader: + idx = 0 + for i in row: + if str(spreadsheet_smdx_304[rowNum][idx]) != str(i): + same_data = False + idx += 1 + rowNum += 1 + assert same_data + + diff --git a/build/lib/sunspec2/tests/test_xlsx.py b/build/lib/sunspec2/tests/test_xlsx.py new file mode 100644 index 0000000..2c1c3bf --- /dev/null +++ b/build/lib/sunspec2/tests/test_xlsx.py @@ -0,0 +1,910 @@ +import sunspec2.xlsx as xlsx +import pytest +import openpyxl +import openpyxl.styles as styles +import json + + +def test___init__(): + wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') + assert wb.filename == 'sunspec2/tests/test_data/wb_701-705.xlsx' + assert wb.params == {} + + wb2 = xlsx.ModelWorkbook() + assert wb2.filename is None + assert wb2.params == {} + + +def test_get_models(): + wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') + assert wb.get_models() == [701, 702, 703, 704, 705] + wb2 = xlsx.ModelWorkbook() + assert wb2.get_models() == [] + + +def test_save(tmp_path): + wb = xlsx.ModelWorkbook() + wb.save(tmp_path / 'test.xlsx') + wb2 = xlsx.ModelWorkbook(filename=tmp_path / 'test.xlsx') + iter_rows = wb2.xlsx_iter_rows(wb.wb['Index']) + assert next(iter_rows) == ['Model', 'Label', 'Description'] + + +def test_xlsx_iter_rows(): + wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') + iter_rows = wb.xlsx_iter_rows(wb.wb['704']) + assert next(iter_rows) == ['Address Offset', 'Group Offset', 'Name', + 'Value', 'Count', 'Type', 'Size', 'Scale Factor', + 'Units', 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', + 'Label', 'Description', 'Detailed Description'] + assert next(iter_rows) == [None, None, 'DERCtlAC', None, None, 'group', + None, None, None, None, None, None, 'DER AC Controls', + 'DER AC controls model.', None] + + +def test_spreadsheet_from_xlsx(): + wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') + assert wb.spreadsheet_from_xlsx(704)[0:2] == [['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', + 'Type', 'Size', 'Scale Factor', 'Units', 'RW Access (RW)', + 'Mandatory (M)', 'Static (S)', 'Label', 'Description', + 'Detailed Description'], + ['', '', 'DERCtlAC', None, None, 'group', None, None, None, + None, None, None, 'DER AC Controls', 'DER AC controls model.', None]] + + +# need deep diff to compare from_xlsx to json file, right now just compares with its own output +def test_from_xlsx(): + wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') + with open('sunspec2/models/json/model_704.json') as f: + model_json_704 = json.load(f) + from_xlsx_output = { + "group": { + "name": "DERCtlAC", + "type": "group", + "label": "DER AC Controls", + "desc": "DER AC controls model.", + "points": [ + { + "name": "ID", + "type": "uint16", + "mandatory": "M", + "static": "S", + "label": "Model ID", + "desc": "Model name model id.", + "value": 704 + }, + { + "name": "L", + "type": "uint16", + "mandatory": "M", + "static": "S", + "label": "Model Length", + "desc": "Model name model length." + }, + { + "name": "PFWInjEna", + "type": "enum16", + "access": "RW", + "label": "Power Factor Enable (W Inj) Enable", + "desc": "Power factor enable when injecting active power.", + "comments": [ + "Set Power Factor (when injecting active power)" + ], + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "PFWInjEnaRvrt", + "type": "enum16", + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "PFWInjRvrtTms", + "type": "uint32", + "units": "Secs", + "access": "RW", + "label": "PF Reversion Time (W Inj)", + "desc": "Power factor reversion timer when injecting active power." + }, + { + "name": "PFWInjRvrtRem", + "type": "uint32", + "units": "Secs", + "label": "PF Reversion Time Rem (W Inj)", + "desc": "Power factor reversion time remaining when injecting active power." + }, + { + "name": "PFWAbsEna", + "type": "enum16", + "access": "RW", + "label": "Power Factor Enable (W Abs) Enable", + "desc": "Power factor enable when absorbing active power.", + "comments": [ + "Set Power Factor (when absorbing active power)" + ], + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "PFWAbsEnaRvrt", + "type": "enum16", + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "PFWAbsRvrtTms", + "type": "uint32", + "units": "Secs", + "access": "RW", + "label": "PF Reversion Time (W Abs)", + "desc": "Power factor reversion timer when absorbing active power." + }, + { + "name": "PFWAbsRvrtRem", + "type": "uint32", + "units": "Secs", + "label": "PF Reversion Time Rem (W Abs)", + "desc": "Power factor reversion time remaining when absorbing active power." + }, + { + "name": "WMaxLimEna", + "type": "enum16", + "access": "RW", + "label": "Limit Max Active Power Enable", + "desc": "Limit maximum active power enable.", + "comments": [ + "Limit Maximum Active Power Generation" + ], + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "WMaxLim", + "type": "uint16", + "sf": "WMaxLim_SF", + "units": "Pct", + "access": "RW", + "label": "Limit Max Power Setpoint", + "desc": "Limit maximum active power value." + }, + { + "name": "WMaxLimRvrt", + "type": "uint16", + "sf": "WMaxLim_SF", + "units": "Pct", + "access": "RW", + "label": "Reversion Limit Max Power", + "desc": "Reversion limit maximum active power value." + }, + { + "name": "WMaxLimEnaRvrt", + "type": "enum16", + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "WMaxLimRvrtTms", + "type": "uint32", + "units": "Secs", + "access": "RW", + "label": "Limit Max Power Reversion Time", + "desc": "Limit maximum active power reversion time." + }, + { + "name": "WMaxLimRvrtRem", + "type": "uint32", + "units": "Secs", + "label": "Limit Max Power Rev Time Rem", + "desc": "Limit maximum active power reversion time remaining." + }, + { + "name": "WSetEna", + "type": "enum16", + "access": "RW", + "label": "Set Active Power Enable", + "desc": "Set active power enable.", + "comments": [ + "Set Active Power Level (may be negative for charging)" + ], + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "WSetMod", + "type": "enum16", + "access": "RW", + "label": "Set Active Power Mode", + "desc": "Set active power mode.", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1, + "label": "Active Power As Max Percent", + "desc": "Active power setting is percentage of maximum active power." + }, + { + "name": "WATTS", + "value": 2, + "label": "Active Power As Watts", + "desc": "Active power setting is in watts." + } + ] + }, + { + "name": "WSet", + "type": "int32", + "sf": "WSet_SF", + "units": "W", + "access": "RW", + "label": "Active Power Setpoint (W)", + "desc": "Active power setting value in watts." + }, + { + "name": "WSetRvrt", + "type": "int32", + "sf": "WSet_SF", + "units": "W", + "access": "RW", + "label": "Reversion Active Power (W)", + "desc": "Reversion active power setting value in watts." + }, + { + "name": "WSetPct", + "type": "int32", + "sf": "WSetPct_SF", + "units": "Pct", + "access": "RW", + "label": "Active Power Setpoint (Pct)", + "desc": "Active power setting value as percent." + }, + { + "name": "WSetPctRvrt", + "type": "int32", + "sf": "WSetPct_SF", + "units": "Pct", + "access": "RW", + "label": "Reversion Active Power (Pct)", + "desc": "Reversion active power setting value as percent." + }, + { + "name": "WSetEnaRvrt", + "type": "enum16", + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "WSetRvrtTms", + "type": "uint32", + "units": "Secs", + "access": "RW", + "label": "Active Power Reversion Time", + "desc": "Set active power reversion time." + }, + { + "name": "WSetRvrtRem", + "type": "uint32", + "units": "Secs", + "label": "Active Power Rev Time Rem", + "desc": "Set active power reversion time remaining." + }, + { + "name": "VarSetEna", + "type": "enum16", + "access": "RW", + "label": "Set Reactive Power Enable", + "desc": "Set reactive power enable.", + "comments": [ + "Set Reacitve Power Level" + ], + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "VarSetMod", + "type": "enum16", + "access": "RW", + "label": "Set Reactive Power Mode", + "desc": "Set reactive power mode.", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1, + "label": "Reactive Power as Watt Max Pct", + "desc": "Reactive power setting is percent of maximum active power." + }, + { + "name": "VAR_MAX_PCT", + "value": 2, + "label": "Reactive Power as Var Max Pct", + "desc": "Reactive power setting is percent of maximum reactive power." + }, + { + "name": "VAR_AVAIL_PCT", + "value": 3, + "label": "Reactive Power as Var Avail Pct", + "desc": "Reactive power setting is percent of available reactive power." + }, + { + "name": "VARS", + "value": 4, + "label": "Reactive Power as Vars", + "desc": "Reactive power is in vars." + } + ] + }, + { + "name": "VarSetPri", + "type": "enum16", + "symbols": [ + { + "name": "ACTIVE", + "value": 1, + "label": "Active Power Priority", + "desc": "Active power priority." + }, + { + "name": "REACTIVE", + "value": 2, + "label": "Reactive Power Priority", + "desc": "Reactive power priority." + }, + { + "name": "IEEE_1547", + "value": 3, + "label": "IEEE 1547 Power Priority", + "desc": "IEEE 1547-2018 power priority mode." + }, + { + "name": "PF", + "value": 4, + "label": "PF Power Priority", + "desc": "Track PF setting derived from current active and reactive power settings." + }, + { + "name": "VENDOR", + "value": 5, + "label": "Vendor Power Priority", + "desc": "Power priority is vendor specific mode." + } + ] + }, + { + "name": "VarSet", + "type": "int32", + "sf": "VarSet_SF", + "units": "Var", + "access": "RW", + "label": "Reactive Power Setpoint (Vars)", + "desc": "Reactive power setting value in vars." + }, + { + "name": "VarSetRvrt", + "type": "int32", + "sf": "VarSet_SF", + "units": "Var", + "access": "RW", + "label": "Reversion Reactive Power (Vars)", + "desc": "Reversion reactive power setting value in vars." + }, + { + "name": "VarSetPct", + "type": "int32", + "sf": "VarSetPct_SF", + "units": "Pct", + "access": "RW", + "label": "Reactive Power Setpoint (Pct)", + "desc": "Reactive power setting value as percent." + }, + { + "name": "VarSetPctRvrt", + "type": "enum16", + "sf": "VarSetPct_SF", + "units": "Pct", + "access": "RW", + "label": "Reversion Reactive Power (Pct)", + "desc": "Reversion reactive power setting value as percent.", + "symbols": [ + { + "name": "DISABLED", + "value": 0, + "label": "Disabled", + "desc": "Function is disabled." + }, + { + "name": "ENABLED", + "value": 1, + "label": "Enabled", + "desc": "Function is enabled." + } + ] + }, + { + "name": "VarSetRvrtTms", + "type": "uint32", + "units": "Secs", + "access": "RW", + "label": "Reactive Power Reversion Time", + "desc": "Set reactive power reversion time." + }, + { + "name": "VarSetRvrtRem", + "type": "uint32", + "units": "Secs", + "label": "Reactive Power Rev Time Rem", + "desc": "Set reactive power reversion time remaining." + }, + { + "name": "RGra", + "type": "uint32", + "units": "%WMax/Sec", + "access": "RW", + "label": "Normal Ramp Rate", + "desc": "Ramp rate for increases in active power during normal generation.", + "comments": [ + "Ramp Rate" + ], + "symbols": [ + { + "name": "A_MAX", + "value": 1, + "label": "Max Current Ramp", + "desc": "Ramp based on percent of max current per second." + }, + { + "name": "W_MAX", + "value": 2, + "label": "Max Active Power Ramp", + "desc": "Ramp based on percent of max active power per second." + } + ] + }, + { + "name": "PF_SF", + "type": "sunssf", + "static": "S", + "label": "Power Factor Scale Factor", + "desc": "Power factor scale factor.", + "comments": [ + "Scale Factors" + ] + }, + { + "name": "WMaxLim_SF", + "type": "sunssf", + "static": "S", + "label": "Limit Max Power Scale Factor", + "desc": "Limit maximum power scale factor." + }, + { + "name": "WSet_SF", + "type": "sunssf", + "static": "S", + "label": "Active Power Scale Factor", + "desc": "Active power scale factor." + }, + { + "name": "WSetPct_SF", + "type": "sunssf", + "static": "S", + "label": "Active Power Pct Scale Factor", + "desc": "Active power pct scale factor." + }, + { + "name": "VarSet_SF", + "type": "sunssf", + "static": "S", + "label": "Reactive Power Scale Factor", + "desc": "Reactive power scale factor." + }, + { + "name": "VarSetPct_SF", + "type": "sunssf", + "static": "S", + "label": "Reactive Power Pct Scale Factor", + "desc": "Reactive power pct scale factor." + } + ], + "groups": [ + { + "name": "PFWInj", + "type": "sync", + "label": " ", + "desc": " ", + "comments": [ + "Power Factor Settings" + ], + "points": [ + { + "name": "PF", + "type": "uint16", + "sf": "PF_SF", + "access": "RW", + "label": "Power Factor (W Inj) ", + "desc": "Power factor setpoint when injecting active power." + }, + { + "name": "Ext", + "type": "enum16", + "access": "RW", + "label": "Power Factor Excitation (W Inj)", + "desc": "Power factor excitation setpoint when injecting active power.", + "symbols": [ + { + "name": "OVER_EXCITED", + "value": 0, + "label": "Over-excited", + "desc": "Power factor over-excited excitation." + }, + { + "name": "UNDER_EXCITED", + "value": 1, + "label": "Under-excited", + "desc": "Power factor under-excited excitation." + } + ] + } + ] + }, + { + "name": "PFWInjRvrt", + "type": "sync", + "label": " ", + "desc": " ", + "points": [ + { + "name": "PF", + "type": "uint16", + "sf": "PF_SF", + "access": "RW", + "label": "Reversion Power Factor (W Inj) ", + "desc": "Reversion power factor setpoint when injecting active power." + }, + { + "name": "Ext", + "type": "enum16", + "access": "RW", + "label": "Reversion PF Excitation (W Inj)", + "desc": "Reversion power factor excitation setpoint when injecting active power.", + "symbols": [ + { + "name": "OVER_EXCITED", + "value": 0, + "label": "Over-excited", + "desc": "Power factor over-excited excitation." + }, + { + "name": "UNDER_EXCITED", + "value": 1, + "label": "Under-excited", + "desc": "Power factor under-excited excitation." + } + ] + } + ] + }, + { + "name": "PFWAbs", + "type": "sync", + "label": " ", + "desc": " ", + "points": [ + { + "name": "PF", + "type": "uint16", + "sf": "PF_SF", + "access": "RW", + "label": "Power Factor (W Abs) ", + "desc": "Power factor setpoint when absorbing active power." + }, + { + "name": "Ext", + "type": "enum16", + "access": "RW", + "label": "Power Factor Excitation (W Abs)", + "desc": "Power factor excitation setpoint when absorbing active power.", + "symbols": [ + { + "name": "OVER_EXCITED", + "value": 0, + "label": "Over-excited", + "desc": "Power factor over-excited excitation." + }, + { + "name": "UNDER_EXCITED", + "value": 1, + "label": "Under-excited", + "desc": "Power factor under-excited excitation." + } + ] + } + ] + }, + { + "name": "PFWAbsRvrt", + "type": "sync", + "label": " ", + "desc": " ", + "points": [ + { + "name": "PF", + "type": "uint16", + "sf": "PF_SF", + "access": "RW", + "label": "Reversion Power Factor (W Abs) ", + "desc": "Reversion power factor setpoint when absorbing active power." + }, + { + "name": "Ext", + "type": "enum16", + "access": "RW", + "label": "Reversion PF Excitation (W Abs)", + "desc": "Reversion power factor excitation setpoint when absorbing active power.", + "symbols": [ + { + "name": "OVER_EXCITED", + "value": 0, + "label": "Over-excited", + "desc": "Power factor over-excited excitation." + }, + { + "name": "UNDER_EXCITED", + "value": 1, + "label": "Under-excited", + "desc": "Power factor under-excited excitation." + } + ] + } + ] + } + ] + }, + "id": 704 + } + assert wb.from_xlsx(704) == from_xlsx_output + + +def test_set_cell(): + wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') + with pytest.raises(ValueError) as exc: + wb.set_cell(wb.wb['704'], 1, 2, 3) + assert 'Workbooks opened with existing file are read only' in str(exc.value) + + wb2 = xlsx.ModelWorkbook() + assert wb2.set_cell(wb2.wb['Index'], 2, 1, 3, style='suns_comment').value == 3 + + +def test_set_info(): + wb = xlsx.ModelWorkbook() + values = [''] * 15 + values[14] = 'detail' + values[13] = 'description' + values[12] = 'label' + wb.set_info(wb.wb['Index'], 2, values) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) + assert next(iter_rows) == [None, None, None, None, None, None, + None, None, None, None, None, None, 'label', 'description', 'detail'] + + +def test_set_group(): + wb = xlsx.ModelWorkbook() + values = [''] * 15 + values[2] = 'name' + values[5] = 'type' + values[4] = 'count' + values[14] = 'detail' + values[13] = 'description' + values[12] = 'label' + wb.set_group(wb.wb['Index'], 2, values, 2) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) + assert next(iter_rows) == ['', '', 'name', '', 'count', 'type', '', '', '', '', '', '', + 'label', 'description', 'detail'] + + +def test_set_point(): + wb = xlsx.ModelWorkbook() + values = [''] * 15 + values[0] = 'addr_offset' + values[1] = 'group_offset' + values[2] = 'name' + values[3] = 'value' + values[4] = 'count' + values[5] = 'type' + values[6] = 'size' + values[7] = 'sf' + values[8] = 'units' + values[9] = 'access' + values[10] = 'mandatory' + values[11] = 'static' + wb.set_point(wb.wb['Index'], 2, values, 1) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) + assert next(iter_rows) == ['addr_offset', 'group_offset', 'name', 'value', 'count', + 'type', 'size', 'sf', 'units', 'access', 'mandatory', 'static', '', '', ''] + + +def test_set_symbol(): + wb = xlsx.ModelWorkbook() + values = [''] * 15 + values[2] = 'name' + values[3] = 'value' + values[14] = 'detail' + values[13] = 'description' + values[12] = 'label' + wb.set_symbol(wb.wb['Index'], 2, values) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) + assert next(iter_rows) == ['', '', 'name', 'value', '', '', '', + '', '', '', '', '', 'label', 'description', 'detail'] + + +def test_set_comment(): + wb = xlsx.ModelWorkbook() + wb.set_comment(wb.wb['Index'], 2, ['This is a comment']) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) + assert next(iter_rows)[0] == 'This is a comment' + + +def test_set_hdr(): + wb = xlsx.ModelWorkbook() + wb.set_hdr(wb.wb['Index'], ['This', 'is', 'a', 'test', 'header']) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + assert next(iter_rows) == ['This', 'is', 'a', 'test', 'header'] + + +def test_spreadsheet_to_xlsx(): + wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') + with pytest.raises(ValueError) as exc: + wb.spreadsheet_to_xlsx(702, []) + assert 'Workbooks opened with existing file are read only' in str(exc.value) + + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', ''], + ['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], + ['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], + ['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], + ['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], + ['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] + ] + wb2 = xlsx.ModelWorkbook() + wb2.spreadsheet_to_xlsx(304, spreadsheet_smdx_304) + iter_rows = wb2.xlsx_iter_rows(wb2.wb['304']) + for row in spreadsheet_smdx_304: + assert next(iter_rows) == row + + +def test_to_xlsx(tmp_path): + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements'], + [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier'], + [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length'], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', ''], + ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination'], + ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination'], + ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination'] + ] + with open('sunspec2/models/json/model_304.json') as f: + m_703 = json.load(f) + wb = xlsx.ModelWorkbook() + wb.to_xlsx(m_703) + iter_rows = wb.xlsx_iter_rows(wb.wb['304']) + for row in spreadsheet_smdx_304: + assert next(iter_rows) == row diff --git a/build/lib/sunspec2/xlsx.py b/build/lib/sunspec2/xlsx.py new file mode 100644 index 0000000..46c2838 --- /dev/null +++ b/build/lib/sunspec2/xlsx.py @@ -0,0 +1,387 @@ + +""" + Copyright (C) 2020 SunSpec Alliance + + Permission is hereby granted, free of charge, to any person obtaining a + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +""" + +import sunspec2.mdef as mdef +import sunspec2.spreadsheet as ss + +models_hdr = [('Model', 0), + ('Label', 30), + ('Description', 60)] + +column_width = { + ss.ADDRESS_OFFSET_IDX: 0, + ss.GROUP_OFFSET_IDX: 0, + ss.NAME_IDX: 25, + ss.VALUE_IDX: 12, + ss.COUNT_IDX: 12, + ss.TYPE_IDX: 12, + ss.SIZE_IDX: 12, + ss.SCALE_FACTOR_IDX: 12, + ss.UNITS_IDX: 12, + ss.ACCESS_IDX: 12, + ss.MANDATORY_IDX: 12, + ss.STATIC_IDX: 12, + ss.LABEL_IDX: 30, + ss.DESCRIPTION_IDX: 60, + ss.NOTES_IDX: 60 +} + +group_styles = { + 'suns_group_1': { + 'group_color': 'b8cce4', # 184, 204, 228 + 'point_color': 'dce6f1', # 220, 230, 241 + }, + 'suns_group_2': { + 'group_color': 'd8e4bc', # 216, 228, 188 + 'point_color': 'ebf1de', # 235, 241, 222 + }, + 'suns_group_3': { + 'group_color': 'ccc0da', # 204, 192, 218 + 'point_color': 'e4dfec', # 228, 223, 236 + }, + 'suns_group_4': { + 'group_color': 'fcd5b4', # 252, 213, 180 + 'point_color': 'fde9d9', # 253, 233, 217 + }, + 'suns_group_5': { + 'group_color': 'e6b8b7', # 230, 184, 183 + 'point_color': 'f2dcdb', # 242, 220, 219 + } +} + +try: + import openpyxl + import openpyxl.styles as styles + + + class ModelWorkbook(object): + def __init__(self, filename=None, model_dir=None, license_summary=False, params=None): + self.wb = None + self.filename = filename + self.params = params + if self.params is None: + self.params = {} + + if filename is not None: + self.wb = openpyxl.load_workbook(filename=filename) + else: + self.wb = openpyxl.Workbook() + + self.ws_models = self.wb.active + self.ws_models.title = 'Index' + + thin = styles.Side(border_style=self.params.get('side_border', 'thin'), + color=self.params.get('side_color', '999999')) + + for i in range(1, len(group_styles) + 1): + key = 'suns_group_%s' % i + name = 'suns_group_entry_%s' % i + style = styles.NamedStyle(name=name) + color = group_styles[key]['group_color'] + # self.params.get('group_color', color) + style.fill = styles.PatternFill('solid', fgColor=color) + style.font = styles.Font() + style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(style) + + name = 'suns_group_text_%s' % i + style = styles.NamedStyle(name=name) + style.fill = styles.PatternFill('solid', fgColor=color) + style.font = styles.Font() + style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(style) + + name = 'suns_point_entry_%s' % i + style = styles.NamedStyle(name=name) + color = group_styles[key]['point_color'] + # self.params.get('group_color', color) + style.fill = styles.PatternFill('solid', fgColor=color) + style.font = styles.Font() + style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(style) + + name = 'suns_point_text_%s' % i + style = styles.NamedStyle(name=name) + style.fill = styles.PatternFill('solid', fgColor=color) + style.font = styles.Font() + style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(style) + + if 'suns_hdr' not in self.wb.named_styles: + hdr_style = styles.NamedStyle(name='suns_hdr') + hdr_style.fill = styles.PatternFill('solid', fgColor=self.params.get('hdr_color', 'dddddd')) + hdr_style.font = styles.Font(bold=True) + hdr_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + hdr_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(hdr_style) + if 'suns_group_entry' not in self.wb.named_styles: + model_entry_style = styles.NamedStyle(name='suns_group_entry') + model_entry_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('group_color', 'fff9e5')) + model_entry_style.font = styles.Font() + model_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + model_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(model_entry_style) + if 'suns_group_text' not in self.wb.named_styles: + model_text_style = styles.NamedStyle(name='suns_group_text') + model_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('group_color', 'fff9e5')) + model_text_style.font = styles.Font() + model_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + model_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(model_text_style) + if 'suns_point_entry' not in self.wb.named_styles: + fixed_entry_style = styles.NamedStyle(name='suns_point_entry') + fixed_entry_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('point_color', 'e6f2ff')) + fixed_entry_style.font = styles.Font() + fixed_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + fixed_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(fixed_entry_style) + if 'suns_point_text' not in self.wb.named_styles: + fixed_text_style = styles.NamedStyle(name='suns_point_text') + fixed_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('point_color', 'e6f2ff')) + fixed_text_style.font = styles.Font() + fixed_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + fixed_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(fixed_text_style) + if 'suns_point_variable_entry' not in self.wb.named_styles: + fixed_entry_style = styles.NamedStyle(name='suns_point_variable_entry') + fixed_entry_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('point_variable_color', 'ecf9ec')) + fixed_entry_style.font = styles.Font() + fixed_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + fixed_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(fixed_entry_style) + if 'suns_point_variable_text' not in self.wb.named_styles: + fixed_text_style = styles.NamedStyle(name='suns_point_variable_text') + fixed_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('point_variable_color', 'ecf9ec')) + fixed_text_style.font = styles.Font() + fixed_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + fixed_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(fixed_text_style) + if 'suns_symbol_entry' not in self.wb.named_styles: + repeating_entry_style = styles.NamedStyle(name='suns_symbol_entry') + repeating_entry_style.fill =styles.PatternFill('solid', + fgColor=self.params.get('symbol_color', 'fafafa')) + repeating_entry_style.font = styles.Font() + repeating_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + repeating_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(repeating_entry_style) + if 'suns_symbol_text' not in self.wb.named_styles: + repeating_text_style = styles.NamedStyle(name='suns_symbol_text') + repeating_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('symbol_color', 'fafafa')) + repeating_text_style.font = styles.Font() + repeating_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + repeating_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(repeating_text_style) + if 'suns_comment' not in self.wb.named_styles: + symbol_text_style = styles.NamedStyle(name='suns_comment') + symbol_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('comment_color', 'dddddd')) + # fgColor=self.params.get('symbol_color', 'fffcd9')) + symbol_text_style.font = styles.Font() + symbol_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + symbol_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(symbol_text_style) + if 'suns_entry' not in self.wb.named_styles: + entry_style = styles.NamedStyle(name='suns_entry') + entry_style.fill = styles.PatternFill('solid', fgColor='ffffff') + entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(entry_style) + if 'suns_text' not in self.wb.named_styles: + text_style = styles.NamedStyle(name='suns_text') + text_style.font = styles.Font() + text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(text_style) + if 'suns_hyper' not in self.wb.named_styles: + hyper_style = openpyxl.styles.NamedStyle(name='suns_hyper') + hyper_style.font = openpyxl.styles.Font(color='0000ee', underline='single') + hyper_style.alignment = openpyxl.styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(hyper_style) + + for i in range(len(models_hdr)): + self.set_cell(self.ws_models, 1, i + 1, models_hdr[i][0], 'suns_hdr') + if models_hdr[i][1]: + self.ws_models.column_dimensions[chr(65 + i)].width = models_hdr[i][1] + + def get_models(self): + models = [] + if self.wb is not None: + for m in self.wb.sheetnames: + try: + mid = int(m) + models.append(mid) + except: + pass + return models + + def save(self, filename): + self.wb.save(filename) + + def xlsx_iter_rows(self, ws): + for row in ws.iter_rows(): + yield [cell.value for cell in row] + + def spreadsheet_from_xlsx(self, mid=None): + spreadsheet = [] + ws = self.wb[str(mid)] + for row in self.xlsx_iter_rows(ws): + # filter out informative offset information from the normative model definition + if row[ss.TYPE_IDX] and row[ss.TYPE_IDX] != ss.TYPE: + row[ss.ADDRESS_OFFSET_IDX] = '' + row[ss.GROUP_OFFSET_IDX] = '' + spreadsheet.append(row) + return spreadsheet + + def from_xlsx(self, mid=None): + return ss.from_spreadsheet(self.spreadsheet_from_xlsx(mid)) + + def set_cell(self, ws, row, col, value, style=None): + if self.filename: + raise ValueError('Workbooks opened with existing file are read only') + cell = ws.cell(row=row, column=col) + cell.value = value + if style: + cell.style = style + return cell + + def set_info(self, ws, row, values, style=None): + self.set_cell(ws, row, ss.LABEL_IDX + 1, values[ss.LABEL_IDX], style=style) + self.set_cell(ws, row, ss.DESCRIPTION_IDX + 1, values[ss.DESCRIPTION_IDX], style=style) + if len(values) > ss.NOTES_IDX: + self.set_cell(ws, row, ss.NOTES_IDX + 1, values[ss.NOTES_IDX], style=style) + + def set_group(self, ws, row, values, level): + for i in range(len(values)): + self.set_cell(ws, row, i + 1, '', 'suns_group_entry_%s' % level) + self.set_cell(ws, row, ss.NAME_IDX + 1, values[ss.NAME_IDX]) + self.set_cell(ws, row, ss.TYPE_IDX + 1, values[ss.TYPE_IDX]) + self.set_cell(ws, row, ss.COUNT_IDX + 1, values[ss.COUNT_IDX]) + self.set_info(ws, row, values, 'suns_group_text_%s' % level) + + def set_point(self, ws, row, values, level): + entry_style = 'suns_point_entry_%s' % level + text_style = 'suns_point_text_%s' % level + self.set_cell(ws, row, ss.ADDRESS_OFFSET_IDX + 1, values[ss.ADDRESS_OFFSET_IDX], entry_style) + self.set_cell(ws, row, ss.GROUP_OFFSET_IDX + 1, values[ss.GROUP_OFFSET_IDX], entry_style) + self.set_cell(ws, row, ss.NAME_IDX + 1, values[ss.NAME_IDX], entry_style) + self.set_cell(ws, row, ss.VALUE_IDX + 1, values[ss.VALUE_IDX], entry_style) + self.set_cell(ws, row, ss.COUNT_IDX + 1, values[ss.COUNT_IDX], entry_style) + self.set_cell(ws, row, ss.TYPE_IDX + 1, values[ss.TYPE_IDX], entry_style) + self.set_cell(ws, row, ss.SIZE_IDX + 1, values[ss.SIZE_IDX], entry_style) + self.set_cell(ws, row, ss.SCALE_FACTOR_IDX + 1, values[ss.SCALE_FACTOR_IDX], entry_style) + self.set_cell(ws, row, ss.UNITS_IDX + 1, values[ss.UNITS_IDX], entry_style) + self.set_cell(ws, row, ss.ACCESS_IDX + 1, values[ss.ACCESS_IDX], entry_style) + self.set_cell(ws, row, ss.MANDATORY_IDX + 1, values[ss.MANDATORY_IDX], entry_style) + self.set_cell(ws, row, ss.STATIC_IDX + 1, values[ss.STATIC_IDX], entry_style) + self.set_info(ws, row, values, text_style) + + def set_symbol(self, ws, row, values): + for i in range(len(values)): + self.set_cell(ws, row, i + 1, '', 'suns_symbol_entry') + self.set_cell(ws, row, ss.NAME_IDX + 1, values[ss.NAME_IDX]) + self.set_cell(ws, row, ss.VALUE_IDX + 1, values[ss.VALUE_IDX]) + self.set_info(ws, row, values, 'suns_symbol_text') + + def set_comment(self, ws, row, values): + ws.merge_cells('A%s:%s%s' % (row, chr(65+len(values)-1), row)) + self.set_cell(ws, row, 1, values[0], 'suns_comment') + + def set_hdr(self, ws, values): + for i in range(len(values)): + self.set_cell(ws, 1, i + 1, values[i], 'suns_hdr') + width = column_width[i] + if width: + ws.column_dimensions[chr(65+i)].width = column_width[i] + + def spreadsheet_to_xlsx(self, mid, spreadsheet): + if self.filename: + raise ValueError('Workbooks opened with existing file are read only') + has_notes = 'Notes' in spreadsheet[0] + info = False + label = None + description = None + notes = None + level = 1 + + ws = self.wb.create_sheet(title=str(mid)) + self.set_hdr(ws, spreadsheet[0]) + row = 2 + for values in spreadsheet[1:]: + # point - has type + etype = values[ss.TYPE_IDX] + if etype: + # group + if etype in mdef.group_types: + level = len(values[ss.NAME_IDX].split('.')) + self.set_group(ws, row, values, level) + if not info: + label = values[ss.LABEL_IDX] + description = values[ss.DESCRIPTION_IDX] + if has_notes: + notes = values[ss.NOTES_IDX] + info = True + # point + elif etype in mdef.point_type_info: + self.set_point(ws, row, values, level) + else: + raise Exception('Unknown element type: %s' % etype) + elif values[ss.NAME_IDX]: + # symbol - has name and value with no type + if values[ss.VALUE_IDX] is not None and values[ss.VALUE_IDX] != '': + self.set_symbol(ws, row, values) + # comment - no name, value, or type + elif values[0]: + self.set_comment(ws, row, values) + row += 1 + + if self.ws_models is not None: + row = self.ws_models.max_row + 1 + self.set_cell(self.ws_models, row, 1, str(mid), 'suns_entry') + cell = self.set_cell(self.ws_models, row, 2, label, 'suns_hyper') + cell.hyperlink = '#%s!%s' % (str(mid), 'A1') + self.set_cell(self.ws_models, row, 3, description, 'suns_text') + + def to_xlsx(self, model_def): + mid = model_def[mdef.ID] + spreadsheet = ss.to_spreadsheet(model_def) + self.spreadsheet_to_xlsx(mid, spreadsheet) + +except: + # provide indication the openpyxl library not available + class ModelWorkbook(object): + def __init__(self, filename=None, model_dir=None, license_summary=False): + raise Exception('openpyxl library not installed, it is required for working with .xlsx files') + +if __name__ == "__main__": + pass diff --git a/sunspec2/modbus/client.py b/sunspec2/modbus/client.py index 17387ac..b294a9c 100644 --- a/sunspec2/modbus/client.py +++ b/sunspec2/modbus/client.py @@ -31,6 +31,10 @@ modbus_rtu_clients = {} +# Reference for SVP driver +MAPPED = 'Mapped SunSpec Device' +RTU = 'Modbus RTU' +TCP = 'Modbus TCP' class SunSpecModbusClientError(Exception): pass @@ -247,8 +251,10 @@ def scan(self, progress=None, delay=None, connect=False): class SunSpecModbusClientDeviceTCP(SunSpecModbusClientDevice): def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None, + tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False, max_count=modbus_client.REQ_COUNT_MAX, test=False, model_class=SunSpecModbusClientModel): SunSpecModbusClientDevice.__init__(self, model_class=model_class) + self.slave_id = slave_id self.ipaddr = ipaddr self.ipport = ipport @@ -257,9 +263,16 @@ def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx self.socket = None self.trace_func = trace_func self.max_count = max_count + self.tls = tls + self.cafile = cafile + self.certfile = certfile + self.keyfile = keyfile + self.insecure_skip_tls_verify = insecure_skip_tls_verify self.client = modbus_client.ModbusClientTCP(slave_id=slave_id, ipaddr=ipaddr, ipport=ipport, timeout=timeout, ctx=ctx, trace_func=trace_func, + tls=tls, cafile=cafile, certfile=certfile, keyfile=keyfile, + insecure_skip_tls_verify=insecure_skip_tls_verify, max_count=modbus_client.REQ_COUNT_MAX, test=test) if self.client is None: raise SunSpecModbusClientError('No modbus tcp client set for device') diff --git a/sunspec2/modbus/modbus.py b/sunspec2/modbus/modbus.py index cc7679f..887ed5d 100644 --- a/sunspec2/modbus/modbus.py +++ b/sunspec2/modbus/modbus.py @@ -20,6 +20,10 @@ import socket import struct import serial +try: + import ssl +except Exception as e: + print('Missing ssl python package: %s' % e) PARITY_NONE = 'N' PARITY_EVEN = 'E' @@ -428,7 +432,41 @@ def write(self, slave_id, addr, data, trace_func=None, max_count=REQ_COUNT_MAX): class ModbusClientTCP(object): + """Provides access to a Modbus TCP device. + + Parameters: + slave_id : + Modbus slave id. + ipaddr : + IP address string. + ipport : + IP port. + timeout : + Modbus request timeout in seconds. Fractional seconds are permitted such as .5. + ctx : + Context variable to be used by the object creator. Not used by the modbus module. + trace_func : + Trace function to use for detailed logging. No detailed logging is perform is a trace function is + not supplied. + tls : + Use TLS (Modbus/TCP Security). Defaults to `tls=False`. + cafile : + Path to certificate authority (CA) certificate to use for validating server certificates. + Only used if `tls=True`. + certfile : + Path to client TLS certificate to use for client authentication. Only used if `tls=True`. + keyfile : + Path to client TLS key to use for client authentication. Only used if `tls=True`. + insecure_skip_tls_verify : + Skip verification of server TLS certificate. Only used if `tls=True`. + max_count : + Maximum register count for a single Modbus request. + test : + Use test socket. If True use the fake socket module for network communications. + """ + def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None, + tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False, max_count=REQ_COUNT_MAX, test=False): self.slave_id = slave_id self.ipaddr = ipaddr @@ -437,6 +475,11 @@ def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx self.ctx = ctx self.socket = None self.trace_func = trace_func + self.tls = tls + self.cafile = cafile + self.certfile = certfile + self.keyfile = keyfile + self.tls_verify = not insecure_skip_tls_verify self.max_count = max_count if ipport is None: @@ -465,6 +508,14 @@ def connect(self, timeout=None): try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.settimeout(timeout) + + if self.tls: + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=self.cafile) + context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile) + context.check_hostname = self.tls_verify + + self.socket = context.wrap_socket(self.socket, server_side=False, server_hostname=self.ipaddr) + self.socket.connect((self.ipaddr, self.ipport)) except Exception as e: raise ModbusClientError('Connection error: %s' % str(e)) From c1e4ea2ee0dd22a1de46ba4afb441a362e140321 Mon Sep 17 00:00:00 2001 From: Johnson Date: Thu, 1 Oct 2020 19:25:30 -0600 Subject: [PATCH 2/2] build cleanup --- .gitignore | 2 + build/lib/sunspec2/__init__.py | 2 - build/lib/sunspec2/device.py | 668 ---- build/lib/sunspec2/file/__init__.py | 0 build/lib/sunspec2/file/client.py | 97 - build/lib/sunspec2/mb.py | 341 -- build/lib/sunspec2/mdef.py | 399 --- build/lib/sunspec2/modbus/__init__.py | 0 build/lib/sunspec2/modbus/client.py | 379 --- build/lib/sunspec2/modbus/modbus.py | 717 ----- build/lib/sunspec2/smdx.py | 372 --- build/lib/sunspec2/spreadsheet.py | 457 --- build/lib/sunspec2/tests/__init__.py | 0 build/lib/sunspec2/tests/mock_port.py | 43 - build/lib/sunspec2/tests/mock_socket.py | 37 - .../lib/sunspec2/tests/test_data/__init__.py | 0 .../sunspec2/tests/test_data/device_1547.json | 612 ---- .../lib/sunspec2/tests/test_data/smdx_304.csv | 8 - .../sunspec2/tests/test_data/wb_701-705.xlsx | Bin 34344 -> 0 bytes build/lib/sunspec2/tests/test_device.py | 2845 ----------------- build/lib/sunspec2/tests/test_file_client.py | 2668 ---------------- build/lib/sunspec2/tests/test_mb.py | 221 -- build/lib/sunspec2/tests/test_mdef.py | 307 -- .../lib/sunspec2/tests/test_modbus_client.py | 731 ----- .../lib/sunspec2/tests/test_modbus_modbus.py | 206 -- build/lib/sunspec2/tests/test_smdx.py | 250 -- build/lib/sunspec2/tests/test_spreadsheet.py | 591 ---- build/lib/sunspec2/tests/test_xlsx.py | 910 ------ build/lib/sunspec2/xlsx.py | 387 --- 29 files changed, 2 insertions(+), 13248 deletions(-) delete mode 100644 build/lib/sunspec2/__init__.py delete mode 100644 build/lib/sunspec2/device.py delete mode 100644 build/lib/sunspec2/file/__init__.py delete mode 100644 build/lib/sunspec2/file/client.py delete mode 100644 build/lib/sunspec2/mb.py delete mode 100644 build/lib/sunspec2/mdef.py delete mode 100644 build/lib/sunspec2/modbus/__init__.py delete mode 100644 build/lib/sunspec2/modbus/client.py delete mode 100644 build/lib/sunspec2/modbus/modbus.py delete mode 100644 build/lib/sunspec2/smdx.py delete mode 100644 build/lib/sunspec2/spreadsheet.py delete mode 100644 build/lib/sunspec2/tests/__init__.py delete mode 100644 build/lib/sunspec2/tests/mock_port.py delete mode 100644 build/lib/sunspec2/tests/mock_socket.py delete mode 100644 build/lib/sunspec2/tests/test_data/__init__.py delete mode 100644 build/lib/sunspec2/tests/test_data/device_1547.json delete mode 100644 build/lib/sunspec2/tests/test_data/smdx_304.csv delete mode 100644 build/lib/sunspec2/tests/test_data/wb_701-705.xlsx delete mode 100644 build/lib/sunspec2/tests/test_device.py delete mode 100644 build/lib/sunspec2/tests/test_file_client.py delete mode 100644 build/lib/sunspec2/tests/test_mb.py delete mode 100644 build/lib/sunspec2/tests/test_mdef.py delete mode 100644 build/lib/sunspec2/tests/test_modbus_client.py delete mode 100644 build/lib/sunspec2/tests/test_modbus_modbus.py delete mode 100644 build/lib/sunspec2/tests/test_smdx.py delete mode 100644 build/lib/sunspec2/tests/test_spreadsheet.py delete mode 100644 build/lib/sunspec2/tests/test_xlsx.py delete mode 100644 build/lib/sunspec2/xlsx.py diff --git a/.gitignore b/.gitignore index bebd3f8..206e3a5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ venv .pytest_cache/ __pycache__/ +build/ + diff --git a/build/lib/sunspec2/__init__.py b/build/lib/sunspec2/__init__.py deleted file mode 100644 index b60550d..0000000 --- a/build/lib/sunspec2/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# pySunSpec version -VERSION = '1.0.0' diff --git a/build/lib/sunspec2/device.py b/build/lib/sunspec2/device.py deleted file mode 100644 index 6661909..0000000 --- a/build/lib/sunspec2/device.py +++ /dev/null @@ -1,668 +0,0 @@ -import json -import math -from collections import OrderedDict -import os -import sunspec2.mdef as mdef -import sunspec2.smdx as smdx -import sunspec2.mb as mb - - -class ModelError(Exception): - pass - - -this_dir, this_filename = os.path.split(__file__) -models_dir = os.path.join(this_dir, 'models') - -model_defs_path = ['.', models_dir] -model_path_options = ['.', 'json', 'smdx'] - - -def get_model_defs_path(): - return model_defs_path - - -def set_model_defs_path(path_list): - if not isinstance(path_list, list): - raise mdef.ModelDefinitionError('Invalid path list type, path list is not a list') - global model_defs_path - model_defs_path = path_list - - -def get_model_info(model_id): - try: - glen = 0 - model_def = get_model_def(model_id) - gdef = model_def.get(mdef.GROUP) - # check if groups have a count point - has_group_count = check_group_count(gdef) - # if group has count point, compute the length of top-level points - if has_group_count: - points = gdef.get(mdef.POINTS) - if points: - for pdef in points: - info = mb.point_type_info.get(pdef[mdef.TYPE]) - plen = pdef.get(mdef.SIZE, None) - if plen is None: - glen += info.len - except: - raise - - return (model_def, has_group_count, glen) - - -def check_group_count(gdef): - has_group_count = (gdef.get(mdef.COUNT) is not None) - if not has_group_count: - groups = gdef.get(mdef.GROUPS) - if groups: - for g in groups: - has_group_count = check_group_count(g) - if has_group_count: - break - return has_group_count - - -def get_model_def(model_id, mapping=True): - try: - model_id = int(model_id) - except: - raise mdef.ModelDefinitionError('Invalid model id: %s' % model_id) - - model_def_file_json = mdef.to_json_filename(model_id) - model_def_file_smdx = smdx.to_smdx_filename(model_id) - model_def = None - for path in model_defs_path: - # look in directory, then json/, then smdx/ - for path_option in model_path_options: - try: - model_def = mdef.from_json_file(os.path.join(path, path_option, model_def_file_json)) - except FileNotFoundError: - pass - except Exception as e: - raise mdef.ModelDefinitionError('Error loading model definition for model %s: %s' % - (model_id, str(e))) - - if model_def is None: - try: - model_def = smdx.from_smdx_file(os.path.join(path, path_option, model_def_file_smdx)) - except FileNotFoundError: - pass - except Exception as e: - raise mdef.ModelDefinitionError('Error loading model definition for model %s: %s' % - (model_id, str(e))) - - if model_def is not None: - if mapping: - add_mappings(model_def[mdef.GROUP]) - return model_def - raise mdef.ModelDefinitionError('Model definition not found for model %s\nLooking in: %s' % - (model_id, os.path.join(path, path_option, model_def_file_json))) - - -# add id mapping for points and groups for more efficient lookup by id -def add_mappings(group_def): - point_defs = {} - group_defs = {} - - points = group_def.get(mdef.POINTS, None) - if points: - for p in group_def[mdef.POINTS]: - point_defs[p[mdef.NAME]] = p - - groups = group_def.get(mdef.GROUPS, None) - if groups: - for g in group_def[mdef.GROUPS]: - group_defs[g[mdef.NAME]] = g - add_mappings(g) - - group_def['point_defs'] = point_defs - group_def['group_defs'] = group_defs - - -class Point(object): - def __init__(self, pdef=None, model=None, group=None, model_offset=0, data=None, data_offset=0): - self.model = model # model object containing the point - self.group = group - self.pdef = pdef # point definition - self.len = 0 # mb register len of point - self.info = None # point def info - self.offset = model_offset # mb register offset from beginning of the model - self._value = None # value - self.dirty = False # value has been changed without being written - self.sf = None # scale factor point name - self.sf_value = None # value of scale factor - self.sf_required = False # point has a scale factor - if pdef: - self.sf_required = (pdef.get(mdef.SF) is not None) - if self.sf_required: - sf = self.pdef.get(mdef.SF) - try: - self.sf_value = int(sf) - except ValueError: - self.sf = sf - - self.info = mb.point_type_info.get(pdef[mdef.TYPE]) - plen = pdef.get(mdef.SIZE, None) - self.len = self.info.len - if plen is not None: - self.len = int(plen) - - if data is not None: - self._set_data(data=data, offset=data_offset) - - def __str__(self): - return self.disp() - - def disp(self, indent=None): - if indent is None: - indent = '' - return '%s%s: %s\n' % (indent, self.pdef[mdef.NAME], self.value) - - def _set_data(self, data=None, offset=0): - if isinstance(data, (bytes, bytearray)): - byte_offset = offset * 2 - if byte_offset < len(data): - self.set_mb(data=data[byte_offset:], dirty=False) - elif isinstance(data, dict): - value = data.get(self.pdef[mdef.NAME]) - if value is not None: - self.set_value(data=value) - - def resolve_sf(self): - pass - - @property - def value(self): - return self.get_value() - - @value.setter - def value(self, v): - self.set_value(v, dirty=True) - - @property - def cvalue(self): - return self.get_value(computed=True) - - @cvalue.setter - def cvalue(self, v): - self.set_value(v, computed=True, dirty=True) - - def get_value(self, computed=False): - v = self._value - if computed and v is not None: - if self.sf_required: - if self.sf_value is None: - if self.sf: - sf = self.group.points.get(self.sf) - if sf is None: - sf = self.model.points.get(self.sf) - if sf is not None: - self.sf_value = sf.value - else: - raise ModelError('Scale factor %s for point %s not found' % (self.sf, self.pdef['name'])) - if self.sf_value: - sfv = self.sf_value - if sfv: - v = v * math.pow(10, sfv) - return v - - def set_value(self, data=None, computed=False, dirty=None): - v = data - if dirty is not None: - self.dirty = dirty - if computed: - if self.sf_required: - if self.sf_value is None: - if self.sf: - sf = self.group.points.get(self.sf) - if sf is None: - sf = self.model.points.get(self.sf) - if sf is not None: - self.sf_value = sf.value - if sf.value is not None: - self.sf_value = sf.value - else: - raise ModelError('SF field %s value not initialized for point %s' % - (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: - self._value = int(round(float(v), abs(self.sf_value)) / math.pow(10, self.sf_value)) - else: - self._value = v - else: - self._value = v - - def get_mb(self, computed=False): - v = self._value - data = None - if computed and v is not None: - if self.sf_required: - if self.sf_value is None: - if self.sf: - sf = self.group.points.get(self.sf) - if sf is None: - sf = self.model.points.get(self.sf) - if sf is not None: - self.sf_value = sf.value - else: - raise ModelError('Scale factor %s for point %s not found' % (self.sf, self.pdef['name'])) - if self.sf_value: - sfv = self.sf_value - if sfv: - v = int(v * math.pow(10, sfv)) - data = self.info.to_data(v, (int(self.len) * 2)) - 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)) - return data - - def set_mb(self, data=None, computed=False, dirty=None): - try: - mb_len = self.len - # if not enough data, do not set but consume the data - if len(data) < mb_len: - return len(data) - self.set_value(self.info.data_to(data[:mb_len * 2]), computed=computed, dirty=dirty) - if not self.info.is_impl(self.value): - self.set_value(None) - self.sf_value = None - except Exception as e: - self.model.add_error('Error setting value for %s: %s' % (self.pdef[mdef.NAME], str(e))) - return mb_len - - -class Group(object): - def __init__(self, gdef=None, model=None, model_offset=0, group_len=0, data=None, data_offset=0, group_class=None, - point_class=Point, index=None): - self.gdef = gdef - self.model = model - self.gname = None - self.offset = model_offset - self.len = group_len - self.points = OrderedDict() - self.groups = OrderedDict() - self.points_len = 0 - self.group_class = group_class - self.index = index - - if group_class is None: - self.group_class = self.__class__ - - if gdef is not None: - self.gname = gdef[mdef.NAME] - self.gdef = gdef - - # initialize points and point values, if present - 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 - # initialize groups - groups = self.gdef.get(mdef.GROUPS) - if groups: - for gdef in groups: - gdata = self._group_data(data=data, name=gdef[mdef.NAME]) - if gdef.get(mdef.COUNT) is not None: - g = self._init_repeating_group(gdef=gdef, model_offset=model_offset, data=gdata, - data_offset=data_offset) - group_count = len(g) - if group_count: - self.groups[gdef[mdef.NAME]] = g - glen = g[0].len * group_count - model_offset += glen - data_offset += glen - else: - g = self.group_class(gdef, model=self.model, model_offset=model_offset, data=gdata, - data_offset=data_offset) - self.groups[gdef[mdef.NAME]] = g - model_offset += g.len - data_offset += g.len - mlen = model_offset - self.offset - if self.len: - if self.len + 2 != mlen: - self.model.add_error('Model length %s not equal to calculated model length %s for model %s' % - (self.len + 2, mlen, self.model.model_id )) - self.len = mlen - - len_point = self.points.get('L') - if len_point: - len_point.set_value(self.len - 2) - - id_point = self.points.get('ID') - if id_point: - id_val = id_point.pdef.get('value') - if id_val: - id_point.set_value(id_point.pdef['value']) - - def __getattr__(self, attr): - v = self.points.get(attr) - if v is None: - v = self.groups.get(attr) - if v is None: - raise AttributeError("%s object has no attribute %s" % (self.group_class.__name__, attr)) - return v - - def __str__(self): - return self.disp() - - def disp(self, indent=None): - if indent is None: - indent = '' - if self.index is not None: - index = '(%s)' % self.index - else: - index = '' - s = '%s%s%s:\n' % (indent, self.gdef[mdef.NAME], index) - - indent += ' ' - for k, p in self.points.items(): - s += p.disp(indent) - - for k, g in self.groups.items(): - if isinstance(g, list): - for i in range(len(g)): - s += g[i].disp(indent=indent) - else: - s += g.disp(indent) - - return s - - def _group_data(self, data=None, name=None, index=None): - if isinstance(data, dict): - data = data.get(name) - elif isinstance(data, list): - if index is not None and len(data) > index: - data = data[index] - else: - data = None - return data - - # check group count in dict data - def _get_data_group_count(self, data=None): - if isinstance(data, list): - return len(data) - - def _init_repeating_group(self, gdef=None, model_offset=None, data=None, data_offset=0): - groups = [] - # get group count as a constant - count = None - try: - count = int(gdef[mdef.COUNT]) - except ValueError: - pass - except AttributeError: - self.model.add_error('Count definition %s missing for group %s' % (gdef[mdef.COUNT], gdef[mdef.NAME])) - if count is None: - # get count as model point - count_attr = getattr(self.model, gdef[mdef.COUNT], None) - if count_attr is None: - raise ModelError('Count field %s undefined for group %s' % (gdef[mdef.COUNT], gdef[mdef.NAME])) - count = count_attr.value - if count is None: - raise ModelError('Count field %s value not initialized for group %s ' % - (gdef[mdef.COUNT], gdef[mdef.NAME])) - - data_group_count = self._get_data_group_count(data=data) - model_len = self.model.len - if model_len <= self.model.points_len: - # if legacy model definition but it is defined in format that number of groups are known, use that count - # to avoid having to figure out the length in the model data - if count == 0 and data_group_count: - count = data_group_count - - # allocate the group entries if the count is available - if count > 0: - for i in range(count): - gdata = self._group_data(data=data, index=i) - g = self.group_class(gdef=gdef, model=self.model, model_offset=model_offset, data=gdata, - data_offset=data_offset, index=i+1) - model_offset += g.len - data_offset += g.len - groups.append(g) - elif count == 0: - data_group_count = self._get_data_group_count(data=data) - # legacy model definition - need to calculate repeating count by model length - # 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]) - 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 - # count is model.len-model.points_len/group_points_len - # (ID and L points are not included in model length) - repeating_len = model_len - (self.model.points_len - 2) - if repeating_len > 0: - remaining = repeating_len % group_points_len - if remaining != 0: - raise ModelError('Repeating group count not consistent with model length for model %s,' - 'model repeating len = %s, model repeating group len = %s' % - (self.model._id, repeating_len, group_points_len)) - - count = int(repeating_len / group_points_len) - if count > 0: - groups.append(g) - 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, - data_offset=data_offset, index=i+2) - model_offset += g.len - data_offset += g.len - groups.append(g) - return groups - - def get_dict(self, computed=False): - d = {} - for pid, p in self.points.items(): - d[pid] = p.get_value(computed=computed) - for gid, group in self.groups.items(): - if isinstance(group, list): - glist = [] - for g in group: - glist.append(g.get_dict(computed=computed)) - d[gid] = glist - else: - d[gid] = group.get_dict(computed=computed) - return d - - def set_dict(self, data=None, computed=False, dirty=None): - groups = [] - group_def = self.gdef - for k, v in data.items(): - if k in group_def['point_defs']: - self.points[k].set_value(data=v, computed=computed, dirty=dirty) - elif k in group_def['group_defs']: - # write points first as group initialization may depend on point value for group counts - groups.append(k) - for k in groups: - if isinstance(self.groups[k], list): - i = 0 - for rg in self.groups[k]: - rg.set_dict(data[k][i], computed=computed, dirty=dirty) - i += 1 - else: - self.groups[k].set_dict(data[k], computed=computed, dirty=dirty) - - def get_json(self, computed=False): - return json.dumps(self.get_dict(computed=computed)) - - def set_json(self, data=None, computed=False, dirty=None): - if data is not None: - d = json.loads(data) - self.set_dict(d, computed=computed, dirty=dirty) - - def get_mb(self, computed=False): - data = bytearray() - for pid, point in self.points.items(): - data.extend(point.get_mb(computed=computed)) - for gid, group in self.groups.items(): - if isinstance(group, list): - for g in group: - data.extend(g.get_mb(computed=computed)) - else: - data.extend(group.get_mb(computed=computed)) - return bytes(data) - - def set_mb(self, data=None, computed=False, dirty=None): - if data: - data_len = len(data) - else: - data_len = 0 - offset = 0 - for pid, point in self.points.items(): - if data_len > offset: - mb_len = point.set_mb(data[offset:], computed=computed, dirty=dirty) - if mb_len is not None: - offset += mb_len * 2 - - for gid, group in self.groups.items(): - if isinstance(group, list): - for g in group: - if data_len > offset: - mb_len = g.set_mb(data[offset:], computed=computed, dirty=dirty) - if mb_len is not None: - offset += mb_len * 2 - else: - return None - else: - if data_len > offset: - mb_len = group.set_mb(data[offset:], computed=computed, dirty=dirty) - if mb_len is not None: - offset += mb_len * 2 - else: - return None - return int(offset/2) - - -class Model(Group): - def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, data=None, group_class=Group): - self.model_id = model_id - self.model_addr = model_addr - self.model_len = model_len - self.model_def = model_def - self.error_info = '' - self.mid = None - self.device = None - self.model = self - - gdef = None - try: - if self.model_def is None and model_id is not None: - self.model_def = get_model_def(model_id) - if self.model_def is not None: - gdef = self.model_def.get(mdef.GROUP) - except Exception as e: - self.add_error(str(e)) - - Group.__init__(self, gdef=gdef, model=self.model, model_offset=0, group_len=self.model_len, data=data, - data_offset=0, group_class=group_class) - - def add_error(self, error_info): - self.error_info = '%s%s\n' % (self.error_info, error_info) - - def get_dict(self, computed=False): - d = Group.get_dict(self, computed=computed) - d['mid'] = self.mid - d['error'] = self.error_info - d['model_id'] = self.model_id - return d - - -class Device(object): - def __init__(self, model_class=Model): - self.name = None - self.did = None - self.models = {} - self.model_list = [] - self.model_class = model_class - - def __getattr__(self, attr): - v = self.models.get(attr) - if v is None: - raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)) - return v - - def scan(self, data=None): - pass - - def add_model(self, model): - # add by model id - model_id = model.model_id - model_list = self.models.get(model_id) - if model_list is None: - model_list = [] - self.models[model_id] = model_list - 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) - - model.device = self - - def get_dict(self, computed=False): - d = {'name': self.name, 'did': self.did, 'models': []} - for m in self.model_list: - d['models'].append(m.get_dict(computed=computed)) - return d - - def get_json(self, computed=False): - return json.dumps(self.get_dict(computed=computed)) - - def get_mb(self, computed=False): - data = bytearray() - for m in self.model_list: - data.extend(m.get_mb(computed=computed)) - return bytes(data) - - def set_mb(self, data=None, computed=False, dirty=None): - if data: - data_len = len(data) - else: - data_len = 0 - offset = 0 - for m in self.model_list: - if data_len > offset: - mb_len = m.set_mb(data[offset:], dirty=dirty, computed=computed) - if mb_len is not None: - offset += mb_len * 2 - else: - return None - return int(offset/2) - - def find_mid(self, mid=None): - if mid is not None: - for m in self.model_list: - if m.mid == mid: - return m - - # assumes data should be used to create and initialize the models, does not currently update an initialized device - def _set_dict(self, data, computed=False, detail=False): - if self.model_list: - raise ModelError('Device already initialized') - self.name = data.get('name') - models = data.get('models') - for m in models: - if detail: - model_id = m['ID']['value'] - else: - model_id = m['ID'] - if model_id != mdef.END_MODEL_ID: - model_def = get_model_def(model_id) - model = Model(model_def=model_def, data=m, model_id=m['ID']) - self.add_model(model=model) diff --git a/build/lib/sunspec2/file/__init__.py b/build/lib/sunspec2/file/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/sunspec2/file/client.py b/build/lib/sunspec2/file/client.py deleted file mode 100644 index 398d5a2..0000000 --- a/build/lib/sunspec2/file/client.py +++ /dev/null @@ -1,97 +0,0 @@ -import json -import uuid -import sunspec2.mdef as mdef -import sunspec2.device as device -import sunspec2.mb as mb - - -class FileClientError(Exception): - pass - - -class FileClientPoint(device.Point): - - def read(self): - pass - - def write(self): - pass - - -class FileClientGroup(device.Group): - - def read(self): - pass - - def write(self): - pass - - -class FileClientModel(FileClientGroup): - def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, data=None, - group_class=FileClientGroup, point_class=FileClientPoint): - self.model_id = model_id - self.model_addr = model_addr - if model_len is None: - self.model_len = 0 - else: - self.model_len = model_len - self.model_def = model_def - self.error_info = '' - self.mid = None - self.device = None - self.model = self - - gdef = None - try: - if self.model_def is None and model_id is not None: - self.model_def = device.get_model_def(model_id) - if self.model_def is not None: - gdef = self.model_def.get(mdef.GROUP) - except Exception as e: - 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) - - def add_error(self, error_info): - self.error_info = '%s%s\n' % (self.error_info, error_info) - - -class FileClientDevice(device.Device): - def __init__(self, filename=None, addr=40002, model_class=FileClientModel): - device.Device.__init__(self, model_class=model_class) - self.did = str(uuid.uuid4()) - self.filename = filename - self.addr = addr - - def scan(self, data=None): - try: - if self.filename: - f = open(self.filename) - data = json.load(f) - - mid = 0 - addr = self.addr - for m in data.get('models'): - model_id = m.get('ID') - model_len = m.get('L') - if model_id != mb.SUNS_END_MODEL_ID: - model = self.model_class(model_id=model_id, model_addr=addr, model_len=model_len, model_def=None, - data=m) - model.mid = '%s_%s' % (self.did, mid) - mid += 1 - self.add_model(model) - addr += model.len - except Exception as e: - raise FileClientError(str(e)) - - def read(self): - return '' - - def write(self): - return - - -class FileClient(FileClientDevice): - pass diff --git a/build/lib/sunspec2/mb.py b/build/lib/sunspec2/mb.py deleted file mode 100644 index 93ce8c2..0000000 --- a/build/lib/sunspec2/mb.py +++ /dev/null @@ -1,341 +0,0 @@ -""" - Copyright (C) 2020 SunSpec Alliance - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. -""" - -import struct -import base64 -import collections - -import sunspec2.mdef as mdef - -SUNS_BASE_ADDR_DEFAULT = 40000 -SUNS_SUNS_LEN = 2 - -SUNS_UNIMPL_INT16 = -32768 -SUNS_UNIMPL_UINT16 = 0xffff -SUNS_UNIMPL_ACC16 = 0 -SUNS_UNIMPL_ENUM16 = 0xffff -SUNS_UNIMPL_BITFIELD16 = 0xffff -SUNS_UNIMPL_INT32 = -2147483648 -SUNS_UNIMPL_UINT32 = 0xffffffff -SUNS_UNIMPL_ACC32 = 0 -SUNS_UNIMPL_ENUM32 = 0xffffffff -SUNS_UNIMPL_BITFIELD32 = 0xffffffff -SUNS_UNIMPL_IPADDR = 0 -SUNS_UNIMPL_INT64 = -9223372036854775808 -SUNS_UNIMPL_UINT64 = 0xffffffffffffffff -SUNS_UNIMPL_ACC64 = 0 -SUNS_UNIMPL_IPV6ADDR = 0 -SUNS_UNIMPL_FLOAT32 = 0x7fc00000 -SUNS_UNIMPL_FLOAT64 = 0x7ff8000000000000 -SUNS_UNIMPL_STRING = '\0' -SUNS_UNIMPL_SUNSSF = -32768 -SUNS_UNIMPL_EUI48 = 'FF:FF:FF:FF:FF:FF' -SUNS_UNIMPL_PAD = 0 - -SUNS_BLOCK_FIXED = 'fixed' -SUNS_BLOCK_REPEATING = 'repeating' - -SUNS_END_MODEL_ID = 0xffff - -unimpl_value = { - mdef.TYPE_INT16: SUNS_UNIMPL_INT16, - mdef.TYPE_UINT16: SUNS_UNIMPL_UINT16, - mdef.TYPE_ACC16: SUNS_UNIMPL_ACC16, - mdef.TYPE_ENUM16: SUNS_UNIMPL_ENUM16, - mdef.TYPE_BITFIELD16: SUNS_UNIMPL_BITFIELD16, - mdef.TYPE_INT32: SUNS_UNIMPL_INT32, - mdef.TYPE_UINT32: SUNS_UNIMPL_UINT32, - mdef.TYPE_ACC32: SUNS_UNIMPL_ACC32, - mdef.TYPE_ENUM32: SUNS_UNIMPL_ENUM32, - mdef.TYPE_BITFIELD32: SUNS_UNIMPL_BITFIELD32, - mdef.TYPE_IPADDR: SUNS_UNIMPL_IPADDR, - mdef.TYPE_INT64: SUNS_UNIMPL_INT64, - mdef.TYPE_UINT64: SUNS_UNIMPL_UINT64, - mdef.TYPE_ACC64: SUNS_UNIMPL_ACC64, - mdef.TYPE_IPV6ADDR: SUNS_UNIMPL_IPV6ADDR, - mdef.TYPE_FLOAT32: SUNS_UNIMPL_FLOAT32, - mdef.TYPE_STRING: SUNS_UNIMPL_STRING, - mdef.TYPE_SUNSSF: SUNS_UNIMPL_SUNSSF, - mdef.TYPE_EUI48: SUNS_UNIMPL_EUI48, - mdef.TYPE_PAD: SUNS_UNIMPL_PAD -} - - -def create_unimpl_value(vtype, len=None): - value = unimpl_value.get(vtype) - if vtype is None: - raise ValueError('Unknown SunSpec value type: %s' % vtype) - if vtype == mdef.TYPE_STRING: - if len is not None: - return b'\0' * len - else: - raise ValueError('Unimplemented value creation for string requires a length') - elif vtype == mdef.TYPE_IPV6ADDR: - return b'\0' * 16 - return point_type_info[vtype][3](value) - - -class SunSpecError(Exception): - pass - - -""" -Functions to pack and unpack data string values -""" - - -def data_to_s16(data): - s16 = struct.unpack('>h', data[:2]) - return s16[0] - - -def data_to_u16(data): - u16 = struct.unpack('>H', data[:2]) - return u16[0] - - -def data_to_s32(data): - s32 = struct.unpack('>l', data[:4]) - return s32[0] - - -def data_to_u32(data): - u32 = struct.unpack('>L', data[:4]) - return u32[0] - - -def data_to_s64(data): - s64 = struct.unpack('>q', data[:8]) - return s64[0] - - -def data_to_u64(data): - u64 = struct.unpack('>Q', data[:8]) - return u64[0] - - -def data_to_ipv6addr(data): - value = False - for i in data: - if i != 0: - value = True - break - if value and len(data) == 16: - return '%02X%02X%02X%02X:%02X%02X%02X%02X:%02X%02X%02X%02X:%02X%02X%02X%02X' % ( - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]) - - -def data_to_eui48(data): - value = False - for i in data: - if i != 0: - value = True - break - if value and len(data) == 8: - return '%02X:%02X:%02X:%02X:%02X:%02X' % ( - data[2], data[3], data[4], data[5], data[6], data[7]) - - -def data_to_f32(data): - f = struct.unpack('>f', data[:4]) - if str(f[0]) != str(float('nan')): - return f[0] - - -def data_to_f64(data): - d = struct.unpack('>d', data[:8]) - if str(d[0]) != str(float('nan')): - return d[0] - - -def data_to_str(data): - data = str(data, 'utf-8') - - if len(data) > 1: - data = data[0] + data[1:].rstrip('\0') - return data - - -def s16_to_data(s16, len=None): - return struct.pack('>h', s16) - - -def u16_to_data(u16, len=None): - return struct.pack('>H', u16) - - -def s32_to_data(s32, len=None): - return struct.pack('>l', s32) - - -def u32_to_data(u32, len=None): - return struct.pack('>L', u32) - - -def s64_to_data(s64, len=None): - return struct.pack('>q', s64) - - -def u64_to_data(u64, len=None): - return struct.pack('>Q', u64) - - -def ipv6addr_to_data(addr, slen=None): - s = base64.b16decode(addr.replace(':', '')) - if slen is None: - slen = len(s) - return struct.pack(str(slen) + 's', s) - - -def f32_to_data(f, len=None): - return struct.pack('>f', f) - - -def f64_to_data(f, len=None): - return struct.pack('>d', f) - - -def str_to_data(s, slen=None): - if slen is None: - slen = len(s) - s = bytes(s, 'utf-8') - return struct.pack(str(slen) + 's', s) - - -def eui48_to_data(eui48): - return (b'\x00\x00' + base64.b16decode(eui48.replace(':', ''))) - - -def is_impl_int16(value): - return not value == SUNS_UNIMPL_INT16 - - -def is_impl_uint16(value): - return not value == SUNS_UNIMPL_UINT16 - - -def is_impl_acc16(value): - return not value == SUNS_UNIMPL_ACC16 - - -def is_impl_enum16(value): - return not value == SUNS_UNIMPL_ENUM16 - - -def is_impl_bitfield16(value): - return not value == SUNS_UNIMPL_BITFIELD16 - - -def is_impl_int32(value): - return not value == SUNS_UNIMPL_INT32 - - -def is_impl_uint32(value): - return not value == SUNS_UNIMPL_UINT32 - - -def is_impl_acc32(value): - return not value == SUNS_UNIMPL_ACC32 - - -def is_impl_enum32(value): - return not value == SUNS_UNIMPL_ENUM32 - - -def is_impl_bitfield32(value): - return not value == SUNS_UNIMPL_BITFIELD32 - - -def is_impl_ipaddr(value): - return not value == SUNS_UNIMPL_IPADDR - - -def is_impl_int64(value): - return not value == SUNS_UNIMPL_INT64 - - -def is_impl_uint64(value): - return not value == SUNS_UNIMPL_UINT64 - - -def is_impl_acc64(value): - return not value == SUNS_UNIMPL_ACC64 - - -def is_impl_ipv6addr(value): - if value: - return not value[0] == '\0' - return False - - -def is_impl_float32(value): - return (value == value) and (value != None) - - -def is_impl_float64(value): - return (value == value) and (value != None) - - -def is_impl_string(value): - if value: - return not value[0] == '\0' - return False - - -def is_impl_sunssf(value): - return not value == SUNS_UNIMPL_SUNSSF - - -def is_impl_eui48(value): - return not value == SUNS_UNIMPL_EUI48 - - -def is_impl_pad(value): - return True - - -PointInfo = collections.namedtuple('PointInfo', 'len is_impl data_to to_data to_type default') -point_type_info = { - mdef.TYPE_INT16: PointInfo(1, is_impl_int16, data_to_s16, s16_to_data, mdef.to_int, 0), - mdef.TYPE_UINT16: PointInfo(1, is_impl_uint16, data_to_u16, u16_to_data, mdef.to_int, 0), - mdef.TYPE_COUNT: PointInfo(1, is_impl_uint16, data_to_u16, u16_to_data, mdef.to_int, 0), - mdef.TYPE_ACC16: PointInfo(1, is_impl_acc16, data_to_u16, u16_to_data, mdef.to_int, 0), - mdef.TYPE_ENUM16: PointInfo(1, is_impl_enum16, data_to_u16, u16_to_data, mdef.to_int, 0), - mdef.TYPE_BITFIELD16: PointInfo(1, is_impl_bitfield16, data_to_u16, u16_to_data, mdef.to_int, 0), - mdef.TYPE_PAD: PointInfo(1, is_impl_pad, data_to_u16, u16_to_data, mdef.to_int, 0), - mdef.TYPE_INT32: PointInfo(2, is_impl_int32, data_to_s32, s32_to_data, mdef.to_int, 0), - mdef.TYPE_UINT32: PointInfo(2, is_impl_uint32, data_to_u32, u32_to_data, mdef.to_int, 0), - mdef.TYPE_ACC32: PointInfo(2, is_impl_acc32, data_to_u32, u32_to_data, mdef.to_int, 0), - mdef.TYPE_ENUM32: PointInfo(2, is_impl_enum32, data_to_u32, u32_to_data, mdef.to_int, 0), - mdef.TYPE_BITFIELD32: PointInfo(2, is_impl_bitfield32, data_to_u32, u32_to_data, mdef.to_int, 0), - mdef.TYPE_IPADDR: PointInfo(2, is_impl_ipaddr, data_to_u32, u32_to_data, mdef.to_int, 0), - mdef.TYPE_INT64: PointInfo(4, is_impl_int64, data_to_s64, s64_to_data, mdef.to_int, 0), - mdef.TYPE_UINT64: PointInfo(4, is_impl_uint64, data_to_u64, u64_to_data, mdef.to_int, 0), - mdef.TYPE_ACC64: PointInfo(4, is_impl_acc64, data_to_u64, u64_to_data, mdef.to_int, 0), - mdef.TYPE_IPV6ADDR: PointInfo(8, is_impl_ipv6addr, data_to_ipv6addr, ipv6addr_to_data, mdef.to_str, 0), - mdef.TYPE_FLOAT32: PointInfo(2, is_impl_float32, data_to_f32, f32_to_data, mdef.to_float, 0), - mdef.TYPE_FLOAT64: PointInfo(4, is_impl_float64, data_to_f64, f64_to_data, mdef.to_float, 0), - mdef.TYPE_STRING: PointInfo(None, is_impl_string, data_to_str, str_to_data, mdef.to_str, ''), - mdef.TYPE_SUNSSF: PointInfo(1, is_impl_sunssf, data_to_s16, s16_to_data, mdef.to_int, 0), - mdef.TYPE_EUI48: PointInfo(4, is_impl_eui48, data_to_eui48, eui48_to_data, mdef.to_str, 0) -} diff --git a/build/lib/sunspec2/mdef.py b/build/lib/sunspec2/mdef.py deleted file mode 100644 index bd9a561..0000000 --- a/build/lib/sunspec2/mdef.py +++ /dev/null @@ -1,399 +0,0 @@ -import json -import os - -''' -1. JSON is used for the native encoding of information model definitions. -2. JSON can be used to represent the values associated with information model points at a specific point in time. - -Python support for information models: -- Python model support is based on dictionaries and their afinity with JSON objects. - -Model instance notes: - -- If a model contains repeating groups, the group counts must be known to fully initialized the model. If fields are - accessed that depend on group counts that have not been initialized, a ModelError exception is generated. -- Points that have not been read or written contain a value of None. -- If a point that can not be changed from the initialized value is changed, a ModelError exception is generated. - -A model definition is represented as a dictionary using the constants defined in this file as the entry keys. - -A model definition is required to have a single top level group. - -A model dict - - must contain: 'id' and 'group' - -A group dict - - must contain: 'name', 'type', 'points' - - may contain: 'count', 'groups', 'label', 'description', 'notes', 'comments' - -A point dict - - must contain: 'name', 'type' - - may contain: 'count', 'size', 'sf', 'units', 'mandatory', 'access', 'symbols', 'label', 'description', 'notes', - 'comments' - -A symbol dict - - must contain: 'name', 'value' - - may contain: 'label', 'description', 'notes', 'comments' - -Example: - model_def = { - 'id': 123, - 'group': { - 'id': model_name, - 'groups': [], - 'points': [] - } -''' - -DEVICE = 'device' # device (device dict) ### currently not in the spec -MODEL = 'model' # model (model dict) ### currently not in the spec -GROUP = 'group' # top level model group (group dict) -GROUPS = 'groups' # groups in group (list of group dicts) -POINTS = 'points' # points in group (list of point dicts) - -ID = 'id' # id (int or str) -NAME = 'name' # name (str) -VALUE = 'value' # value (int, float, str) -COUNT = 'count' # instance count (int or str) - -TYPE = 'type' # point type (str of TYPE_XXX) -MANDATORY = 'mandatory' # point mandatory (str of MANDATORY_XXX) -ACCESS = 'access' # point access (str of ACCESS_XXX) -STATIC = 'static' # point value is static (str of STATIC_XXX) -SF = 'sf' # point scale factor (int) -UNITS = 'units' # point units (str) -SIZE = 'size' # point string length (int) - -LABEL = 'label' # label (str) -DESCRIPTION = 'desc' # description (str) -NOTES = 'notes' # notes (str) -DETAIL = 'detail' # detailed description (str) -SYMBOLS = 'symbols' # symbols (list of symbol dicts) -COMMENTS = 'comments' # comments (list of str) - -TYPE_GROUP = 'group' -TYPE_SYNC_GROUP = 'sync' - -TYPE_INT16 = 'int16' -TYPE_UINT16 = 'uint16' -TYPE_COUNT = 'count' -TYPE_ACC16 = 'acc16' -TYPE_ENUM16 = 'enum16' -TYPE_BITFIELD16 = 'bitfield16' -TYPE_PAD = 'pad' -TYPE_INT32 = 'int32' -TYPE_UINT32 = 'uint32' -TYPE_ACC32 = 'acc32' -TYPE_ENUM32 = 'enum32' -TYPE_BITFIELD32 = 'bitfield32' -TYPE_IPADDR = 'ipaddr' -TYPE_INT64 = 'int64' -TYPE_UINT64 = 'uint64' -TYPE_ACC64 = 'acc64' -TYPE_IPV6ADDR = 'ipv6addr' -TYPE_FLOAT32 = 'float32' -TYPE_FLOAT64 = 'float64' -TYPE_STRING = 'string' -TYPE_SUNSSF = 'sunssf' -TYPE_EUI48 = 'eui48' - -ACCESS_R = 'R' -ACCESS_RW = 'RW' - -MANDATORY_FALSE = 'O' -MANDATORY_TRUE = 'M' - -STATIC_FALSE = 'D' -STATIC_TRUE = 'S' - -MODEL_ID_POINT_NAME = 'ID' -MODEL_LEN_POINT_NAME = 'L' - -END_MODEL_ID = 65535 - -MODEL_DEF_EXT = '.json' - - -def to_int(x): - try: - return int(x, 0) - except TypeError: - return int(x) - - -def to_str(s): - return str(s) - - -def to_float(f): - try: - return float(f) - except ValueError: - return None - - -# valid model attributes -model_attr = {ID: {'type': int, 'mand': True}, GROUP: {'mand': True}, COMMENTS: {}} - -# valid group attributes -group_attr = {NAME: {'type': str, 'mand': True}, COUNT: {'type': [int, str]}, TYPE: {'mand': True}, - GROUPS: {}, POINTS: {'mand': True}, LABEL: {'type': str}, - DESCRIPTION: {'type': str}, NOTES: {'type': str}, COMMENTS: {}, DETAIL: {'type': str}} - -# valid point attributes -point_attr = {NAME: {'type': str, 'mand': True}, COUNT: {'type': int}, VALUE: {}, TYPE: {'mand': True}, - SIZE: {'type': int}, SF: {}, UNITS: {'type': str}, - ACCESS: {'type': str, 'values': ['R', 'RW'], 'default': 'R'}, - MANDATORY: {'type': str, 'values': ['O', 'M'], 'default': 'O'}, - STATIC: {'type': str, 'values': ['D', 'S'], 'default': 'D'}, - LABEL: {'type': str}, DESCRIPTION: {'type': str}, NOTES: {'type': str}, SYMBOLS: {}, COMMENTS: {}, - DETAIL: {'type': str}} - -# valid symbol attributes -symbol_attr = {NAME: {'type': str, 'mand': True}, VALUE: {'mand': True}, LABEL: {'type': str}, - DESCRIPTION: {'type': str}, NOTES: {'type': str}, COMMENTS: {}, DETAIL: {'type': str}} - -group_types = [TYPE_GROUP, TYPE_SYNC_GROUP] - -point_type_info = { - TYPE_INT16: {'len': 1, 'to_type': to_int, 'default': 0}, - TYPE_UINT16: {'len': 1, 'to_type': to_int, 'default': 0}, - TYPE_COUNT: {'len': 1, 'to_type': to_int, 'default': 0}, - TYPE_ACC16: {'len': 1, 'to_type': to_int, 'default': 0}, - TYPE_ENUM16: {'len': 1, 'to_type': to_int, 'default': 0}, - TYPE_BITFIELD16: {'len': 1, 'to_type': to_int, 'default': 0}, - TYPE_PAD: {'len': 1, 'to_type': to_int, 'default': 0}, - TYPE_INT32: {'len': 2, 'to_type': to_int, 'default': 0}, - TYPE_UINT32: {'len': 2, 'to_type': to_int, 'default': 0}, - TYPE_ACC32: {'len': 2, 'to_type': to_int, 'default': 0}, - TYPE_ENUM32: {'len': 2, 'to_type': to_int, 'default': 0}, - TYPE_BITFIELD32: {'len': 2, 'to_type': to_int, 'default': 0}, - TYPE_IPADDR: {'len': 2, 'to_type': to_int, 'default': 0}, - TYPE_INT64: {'len': 4, 'to_type': to_int, 'default': 0}, - TYPE_UINT64: {'len': 4, 'to_type': to_int, 'default': 0}, - TYPE_ACC64: {'len': 4, 'to_type': to_int, 'default': 0}, - TYPE_IPV6ADDR: {'len': 8, 'to_type': to_str, 'default': 0}, - TYPE_FLOAT32: {'len': 2, 'to_type': to_float, 'default': 0}, - TYPE_FLOAT64: {'len': 4, 'to_type': to_float, 'default': 0}, - TYPE_STRING: {'len': None, 'to_type': to_str, 'default': ''}, - TYPE_SUNSSF: {'len': 1, 'to_type': to_int, 'default': 0}, - TYPE_EUI48: {'len': 4, 'to_type': to_str, 'default': 0} -} - - -class ModelDefinitionError(Exception): - pass - - -def to_number_type(n): - if isinstance(n, str): - try: - n = int(n) - except ValueError: - try: - n = float(n) - except ValueError: - pass - return n - - -def validate_find_point(group, pname): - points = group.get(POINTS, list()) - for p in points: - pxname = p.get(NAME) - if pxname: - if p[NAME] == pname: - return p - - -def validate_attrs(element, attrs, result=''): - # check for unexpected attributes - for k in element: - if k not in attrs: - result += 'Unexpected model definition attribute: %s in %s\n' % (k, element.get(NAME)) - # check for missing attributes - for k, a in attrs.items(): - if k in element and element[k] is not None: - # check type if specified - t = a.get('type') - if isinstance(t, list): - if t and type(element[k]) not in t: - result += 'Unexpected type for model attribute %s, expected %s, found %s\n' % \ - (k, t, type(element[k])) - else: - if t and type(element[k]) != t: - result += 'Unexpected type for model attribute %s, expected %s, found %s\n' % (k, t, type(element[k])) - values = a.get('values') - if values and element[k] not in values: - result += 'Unexpected value for model attribute %s: %s\n' % (k, element[k]) - elif a.get('mand', False): - result += 'Mandatory attribute missing from model definition: %s\n' % k - return result - - -def validate_group_point_dup(group, result=''): - groups = group.get(GROUPS, list()) - for g in groups: - gname = g.get(NAME) - if gname: - count = 0 - for gx in groups: - gxname = gx.get(NAME) - if gxname: - if gx[NAME] == gname: - count += 1 - if count > 1: - result += 'Duplicate group id %s in group %s' % (gname, group[NAME]) - if validate_find_point(group, gname): - result += 'Duplicate group and point id %s in group %s' % (gname, group[NAME]) - else: - result += 'Mandatory %s attribute missing in group definition element\n' % (NAME) - points = group.get(POINTS, list()) - for p in points: - pname = p.get(NAME) - if pname: - count = 0 - for px in points: - pxname = px.get(NAME) - if pxname: - if px[NAME] == pname: - count += 1 - if count > 1: - result += 'Duplicate point id %s in group %s' % (pname, group[NAME]) - else: - result += 'Mandatory attribute missing in point definition element: %s\n' % (NAME) - return result - - -def validate_symbols(symbols, model_group, result=''): - for symbol in symbols: - result = validate_attrs(symbol, model_group, result) - return result - - -def validate_sf(point, sf, sf_groups, result=''): - found = False - if type(sf) == str: - for group in sf_groups: - p = validate_find_point(group, sf) - if p: - found = True - if p[TYPE] != TYPE_SUNSSF: - result += 'Scale factor %s for point %s is not scale factor type: %s\n' % (sf, point[NAME], p[TYPE]) - break - if not found: - result += 'Scale factor %s for point %s not found\n' % (sf, point[NAME]) - elif type(sf) == int: - if sf < - 10 or sf > 10: - result += 'Scale factor %s for point %s out of range\n' % (sf, point[NAME]) - else: - result += 'Scale factor %s for point %s has invalid type %s\n' % (sf, point[NAME], type(sf)) - return result - - -def validate_point_def(point, model_group, group, result=''): - # validate general point attributes - result = validate_attrs(point, point_attr, result) - # validate point type - ptype = point.get(TYPE) - if ptype not in point_type_info: - result += 'Unknown point type %s for point %s\n' % (ptype, point[NAME]) - # validate scale foctor, if present - sf = point.get(SF) - if sf: - result = validate_sf(point, sf, [model_group, group], result) - # validate symbols - symbols = point.get(SYMBOLS, list()) - result = validate_symbols(symbols, symbol_attr, result) - # check for duplicate symbols - for s in symbols: - sname = s.get(NAME) - if sname: - count = 0 - for sx in symbols: - if sx[NAME] == sname: - count += 1 - if count > 1: - result += 'Duplicate symbol id %s in point %s\n' % (sname, point[NAME]) - else: - result += 'Mandatory attribute missing in symbol definition element: %s\n' % (NAME) - return result - - -def validate_group_def(group, model_group, result=''): - # validate general group attributes - result = validate_attrs(group, group_attr, result) - # validate points - points = group.get(POINTS, list()) - for p in points: - result = validate_point_def(p, model_group, group, result) - # validate groups - groups = group.get(GROUPS, list()) - for g in groups: - result = validate_group_def(g, model_group, result) - # check for group and point duplicates - result = validate_group_point_dup(group, result) - return result - - -def validate_model_group_def(model_def, group, result=''): - # must contain ID and length points - points = group.get(POINTS) - if points: - if len(points) >= 2: - pname = points[0].get(NAME) - if pname != MODEL_ID_POINT_NAME: - result += "First point in top-level group must be %s, found: %s\n" % (MODEL_ID_POINT_NAME, pname) - if points[0].get(VALUE) != model_def.get(ID): - result += 'Model ID does not match top-level group ID: %s %s %s %s\n' % ( - model_def.get(ID), type(model_def.get(ID)), points[0].get(VALUE), type(points[0].get(VALUE))) - pname = points[1].get(NAME) - if pname != MODEL_LEN_POINT_NAME: - result += "Second point in top-level group must be %s, found: %s\n" % (MODEL_LEN_POINT_NAME, pname) - else: - result += "Top-level group must contain at least two points: %s and %s\n" % (MODEL_ID_POINT_NAME, - MODEL_LEN_POINT_NAME) - else: - result += 'Top-level group missing point definitions\n' - # perform normal group validation - result = validate_group_def(group, group, result) - return result - - -def validate_model_def(model_def, result=''): - result = validate_attrs(model_def, model_attr, result) - group = model_def.get(GROUP) - result = validate_model_group_def(model_def, group, result) - return result - - -def from_json_str(s): - return json.loads(s) - - -def from_json_file(filename): - f = open(filename) - model_def = json.load(f) - f.close() - return(model_def) - - -def to_json_str(model_def, indent=4): - return json.dumps(model_def, indent=indent, sort_keys=True) - - -def to_json_filename(model_id): - return 'model_%s%s' % (model_id, MODEL_DEF_EXT) - - -def to_json_file(model_def, filename=None, filedir=None, indent=4): - if filename is None: - filename = to_json_filename(model_def[ID]) - if filedir is not None: - filename = os.path.join(filedir, filename) - f = open(filename, 'w') - json.dump(model_def, f, indent=indent, sort_keys=True) - - -if __name__ == "__main__": - pass diff --git a/build/lib/sunspec2/modbus/__init__.py b/build/lib/sunspec2/modbus/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/sunspec2/modbus/client.py b/build/lib/sunspec2/modbus/client.py deleted file mode 100644 index b294a9c..0000000 --- a/build/lib/sunspec2/modbus/client.py +++ /dev/null @@ -1,379 +0,0 @@ -""" - Copyright (C) 2020 SunSpec Alliance - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. -""" - -import time -import uuid -import sunspec2.mdef as mdef -import sunspec2.device as device -import sunspec2.mb as mb -import sunspec2.modbus.modbus as modbus_client - -TEST_NAME = 'test_name' - -modbus_rtu_clients = {} - -# Reference for SVP driver -MAPPED = 'Mapped SunSpec Device' -RTU = 'Modbus RTU' -TCP = 'Modbus TCP' - -class SunSpecModbusClientError(Exception): - pass - - -class SunSpecModbusClientTimeout(SunSpecModbusClientError): - pass - - -class SunSpecModbusClientException(SunSpecModbusClientError): - pass - - -class SunSpecModbusClientPoint(device.Point): - - def read(self): - data = self.model.device.read(self.model.model_addr + self.offset, self.len) - self.set_mb(data=data, dirty=False) - - def write(self): - """Write the point to the physical device""" - - data = self.info.to_data(self.value, int(self.len) * 2) - model_addr = self.model.model_addr - point_offset = self.offset - addr = model_addr + point_offset - self.model.device.write(addr, data) - self.dirty = False - - -class SunSpecModbusClientGroup(device.Group): - - def read(self): - - data = self.model.device.read(self.model.model_addr + self.offset, self.len) - self.set_mb(data=data, dirty=False) - - def write(self): - - start_addr = next_addr = self.model.model_addr + self.offset - data = b'' - start_addr, next_addr, data = self.write_points(start_addr, next_addr, data) - if data: - self.model.device.write(start_addr, data) - - def write_points(self, start_addr=None, next_addr=None, data=None): - """ - Write all points that have been modified since the last write operation to the physical device - """ - - for name, point in self.points.items(): - model_addr = self.model.model_addr - point_offset = point.offset - point_addr = model_addr + point_offset - if data and (not point.dirty or point_addr != next_addr): - self.model.device.write(start_addr, data) - data = b'' - if point.dirty: - point_len = point.len - point_data = point.info.to_data(point.value, int(point_len) * 2) - if not data: - start_addr = point_addr - next_addr = point_addr + point_len - data += point_data - point.dirty = False - - for name, group in self.groups.items(): - if isinstance(group, list): - for g in group: - start_addr, next_addr, data = g.write_points(start_addr, next_addr, data) - else: - start_addr, next_addr, data = group.write_points(start_addr, next_addr, data) - - return start_addr, next_addr, data - - -class SunSpecModbusClientModel(SunSpecModbusClientGroup): - def __init__(self, model_id=None, model_addr=0, model_len=0, model_def=None, data=None, mb_device=None, - group_class=SunSpecModbusClientGroup, point_class=SunSpecModbusClientPoint): - self.model_id = model_id - self.model_addr = model_addr - self.model_len = model_len - self.model_def = model_def - self.error_info = '' - self.mid = None - self.device = mb_device - self.model = self - - gdef = None - try: - if self.model_def is None: - self.model_def = device.get_model_def(model_id) - if self.model_def is not None: - gdef = self.model_def.get(mdef.GROUP) - except Exception as e: - self.add_error(str(e)) - - SunSpecModbusClientGroup.__init__(self, gdef=gdef, model=self.model, model_offset=0, group_len=self.model_len, - data=data, data_offset=0, group_class=group_class, point_class=point_class) - - if self.model_len is not None: - self.len = self.model_len - - if self.model_len and self.len: - if self.model_len != self.len: - self.add_error('Model error: Discovered length %s does not match computed length %s' % - (self.model_len, self.len)) - - def add_error(self, error_info): - self.error_info = '%s%s\n' % (self.error_info, error_info) - - -class SunSpecModbusClientDevice(device.Device): - def __init__(self, model_class=SunSpecModbusClientModel): - device.Device.__init__(self, model_class=model_class) - self.did = str(uuid.uuid4()) - self.retry_count = 2 - self.base_addr_list = [40000, 0, 50000] - self.base_addr = None - - def connect(self): - pass - - def disconnect(self): - pass - - def close(self): - pass - - # must be overridden by Modbus protocol implementation - def read(self, addr, count): - return '' - - # must be overridden by Modbus protocol implementation - def write(self, addr, data): - return - - def scan(self, progress=None, delay=None, connect=False): - """Scan all the models of the physical device and create the - corresponding model objects within the device object based on the - SunSpec model definitions. - """ - - data = error = '' - connected = False - - if connect: - self.connect() - connected = True - - if delay is not None: - time.sleep(delay) - - if self.base_addr is None: - for addr in self.base_addr_list: - try: - data = self.read(addr, 3) - if data[:4] == b'SunS': - self.base_addr = addr - break - else: - error = 'Device responded - not SunSpec register map' - except SunSpecModbusClientError as e: - if not error: - error = str(e) - - if delay is not None: - time.sleep(delay) - - if self.base_addr is not None: - model_id = mb.data_to_u16(data[4:6]) - addr = self.base_addr + 2 - - mid = 0 - while model_id != mb.SUNS_END_MODEL_ID: - # read model and model len separately due to some devices not supplying - # count for the end model id - data = self.read(addr + 1, 1) - if data and len(data) == 2: - if progress is not None: - cont = progress('Scanning model %s' % (model_id)) - if not cont: - raise SunSpecModbusClientError('Device scan terminated') - model_len = mb.data_to_u16(data) - - # read model data - model_data = self.read(addr, model_len + 2) - model = self.model_class(model_id=model_id, model_addr=addr, model_len=model_len, data=model_data, - mb_device=self) - model.mid = '%s_%s' % (self.did, mid) - mid += 1 - self.add_model(model) - - addr += model_len + 2 - data = self.read(addr, 1) - if data and len(data) == 2: - model_id = mb.data_to_u16(data) - else: - break - else: - break - - if delay is not None: - time.sleep(delay) - - else: - if not error: - error = 'Unknown error' - raise SunSpecModbusClientError(error) - - if connected: - self.disconnect() - - -class SunSpecModbusClientDeviceTCP(SunSpecModbusClientDevice): - def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None, - tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False, - max_count=modbus_client.REQ_COUNT_MAX, test=False, model_class=SunSpecModbusClientModel): - SunSpecModbusClientDevice.__init__(self, model_class=model_class) - - self.slave_id = slave_id - self.ipaddr = ipaddr - self.ipport = ipport - self.timeout = timeout - self.ctx = ctx - self.socket = None - self.trace_func = trace_func - self.max_count = max_count - self.tls = tls - self.cafile = cafile - self.certfile = certfile - self.keyfile = keyfile - self.insecure_skip_tls_verify = insecure_skip_tls_verify - - self.client = modbus_client.ModbusClientTCP(slave_id=slave_id, ipaddr=ipaddr, ipport=ipport, timeout=timeout, - ctx=ctx, trace_func=trace_func, - tls=tls, cafile=cafile, certfile=certfile, keyfile=keyfile, - insecure_skip_tls_verify=insecure_skip_tls_verify, - max_count=modbus_client.REQ_COUNT_MAX, test=test) - if self.client is None: - raise SunSpecModbusClientError('No modbus tcp client set for device') - - def connect(self): - self.client.connect() - - def disconnect(self): - self.client.disconnect() - - def read(self, addr, count, op=modbus_client.FUNC_READ_HOLDING): - return self.client.read(addr, count, op) - - def write(self, addr, data): - return self.client.write(addr, data) - - -class SunSpecModbusClientDeviceRTU(SunSpecModbusClientDevice): - """Provides access to a Modbus RTU device. - Parameters: - slave_id : - Modbus slave id. - name : - Name of the serial port such as 'com4' or '/dev/ttyUSB0'. - baudrate : - Baud rate such as 9600 or 19200. Default is 9600 if not specified. - parity : - Parity. Possible values: - :const:`sunspec.core.modbus.client.PARITY_NONE`, - :const:`sunspec.core.modbus.client.PARITY_EVEN` Defaulted to - :const:`PARITY_NONE`. - timeout : - Modbus request timeout in seconds. Fractional seconds are permitted - such as .5. - ctx : - Context variable to be used by the object creator. Not used by the - modbus module. - trace_func : - Trace function to use for detailed logging. No detailed logging is - perform is a trace function is not supplied. - max_count : - Maximum register count for a single Modbus request. - Raises: - SunSpecModbusClientError: Raised for any general modbus client error. - SunSpecModbusClientTimeoutError: Raised for a modbus client request timeout. - SunSpecModbusClientException: Raised for an exception response to a modbus - client request. - """ - - def __init__(self, slave_id, name, baudrate=None, parity=None, timeout=None, ctx=None, trace_func=None, - max_count=modbus_client.REQ_COUNT_MAX, model_class=SunSpecModbusClientModel): - # test if this super class init is needed - SunSpecModbusClientDevice.__init__(self, model_class=model_class) - self.slave_id = slave_id - self.name = name - self.client = None - self.ctx = ctx - self.trace_func = trace_func - self.max_count = max_count - - self.client = modbus_client.modbus_rtu_client(name, baudrate, parity) - if self.client is None: - raise SunSpecModbusClientError('No modbus rtu client set for device') - self.client.add_device(self.slave_id, self) - - if timeout is not None and self.client.serial is not None: - self.client.serial.timeout = timeout - self.client.serial.writeTimeout = timeout - - def open(self): - self.client.open() - - def close(self): - """Close the device. Called when device is not longer in use. - """ - - if self.client: - self.client.remove_device(self.slave_id) - - def read(self, addr, count, op=modbus_client.FUNC_READ_HOLDING): - """Read Modbus device registers. - Parameters: - addr : - Starting Modbus address. - count : - Read length in Modbus registers. - op : - Modbus function code for request. - Returns: - Byte string containing register contents. - """ - - return self.client.read(self.slave_id, addr, count, op=op, trace_func=self.trace_func, max_count=self.max_count) - - def write(self, addr, data): - """Write Modbus device registers. - Parameters: - addr : - Starting Modbus address. - count : - Byte string containing register contents. - """ - - return self.client.write(self.slave_id, addr, data, trace_func=self.trace_func, max_count=self.max_count) diff --git a/build/lib/sunspec2/modbus/modbus.py b/build/lib/sunspec2/modbus/modbus.py deleted file mode 100644 index 887ed5d..0000000 --- a/build/lib/sunspec2/modbus/modbus.py +++ /dev/null @@ -1,717 +0,0 @@ -""" - Copyright (C) 2018 SunSpec Alliance - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. -""" - -import socket -import struct -import serial -try: - import ssl -except Exception as e: - print('Missing ssl python package: %s' % e) - -PARITY_NONE = 'N' -PARITY_EVEN = 'E' - -REQ_COUNT_MAX = 125 - -FUNC_READ_HOLDING = 3 -FUNC_READ_INPUT = 4 -FUNC_WRITE_MULTIPLE = 16 - -TEST_NAME = 'test_name' - -modbus_rtu_clients = {} - -TCP_HDR_LEN = 6 -TCP_RESP_MIN_LEN = 3 -TCP_HDR_O_LEN = 4 -TCP_READ_REQ_LEN = 6 -TCP_WRITE_MULT_REQ_LEN = 7 - -TCP_DEFAULT_PORT = 502 -TCP_DEFAULT_TIMEOUT = 2 - - -class ModbusClientError(Exception): - pass - - -class ModbusClientTimeout(ModbusClientError): - pass - - -class ModbusClientException(ModbusClientError): - pass - - -def modbus_rtu_client(name=None, baudrate=None, parity=None): - global modbus_rtu_clients - - client = modbus_rtu_clients.get(name) - if client is not None: - if baudrate is not None and client.baudrate != baudrate: - raise ModbusClientError('Modbus client baudrate mismatch') - if parity is not None and client.parity != parity: - raise ModbusClientError('Modbus client parity mismatch') - else: - if baudrate is None: - baudrate = 9600 - if parity is None: - parity = PARITY_NONE - client = ModbusClientRTU(name, baudrate, parity) - modbus_rtu_clients[name] = client - return client - - -def modbus_rtu_client_remove(name=None): - - global modbus_rtu_clients - - if modbus_rtu_clients.get(name): - del modbus_rtu_clients[name] - - -def __generate_crc16_table(): - ''' Generates a crc16 lookup table - .. note:: This will only be generated once - ''' - result = [] - for byte in range(256): - crc = 0x0000 - for bit in range(8): - if (byte ^ crc) & 0x0001: - crc = (crc >> 1) ^ 0xa001 - else: crc >>= 1 - byte >>= 1 - result.append(crc) - return result - - -__crc16_table = __generate_crc16_table() - - -def computeCRC(data): - ''' Computes a crc16 on the passed in string. For modbus, - this is only used on the binary serial protocols (in this - case RTU). - The difference between modbus's crc16 and a normal crc16 - is that modbus starts the crc value out at 0xffff. - :param data: The data to create a crc16 of - :returns: The calculated CRC - ''' - crc = 0xffff - - for a in data: - idx = __crc16_table[(crc ^ a) & 0xff]; - crc = ((crc >> 8) & 0xff) ^ idx - swapped = ((crc << 8) & 0xff00) | ((crc >> 8) & 0x00ff) - return swapped - - -def checkCRC(data, check): - ''' Checks if the data matches the passed in CRC - :param data: The data to create a crc16 of - :param check: The CRC to validate - :returns: True if matched, False otherwise - ''' - return computeCRC(data) == check - - -class ModbusClientRTU(object): - """A Modbus RTU client that multiple devices can use to access devices over - the same serial interface. Currently, the implementation does not support - concurent device requests so the support of multiple devices must be single - threaded. - Parameters: - name : - Name of the serial port such as 'com4' or '/dev/ttyUSB0'. - baudrate : - Baud rate such as 9600 or 19200. Default is 9600 if not specified. - parity : - Parity. Possible values: - :const:`sunspec.core.modbus.client.PARITY_NONE`, - :const:`sunspec.core.modbus.client.PARITY_EVEN`. Defaults to - :const:`PARITY_NONE`. - Raises: - ModbusClientError: Raised for any general modbus client error. - ModbusClientTimeoutError: Raised for a modbus client request timeout. - ModbusClientException: Raised for an exception response to a modbus - client request. - Attributes: - name - Name of the serial port such as 'com4' or '/dev/ttyUSB0'. - baudrate - Baud rate. - parity - Parity. Possible values: - :const:`sunspec.core.modbus.client.PARITY_NONE`, - :const:`sunspec.core.modbus.client.PARITY_EVEN` - serial - The pyserial.Serial object used for serial communications. - timeout - Read timeout in seconds. Fractional values are permitted. - write_timeout - Write timeout in seconds. Fractional values are permitted. - devices - List of :const:`sunspec.core.modbus.client.ModbusClientDeviceRTU` - devices currently using the client. - """ - - def __init__(self, name='/dev/ttyUSB0', baudrate=9600, parity=None): - self.name = name - self.baudrate = baudrate - self.parity = parity - self.serial = None - self.timeout = .5 - self.write_timeout = .5 - self.devices = {} - - self.open() - - def open(self): - """Open the RTU client serial interface. - """ - - try: - if self.parity == PARITY_EVEN: - parity = serial.PARITY_EVEN - else: - parity = serial.PARITY_NONE - - self.serial = serial.Serial(port=self.name, baudrate=self.baudrate, - bytesize=8, parity=parity, - stopbits=1, xonxoff=0, - timeout=self.timeout, writeTimeout=self.write_timeout) - - except Exception as e: - if self.serial is not None: - self.serial.close() - self.serial = None - raise ModbusClientError('Serial init error: %s' % str(e)) - - def close(self): - """Close the RTU client serial interface. - """ - - try: - if self.serial is not None: - self.serial.close() - except Exception as e: - raise ModbusClientError('Serial close error: %s' % str(e)) - - def add_device(self, slave_id, device): - """Add a device to the RTU client. - Parameters: - slave_id : - Modbus slave id. - device : - Device to add to the client. - """ - - self.devices[slave_id] = device - - def remove_device(self, slave_id): - """Remove a device from the RTU client. - Parameters: - slave_id : - Modbus slave id. - """ - - if self.devices.get(slave_id): - del self.devices[slave_id] - - # if no more devices using the client interface, close and remove the client - if len(self.devices) == 0: - self.close() - modbus_rtu_client_remove(self.name) - - def _read(self, slave_id, addr, count, op=FUNC_READ_HOLDING, trace_func=None): - resp = bytearray() - len_remaining = 5 - len_found = False - except_code = None - - req = struct.pack('>BBHH', int(slave_id), op, int(addr), int(count)) - req += struct.pack('>H', computeCRC(req)) - - if trace_func: - s = '{}:{}[addr={}] ->'.format(self.name, str(slave_id), addr) - for c in req: - s += '%02X' % (ord(c)) - trace_func(s) - - self.serial.flushInput() - try: - self.serial.write(req) - except Exception as e: - raise ModbusClientError('Serial write error: %s' % str(e)) - - while len_remaining > 0: - c = self.serial.read(len_remaining) - - len_read = len(c) - if len_read > 0: - resp += c - len_remaining -= len_read - if len_found is False and len(resp) >= 5: - if not (resp[1] & 0x80): - len_remaining = (resp[2] + 5) - len(resp) - len_found = True - else: - except_code = resp[2] - else: - raise ModbusClientTimeout('Response timeout') - - if trace_func: - s = '{}:{}[addr={}] <--'.format(self.name, str(slave_id), addr) - for c in resp: - s += '%02X' % (ord(c)) - trace_func(s) - - crc = (resp[-2] << 8) | resp[-1] - if not checkCRC(resp[:-2], crc): - raise ModbusClientError('CRC error') - - if except_code: - raise ModbusClientException('Modbus exception %d' % (except_code)) - - return resp[3:-2] - - def read(self, slave_id, addr, count, op=FUNC_READ_HOLDING, trace_func=None, max_count=REQ_COUNT_MAX): - """ - Parameters: - slave_id : - Modbus slave id. - addr : - Starting Modbus address. - count : - Read length in Modbus registers. - op : - Modbus function code for request. Possible values: - :const:`FUNC_READ_HOLDING`, :const:`FUNC_READ_INPUT`. - trace_func : - Trace function to use for detailed logging. No detailed logging - is perform is a trace function is not supplied. - max_count : - Maximum register count for a single Modbus request. - Returns: - Byte string containing register contents. - """ - - resp = bytearray() - read_count = 0 - read_offset = 0 - - if self.serial is not None: - while count > 0: - if count > max_count: - read_count = max_count - else: - read_count = count - data = self._read(slave_id, addr + read_offset, read_count, op=op, trace_func=trace_func) - if data: - resp += data - count -= read_count - read_offset += read_count - else: - return - else: - raise ModbusClientError('Client serial port not open: %s' % self.name) - - return bytes(resp) - - def _write(self, slave_id, addr, data, trace_func=None): - resp = bytearray() - len_remaining = 5 - len_found = False - except_code = None - func = FUNC_WRITE_MULTIPLE - len_data = len(data) - count = int(len_data/2) - - req = struct.pack('>BBHHB', int(slave_id), func, int(addr), count, len_data) - - req += data - req += struct.pack('>H', computeCRC(req)) - - if trace_func: - s = '{}:{}[addr={}] ->'.format(self.name, str(slave_id), addr) - for c in req: - s += '%02X' % (ord(c)) - trace_func(s) - - self.serial.flushInput() - - try: - self.serial.write(bytes(req)) - except Exception as e: - raise ModbusClientError('Serial write error: %s' % str(e)) - - while len_remaining > 0: - c = self.serial.read(len_remaining) - - len_read = len(c) - if len_read > 0: - resp += c - len_remaining -= len_read - if len_found is False and len(resp) >= 5: - if not (resp[1] & 0x80): - len_remaining = 8 - len(resp) - len_found = True - else: - except_code = resp[2] - else: - raise ModbusClientTimeout('Response timeout') - - if trace_func: - s = '{}:{}[addr={}] <--'.format(self.name, str(slave_id), addr) - for c in resp: - s += '%02X' % (ord(c)) - trace_func(s) - - crc = (resp[-2] << 8) | resp[-1] - if not checkCRC(resp[:-2], crc): - raise ModbusClientError('CRC error') - - if except_code: - raise ModbusClientException('Modbus exception: %d' % (except_code)) - else: - resp_slave_id, resp_func, resp_addr, resp_count, resp_crc = struct.unpack('>BBHHH', bytes(resp)) - if resp_slave_id != slave_id or resp_func != func or resp_addr != addr or resp_count != count: - raise ModbusClientError('Mobus response format error') - - def write(self, slave_id, addr, data, trace_func=None, max_count=REQ_COUNT_MAX): - """ - Parameters: - slave_id : - Modbus slave id. - addr : - Starting Modbus address. - data : - Byte string containing register contents. - trace_func : - Trace function to use for detailed logging. No detailed logging - is perform is a trace function is not supplied. - max_count : - Maximum register count for a single Modbus request. - """ - - write_offset = 0 - count = len(data)/2 - - if self.serial is not None: - while (count > 0): - if count > max_count: - write_count = max_count - else: - write_count = count - start = int(write_offset * 2) - end = int((write_offset + write_count) * 2) - self._write(slave_id, addr + write_offset, data[start:end], - trace_func=trace_func) - count -= write_count - write_offset += write_count - else: - raise ModbusClientError('Client serial port not open: %s' % self.name) - - -class ModbusClientTCP(object): - """Provides access to a Modbus TCP device. - - Parameters: - slave_id : - Modbus slave id. - ipaddr : - IP address string. - ipport : - IP port. - timeout : - Modbus request timeout in seconds. Fractional seconds are permitted such as .5. - ctx : - Context variable to be used by the object creator. Not used by the modbus module. - trace_func : - Trace function to use for detailed logging. No detailed logging is perform is a trace function is - not supplied. - tls : - Use TLS (Modbus/TCP Security). Defaults to `tls=False`. - cafile : - Path to certificate authority (CA) certificate to use for validating server certificates. - Only used if `tls=True`. - certfile : - Path to client TLS certificate to use for client authentication. Only used if `tls=True`. - keyfile : - Path to client TLS key to use for client authentication. Only used if `tls=True`. - insecure_skip_tls_verify : - Skip verification of server TLS certificate. Only used if `tls=True`. - max_count : - Maximum register count for a single Modbus request. - test : - Use test socket. If True use the fake socket module for network communications. - """ - - def __init__(self, slave_id=1, ipaddr='127.0.0.1', ipport=502, timeout=None, ctx=None, trace_func=None, - tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False, - max_count=REQ_COUNT_MAX, test=False): - self.slave_id = slave_id - self.ipaddr = ipaddr - self.ipport = ipport - self.timeout = timeout - self.ctx = ctx - self.socket = None - self.trace_func = trace_func - self.tls = tls - self.cafile = cafile - self.certfile = certfile - self.keyfile = keyfile - self.tls_verify = not insecure_skip_tls_verify - self.max_count = max_count - - if ipport is None: - self.ipport = TCP_DEFAULT_PORT - if timeout is None: - self.timeout = TCP_DEFAULT_TIMEOUT - - def close(self): - - self.disconnect() - - def connect(self, timeout=None): - """Connect to TCP destination. - - Parameters: - - timeout : - Connection timeout in seconds. - """ - if self.socket: - self.disconnect() - - if timeout is None: - timeout = self.timeout - - try: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.settimeout(timeout) - - if self.tls: - context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=self.cafile) - context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile) - context.check_hostname = self.tls_verify - - self.socket = context.wrap_socket(self.socket, server_side=False, server_hostname=self.ipaddr) - - self.socket.connect((self.ipaddr, self.ipport)) - except Exception as e: - raise ModbusClientError('Connection error: %s' % str(e)) - - def disconnect(self): - """Disconnect from TCP destination. - """ - - try: - if self.socket: - self.socket.close() - self.socket = None - except Exception: - pass - - def _read(self, addr, count, op=FUNC_READ_HOLDING): - resp = bytearray() - len_remaining = TCP_HDR_LEN + TCP_RESP_MIN_LEN - len_found = False - except_code = None - - req = struct.pack('>HHHBBHH', 0, 0, TCP_READ_REQ_LEN, int(self.slave_id), op, int(addr), int(count)) - - if self.trace_func: - s = '%s:%s:%s[addr=%s] ->' % (self.ipaddr, str(self.ipport), str(self.slave_id), addr) - for c in req: - s += '%02X' % (ord(c)) - self.trace_func(s) - - try: - self.socket.sendall(req) - except Exception as e: - raise ModbusClientError('Socket write error: %s' % str(e)) - - while len_remaining > 0: - c = self.socket.recv(len_remaining) - len_read = len(c) - if len_read > 0: - resp += c - len_remaining -= len_read - if len_found is False and len(resp) >= TCP_HDR_LEN + TCP_RESP_MIN_LEN: - data_len = struct.unpack('>H', resp[TCP_HDR_O_LEN:TCP_HDR_O_LEN + 2]) - len_remaining = data_len[0] - (len(resp) - TCP_HDR_LEN) - else: - raise ModbusClientError('Response timeout') - - if not (resp[TCP_HDR_LEN + 1] & 0x80): - len_remaining = (resp[TCP_HDR_LEN + 2] + TCP_HDR_LEN) - len(resp) - len_found = True - else: - except_code = resp[TCP_HDR_LEN + 2] - - if self.trace_func: - s = '%s:%s:%s[addr=%s] <--' % (self.ipaddr, str(self.ipport), str(self.slave_id), addr) - for c in resp: - s += '%02X' % (ord(c)) - self.trace_func(s) - - if except_code: - raise ModbusClientException('Modbus exception %d: addr: %s count: %s' % (except_code, addr, count)) - - return resp[(TCP_HDR_LEN + 3):] - - def read(self, addr, count, op=FUNC_READ_HOLDING): - """ Read Modbus device registers. If no connection exists to the - destination, one is created and disconnected at the end of the request. - - Parameters: - - addr : - Starting Modbus address. - - count : - Read length in Modbus registers. - - op : - Modbus function code for request. - - Returns: - - Byte string containing register contents. - """ - - resp = bytearray() - read_count = 0 - read_offset = 0 - local_connect = False - - if self.socket is None: - local_connect = True - self.connect(self.timeout) - - try: - while (count > 0): - if count > self.max_count: - read_count = self.max_count - else: - read_count = count - data = self._read(addr + read_offset, read_count, op=op) - - if data: - resp += data - count -= read_count - read_offset += read_count - else: - break - finally: - if local_connect: - self.disconnect() - - return bytes(resp) - - def _write(self, addr, data): - resp = bytearray() - len_remaining = TCP_HDR_LEN + TCP_RESP_MIN_LEN - len_found = False - except_code = None - func = FUNC_WRITE_MULTIPLE - - write_len = len(data) - write_count = int(write_len/2) - tmp = TCP_WRITE_MULT_REQ_LEN + write_len - req = struct.pack('>HHHBBHHB', 0, 0, TCP_WRITE_MULT_REQ_LEN + write_len, int(self.slave_id), func, int(addr), - write_count, write_len) - req += data - - if self.trace_func: - s = '%s:%s:%s[addr=%s] ->' % (self.ipaddr, str(self.ipport), str(self.slave_id), addr) - for c in req: - s += '%02X' % (ord(c)) - self.trace_func(s) - - try: - self.socket.sendall(req) - except Exception as e: - raise ModbusClientError('Socket write error: %s' % str(e)) - - while len_remaining > 0: - c = self.socket.recv(len_remaining) - len_read = len(c) - if len_read > 0: - resp += c - len_remaining -= len_read - if len_found is False and len(resp) >= TCP_HDR_LEN + TCP_RESP_MIN_LEN: - data_len = struct.unpack('>H', resp[TCP_HDR_O_LEN:TCP_HDR_O_LEN + 2]) - len_remaining = data_len[0] - (len(resp) - TCP_HDR_LEN) - else: - raise ModbusClientTimeout('Response timeout') - - if not (resp[TCP_HDR_LEN + 1]) & 0x80: - len_remaining = (resp[TCP_HDR_LEN + 2] + TCP_HDR_LEN) - len(resp) - len_found = True - else: - except_code = resp[TCP_HDR_LEN + 2] - - if self.trace_func: - s = '%s:%s:%s[addr=%s] <--' % (self.ipaddr, str(self.ipport), str(self.slave_id), addr) - for c in resp: - s += '%02X' % (ord(c)) - self.trace_func(s) - - if except_code: - raise ModbusClientException('Modbus exception: %d' % (except_code)) - - def write(self, addr, data): - """ Write Modbus device registers. If no connection exists to the - destination, one is created and disconnected at the end of the request. - - Parameters: - - addr : - Starting Modbus address. - - count : - Byte string containing register contents. - """ - write_count = 0 - write_offset = 0 - local_connect = False - count = len(data)/2 - - if self.socket is None: - local_connect = True - self.connect(self.timeout) - - try: - while (count > 0): - if count > self.max_count: - write_count = self.max_count - else: - write_count = count - start = (write_offset * 2) - end = int((write_offset + write_count) * 2) - self._write(addr + write_offset, data[start:end]) - count -= write_count - write_offset += write_count - finally: - if local_connect: - self.disconnect() diff --git a/build/lib/sunspec2/smdx.py b/build/lib/sunspec2/smdx.py deleted file mode 100644 index 37916ce..0000000 --- a/build/lib/sunspec2/smdx.py +++ /dev/null @@ -1,372 +0,0 @@ - -""" - Copyright (C) 2020 SunSpec Alliance - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. -""" - -import os -import xml.etree.ElementTree as ET - -import sunspec2.mdef as mdef - -SMDX_ROOT = 'sunSpecModels' -SMDX_MODEL = mdef.MODEL -SMDX_BLOCK = 'block' -SMDX_POINT = 'point' -SMDX_ATTR_VERS = 'v' -SMDX_ATTR_ID = 'id' -SMDX_ATTR_LEN = 'len' -SMDX_ATTR_NAME = mdef.NAME -SMDX_ATTR_TYPE = mdef.TYPE -SMDX_ATTR_COUNT = mdef.COUNT -SMDX_ATTR_VALUE = mdef.VALUE -SMDX_ATTR_TYPE_FIXED = 'fixed' -SMDX_ATTR_TYPE_REPEATING = 'repeating' -SMDX_ATTR_OFFSET = 'offset' -SMDX_ATTR_MANDATORY = mdef.MANDATORY -SMDX_ATTR_ACCESS = mdef.ACCESS -SMDX_ATTR_SF = mdef.SF -SMDX_ATTR_UNITS = mdef.UNITS - -SMDX_SYMBOL = 'symbol' -SMDX_COMMENT = 'comment' - -SMDX_STRINGS = 'strings' -SMDX_ATTR_LOCALE = 'locale' -SMDX_LABEL = mdef.LABEL -SMDX_DESCRIPTION = 'description' -SMDX_NOTES = 'notes' -SMDX_DETAIL = mdef.DETAIL - -SMDX_TYPE_INT16 = mdef.TYPE_INT16 -SMDX_TYPE_UINT16 = mdef.TYPE_UINT16 -SMDX_TYPE_COUNT = mdef.TYPE_COUNT -SMDX_TYPE_ACC16 = mdef.TYPE_ACC16 -SMDX_TYPE_ENUM16 = mdef.TYPE_ENUM16 -SMDX_TYPE_BITFIELD16 = mdef.TYPE_BITFIELD16 -SMDX_TYPE_PAD = mdef.TYPE_PAD -SMDX_TYPE_INT32 = mdef.TYPE_INT32 -SMDX_TYPE_UINT32 = mdef.TYPE_UINT32 -SMDX_TYPE_ACC32 = mdef.TYPE_ACC32 -SMDX_TYPE_ENUM32 = mdef.TYPE_ENUM32 -SMDX_TYPE_BITFIELD32 = mdef.TYPE_BITFIELD32 -SMDX_TYPE_IPADDR = mdef.TYPE_IPADDR -SMDX_TYPE_INT64 = mdef.TYPE_INT64 -SMDX_TYPE_UINT64 = mdef.TYPE_UINT64 -SMDX_TYPE_ACC64 = mdef.TYPE_ACC64 -SMDX_TYPE_IPV6ADDR = mdef.TYPE_IPV6ADDR -SMDX_TYPE_FLOAT32 = mdef.TYPE_FLOAT32 -SMDX_TYPE_STRING = mdef.TYPE_STRING -SMDX_TYPE_SUNSSF = mdef.TYPE_SUNSSF -SMDX_TYPE_EUI48 = mdef.TYPE_EUI48 - -SMDX_ACCESS_R = 'r' -SMDX_ACCESS_RW = 'rw' - -SMDX_MANDATORY_FALSE = 'false' -SMDX_MANDATORY_TRUE = 'true' - -smdx_access_types = {SMDX_ACCESS_R: mdef.ACCESS_R, SMDX_ACCESS_RW: mdef.ACCESS_RW} - -smdx_mandatory_types = {SMDX_MANDATORY_FALSE: mdef.MANDATORY_FALSE, SMDX_MANDATORY_TRUE: mdef.MANDATORY_TRUE} - -smdx_type_types = [ - SMDX_TYPE_INT16, - SMDX_TYPE_UINT16, - SMDX_TYPE_COUNT, - SMDX_TYPE_ACC16, - SMDX_TYPE_ENUM16, - SMDX_TYPE_BITFIELD16, - SMDX_TYPE_PAD, - SMDX_TYPE_INT32, - SMDX_TYPE_UINT32, - SMDX_TYPE_ACC32, - SMDX_TYPE_ENUM32, - SMDX_TYPE_BITFIELD32, - SMDX_TYPE_IPADDR, - SMDX_TYPE_INT64, - SMDX_TYPE_UINT64, - SMDX_TYPE_ACC64, - SMDX_TYPE_IPV6ADDR, - SMDX_TYPE_FLOAT32, - SMDX_TYPE_STRING, - SMDX_TYPE_SUNSSF, - SMDX_TYPE_EUI48 -] - -SMDX_PREFIX = 'smdx_' -SMDX_EXT = '.xml' - - -def to_smdx_filename(model_id): - - return '%s%05d%s' % (SMDX_PREFIX, int(model_id), SMDX_EXT) - - -def model_filename_to_id(filename): - - model_id = None - - if filename[0:5] == SMDX_PREFIX and filename[-4:] == SMDX_EXT: - try: - model_id = int(filename[5:-4]) - except Exception as e: - pass - - return model_id - -''' - smdx to json mapping: - - fixed block -> top level group - model 'name' attribute -> group 'name' - ID point is created for model ID and 'value' is the model ID value as a number - L point is created for model len - model len has no value specified in the model definition - fixed block points are placed in top level group - repeating block -> group with count = 0 (indicates model len shoud be used to determine number of groups) - repeating block 'name' -> group 'name', if no 'name' is defined 'name' = 'repeating' - - points: - all type, access, and mandatory attributes are preserved - point symbol map to the symbol object and placed in the symbols list for the point - symbol 'name' attribute -> symbol object 'name' - symbol element content -> symbol object 'value' - strings 'label', 'description', 'notes' elements map to point attributes 'label', 'desc', 'detail' -''' - - -def from_smdx_file(filename): - tree = ET.parse(filename) - root = tree.getroot() - return(from_smdx(root)) - - -def from_smdx(element): - """ Sets the model type attributes based on an element tree model type - element contained in an SMDX model definition. - - Parameters: - - element : - Element Tree model type element. - """ - - model_def = {} - - m = element.find(SMDX_MODEL) - if m is None: - raise mdef.ModelDefinitionError('Model definition not found') - try: - mid = mdef.to_number_type(m.attrib.get(SMDX_ATTR_ID)) - except ValueError: - raise mdef.ModelDefinitionError('Invalid model id: %s' % m.attrib.get(SMDX_ATTR_ID)) - - name = m.attrib.get(SMDX_ATTR_NAME) - if name is None: - name = 'model_' + str(mid) - model_def[mdef.NAME] = name - - strings = element.find(SMDX_STRINGS) - - # create top level group with ID and L points - fixed_def = {mdef.NAME: name, - mdef.TYPE: mdef.TYPE_GROUP, - mdef.POINTS: [ - {mdef.NAME: 'ID', mdef.VALUE: mid, - mdef.DESCRIPTION: 'Model identifier', mdef.LABEL: 'Model ID', - 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.MANDATORY: mdef.MANDATORY_TRUE, mdef.STATIC: mdef.STATIC_TRUE, mdef.TYPE: mdef.TYPE_UINT16} - ] - } - - repeating_def = None - - fixed = None - repeating = None - for b in m.findall(SMDX_BLOCK): - btype = b.attrib.get(SMDX_ATTR_TYPE, SMDX_ATTR_TYPE_FIXED) - if btype == SMDX_ATTR_TYPE_FIXED: - if fixed is not None: - raise mdef.ModelDefinitionError('Duplicate fixed block type definition') - fixed = b - elif btype == SMDX_ATTR_TYPE_REPEATING: - if repeating is not None: - raise mdef.ModelDefinitionError('Duplicate repeating block type definition') - repeating = b - else: - raise mdef.ModelDefinitionError('Invalid block type: %s' % btype) - - fixed_points_map = {} - if fixed is not None: - points = [] - for e in fixed.findall(SMDX_POINT): - point_def = from_smdx_point(e) - if point_def[mdef.NAME] not in fixed_points_map: - fixed_points_map[point_def[mdef.NAME]] = point_def - points.append(point_def) - else: - raise mdef.ModelDefinitionError('Duplicate point definition: %s' % point_def[mdef.NAME]) - if points: - fixed_def[mdef.POINTS].extend(points) - - repeating_points_map = {} - if repeating is not None: - name = repeating.attrib.get(SMDX_ATTR_NAME) - if name is None: - name = 'repeating' - repeating_def = {mdef.NAME: name, mdef.TYPE: mdef.TYPE_GROUP, mdef.COUNT: 0} - points = [] - for e in repeating.findall(SMDX_POINT): - point_def = from_smdx_point(e) - if point_def[mdef.NAME] not in repeating_points_map: - repeating_points_map[point_def[mdef.NAME]] = point_def - points.append(point_def) - else: - raise mdef.ModelDefinitionError('Duplicate point definition: %s' % point_def[mdef.NAME]) - if points: - repeating_def[mdef.POINTS] = points - fixed_def[mdef.GROUPS] = [repeating_def] - - e = element.find(SMDX_STRINGS) - if e.attrib.get(SMDX_ATTR_ID) == str(mid): - m = e.find(SMDX_MODEL) - if m is not None: - for a in m.findall('*'): - if a.tag == SMDX_LABEL and a.text: - fixed_def[mdef.LABEL] = a.text - elif a.tag == SMDX_DESCRIPTION and a.text: - fixed_def[mdef.DESCRIPTION] = a.text - elif a.tag == SMDX_NOTES and a.text: - fixed_def[mdef.DETAIL] = a.text - for p in e.findall(SMDX_POINT): - pid = p.attrib.get(SMDX_ATTR_ID) - label = desc = notes = None - for a in p.findall('*'): - if a.tag == SMDX_LABEL and a.text: - label = a.text - elif a.tag == SMDX_DESCRIPTION and a.text: - desc = a.text - 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: - if label: - point_def[mdef.LABEL] = label - if desc: - point_def[mdef.DESCRIPTION] = desc - if notes: - point_def[mdef.DETAIL] = notes - - model_def = {'id': mid, 'group': fixed_def} - return model_def - - -def from_smdx_point(element): - """ Sets the point attributes based on an element tree point element - contained in an SMDX model definition. - - Parameters: - - element : - Element Tree point type element. - - strings : - Indicates if *element* is a subelement of the 'strings' - definintion within the model definition. - """ - point_def = {} - pid = element.attrib.get(SMDX_ATTR_ID) - if pid is None: - raise mdef.ModelDefinitionError('Missing point id attribute') - point_def[mdef.NAME] = pid - ptype = element.attrib.get(SMDX_ATTR_TYPE) - if ptype is None: - raise mdef.ModelDefinitionError('Missing type attribute for point: %s' % pid) - elif ptype not in smdx_type_types: - raise mdef.ModelDefinitionError('Unknown point type %s for point %s' % (ptype, pid)) - point_def[mdef.TYPE] = ptype - plen = mdef.to_number_type(element.attrib.get(SMDX_ATTR_LEN)) - if ptype == SMDX_TYPE_STRING: - if plen is None: - raise mdef.ModelDefinitionError('Missing len attribute for point: %s' % pid) - point_def[mdef.SIZE] = plen - 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) - if mandatory == SMDX_MANDATORY_TRUE: - point_def[mdef.MANDATORY] = smdx_mandatory_types.get(mandatory) - access = element.attrib.get(SMDX_ATTR_ACCESS, SMDX_ACCESS_R) - if access not in smdx_access_types: - raise mdef.ModelDefinitionError('Unknown access type: %s' % access) - if access == SMDX_ACCESS_RW: - point_def[mdef.ACCESS] = smdx_access_types.get(access) - units = element.attrib.get(SMDX_ATTR_UNITS) - if units: - point_def[mdef.UNITS] = units - # if scale factor is an number, convert to correct type - sf = mdef.to_number_type(element.attrib.get(SMDX_ATTR_SF)) - if sf is not None: - point_def[mdef.SF] = sf - # if scale factor is an number, convert to correct type - value = mdef.to_number_type(element.attrib.get(SMDX_ATTR_VALUE)) - if value is not None: - point_def[mdef.VALUE] = value - - symbols = [] - for e in element.findall('*'): - if e.tag == SMDX_SYMBOL: - sid = e.attrib.get(SMDX_ATTR_ID) - value = e.text - try: - value = int(value) - except ValueError: - pass - symbols.append({mdef.NAME: sid, mdef.VALUE: value}) - if symbols: - point_def[mdef.SYMBOLS] = symbols - - return point_def - - -def indent(elem, level=0): - i = os.linesep + level*" " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - indent(elem, level+1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i diff --git a/build/lib/sunspec2/spreadsheet.py b/build/lib/sunspec2/spreadsheet.py deleted file mode 100644 index 384de52..0000000 --- a/build/lib/sunspec2/spreadsheet.py +++ /dev/null @@ -1,457 +0,0 @@ - -""" - Copyright (C) 2020 SunSpec Alliance - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. -""" - -import csv -import json -import copy - -import sunspec2.mdef as mdef - -ADDRESS_OFFSET = 'Address Offset' -GROUP_OFFSET = 'Group Offset' -NAME = 'Name' -VALUE = 'Value' -COUNT = 'Count' -TYPE = 'Type' -SIZE = 'Size' -SCALE_FACTOR = 'Scale Factor' -UNITS = 'Units' -ACCESS = 'RW Access (RW)' -MANDATORY = 'Mandatory (M)' -STATIC = 'Static (S)' -LABEL = 'Label' -DESCRIPTION = 'Description' -NOTES = 'Notes' - -columns = [ADDRESS_OFFSET, GROUP_OFFSET, NAME, VALUE, COUNT, TYPE, SIZE, SCALE_FACTOR, - UNITS, ACCESS, MANDATORY, STATIC, LABEL, DESCRIPTION, NOTES] - -empty_row = [''] * len(columns) - -ADDRESS_OFFSET_IDX = columns.index(ADDRESS_OFFSET) -GROUP_OFFSET_IDX = columns.index(GROUP_OFFSET) -NAME_IDX = columns.index(NAME) -VALUE_IDX = columns.index(VALUE) -COUNT_IDX = columns.index(COUNT) -TYPE_IDX = columns.index(TYPE) -SIZE_IDX = columns.index(SIZE) -SCALE_FACTOR_IDX = columns.index(SCALE_FACTOR) -UNITS_IDX = columns.index(UNITS) -ACCESS_IDX = columns.index(ACCESS) -MANDATORY_IDX = columns.index(MANDATORY) -STATIC_IDX = columns.index(STATIC) -LABEL_IDX = columns.index(LABEL) -DESCRIPTION_IDX = columns.index(DESCRIPTION) -NOTES_IDX = columns.index(NOTES) - - -def idx(row, attr, mandatory=False): - try: - return row.index(attr) - except: - if mandatory: - raise ValueError('Missing required attribute column: %s' % (attr)) - - -def row_is_empty(row, idx): - for e in row[idx:]: - if e is not None and e != '': - return False - return True - - -def find_name(entities, name): - for e in entities: - if e[mdef.NAME] == name: - return e - - -def element_type(row): - type_idx = idx(row, TYPE, mandatory=True) - - -def from_spreadsheet(spreadsheet): - model_def = {} - row = spreadsheet[0] - address_offset_idx = idx(row, ADDRESS_OFFSET) - group_offset_idx = idx(row, GROUP_OFFSET) - name_idx = idx(row, NAME, mandatory=True) - value_idx = mdef.to_number_type(idx(row, VALUE, mandatory=True)) - count_idx = mdef.to_number_type(idx(row, COUNT, mandatory=True)) - type_idx = idx(row, TYPE, mandatory=True) - size_idx = mdef.to_number_type(idx(row, SIZE, mandatory=True)) - scale_factor_idx = mdef.to_number_type(idx(row, SCALE_FACTOR, mandatory=True)) - units_idx = idx(row, UNITS, mandatory=True) - access_idx = idx(row, ACCESS, mandatory=True) - mandatory_idx = idx(row, MANDATORY, mandatory=True) - static_idx = idx(row, STATIC, mandatory=True) - label_idx = idx(row, LABEL) - description_idx = idx(row, DESCRIPTION) - has_notes = False - # if notes col not present, notes_idx will be None - notes_idx = idx(row, NOTES) - if notes_idx and row[notes_idx] == 'Notes': - has_notes = True - row_num = 1 - - group = None - point = None - comments = [] - parent = '' - - for row in spreadsheet[1:]: - row_num += 1 - name = row[name_idx] - value = mdef.to_number_type(row[value_idx]) - etype = row[type_idx] - - label = description = notes = '' - if len(row) > label_idx: - label = row[label_idx] - if len(row) > description_idx: - description = row[description_idx] - if has_notes: - notes = row[notes_idx] - if notes is None: - notes = '' - - # point - if etype in mdef.point_type_info: - # point - if group: - if not group.get(mdef.POINTS): - group[mdef.POINTS] = [] - if find_name(group[mdef.POINTS], name) is not None: - raise Exception('Duplicate point definition in group %s: %s' % (group[mdef.NAME], name)) - else: - raise Exception('Point %s defined outside of group' % name) - - size = mdef.to_number_type(row[size_idx]) - sf = mdef.to_number_type(row[scale_factor_idx]) - units = row[units_idx] - access = row[access_idx] - mandatory = row[mandatory_idx] - static = row[static_idx] - point = {mdef.NAME: name} - if etype: - point[mdef.TYPE] = etype - if size is not None and size != '': - point[mdef.SIZE] = size - if sf: - point[mdef.SF] = sf - if units: - point[mdef.UNITS] = units - if access: - point[mdef.ACCESS] = access - if mandatory: - point[mdef.MANDATORY] = mandatory - if static: - point[mdef.STATIC] = static - if label: - point[mdef.LABEL] = label - if description: - point[mdef.DESCRIPTION] = description - if has_notes: - point[mdef.NOTES] = notes - if value is not None and value != '': - point[mdef.VALUE] = value - if comments: - point[mdef.COMMENTS] = list(comments) - group[mdef.POINTS].append(point) - - # set the model id - if not parent and name == mdef.MODEL_ID_POINT_NAME: - model_def[mdef.ID] = value - comments = [] - # group - elif etype in mdef.group_types: - path = name.split('.') - group = model_def.get(mdef.GROUP) - parent = '' - if len(path) > 1: - parent = group[mdef.NAME] - for g in path[1:-1]: - group = find_name(group[mdef.GROUPS], g) - if group is None: - raise Exception('Unknown parent group id %s in group id %s' % (g, group)) - parent += '.%s' % group[mdef.NAME] - else: - if group is not None: - raise Exception('Redefintion of top-level group %s with %s' % (group[mdef.ID], name)) - if parent: - name = '%s.%s' % (parent, path[-1]) - else: - name = path[-1] - new_group = {mdef.NAME: path[-1], mdef.TYPE: etype} - if label: - new_group[mdef.LABEL] = label - if description: - new_group[mdef.DESCRIPTION] = description - if has_notes: - new_group[mdef.NOTES] = notes - if comments: - new_group[mdef.COMMENTS] = list(comments) - comments = [] - count = mdef.to_number_type(row[count_idx]) - if count is not None and count != '': - new_group[mdef.COUNT] = count - if group is None: - model_def[mdef.GROUP] = new_group - else: - if not group.get(mdef.GROUPS): - group[mdef.GROUPS] = [] - group[mdef.GROUPS].append(new_group) - group = new_group - # symbol - has name and value with no type - elif name and value is not None and value != '': - if point is None: - raise Exception('Unknown point for symbol %s' % name) - if not point.get(mdef.SYMBOLS): - point[mdef.SYMBOLS] = [] - if find_name(point[mdef.SYMBOLS], name) is not None: - raise Exception('Duplicate symbol definition in point %s: %s' % (point[mdef.ID], name)) - symbol = {mdef.NAME: name, mdef.VALUE: value} - point[mdef.SYMBOLS].append(symbol) - if label: - symbol[mdef.LABEL] = label - if description: - symbol[mdef.DESCRIPTION] = description - if has_notes: - symbol[mdef.NOTES] = notes - if comments: - symbol[mdef.COMMENTS] = list(comments) - comments = [] - elif not row_is_empty(row, 1): - raise ValueError('Invalid spreadsheet entry row %s: %s' % (row_num, row)) - # comment - no name, value, or type - elif row[0]: - comments.append(row[0]) - # blank line - comment with nothing in column 1 - return model_def - - -def to_spreadsheet(model_def): - # check if model_def has notes attr by searching string - mdef_str = json.dumps(model_def) - has_notes = '\"notes\"' in mdef_str - c_columns = copy.deepcopy(columns) - if has_notes: - spreadsheet = [columns] - else: - c_columns.remove('Notes') - spreadsheet = [c_columns] - to_spreadsheet_group(spreadsheet, model_def[mdef.GROUP], has_notes, addr_offset=0) - return(spreadsheet) - - -def to_spreadsheet_group(ss, group, has_notes, parent='', addr_offset=None): - # process comments - for c in group.get(mdef.COMMENTS, []): - to_spreadsheet_comment(ss, c, has_notes=has_notes) - # add group info - row = None - if has_notes: - row = [''] * len(columns) - else: - row = [''] * (len(columns) - 1) - - name = group.get(mdef.NAME, '') - if name: - if parent: - name = '%s.%s' % (parent, name) - row[NAME_IDX] = name - else: - raise Exception('Group missing name attribute') - row[TYPE_IDX] = group.get(mdef.TYPE, '') - row[COUNT_IDX] = group.get(mdef.COUNT, '') - row[LABEL_IDX] = group.get(mdef.LABEL, '') - row[DESCRIPTION_IDX] = group.get(mdef.DESCRIPTION, '') - if has_notes: - row[NOTES_IDX] = group.get(mdef.NOTES, '') - ss.append(row) - # process points - group_offset = 0 - for p in group.get(mdef.POINTS, []): - plen = to_spreadsheet_point(ss, p, has_notes=has_notes, addr_offset=addr_offset, group_offset=group_offset) - if addr_offset is not None: - addr_offset += plen - if group_offset is not None: - group_offset += plen - # process groups - addr_offset = None - for g in group.get(mdef.GROUPS, []): - to_spreadsheet_group(ss, g, has_notes=has_notes, parent=name, addr_offset=addr_offset) - - -def to_spreadsheet_point(ss, point, has_notes, addr_offset=None, group_offset=None): - # process comments - for c in point.get(mdef.COMMENTS, []): - to_spreadsheet_comment(ss, c, has_notes=has_notes) - # add point info - row = None - if has_notes: - row = [''] * len(columns) - else: - row = [''] * (len(columns) - 1) - name = point.get(mdef.NAME, '') - if name: - row[NAME_IDX] = name - else: - raise Exception('Point missing name attribute') - ptype = point.get(mdef.TYPE, '') - if ptype != '': - row[TYPE_IDX] = ptype - else: - raise Exception('Point %s missing type attribute' % name) - if addr_offset is not None: - row[ADDRESS_OFFSET_IDX] = addr_offset - elif group_offset is not None: - row[GROUP_OFFSET_IDX] = group_offset - access = point.get(mdef.ACCESS, '') - if access != mdef.ACCESS_RW: - access = '' - row[ACCESS_IDX] = access - mandatory = point.get(mdef.MANDATORY, '') - if mandatory != mdef.MANDATORY_TRUE: - mandatory = '' - row[MANDATORY_IDX] = mandatory - static = point.get(mdef.STATIC, '') - if static != mdef.STATIC_TRUE: - static = '' - row[STATIC_IDX] = static - row[UNITS_IDX] = point.get(mdef.UNITS, '') - row[SCALE_FACTOR_IDX] = mdef.to_number_type(point.get(mdef.SF, '')) - row[SIZE_IDX] = mdef.to_number_type(point.get(mdef.SIZE, '')) - row[VALUE_IDX] = mdef.to_number_type(point.get(mdef.VALUE, '')) - row[LABEL_IDX] = point.get(mdef.LABEL, '') - row[DESCRIPTION_IDX] = point.get(mdef.DESCRIPTION, '') - if has_notes: - row[NOTES_IDX] = point.get(mdef.NOTES, '') - ss.append(row) - # process symbols - for s in point.get(mdef.SYMBOLS, []): - to_spreadsheet_symbol(ss, s, has_notes=has_notes) - # return point length - try: - plen = mdef.point_type_info[ptype]['len'] - except KeyError: - raise Exception('Unknown point type %s for point %s' % (ptype, name)) - if not plen: - try: - plen = int(row[SIZE_IDX]) - except ValueError: - raise Exception('Point size is for point %s not an iteger value: %s' % (name, row[SIZE_IDX])) - return plen - - -def to_spreadsheet_symbol(ss, symbol, has_notes): - # process comments - for c in symbol.get(mdef.COMMENTS, []): - to_spreadsheet_comment(ss, c, has_notes=has_notes) - # add symbol info - row = None - if has_notes: - row = [''] * len(columns) - else: - row = [''] * (len(columns) - 1) - name = symbol.get(mdef.NAME, '') - if name: - row[NAME_IDX] = name - else: - raise Exception('Symbol missing name attribute') - value = symbol.get(mdef.VALUE, '') - if value != '': - row[VALUE_IDX] = value - else: - raise Exception('Symbol %s missing value' % name) - row[LABEL_IDX] = symbol.get(mdef.LABEL, '') - row[DESCRIPTION_IDX] = symbol.get(mdef.DESCRIPTION, '') - if has_notes: - row[NOTES_IDX] = symbol.get(mdef.NOTES, '') - ss.append(row) - - -def to_spreadsheet_comment(ss, comment, has_notes): - # add comment info - row = None - if has_notes: - row = [''] * len(columns) - else: - row = [''] * (len(columns) - 1) - row[0] = comment - ss.append(row) - - -def spreadsheet_equal(ss1, ss2): - count = len(ss1) - if count != len(ss2): - raise Exception('Different length: %s %s' % (count, len(ss2))) - for i in range(count): - if ss1[i] != ss2[i]: - raise Exception('Line %s different: %s %s' % (i + 1, ss1[i], ss2[i])) - return True - - -def from_csv(filename=None, csv_str=None): - return from_spreadsheet(spreadsheet_from_csv(filename=filename, csv_str=csv_str)) - - -def to_csv(model_def, filename=None, csv_str=None): - spreadsheet_to_csv(to_spreadsheet(model_def), filename=filename, csv_str=csv_str) - - -def spreadsheet_from_csv(filename=None, csv_str=None): - spreadsheet = [] - file = '' - - if filename: - import sys - file = open(filename) - if file: - for row in csv.reader(file): - if len(row) > 0: - # filter out informative offset information from the normative model definition - if row[TYPE_IDX] and row[TYPE_IDX] != TYPE: - row[ADDRESS_OFFSET_IDX] = '' - row[GROUP_OFFSET_IDX] = '' - if row[VALUE_IDX]: - row[VALUE_IDX] = mdef.to_number_type(row[VALUE_IDX]) - if row[COUNT_IDX]: - row[COUNT_IDX] = mdef.to_number_type(row[COUNT_IDX]) - if row[SIZE_IDX]: - row[SIZE_IDX] = mdef.to_number_type(row[SIZE_IDX]) - if row[SCALE_FACTOR_IDX]: - row[SCALE_FACTOR_IDX] = mdef.to_number_type(row[SCALE_FACTOR_IDX]) - spreadsheet.append(row) - - return spreadsheet - - -def spreadsheet_to_csv(spreadsheet, filename=None, csv_str=None): - file = None - if filename: - file = open(filename, 'w') - writer = csv.writer(file, lineterminator='\n') - for row in spreadsheet: - writer.writerow(row) - file.close() diff --git a/build/lib/sunspec2/tests/__init__.py b/build/lib/sunspec2/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/sunspec2/tests/mock_port.py b/build/lib/sunspec2/tests/mock_port.py deleted file mode 100644 index 04f9e2b..0000000 --- a/build/lib/sunspec2/tests/mock_port.py +++ /dev/null @@ -1,43 +0,0 @@ -class MockPort(object): - PARITY_NONE = 'N' - PARITY_EVEN = 'E' - - def __init__(self, port, baudrate, bytesize, parity, stopbits, xonxoff, timeout, writeTimeout): - self.connected = True - self.port = port - self.baudrate = baudrate - self.bytesize = bytesize - self.parity = parity - self.stopbits = stopbits - self.xonxoff = xonxoff - self.timeout = timeout - self.writeTimeout = writeTimeout - - self.buffer = [] - self.request = [] - - def open(self): - pass - - def close(self): - self.connected = False - - def read(self, count): - return self.buffer.pop(0) - - def write(self, data): - self.request.append(data) - - def flushInput(self): - pass - - def _set_buffer(self, resp_list): - for bs in resp_list: - self.buffer.append(bs) - - def clear_buffer(self): - self.buffer = [] - - -def mock_port(port, baudrate, bytesize, parity, stopbits, xonxoff, timeout, writeTimeout): - return MockPort(port, baudrate, bytesize, parity, stopbits, xonxoff, timeout, writeTimeout) diff --git a/build/lib/sunspec2/tests/mock_socket.py b/build/lib/sunspec2/tests/mock_socket.py deleted file mode 100644 index d881601..0000000 --- a/build/lib/sunspec2/tests/mock_socket.py +++ /dev/null @@ -1,37 +0,0 @@ -class MockSocket(object): - def __init__(self): - self.connected = False - self.timeout = 0 - self.ipaddr = None - self.ipport = None - self.buffer = [] - - self.request = [] - - def settimeout(self, timeout): - self.timeout = timeout - - def connect(self, ipaddrAndipportTup): - self.connected = True - self.ipaddr = ipaddrAndipportTup[0] - self.ipport = ipaddrAndipportTup[1] - - def close(self): - self.connected = False - - def recv(self, size): - return self.buffer.pop(0) - - def sendall(self, data): - self.request.append(data) - - def _set_buffer(self, resp_list): - for bs in resp_list: - self.buffer.append(bs) - - def clear_buffer(self): - self.buffer = [] - - -def mock_socket(AF_INET, SOCK_STREAM): - return MockSocket() diff --git a/build/lib/sunspec2/tests/test_data/__init__.py b/build/lib/sunspec2/tests/test_data/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/sunspec2/tests/test_data/device_1547.json b/build/lib/sunspec2/tests/test_data/device_1547.json deleted file mode 100644 index 29c4859..0000000 --- a/build/lib/sunspec2/tests/test_data/device_1547.json +++ /dev/null @@ -1,612 +0,0 @@ -{ - "name": "device_1547", - "models": [ - { - "ID": 1, - "Mn": "SunSpecTest", - "Md": "Test-1547-1", - "Opt": "opt_a_b_c", - "Vr": "1.2.3", - "SN": "sn-123456789", - "DA": 1, - "Pad": 0 - }, - { - "ID": 701, - "L": null, - "ACType": 3, - "St": 2, - "Alrm": 0, - "W": 9800, - "VA": 10000, - "Var": 200, - "PF": 985, - "A": 411, - "LLV": 2400, - "LNV": 2400, - "Hz": 60010, - "TotWhInj": 150, - "TotWhAbs": 0, - "TotVarhInj": 9, - "TotVarhAbs": 0, - "TmpAmb": 450, - "TmpCab": 550, - "TmpSnk": 650, - "TmpTrns": 500, - "TmpSw": 400, - "TmpOt": 420, - "WL1": 3200, - "VAL1": 3333, - "VarL1": 80, - "PFL1": 984, - "AL1": 137, - "VL1L2": 120, - "VL1": 120, - "TotWhInjL1": 49, - "TotWhAbsL1": 0, - "TotVarhInjL1": 2, - "TotVarhAbsL1": 0, - "WL2": 3300, - "VAL2": 3333, - "VarL2": 80, - "PFL2": 986, - "AL2": 136, - "VL2L3": 120, - "VL2": 120, - "TotWhInjL2": 50, - "TotWhAbsL2": 0, - "TotVarhInjL2": 3, - "TotVarhAbsL2": 0, - "WL3": 3500, - "VAL3": 3333, - "VarL3": 40, - "PFL3": 987, - "AL3": 138, - "VL3L1": 120, - "VL3N": 120, - "TotWhInjL3": 51, - "TotWhAbsL3": 0, - "TotVarhInjL3": 4, - "TotVarhAbsL3": 0, - "A_SF": -1, - "V_SF": -1, - "Hz_SF": -3, - "W_SF": 0, - "PF_SF": -3, - "VA_SF": 0, - "Var_SF": 0, - "TotWh_SF": 3, - "TotVarh_SF": 3, - "Tmp_SF": -1 - }, - { - "ID": 702, - "L": null, - "WMaxRtg": 10000, - "WOvrExtRtg": 10000, - "WOvrExtRtgPF": 1000, - "WUndExtRtg": 10000, - "WUndExtRtgPF": 1000, - "VAMaxRtg": 11000, - "VarMaxInjRtg": 2500, - "VarMaxAbsRtg": 0, - "WChaRteMaxRtg": 0, - "WDisChaRteMaxRtg": 0, - "VAChaRteMaxRtg": 0, - "VADisChaRteMaxRtg": 0, - "VNomRtg": 240, - "VMaxRtg": 270, - "VMinRtg": 210, - "AMaxRtg": 50, - "PFOvrExtRtg": 850, - "PFUndExtRtg": 850, - "ReactSusceptRtg": null, - "NorOpCatRtg": 2, - "AbnOpCatRtg": 3, - "CtrlModes": null, - "IntIslandCatRtg": null, - "WMax": 10000, - "WMaxOvrExt": null, - "WOvrExtPF": null, - "WMaxUndExt": null, - "WUndExtPF": null, - "VAMax": 10000, - "AMax": null, - "Vnom": null, - "VRefOfs": null, - "VMax": null, - "VMin": null, - "VarMaxInj": null, - "VarMaxAbs": null, - "WChaRteMax": null, - "WDisChaRteMax": null, - "VAChaRteMax": null, - "VADisChaRteMax": null, - "IntIslandCat": null, - "W_SF": 0, - "PF_SF": -3, - "VA_SF": 0, - "Var_SF": 0, - "V_SF": 0, - "A_SF": 0, - "S_SF": 0 - }, - { - "ID": 703, - "ES": 1, - "ESVHi": 1050, - "ESVLo": 917, - "ESHzHi": 6010, - "ESHzLo": 5950, - "ESDlyTms": 300, - "ESRndTms": 100, - "ESRmpTms": 60, - "V_SF": -3, - "Hz_SF": -2 - }, - { - "ID": 704, - "L": null, - "PFWInjEna": 0, - "PFWInjEnaRvrt": null, - "PFWInjRvrtTms": null, - "PFWInjRvrtRem": null, - "PFWAbsEna": 0, - "PFWAbsEnaRvrt": null, - "PFWAbsRvrtTms": null, - "PFWAbsRvrtRem": null, - "WMaxLimEna": 0, - "WMaxLim": 1000, - "WMaxLimRvrt": null, - "WMaxLimEnaRvrt": null, - "WMaxLimRvrtTms": null, - "WMaxLimRvrtRem": null, - "WSetEna": null, - "WSetMod": null, - "WSet": null, - "WSetRvrt": null, - "WSetPct": null, - "WSetPctRvrt": null, - "WSetEnaRvrt": null, - "WSetRvrtTms": null, - "WSetRvrtRem": null, - "VarSetEna": null, - "VarSetMod": null, - "VarSetPri": null, - "VarSet": null, - "VarSetRvrt": null, - "VarSetPct": null, - "VarSetPctRvrt": null, - "VarSetRvrtTms": null, - "VarSetRvrtRem": null, - "RGra": null, - "PF_SF": -3, - "WMaxLim_SF": -1, - "WSet_SF": null, - "WSetPct_SF": null, - "VarSet_SF": null, - "VarSetPct_SF": null, - "PFWInj": { - "PF": 950, - "Ext": 1 - }, - "PFWInjRvrt": { - "PF": null, - "Ext": null - }, - "PFWAbs": { - "PF": null, - "Ext": null - }, - "PFWAbsRvrt": { - "PF": null, - "Ext": null - } - }, - { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - }, - { - "ID": 706, - "Ena": 0, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 2, - "NCrv": 2, - "RvrtTms": null, - "RvrtRem": null, - "RvrtCrv": null, - "V_SF": 0, - "DeptRef_SF": 0, - "Crv": [ - { - "ActPt": 2, - "DeptRef": 1, - "RspTms": 10, - "ReadOnly": 1, - "Pt": [ - { - "V": 106, - "W": 100 - }, - { - "V": 110, - "W": 0 - } - ] - }, - { - "ActPt": 2, - "DeptRef": 1, - "RspTms": 5, - "ReadOnly": 0, - "Pt": [ - { - "V": 105, - "W": 100 - }, - { - "V": 109, - "W": 0 - } - ] - } - ] - }, - { - "ID": 707, - "L": null, - "Ena": 1, - "CrvSt": null, - "AdptCrvReq": null, - "AdptCrvRslt": null, - "NPt": 1, - "NCrvSet": 1, - "V_SF": -2, - "Tms_SF": 0, - "Crv": [ - { - "MustTrip": { - "ActPt": 1, - "Pt": [ - { - "V": 5000, - "Tms": 5 - } - ] - }, - "MayTrip": { - "ActPt": 1, - "Pt": [ - { - "V": 7000, - "Tms": 5 - } - ] - }, - "MomCess": { - "ActPt": 1, - "Pt": [ - { - "V": 6000, - "Tms": 5 - } - ] - } - } - ] - }, - { - "ID": 708, - "L": null, - "Ena": 1, - "CrvSt": null, - "AdptCrvReq": null, - "AdptCrvRslt": null, - "NPt": 1, - "NCrvSet": 1, - "V_SF": -2, - "Tms_SF": 0, - "Crv": [ - { - "MustTrip": { - "ActPt": 1, - "Pt": [ - { - "V": 12000, - "Tms": 5 - } - ] - }, - "MayTrip": { - "ActPt": 1, - "Pt": [ - { - "V": 10000, - "Tms": 5 - } - ] - }, - "MomCess": { - "ActPt": 1, - "Pt": [ - { - "V": 10000, - "Tms": 5 - } - ] - } - } - ] - }, - { - "ID": 709, - "L": null, - "Ena": 1, - "CrvSt": null, - "AdptCrvReq": null, - "AdptCrvRslt": null, - "NPt": 1, - "NCrvSet": 1, - "Freq_SF": null, - "Tms_SF": -2, - "Crv": [ - { - "MustTrip": { - "ActPt": 1, - "Pt": [ - { - "Freq": 5300, - "Tms": 5 - } - ] - }, - "MayTrip": { - "ActPt": 1, - "Pt": [ - { - "Freq": 5850, - "Tms": 5 - } - ] - }, - "MomCess": { - "ActPt": 1, - "Pt": [ - { - "Freq": 5850, - "Tms": 5 - } - ] - } - } - ] - }, - { - "ID": 710, - "L": null, - "Ena": null, - "CrvSt": null, - "AdptCrvReq": null, - "AdptCrvRslt": null, - "NPt": 1, - "NCrvSet": 1, - "Freq_SF": null, - "Tms_SF": -2, - "Crv": [ - { - "MustTrip": { - "ActPt": 1, - "Pt": [ - { - "Freq": 6500, - "Tms": 5 - } - ] - }, - "MayTrip": { - "ActPt": 1, - "Pt": [ - { - "Freq": 6050, - "Tms": 5 - } - ] - }, - "MomCess": { - "ActPt": 1, - "Pt": [ - { - "Freq": 6050, - "Tms": 5 - } - ] - } - } - ] - }, - { - "ID": 711, - "L": null, - "Ena": null, - "CrvSt": null, - "AdptCrvReq": null, - "AdptCrvRslt": null, - "NCtl": 1, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "Db_SF": -2, - "K_SF": -2, - "RspTms_SF": 0, - "Ctl": [ - { - "DbOf": 60030, - "DbUf": 59970, - "KOf": 40, - "KUf": 40, - "RspTms": 600 - } - ] - }, - { - "ID": 712, - "L": null, - "Ena": null, - "CrvSt": null, - "AdptCrvReq": null, - "AdptCrvRslt": null, - "NPt": 1, - "NCrv": 1, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 1, - "DeptRef": null, - "Pri": null, - "ReadOnly": null, - "Pt": [ - { - "W": null, - "Var": null - } - ] - } - ] - }, - { - "ID": 713, - "L": null, - "PrtAlrms": null, - "NPrt": 1, - "DCA": null, - "DCW": null, - "DCWhInj": null, - "DCWhAbs": null, - "DCA_SF": null, - "DCV_SF": null, - "DCW_SF": null, - "DCWH_SF": null, - "Prt": [ - { - "PrtTyp": null, - "ID": null, - "IDStr": null, - "DCA": null, - "DCV": null, - "DCW": null, - "DCWhInj": null, - "DCWhAbs": null, - "Tmp": null, - "DCSt": null, - "DCAlrm": null - } - ] - }, - { - "ID": 65535, - "L": 0 - } - ] -} \ No newline at end of file diff --git a/build/lib/sunspec2/tests/test_data/smdx_304.csv b/build/lib/sunspec2/tests/test_data/smdx_304.csv deleted file mode 100644 index 0580e49..0000000 --- a/build/lib/sunspec2/tests/test_data/smdx_304.csv +++ /dev/null @@ -1,8 +0,0 @@ -Address Offset,Group Offset,Name,Value,Count,Type,Size,Scale Factor,Units,RW Access (RW),Mandatory (M),Static (S),Label,Description,Detailed Description -,,inclinometer,,,group,,,,,,,Inclinometer Model,Include to support orientation measurements, -0,,ID,304,,uint16,,,,,M,S,Model ID,Model identifier, -1,,L,,,uint16,,,,,M,S,Model Length,Model length, -,,inclinometer.incl,,0,group,,,,,,,,, -,0,Inclx,,,int32,,-2,Degrees,,M,,X,X-Axis inclination, -,2,Incly,,,int32,,-2,Degrees,,,,Y,Y-Axis inclination, -,4,Inclz,,,int32,,-2,Degrees,,,,Z,Z-Axis inclination, diff --git a/build/lib/sunspec2/tests/test_data/wb_701-705.xlsx b/build/lib/sunspec2/tests/test_data/wb_701-705.xlsx deleted file mode 100644 index 3edd65ed83dbaa2a42e5a0615a9f6cc833864dc3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34344 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 diff --git a/build/lib/sunspec2/tests/test_device.py b/build/lib/sunspec2/tests/test_device.py deleted file mode 100644 index 110946f..0000000 --- a/build/lib/sunspec2/tests/test_device.py +++ /dev/null @@ -1,2845 +0,0 @@ -import sunspec2.device as device -import sunspec2.mdef as mdef -import sunspec2.mb as mb -import pytest - - -def test_get_model_info(): - model_info = device.get_model_info(705) - assert model_info[0]['id'] == 705 - assert model_info[1] - assert model_info[2] == 15 - - -def test_check_group_count(): - gdef = {'count': 'NPt'} - gdef2 = {'groups': [{'test': 'test'}, {'count': 'NPt'}]} - gdef3 = {'test1': 'test2', 'groups': [{'test3': 'test3'}, {'test4': 'test4'}]} - assert device.check_group_count(gdef) is True - assert device.check_group_count(gdef2) is True - assert device.check_group_count(gdef3) is False - - -def test_get_model_def(): - - with pytest.raises(mdef.ModelDefinitionError) as exc1: - device.get_model_def('z') - assert 'Invalid model id' in str(exc1.value) - - with pytest.raises(Exception) as exc2: - device.get_model_def('000') - assert 'Model definition not found for model' in str(exc2.value) - - assert device.get_model_def(704)['id'] == 704 - - -def test_add_mappings(): - group_def = { - "group": { - "groups": [ - { - "name": "PFWInj", - "points": [ - { - "access": "RW", - "desc": "Power factor setpoint when injecting active power.", - "label": "Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - }, - ], - "type": "sync" - }, - { - "name": "PFWInjRvrt", - "points": [ - { - "access": "RW", - "desc": "Reversion power factor setpoint when injecting active power.", - "label": "Reversion Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - } - ], - "type": "sync" - } - ], - "name": "DERCtlAC", - "points": [ - { - "name": "ID", - "static": "S", - "type": "uint16", - "value": 704 - } - ], - "type": "group" - }, - "id": 704 - } - - group_def_w_mappings = { - "group": { - "groups": [ - { - "name": "PFWInj", - "points": [ - { - "access": "RW", - "desc": "Power factor setpoint when injecting active power.", - "label": "Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - } - ], - "type": "sync", - "point_defs": { - "PF": { - "access": "RW", - "desc": "Power factor setpoint when injecting active power.", - "label": "Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - } - }, - "group_defs": {} - }, - { - "name": "PFWInjRvrt", - "points": [ - { - "access": "RW", - "desc": "Reversion power factor setpoint when injecting active power.", - "label": "Reversion Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - } - ], - "type": "sync", - "point_defs": { - "PF": { - "access": "RW", - "desc": "Reversion power factor setpoint when injecting active power.", - "label": "Reversion Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - } - }, - "group_defs": {} - } - ], - "name": "DERCtlAC", - "points": [ - { - "name": "ID", - "static": "S", - "type": "uint16", - "value": 704 - } - ], - "type": "group", - "point_defs": { - "ID": { - "name": "ID", - "static": "S", - "type": "uint16", - "value": 704 - } - }, - "group_defs": { - "PFWInj": { - "name": "PFWInj", - "points": [ - { - "access": "RW", - "desc": "Power factor setpoint when injecting active power.", - "label": "Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - } - ], - "type": "sync", - "point_defs": { - "PF": { - "access": "RW", - "desc": "Power factor setpoint when injecting active power.", - "label": "Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - } - }, - "group_defs": {} - }, - "PFWInjRvrt": { - "name": "PFWInjRvrt", - "points": [ - { - "access": "RW", - "desc": "Reversion power factor setpoint when injecting active power.", - "label": "Reversion Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - } - ], - "type": "sync", - "point_defs": { - "PF": { - "access": "RW", - "desc": "Reversion power factor setpoint when injecting active power.", - "label": "Reversion Power Factor (W Inj) ", - "mandatory": "O", - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - } - }, - "group_defs": {} - } - } - }, - "id": 704 - } - device.add_mappings(group_def['group']) - assert group_def == group_def_w_mappings - - -class TestPoint: - def test___init__(self): - p_def = { - "name": "Ena", - "type": "enum16", - "sf": 'test sf' - } - - p = device.Point(p_def) - assert p.model is None - assert p.pdef == p_def - assert p.info == mb.point_type_info[mdef.TYPE_ENUM16] - assert p.len == 1 - assert p.offset == 0 - assert p.value is None - assert p.dirty is False - assert p.sf == 'test sf' - assert p.sf_required is True - assert p.sf_value is None - - def test__set_data(self): - p_def = { - "name": 'TestPoint', - "type": "uint16" - } - - # bytes - p = device.Point(p_def) - p._set_data(b'\x00\x03') - assert p.value == 3 - assert not p.dirty - - # dict - data = {"TestPoint": 3} - p2 = device.Point(p_def) - p2._set_data(data) - assert p2.value == 3 - - def test_value_getter(self): - p_def = { - "name": "TestPoint", - "type": "uint16", - } - p = device.Point(p_def) - p.value = 4 - assert p.value == 4 - - def test_value_setter(self): - p_def = { - "name": "TestPoint", - "type": "uint16", - } - p = device.Point(p_def) - p.value = 4 - assert p.value == 4 - - def test_cvalue_getter(self): - p_def = { - "name": "TestPoint", - "type": "uint16", - } - p = device.Point(p_def) - p.sf_required = True - p.sf_value = 3 - p.value = 4 - assert p.cvalue == 4000.0 - - def test_cvalue_setter(self): - p_def = { - "name": "TestPoint", - "type": "uint16" - } - p = device.Point(p_def) - p.sf_required = True - p.sf_value = 3 - p.cvalue = 3000 - assert p.value == 3 - - def test_get_value(self): - p_def = { - "access": "RW", - "desc": "Power factor setpoint when injecting active power.", - "label": "Power Factor (W Inj) ", - "name": "PF", - "type": "uint16" - } - p = device.Point(p_def) - p.value = 3 - assert p.get_value() == 3 - - p2 = device.Point(p_def) - assert p2.get_value() is None - - # pdef w/ sf - pdef_sf = { - "name": "TestPoint", - "type": "uint16", - "sf": "TestSF" - } - # sf point - sf_p = { - "name": "TestSF", - "value": 3, - "type": "sunssf" - } - - # computed - p_sf = device.Point(sf_p) - p_sf.value = 3 - points = {} - points['TestSF'] = p_sf - m2 = device.Model() - setattr(m2, 'points', points) - - g = device.Group() - setattr(g, 'points', points) - - p9 = device.Point(pdef_sf, model=m2) - p9.group = g - p9.value = 2020 - assert p9.get_value(computed=True) == 2020000.0 - - # computed exception - m3 = device.Model() - points2 = {} - setattr(m3, 'points', points2) - - p10 = device.Point(pdef_sf, model=m3) - g2 = device.Group() - setattr(g2, 'points', {}) - p10.group = g2 - p10.value = 2020 - with pytest.raises(device.ModelError) as exc: - p10.get_value(computed=True) - assert 'Scale factor TestSF for point TestPoint not found' in str(exc.value) - - def test_set_value(self): - p_def = { - "access": "RW", - "desc": "Power factor setpoint when injecting active power.", - "label": "Power Factor (W Inj) ", - "name": "PF", - "type": "uint16" - } - p = device.Point(p_def) - p.set_value(3) - assert p.value == 3 - - # test computed - pdef_computed = { - "name": "TestingComputed", - "type": "uint16", - "sf": "TestSF" - } - p_SF = device.Point() - p_SF.value = 2 - - points = {} - points['TestSF'] = p_SF - m = device.Model() - setattr(m, 'points', points) - - p3 = device.Point(pdef_computed, model=m) - g = device.Group - setattr(g, 'points', {}) - p3.group = g - p3.set_value(1000, computed=True, dirty=True) - assert p3.value == 10 - assert p3.dirty - - # test exceptions - p2_sf = device.Point() - m2 = device.Model() - points2 = {} - points2['TestSF'] = p2_sf - setattr(m2, 'points', points2) - - p4 = device.Point(pdef_computed, model=m2) - p4.group = g - with pytest.raises(device.ModelError) as exc: - p4.set_value(1000, computed=True) - assert 'SF field TestSF value not initialized for point TestingComputed' in str(exc.value) - - def test_get_mb(self): - p_def = { - "name": "ESVLo", - "type": "uint16", - } - p = device.Point(p_def) - p.value = 3 - assert p.get_mb() == b'\x00\x03' - p.value = None - assert p.get_mb() == b'\xff\xff' - assert p.get_mb(computed=True) == b'\xff\xff' - - # computed - p.value = 3 - p.sf_required = True - p.sf_value = 4 - assert p.get_mb(computed=True) == b'\x75\x30' - - def test_set_mb(self): - p_def = { - "name": "ESVLo", - "type": "uint16", - } - m = device.Model() - g = device.Group() - g.points = {} - p3 = device.Point(p_def, m) - p3.group = g - p3.set_mb(None) - assert p3.model.error_info == "Error setting value for ESVLo: object of type 'NoneType' has no len()\n" - - # exceptions - p_def2 = { - "name": "ESVLo", - "type": "uint16", - "sf": "TestSF" - } - p_sf = device.Point() - points = {} - points['TestSF'] = p_sf - setattr(m, 'points', points) - - m.error_info = '' - p4 = device.Point(p_def2, model=m) - p4.group = g - p4.set_mb(b'\x00\x03', computed=True) - assert m.error_info == 'Error setting value for ESVLo: SF field TestSF value not initialized for point ESVLo\n' - - del m.points['TestSF'] - m.error_info = '' - p5 = device.Point(p_def2, model=m) - p5.group = g - p5.set_mb(b'\x00\x04', computed=True) - assert m.error_info == 'Error setting value for ESVLo: Scale factor TestSF for point ESVLo not found\n' - - # test computed - pdef_computed = { - "name": "TestingComputed", - "type": "uint16", - "sf": "TestSF" - } - p_SF = device.Point() - p_SF.value = 2 - - points = {} - points['TestSF'] = p_SF - m = device.Model() - setattr(m, 'points', points) - - p6 = device.Point(pdef_computed, model=m) - p6.group = g - p6.set_mb(b'\x0b\xb8', computed=True, dirty=True) - assert p6.value == 30 - assert p6.dirty - - -class TestGroup: - def test___init__(self): - g_704 = { - "group": { - "groups": [ - { - "name": "PFWInj", - "points": [ - { - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - }, - ], - "type": "sync" - }, - { - "name": "PFWInjRvrt", - "points": [ - { - "name": "Ext", - "type": "enum16" - } - ], - "type": "sync" - }, - ], - "name": "DERCtlAC", - "points": [ - { - "name": "ID", - "type": "uint16", - "value": 704 - }, - { - "name": "L", - "static": "S", - "type": "uint16" - }, - - { - "name": "PFWInjRvrtTms", - "type": "uint32", - }, - { - "name": "PFWInjRvrtRem", - "type": "uint32", - }, - { - "name": "PFWAbsEna", - "type": "enum16" - }, - { - "name": "PF_SF", - "type": "sunssf" - } - ], - "type": "group" - }, - "id": 704 - } - g = device.Group(g_704['group']) - - assert g.gdef == g_704['group'] - assert g.model is None - assert g.gname == 'DERCtlAC' - assert g.offset == 0 - assert g.len == 10 - assert len(g.points) == 6 - assert len(g.groups) == 2 - assert g.points_len == 8 - assert g.group_class == device.Group - - def test___getattr__(self): - g_704 = { - "group": { - "groups": [ - { - "name": "PFWInj", - "points": [ - { - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - }, - ], - "type": "sync" - }, - { - "name": "PFWInjRvrt", - "points": [ - { - "name": "Ext", - "type": "enum16" - } - ], - "type": "sync" - }, - ], - "name": "DERCtlAC", - "points": [ - { - "name": "ID", - "type": "uint16", - "value": 704 - }, - { - "name": "L", - "static": "S", - "type": "uint16" - }, - - { - "name": "PFWInjRvrtTms", - "type": "uint32", - }, - { - "name": "PFWInjRvrtRem", - "type": "uint32", - }, - { - "name": "PFWAbsEna", - "type": "enum16" - }, - { - "name": "PF_SF", - "type": "sunssf" - } - ], - "type": "group" - }, - "id": 704 - } - g = device.Group(g_704['group']) - with pytest.raises(AttributeError) as exc: - g.qwerty - assert "Group object has no attribute qwerty" in str(exc.value) - assert g.ID - assert g.PFWAbsEna - - def test__group_data(self): - gdef_705 = { - "group": { - "groups": [ - { - "count": "NCrv", - "groups": [ - { - "count": "NPt", - "name": "Pt", - "points": [ - { - "name": "V", - "sf": "V_SF", - "type": "uint16", - }, - { - "name": "Var", - "sf": "DeptRef_SF", - "type": "int16", - "units": "VarPct" - } - ], - "type": "group" - } - ], - "name": "Crv", - "points": [ - { - "name": "ActPt", - "type": "uint16" - }, - { - "name": "DeptRef", - "symbols": [ - { - "name": "W_MAX_PCT", - "value": 1 - }, - { - "name": "VAR_MAX_PCT", - "value": 2 - }, - { - "name": "VAR_AVAL_PCT", - "value": 3 - } - ], - "type": "enum16" - }, - { - "name": "Pri", - "symbols": [ - { - "name": "ACTIVE", - "value": 1 - }, - { - "name": "REACTIVE", - "value": 2 - }, - { - "name": "IEEE_1547", - "value": 3 - }, - { - "name": "PF", - "value": 4 - }, - { - "name": "VENDOR", - "value": 5 - } - ], - "type": "enum16" - }, - { - "name": "VRef", - "type": "uint16" - }, - { - "name": "VRefAuto", - "symbols": [ - { - "name": "DISABLED", - "value": 0 - }, - { - "name": "ENABLED", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "VRefTms", - "type": "uint16" - }, - { - "name": "RspTms", - "type": "uint16" - }, - { - "name": "ReadOnly", - "symbols": [ - { - "name": "RW", - "value": 0 - }, - { - "name": "R", - "value": 1 - } - ], - "type": "enum16" - } - ], - "type": "group" - } - ], - "name": "DERVoltVar", - "points": [ - { - "name": "ID", - "type": "uint16", - "value": 705 - }, - { - "name": "L", - "type": "uint16" - }, - { - "name": "Ena", - "symbols": [ - { - "name": "DISABLED", - "value": 0 - }, - { - "name": "ENABLED", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "CrvSt", - "symbols": [ - { - "name": "INACTIVE", - "value": 0 - }, - { - "name": "ACTIVE", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "AdptCrvReq", - "type": "uint16" - }, - { - "name": "AdptCrvRslt", - "symbols": [ - { - "name": "IN_PROGRESS", - "value": 0 - }, - { - "name": "COMPLETED", - "value": 1 - }, - { - "name": "FAILED", - "value": 2 - } - ], - "type": "enum16" - }, - { - "name": "NPt", - "type": "uint16" - }, - { - "name": "NCrv", - "type": "uint16" - }, - { - "name": "RvrtTms", - "type": "uint32" - }, - { - "name": "RvrtRem", - "type": "uint32" - }, - { - "name": "RvrtCrv", - "type": "uint16" - }, - { - "name": "V_SF", - "type": "sunssf" - }, - { - "name": "DeptRef_SF", - "type": "sunssf" - } - ], - "type": "group" - }, - "id": 705 - } - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - g = device.Group() - assert g._group_data(gdata_705, 'Crv') == [{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, - {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]}, - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, - 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, - {'V': 10200, 'Var': 0}, {'V': 10600, 'Var': -4000}]}, - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, - 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, - {'V': 10500, 'Var': 0}, {'V': 10800, 'Var': -2000}]}] - - assert g._group_data(gdata_705['Crv'], index=0) == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, - 'VRefAuto': 0, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, - {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} - - def test__get_data_group_count(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - g = device.Group() - assert g._get_data_group_count(gdata_705['Crv']) == 3 - - def test__init_repeating_group(self): - gdef_705 = { - "group": { - "groups": [ - { - "count": "NCrv", - "groups": [ - { - "count": "NPt", - "name": "Pt", - "points": [ - { - "name": "V", - "sf": "V_SF", - "type": "uint16", - }, - { - "name": "Var", - "sf": "DeptRef_SF", - "type": "int16", - "units": "VarPct" - } - ], - "type": "group" - } - ], - "name": "Crv", - "points": [ - { - "name": "ActPt", - "type": "uint16" - }, - { - "name": "DeptRef", - "symbols": [ - { - "name": "W_MAX_PCT", - "value": 1 - }, - { - "name": "VAR_MAX_PCT", - "value": 2 - }, - { - "name": "VAR_AVAL_PCT", - "value": 3 - } - ], - "type": "enum16" - }, - { - "name": "Pri", - "symbols": [ - { - "name": "ACTIVE", - "value": 1 - }, - { - "name": "REACTIVE", - "value": 2 - }, - { - "name": "IEEE_1547", - "value": 3 - }, - { - "name": "PF", - "value": 4 - }, - { - "name": "VENDOR", - "value": 5 - } - ], - "type": "enum16" - }, - { - "name": "VRef", - "type": "uint16" - }, - { - "name": "VRefAuto", - "symbols": [ - { - "name": "DISABLED", - "value": 0 - }, - { - "name": "ENABLED", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "VRefTms", - "type": "uint16" - }, - { - "name": "RspTms", - "type": "uint16" - }, - { - "name": "ReadOnly", - "symbols": [ - { - "name": "RW", - "value": 0 - }, - { - "name": "R", - "value": 1 - } - ], - "type": "enum16" - } - ], - "type": "group" - } - ], - "name": "DERVoltVar", - "points": [ - { - "name": "ID", - "type": "uint16", - "value": 705 - }, - { - "name": "L", - "type": "uint16" - }, - { - "name": "Ena", - "symbols": [ - { - "name": "DISABLED", - "value": 0 - }, - { - "name": "ENABLED", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "CrvSt", - "symbols": [ - { - "name": "INACTIVE", - "value": 0 - }, - { - "name": "ACTIVE", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "AdptCrvReq", - "type": "uint16" - }, - { - "name": "AdptCrvRslt", - "symbols": [ - { - "name": "IN_PROGRESS", - "value": 0 - }, - { - "name": "COMPLETED", - "value": 1 - }, - { - "name": "FAILED", - "value": 2 - } - ], - "type": "enum16" - }, - { - "name": "NPt", - "type": "uint16" - }, - { - "name": "NCrv", - "type": "uint16" - }, - { - "name": "RvrtTms", - "type": "uint32" - }, - { - "name": "RvrtRem", - "type": "uint32" - }, - { - "name": "RvrtCrv", - "type": "uint16" - }, - { - "name": "V_SF", - "type": "sunssf" - }, - { - "name": "DeptRef_SF", - "type": "sunssf" - } - ], - "type": "group" - }, - "id": 705 - } - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - - m = device.Model(705, data=gdata_705) - - pdef_NPt = {"name": "NPt", "type": "uint16"} - p_NPt = device.Point(pdef_NPt) - p_NPt.value = 4 - - pdef_NCrv = {"name": "NCrv", "type": "uint16"} - p_NCrv = device.Point(pdef_NCrv) - points = {'NPt': p_NPt, 'NCrv': p_NCrv} - setattr(m, 'points', points) - - g2 = device.Group(gdef_705['group']['groups'][0], m) - - with pytest.raises(device.ModelError) as exc: - g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) - assert 'Count field NCrv value not initialized for group Crv' in str(exc.value) - - # set value for NCrv count and reset the points attribute on model - p_NCrv.value = 3 - setattr(m, 'points', points) - groups = g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) - assert len(groups) == 3 - assert len(groups[0].groups['Pt']) == 4 - - def test_get_dict(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m2 = device.Model(705, data=gdata_705) - assert m2.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefAutoEna': None, 'VRefTms': 5, 'RspTms': 6, - 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, - {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} - - # test computed - m2.groups['Crv'][0].points['DeptRef'].sf_required = True - m2.groups['Crv'][0].points['DeptRef'].sf_value = 3 - m2.groups['Crv'][0].points['Pri'].sf_required = True - m2.groups['Crv'][0].points['Pri'].sf_value = 3 - computed_dict = m2.groups['Crv'][0].get_dict(computed=True) - assert computed_dict['DeptRef'] == 1000.0 - assert computed_dict['Pri'] == 1000.0 - - def test_set_dict(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = device.Model(705, data=gdata_705) - assert m.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefAutoEna': None, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, - {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} - - new_dict = {'ActPt': 4, 'DeptRef': 4000, 'Pri': 5000, 'VRef': 3, 'VRefAuto': 2, 'VRefAutoEna': None, - 'VRefTms': 2, 'RspTms': 2, 'ReadOnly': 2, - 'Pt': [{'V': 111, 'Var': 111}, {'V': 123, 'Var': 1112}, {'V': 111, 'Var': 111}, - {'V': 123, 'Var': -1112}]} - - m.groups['Crv'][0].set_dict(new_dict, dirty=True) - assert m.groups['Crv'][0].get_dict() == new_dict - assert m.groups['Crv'][0].VRef.value == 3 - assert m.groups['Crv'][0].VRef.dirty - assert m.groups['Crv'][0].Pri.dirty - - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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 - m.groups['Crv'][0].set_dict(new_dict, computed=True) - computed_dict = m.groups['Crv'][0].get_dict() - assert computed_dict['DeptRef'] == 4.0 - assert computed_dict['Pri'] == 5.0 - - def test_get_json(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = device.Model(705, data=gdata_705) - assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \ - ''' "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000},''' + \ - ''' {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0}, {"V": 10700, "Var": -3000}]}''' - - # test computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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,''' + \ - ''' "VRefTms": 5, "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): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = device.Model(705, data=gdata_705) - assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ - ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5,''' + \ - ''' "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000},''' + \ - ''' {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0},''' + \ - ''' {"V": 10700, "Var": -3000}]}''' - - json_to_set = '''{"ActPt": 4, "DeptRef": 9999, "Pri": 9999, "VRef": 99, "VRefAuto": 88,''' + \ - ''' "VRefAutoEna": null, "VRefTms": 88, "RspTms": 88, "ReadOnly": 77, "Pt":''' + \ - ''' [{"V": 77, "Var": 66}, {"V": 55, "Var": 44}, {"V": 33, "Var": 22},''' + \ - ''' {"V": 111, "Var": -2222}]}''' - - m.groups['Crv'][0].set_json(json_to_set) - assert m.groups['Crv'][0].get_json() == json_to_set - assert m.groups['Crv'][0].DeptRef.value == 9999 - - # test computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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 - m.groups['Crv'][0].set_json(json_to_set, computed=True, dirty=True) - assert m.groups['Crv'][0].points['DeptRef'].value == 9 - assert m.groups['Crv'][0].points['DeptRef'].dirty - assert m.groups['Crv'][0].points['Pri'].value == 9 - assert m.groups['Crv'][0].points['Pri'].dirty - - def test_get_mb(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = device.Model(705, data=gdata_705) - assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00' \ - b'\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' - - # test computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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' \ - b'\x00\x05\x00\x06\x00\x01\x00\\\x00\x1e\x00`\x00' \ - b'\x00\x00g\x00\x00\x00k\xff\xe2' - - def test_set_mb(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = device.Model(705, data=gdata_705) - assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00' \ - b'\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' - bs = b'\x00\x04\x03\xe7\x03x\x03\t\x02\x9a\x02+\x01\xbc\x01M' \ - b'\x00\xde\x00o\x00\xde\x01M\x01\xbc\x02+\x02\x9a\xfc\xf7\xf4H' - - m.groups['Crv'][0].set_mb(bs, dirty=True) - assert m.groups['Crv'][0].get_mb() == bs - assert m.groups['Crv'][0].DeptRef.value == 999 - assert m.groups['Crv'][0].DeptRef.dirty - - # test computed - # set points DeptRef and Pri to 3000 w/ byte string - computed_bs = b'\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<' \ - b'\x00\x00)\xcc\xf4H' - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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 - m.groups['Crv'][0].set_mb(computed_bs, computed=True) - assert m.groups['Crv'][0].points['DeptRef'].value == 3 - assert m.groups['Crv'][0].points['Pri'].value == 3 - - -class TestModel: - def test__init__(self): - m = device.Model(704) - assert m.model_id == 704 - assert m.model_addr == 0 - assert m.model_len == 0 - assert m.model_def['id'] == 704 - assert m.error_info == '' - assert m.gdef['name'] == 'DERCtlAC' - assert m.mid is None - assert m.device is None - - assert m.model == m - m2 = device.Model('abc') - assert m2.error_info == 'Invalid model id: abc\n' - - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - # test repeating group model - m2 = device.Model(705, data=gdata_705) - assert m2.model_id == 705 - assert m2.model_addr == 0 - assert m2.model_len == 0 - assert m2.model_def['id'] == 705 - assert m2.error_info == '' - assert m2.gdef['name'] == 'DERVoltVar' - assert m2.mid is None - assert m2.device is None - - def test__error(self): - m = device.Model(704) - m.add_error('test error') - assert m.error_info == 'test error\n' - - -class TestDevice: - def test__init__(self): - d = device.Device() - assert d.name is None - assert d.did is None - assert d.models == {} - assert d.model_list == [] - assert d.model_class == device.Model - - def test__get_attr__(self): - d = device.Device() - m = device.Model() - setattr(m, 'model_id', 'mid_test') - setattr(m, 'gname', 'group_test') - d.add_model(m) - assert d.mid_test - - with pytest.raises(AttributeError) as exc: - d.foo - assert "\'Device\' object has no attribute \'foo\'" in str(exc.value) - - def test_scan(self): - pass - - def test_add_model(self): - d = device.Device() - m = device.Model() - setattr(m, 'model_id', 'mid_test') - setattr(m, 'gname', 'group_test') - d.add_model(m) - assert d.models['mid_test'] - assert d.models['group_test'] - assert m.device == d - - def test_get_dict(self): - d = device.Device() - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = device.Model(705, data=gdata_705) - d.add_model(m) - assert d.get_dict() == {'name': None, 'did': None, 'models': [ - {'ID': 705, 'L': 64, 'Ena': 1, 'CrvSt': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, - 'RvrtTms': 0, 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'Crv': [ - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, - 'RspTms': 6, 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, {'V': 10300, 'Var': 0}, - {'V': 10700, 'Var': -3000}]}, - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, - 'RspTms': 6, 'ReadOnly': 0, - 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, {'V': 10200, 'Var': 0}, - {'V': 10600, 'Var': -4000}]}, - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, - 'RspTms': 6, 'ReadOnly': 0, - 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, {'V': 10500, 'Var': 0}, - {'V': 10800, 'Var': -2000}]}], 'mid': None, 'error': '', 'model_id': 705}]} - - - # computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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_dict(computed=True) == {'name': None, 'did': None, 'models': [ - {'ID': 705, 'L': 64, 'Ena': 1, 'CrvSt': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, - 'RvrtTms': 0, 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'Crv': [ - {'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, - 'VRefTms': 5, '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, 'VRefTms': 5, - '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, 'VRefTms': 5, - '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}]} - - def test_get_json(self): - d = device.Device() - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = device.Model(705, data=gdata_705) - d.add_model(m) - assert d.get_json() == '''{"name": null, "did": null, "models": [{"ID": 705, "L": 64, "Ena": 1,''' + \ - ''' "CrvSt": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3,''' + \ - ''' "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2,''' + \ - ''' "Crv": [{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \ - ''' "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 1, ''' + \ - '''"Pt": [{"V": 9200, "Var": 3000}, {"V": 9670, "Var": 0}, {"V": 10300,''' + \ - ''' "Var": 0}, {"V": 10700, "Var": -3000}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1,''' + \ - ''' "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6,''' + \ - ''' "ReadOnly": 0, "Pt": [{"V": 9300, "Var": 3000}, {"V": 9570, "Var": 0},''' + \ - ''' {"V": 10200, "Var": 0}, {"V": 10600, "Var": -4000}]}, {"ActPt": 4,''' + \ - ''' "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null,''' + \ - ''' "VRefTms": 5, "RspTms": 6, "ReadOnly": 0, "Pt": [{"V": 9400, "Var": 2000},''' + \ - ''' {"V": 9570, "Var": 0}, {"V": 10500, "Var": 0}, {"V": 10800, "Var": -2000}]}],''' + \ - ''' "mid": null, "error": "", "model_id": 705}]}''' - - # computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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": 64,''' + \ - ''' "Ena": 1, "CrvSt": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0,''' + \ - ''' "NPt": 4, "NCrv": 3, "RvrtTms": 0, "RvrtRem": 0,''' + \ - ''' "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2, "Crv": [{"ActPt": 4,''' + \ - ''' "DeptRef": 1000.0, "Pri": 1000.0, "VRef": 1, "VRefAuto": 0,''' + \ - ''' "VRefAutoEna": null, "VRefTms": 5, "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, "VRefTms": 5, "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": null, "VRefTms": 5, "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}]}''' - - def test_get_mb(self): - d = device.Device() - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = device.Model(705, data=gdata_705) - d.add_model(m) - assert d.get_mb() == b"\x02\xc1\x00@\x00\x01\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\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff" \ - b"\xff\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04" \ - b"\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00" \ - b"\x00'\xd8\x00\x00)h\xf0`\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00" \ - b"\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" - - # computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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\x00@\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00' \ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x03' \ - b'\xe8\x03\xe8\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x01\x00' \ - b'\\\x00\x1e\x00`\x00\x00\x00g\x00\x00\x00k\xff\xe2\x00\x04\x00\x01' \ - b'\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00\x00]\x00' \ - b'\x1e\x00_\x00\x00\x00f\x00\x00\x00j\xff\xd8\x00\x04\x00\x01\x00' \ - b'\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00\x00^\x00\x14' \ - b'\x00_\x00\x00\x00i\x00\x00\x00l\xff\xec' - - def test_set_mb(self): - d = device.Device() - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = device.Model(705, data=gdata_705) - d.add_model(m) - assert d.get_mb() == b"\x02\xc1\x00@\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ - b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00" \ - b"\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01" \ - b"\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8" \ - b"\x00\x00)h\xf0`\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00" \ - b"\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" - - # DeptRef and Pri set to 3000 in byte string - bs = b"\x02\xc1\x00?\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ - b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00" \ - b"\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01\x00\x01\x00" \ - b"\x01\x00\x00\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8\x00\x00)h\xf0`\x00\x04" \ - b"\x00\x01\x00\x01\x00\x01\x00\x00\x00\x05\x00\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" - d.set_mb(bs, dirty=True) - assert m.groups['Crv'][0].DeptRef.value == 3000 - assert m.groups['Crv'][0].DeptRef.dirty - assert m.groups['Crv'][0].Pri.value == 3000 - assert m.groups['Crv'][0].Pri.dirty - - # computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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 - d.set_mb(bs, computed=True, dirty=False) - assert m.groups['Crv'][0].DeptRef.value == 3 - assert not m.groups['Crv'][0].DeptRef.dirty - assert m.groups['Crv'][0].Pri.value == 3 - assert not m.groups['Crv'][0].Pri.dirty - - def test_find_mid(self): - d = device.Device() - m = device.Model() - setattr(m, 'model_id', 'mid_test') - setattr(m, 'gname', 'group_test') - setattr(m, 'mid', 'mid_test') - d.add_model(m) - assert d.find_mid('mid_test') == m diff --git a/build/lib/sunspec2/tests/test_file_client.py b/build/lib/sunspec2/tests/test_file_client.py deleted file mode 100644 index 4bf9192..0000000 --- a/build/lib/sunspec2/tests/test_file_client.py +++ /dev/null @@ -1,2668 +0,0 @@ -import sunspec2.file.client as file_client -import sunspec2.mdef as mdef -import sunspec2.mb as mb -import sunspec2.device as device -import pytest - - -class TestFileClientPoint: - def test___init__(self): - p_def = { - "name": "Ena", - "type": "enum16", - "sf": 'test sf' - } - - p = file_client.FileClientPoint(p_def) - assert p.model is None - assert p.pdef == p_def - assert p.info == mb.point_type_info[mdef.TYPE_ENUM16] - assert p.len == 1 - assert p.offset == 0 - assert p.value is None - assert p.dirty is False - assert p.sf == 'test sf' - assert p.sf_required is True - assert p.sf_value is None - - def test__set_data(self): - p_def = { - "name": 'TestPoint', - "type": "uint16" - } - - # bytes - p = file_client.FileClientPoint(p_def) - p._set_data(b'\x00\x03') - assert p.value == 3 - assert not p.dirty - - # dict - data = {"TestPoint": 3} - p2 = file_client.FileClientPoint(p_def) - p2._set_data(data) - assert p2.value == 3 - - def test_value_getter(self): - p_def = { - "name": "TestPoint", - "type": "uint16", - } - p = file_client.FileClientPoint(p_def) - p.value = 4 - assert p.value == 4 - - def test_value_setter(self): - p_def = { - "name": "TestPoint", - "type": "uint16", - } - p = file_client.FileClientPoint(p_def) - p.value = 4 - assert p.value == 4 - - def test_cvalue_getter(self): - p_def = { - "name": "TestPoint", - "type": "uint16", - } - p = file_client.FileClientPoint(p_def) - p.sf_required = True - p.sf_value = 3 - p.value = 4 - assert p.cvalue == 4000.0 - - def test_cvalue_setter(self): - p_def = { - "name": "TestPoint", - "type": "uint16" - } - p = file_client.FileClientPoint(p_def) - p.sf_required = True - p.sf_value = 3 - p.cvalue = 3000 - assert p.value == 3 - - def test_get_value(self): - p_def = { - "access": "RW", - "desc": "Power factor setpoint when injecting active power.", - "label": "Power Factor (W Inj) ", - "name": "PF", - "type": "uint16" - } - p = file_client.FileClientPoint(p_def) - p.value = 3 - assert p.get_value() == 3 - - p2 = file_client.FileClientPoint(p_def) - assert p2.get_value() is None - - # pdef w/ sf - pdef_sf = { - "name": "TestPoint", - "type": "uint16", - "sf": "TestSF" - } - # sf point - sf_p = { - "name": "TestSF", - "value": 3, - "type": "sunssf" - } - - # computed - p_sf = file_client.FileClientPoint(sf_p) - p_sf.value = 3 - points = {} - points['TestSF'] = p_sf - m2 = file_client.FileClientModel() - setattr(m2, 'points', points) - - p9 = file_client.FileClientPoint(pdef_sf, model=m2) - g = file_client.FileClientGroup() - g.points = {} - p9.group = g - p9.value = 2020 - assert p9.get_value(computed=True) == 2020000.0 - - # computed exception - m3 = file_client.FileClientModel() - points2 = {} - setattr(m3, 'points', points2) - - p10 = file_client.FileClientPoint(pdef_sf, model=m3) - p10.value = 2020 - p10.group = g - with pytest.raises(device.ModelError) as exc: - p10.get_value(computed=True) - assert 'Scale factor TestSF for point TestPoint not found' in str(exc.value) - - def test_set_value(self): - p_def = { - "access": "RW", - "desc": "Power factor setpoint when injecting active power.", - "label": "Power Factor (W Inj) ", - "name": "PF", - "type": "uint16" - } - p = file_client.FileClientPoint(p_def) - p.set_value(3) - assert p.value == 3 - - # test computed - pdef_computed = { - "name": "TestingComputed", - "type": "uint16", - "sf": "TestSF" - } - p_SF = file_client.FileClientPoint() - p_SF.value = 2 - - points = {} - points['TestSF'] = p_SF - m = file_client.FileClientModel() - setattr(m, 'points', points) - - g = file_client.FileClientGroup() - g.points = {} - - p3 = file_client.FileClientPoint(pdef_computed, model=m, group=g) - p3.set_value(1000, computed=True, dirty=True) - assert p3.value == 10 - assert p3.dirty - - # test exceptions - p2_sf = file_client.FileClientPoint() - m2 = file_client.FileClientModel() - points2 = {} - points2['TestSF'] = p2_sf - setattr(m2, 'points', points2) - - p4 = file_client.FileClientPoint(pdef_computed, model=m2, group=g) - with pytest.raises(device.ModelError) as exc: - p4.set_value(1000, computed=True) - assert 'SF field TestSF value not initialized for point TestingComputed' in str(exc.value) - - del m2.points['TestSF'] - with pytest.raises(device.ModelError) as exc: - p4.set_value(1000, computed=True) - assert 'Scale factor TestSF for point TestingComputed not found' in str(exc.value) - - def test_get_mb(self): - p_def = { - "name": "ESVLo", - "type": "uint16", - } - p = file_client.FileClientPoint(p_def) - p.value = 3 - assert p.get_mb() == b'\x00\x03' - p.value = None - assert p.get_mb() == b'\xff\xff' - assert p.get_mb(computed=True) == b'\xff\xff' - - # computed - p.value = 3 - p.sf_required = True - p.sf_value = 4 - assert p.get_mb(computed=True) == b'\x75\x30' - - def test_set_mb(self): - p_def = { - "name": "ESVLo", - "type": "uint16", - } - p = file_client.FileClientPoint(p_def) - - p.set_mb(b'\x00\x03', dirty=True) - assert p.value == 3 - assert p.dirty is True - - # unimplemented - p.set_mb(b'\xff\xff') - assert p.value is None - assert p.sf_value is None - - p2 = file_client.FileClientPoint(p_def) - p2.len = 100 - assert p2.set_mb(b'\x00\x03') == 2 - assert p2.value is None - - m = file_client.FileClientModel() - p3 = file_client.FileClientPoint(p_def, m) - p3.set_mb(None) - assert p3.model.error_info == '''Error setting value for ESVLo: object of type 'NoneType' has no len()\n''' - - # exceptions - p_def2 = { - "name": "ESVLo", - "type": "uint16", - "sf": "TestSF" - } - p_sf = file_client.FileClientPoint() - points = {} - points['TestSF'] = p_sf - setattr(m, 'points', points) - - g = file_client.FileClientGroup() - g.points = {} - - m.error_info = '' - p4 = file_client.FileClientPoint(p_def2, model=m, group=g) - p4.set_mb(b'\x00\x03', computed=True) - assert p4.model.error_info == "Error setting value for ESVLo: SF field TestSF value not initialized for point ESVLo\n" - - m.error_info = '' - del m.points['TestSF'] - p5 = file_client.FileClientPoint(p_def2, model=m, group=g) - p5.set_mb(b'\x00\x04', computed=True) - assert p5.model.error_info == '''Error setting value for ESVLo: Scale factor TestSF for point ESVLo not found\n''' - - # test computed - pdef_computed = { - "name": "TestingComputed", - "type": "uint16", - "sf": "TestSF" - } - p_SF = file_client.FileClientPoint() - p_SF.value = 2 - - points = {} - points['TestSF'] = p_SF - m = file_client.FileClientModel() - setattr(m, 'points', points) - - p3 = file_client.FileClientPoint(pdef_computed, model=m, group=g) - p3.set_mb(b'\x0b\xb8', computed=True, dirty=True) - assert p3.value == 30 - assert p3.dirty - - -class TestFileClientGroup: - def test___init__(self): - g_704 = { - "group": { - "groups": [ - { - "name": "PFWInj", - "points": [ - { - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - }, - ], - "type": "sync" - }, - { - "name": "PFWInjRvrt", - "points": [ - { - "name": "Ext", - "type": "enum16" - } - ], - "type": "sync" - }, - ], - "name": "DERCtlAC", - "points": [ - { - "name": "ID", - "type": "uint16", - "value": 704 - }, - { - "name": "L", - "static": "S", - "type": "uint16" - }, - - { - "name": "PFWInjRvrtTms", - "type": "uint32", - }, - { - "name": "PFWInjRvrtRem", - "type": "uint32", - }, - { - "name": "PFWAbsEna", - "type": "enum16" - }, - { - "name": "PF_SF", - "type": "sunssf" - } - ], - "type": "group" - }, - "id": 704 - } - g = file_client.FileClientGroup(g_704['group']) - - assert g.gdef == g_704['group'] - assert g.model is None - assert g.gname == 'DERCtlAC' - assert g.offset == 0 - assert g.len == 10 - assert len(g.points) == 6 - assert len(g.groups) == 2 - assert g.points_len == 8 - assert g.group_class == file_client.FileClientGroup - - def test___getattr__(self): - g_704 = { - "group": { - "groups": [ - { - "name": "PFWInj", - "points": [ - { - "name": "PF", - "sf": "PF_SF", - "type": "uint16" - }, - ], - "type": "sync" - }, - { - "name": "PFWInjRvrt", - "points": [ - { - "name": "Ext", - "type": "enum16" - } - ], - "type": "sync" - }, - ], - "name": "DERCtlAC", - "points": [ - { - "name": "ID", - "type": "uint16", - "value": 704 - }, - { - "name": "L", - "static": "S", - "type": "uint16" - }, - - { - "name": "PFWInjRvrtTms", - "type": "uint32", - }, - { - "name": "PFWInjRvrtRem", - "type": "uint32", - }, - { - "name": "PFWAbsEna", - "type": "enum16" - }, - { - "name": "PF_SF", - "type": "sunssf" - } - ], - "type": "group" - }, - "id": 704 - } - g = file_client.FileClientGroup(g_704['group']) - with pytest.raises(AttributeError) as exc: - g.qwerty - assert "Group object has no attribute qwerty" in str(exc.value) - assert g.ID - assert g.PFWAbsEna - - def test__group_data(self): - gdef_705 = { - "group": { - "groups": [ - { - "count": "NCrv", - "groups": [ - { - "count": "NPt", - "name": "Pt", - "points": [ - { - "name": "V", - "sf": "V_SF", - "type": "uint16", - }, - { - "name": "Var", - "sf": "DeptRef_SF", - "type": "int16", - "units": "VarPct" - } - ], - "type": "group" - } - ], - "name": "Crv", - "points": [ - { - "name": "ActPt", - "type": "uint16" - }, - { - "name": "DeptRef", - "symbols": [ - { - "name": "W_MAX_PCT", - "value": 1 - }, - { - "name": "VAR_MAX_PCT", - "value": 2 - }, - { - "name": "VAR_AVAL_PCT", - "value": 3 - } - ], - "type": "enum16" - }, - { - "name": "Pri", - "symbols": [ - { - "name": "ACTIVE", - "value": 1 - }, - { - "name": "REACTIVE", - "value": 2 - }, - { - "name": "IEEE_1547", - "value": 3 - }, - { - "name": "PF", - "value": 4 - }, - { - "name": "VENDOR", - "value": 5 - } - ], - "type": "enum16" - }, - { - "name": "VRef", - "type": "uint16" - }, - { - "name": "VRefAuto", - "symbols": [ - { - "name": "DISABLED", - "value": 0 - }, - { - "name": "ENABLED", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "VRefTms", - "type": "uint16" - }, - { - "name": "RspTms", - "type": "uint16" - }, - { - "name": "ReadOnly", - "symbols": [ - { - "name": "RW", - "value": 0 - }, - { - "name": "R", - "value": 1 - } - ], - "type": "enum16" - } - ], - "type": "group" - } - ], - "name": "DERVoltVar", - "points": [ - { - "name": "ID", - "type": "uint16", - "value": 705 - }, - { - "name": "L", - "type": "uint16" - }, - { - "name": "Ena", - "symbols": [ - { - "name": "DISABLED", - "value": 0 - }, - { - "name": "ENABLED", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "CrvSt", - "symbols": [ - { - "name": "INACTIVE", - "value": 0 - }, - { - "name": "ACTIVE", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "AdptCrvReq", - "type": "uint16" - }, - { - "name": "AdptCrvRslt", - "symbols": [ - { - "name": "IN_PROGRESS", - "value": 0 - }, - { - "name": "COMPLETED", - "value": 1 - }, - { - "name": "FAILED", - "value": 2 - } - ], - "type": "enum16" - }, - { - "name": "NPt", - "type": "uint16" - }, - { - "name": "NCrv", - "type": "uint16" - }, - { - "name": "RvrtTms", - "type": "uint32" - }, - { - "name": "RvrtRem", - "type": "uint32" - }, - { - "name": "RvrtCrv", - "type": "uint16" - }, - { - "name": "V_SF", - "type": "sunssf" - }, - { - "name": "DeptRef_SF", - "type": "sunssf" - } - ], - "type": "group" - }, - "id": 705 - } - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - g = file_client.FileClientGroup() - assert g._group_data(gdata_705, 'Crv') == [{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, - {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]}, - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, - 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, - {'V': 10200, 'Var': 0}, {'V': 10600, 'Var': -4000}]}, - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, - 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, - {'V': 10500, 'Var': 0}, {'V': 10800, 'Var': -2000}]}] - - assert g._group_data(gdata_705['Crv'], index=0) == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, - 'VRefAuto': 0, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, - {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} - - def test__get_data_group_count(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - g = file_client.FileClientGroup() - assert g._get_data_group_count(gdata_705['Crv']) == 3 - - def test__init_repeating_group(self): - gdef_705 = { - "group": { - "groups": [ - { - "count": "NCrv", - "groups": [ - { - "count": "NPt", - "name": "Pt", - "points": [ - { - "name": "V", - "sf": "V_SF", - "type": "uint16", - }, - { - "name": "Var", - "sf": "DeptRef_SF", - "type": "int16", - "units": "VarPct" - } - ], - "type": "group" - } - ], - "name": "Crv", - "points": [ - { - "name": "ActPt", - "type": "uint16" - }, - { - "name": "DeptRef", - "symbols": [ - { - "name": "W_MAX_PCT", - "value": 1 - }, - { - "name": "VAR_MAX_PCT", - "value": 2 - }, - { - "name": "VAR_AVAL_PCT", - "value": 3 - } - ], - "type": "enum16" - }, - { - "name": "Pri", - "symbols": [ - { - "name": "ACTIVE", - "value": 1 - }, - { - "name": "REACTIVE", - "value": 2 - }, - { - "name": "IEEE_1547", - "value": 3 - }, - { - "name": "PF", - "value": 4 - }, - { - "name": "VENDOR", - "value": 5 - } - ], - "type": "enum16" - }, - { - "name": "VRef", - "type": "uint16" - }, - { - "name": "VRefAuto", - "symbols": [ - { - "name": "DISABLED", - "value": 0 - }, - { - "name": "ENABLED", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "VRefTms", - "type": "uint16" - }, - { - "name": "RspTms", - "type": "uint16" - }, - { - "name": "ReadOnly", - "symbols": [ - { - "name": "RW", - "value": 0 - }, - { - "name": "R", - "value": 1 - } - ], - "type": "enum16" - } - ], - "type": "group" - } - ], - "name": "DERVoltVar", - "points": [ - { - "name": "ID", - "type": "uint16", - "value": 705 - }, - { - "name": "L", - "type": "uint16" - }, - { - "name": "Ena", - "symbols": [ - { - "name": "DISABLED", - "value": 0 - }, - { - "name": "ENABLED", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "CrvSt", - "symbols": [ - { - "name": "INACTIVE", - "value": 0 - }, - { - "name": "ACTIVE", - "value": 1 - } - ], - "type": "enum16" - }, - { - "name": "AdptCrvReq", - "type": "uint16" - }, - { - "name": "AdptCrvRslt", - "symbols": [ - { - "name": "IN_PROGRESS", - "value": 0 - }, - { - "name": "COMPLETED", - "value": 1 - }, - { - "name": "FAILED", - "value": 2 - } - ], - "type": "enum16" - }, - { - "name": "NPt", - "type": "uint16" - }, - { - "name": "NCrv", - "type": "uint16" - }, - { - "name": "RvrtTms", - "type": "uint32" - }, - { - "name": "RvrtRem", - "type": "uint32" - }, - { - "name": "RvrtCrv", - "type": "uint16" - }, - { - "name": "V_SF", - "type": "sunssf" - }, - { - "name": "DeptRef_SF", - "type": "sunssf" - } - ], - "type": "group" - }, - "id": 705 - } - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - g = file_client.FileClientGroup() - - with pytest.raises(device.ModelError) as exc: - g._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) - assert 'Count field NCrv undefined for group Crv' in str(exc.value) - - m = file_client.FileClientModel() - pdef_NPt = {"name": "NPt", "type": "uint16"} - p_NPt = file_client.FileClientPoint(pdef_NPt) - p_NPt.value = 4 - - pdef_NCrv = {"name": "NCrv", "type": "uint16"} - p_NCrv = file_client.FileClientPoint(pdef_NCrv) - points = {'NPt': p_NPt, 'NCrv': p_NCrv} - setattr(m, 'points', points) - - g2 = file_client.FileClientGroup(gdef_705['group']['groups'][0], m) - - with pytest.raises(device.ModelError) as exc: - g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) - assert 'Count field NCrv value not initialized for group Crv' in str(exc.value) - - # set value for NCrv count and reset the points attribute on model - p_NCrv.value = 3 - setattr(m, 'points', points) - groups = g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) - assert len(groups) == 3 - assert len(groups[0].groups['Pt']) == 4 - - def test_get_dict(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m2 = file_client.FileClientModel(705, data=gdata_705) - assert m2.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefAutoEna': None, 'VRefTms': 5, 'RspTms': 6, - 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, - {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} - - # test computed - m2.groups['Crv'][0].points['DeptRef'].sf_required = True - m2.groups['Crv'][0].points['DeptRef'].sf_value = 3 - m2.groups['Crv'][0].points['Pri'].sf_required = True - m2.groups['Crv'][0].points['Pri'].sf_value = 3 - computed_dict = m2.groups['Crv'][0].get_dict(computed=True) - assert computed_dict['DeptRef'] == 1000.0 - assert computed_dict['Pri'] == 1000.0 - - def test_set_dict(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = file_client.FileClientModel(705, data=gdata_705) - assert m.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, - 'VRefAutoEna': None, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, - {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} - - new_dict = {'ActPt': 4, 'DeptRef': 4000, 'Pri': 5000, 'VRef': 3, 'VRefAuto': 2, 'VRefAutoEna': None, - 'VRefTms': 2, 'RspTms': 2, 'ReadOnly': 2, - 'Pt': [{'V': 111, 'Var': 111}, {'V': 123, 'Var': 1112}, {'V': 111, 'Var': 111}, - {'V': 123, 'Var': -1112}]} - - m.groups['Crv'][0].set_dict(new_dict, dirty=True) - assert m.groups['Crv'][0].get_dict() == new_dict - assert m.groups['Crv'][0].VRef.value == 3 - assert m.groups['Crv'][0].VRef.dirty - assert m.groups['Crv'][0].Pri.dirty - - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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 - m.groups['Crv'][0].set_dict(new_dict, computed=True) - computed_dict = m.groups['Crv'][0].get_dict() - assert computed_dict['DeptRef'] == 4.0 - assert computed_dict['Pri'] == 5.0 - - def test_get_json(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = file_client.FileClientModel(705, data=gdata_705) - assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \ - ''' "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000},''' + \ - ''' {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0}, {"V": 10700, "Var": -3000}]}''' - - # test computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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,''' + \ - ''' "VRefTms": 5, "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): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = file_client.FileClientModel(705, data=gdata_705) - assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ - ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5,''' + \ - ''' "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000},''' + \ - ''' {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0},''' + \ - ''' {"V": 10700, "Var": -3000}]}''' - - json_to_set = '''{"ActPt": 4, "DeptRef": 9999, "Pri": 9999, "VRef": 99, "VRefAuto": 88,''' + \ - ''' "VRefAutoEna": null, "VRefTms": 88, "RspTms": 88, "ReadOnly": 77, "Pt":''' + \ - ''' [{"V": 77, "Var": 66}, {"V": 55, "Var": 44}, {"V": 33, "Var": 22},''' + \ - ''' {"V": 111, "Var": -2222}]}''' - - m.groups['Crv'][0].set_json(json_to_set) - assert m.groups['Crv'][0].get_json() == json_to_set - assert m.groups['Crv'][0].DeptRef.value == 9999 - - # test computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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 - m.groups['Crv'][0].set_json(json_to_set, computed=True, dirty=True) - assert m.groups['Crv'][0].points['DeptRef'].value == 9 - assert m.groups['Crv'][0].points['DeptRef'].dirty - assert m.groups['Crv'][0].points['Pri'].value == 9 - assert m.groups['Crv'][0].points['Pri'].dirty - - def test_get_mb(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = file_client.FileClientModel(705, data=gdata_705) - assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00' \ - b'\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' - - # test computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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' \ - b'\x00\x05\x00\x06\x00\x01\x00\\\x00\x1e\x00`\x00' \ - b'\x00\x00g\x00\x00\x00k\xff\xe2' - - def test_set_mb(self): - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = file_client.FileClientModel(705, data=gdata_705) - assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00' \ - b'\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' - bs = b'\x00\x04\x03\xe7\x03x\x03\t\x02\x9a\x02+\x01\xbc\x01M' \ - b'\x00\xde\x00o\x00\xde\x01M\x01\xbc\x02+\x02\x9a\xfc\xf7\xf4H' - - m.groups['Crv'][0].set_mb(bs, dirty=True) - assert m.groups['Crv'][0].get_mb() == bs - assert m.groups['Crv'][0].DeptRef.value == 999 - assert m.groups['Crv'][0].DeptRef.dirty - - # test computed - # set points DeptRef and Pri to 3000 w/ byte string - computed_bs = b'\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<' \ - b'\x00\x00)\xcc\xf4H' - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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 - m.groups['Crv'][0].set_mb(computed_bs, computed=True) - assert m.groups['Crv'][0].points['DeptRef'].value == 3 - assert m.groups['Crv'][0].points['Pri'].value == 3 - - -class TestFileClientModel: - def test__init__(self): - m = file_client.FileClientModel(704) - assert m.model_id == 704 - assert m.model_addr == 0 - assert m.model_len == 0 - assert m.model_def['id'] == 704 - assert m.error_info == '' - assert m.gdef['name'] == 'DERCtlAC' - assert m.mid is None - assert m.device is None - - assert m.model == m - m2 = file_client.FileClientModel('abc') - assert m2.error_info == 'Invalid model id: abc\n' - - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - # test repeating group model - m2 = file_client.FileClientModel(705, data=gdata_705) - assert m2.model_id == 705 - assert m2.model_addr == 0 - assert m2.model_len == 0 - assert m2.model_def['id'] == 705 - assert m2.error_info == '' - assert m2.gdef['name'] == 'DERVoltVar' - assert m2.mid is None - assert m2.device is None - - def test__error(self): - m = file_client.FileClientModel(704) - m.add_error('test error') - assert m.error_info == 'test error\n' - - -class TestFileClientDevice: - def test__init__(self): - d = file_client.FileClientDevice() - assert d.name is None - assert d.did - assert d.models == {} - assert d.model_list == [] - assert d.model_class == file_client.FileClientModel - - def test__get_attr__(self): - d = file_client.FileClientDevice() - m = file_client.FileClientModel() - setattr(m, 'model_id', 'mid_test') - setattr(m, 'gname', 'group_test') - d.add_model(m) - assert d.mid_test - - with pytest.raises(AttributeError) as exc: - d.foo - assert "\'FileClientDevice\' object has no attribute \'foo\'" in str(exc.value) - - def test_add_model(self): - d = file_client.FileClientDevice() - m = file_client.FileClientModel() - setattr(m, 'model_id', 'mid_test') - setattr(m, 'gname', 'group_test') - d.add_model(m) - assert d.models['mid_test'] - assert d.models['group_test'] - assert m.device == d - - def test_get_dict(self): - d = file_client.FileClientDevice() - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = file_client.FileClientModel(705, data=gdata_705) - d.add_model(m) - assert d.get_dict()['models'] == [ - {'ID': 705, 'L': 64, 'Ena': 1, 'CrvSt': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, - 'RvrtTms': 0, 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'Crv': [ - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, - 'RspTms': 6, 'ReadOnly': 1, - 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, {'V': 10300, 'Var': 0}, - {'V': 10700, 'Var': -3000}]}, - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, - 'RspTms': 6, 'ReadOnly': 0, - 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, {'V': 10200, 'Var': 0}, - {'V': 10600, 'Var': -4000}]}, - {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefTms': 5, - 'RspTms': 6, 'ReadOnly': 0, - 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, {'V': 10500, 'Var': 0}, - {'V': 10800, 'Var': -2000}]}]}] - - # computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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_dict(computed=True)['models'] == [ - {'ID': 705, 'L': 64, 'Ena': 1, 'CrvSt': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, - 'RvrtTms': 0, 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'Crv': [ - {'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, - 'VRefTms': 5, '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, 'VRefTms': 5, - '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, 'VRefTms': 5, - '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}]}]}] - - def test_get_json(self): - d = file_client.FileClientDevice() - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = file_client.FileClientModel(705, data=gdata_705) - d.add_model(m) - - assert d.get_json() == '''{"name": null, "did": "''' + str(d.did) + '''", "models": [{"ID": 705,''' + \ - ''' "L": 64, "Ena": 1, "CrvSt": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4,''' + \ - ''' "NCrv": 3, "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2,''' + \ - ''' "DeptRef_SF": -2, "Crv": [{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ - ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 1,''' + \ - ''' "Pt": [{"V": 9200, "Var": 3000}, {"V": 9670, "Var": 0}, {"V": 10300,''' + \ - ''' "Var": 0}, {"V": 10700, "Var": -3000}]}, {"ActPt": 4, "DeptRef": 1,''' + \ - ''' "Pri": 1, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5,''' + \ - ''' "RspTms": 6, "ReadOnly": 0, "Pt": [{"V": 9300, "Var": 3000},''' + \ - ''' {"V": 9570, "Var": 0}, {"V": 10200, "Var": 0}, {"V": 10600, "Var": -4000}]},''' + \ - ''' {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0,''' + \ - ''' "VRefAutoEna": null, "VRefTms": 5, "RspTms": 6, "ReadOnly": 0, "Pt":''' + \ - ''' [{"V": 9400, "Var": 2000}, {"V": 9570, "Var": 0}, {"V": 10500, "Var": 0},''' + \ - ''' {"V": 10800, "Var": -2000}]}]}]}''' - - # computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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 - - get_json_output2 = '''{"name": null, "did": "''' + str(d.did) + '''", "models": [{"ID": 705, "L": 63,''' + \ - ''' "Ena": 1, "CrvSt": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3,''' + \ - ''' "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2, "Crv":''' + \ - ''' [{"ActPt": 4, "DeptRef": 1000.0, "Pri": 1000.0, "VRef": 1, "VRefAuto": 0,''' + \ - ''' "VRefTms": 5, "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, "VRefTms": 5,''' + \ - ''' "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, "VRefTms": 5, "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}]}]}]}''' - assert d.get_json(computed=True) == '''{"name": null, "did": "''' + str(d.did) + '''", "models":''' + \ - ''' [{"ID": 705, "L": 64, "Ena": 1, "CrvSt": 1, "AdptCrvReq": 0,''' + \ - ''' "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3, "RvrtTms": 0,''' + \ - ''' "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2,''' + \ - ''' "Crv": [{"ActPt": 4, "DeptRef": 1000.0, "Pri": 1000.0,''' + \ - ''' "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefTms": 5,''' + \ - ''' "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, "VRefTms": 5, "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": null, "VRefTms": 5, "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}]}]}]}''' - - def test_get_mb(self): - d = file_client.FileClientDevice() - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = file_client.FileClientModel(705, data=gdata_705) - d.add_model(m) - assert d.get_mb() == b"\x02\xc1\x00@\x00\x01\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\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff" \ - b"\xff\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04" \ - b"\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00" \ - b"\x00'\xd8\x00\x00)h\xf0`\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00" \ - b"\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" - - # computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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\x00@\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00' \ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x03' \ - b'\xe8\x03\xe8\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x01\x00' \ - b'\\\x00\x1e\x00`\x00\x00\x00g\x00\x00\x00k\xff\xe2\x00\x04\x00\x01' \ - b'\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00\x00]\x00' \ - b'\x1e\x00_\x00\x00\x00f\x00\x00\x00j\xff\xd8\x00\x04\x00\x01\x00' \ - b'\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00\x00^\x00\x14' \ - b'\x00_\x00\x00\x00i\x00\x00\x00l\xff\xec' - - def test_set_mb(self): - d = file_client.FileClientDevice() - gdata_705 = { - "ID": 705, - "Ena": 1, - "CrvSt": 1, - "AdptCrvReq": 0, - "AdptCrvRslt": 0, - "NPt": 4, - "NCrv": 3, - "RvrtTms": 0, - "RvrtRem": 0, - "RvrtCrv": 0, - "V_SF": -2, - "DeptRef_SF": -2, - "Crv": [ - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 1, - "Pt": [ - { - "V": 9200, - "Var": 3000 - }, - { - "V": 9670, - "Var": 0 - }, - { - "V": 10300, - "Var": 0 - }, - { - "V": 10700, - "Var": -3000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9300, - "Var": 3000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10200, - "Var": 0 - }, - { - "V": 10600, - "Var": -4000 - } - ] - }, - { - "ActPt": 4, - "DeptRef": 1, - "Pri": 1, - "VRef": 1, - "VRefAuto": 0, - "VRefTms": 5, - "RspTms": 6, - "ReadOnly": 0, - "Pt": [ - { - "V": 9400, - "Var": 2000 - }, - { - "V": 9570, - "Var": 0 - }, - { - "V": 10500, - "Var": 0 - }, - { - "V": 10800, - "Var": -2000 - } - ] - } - ] - } - m = file_client.FileClientModel(705, data=gdata_705) - d.add_model(m) - assert d.get_mb() == b"\x02\xc1\x00@\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ - b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00" \ - b"\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01" \ - b"\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8" \ - b"\x00\x00)h\xf0`\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\x00\x05\x00\x06\x00" \ - b"\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" - - # DeptRef and Pri set to 3000 in byte string - bs = b"\x02\xc1\x00?\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ - b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00\x06" \ - b"\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01\x00\x01\x00\x01" \ - b"\x00\x00\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8\x00\x00)h\xf0`\x00\x04\x00" \ - b"\x01\x00\x01\x00\x01\x00\x00\x00\x05\x00\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" - - d.set_mb(bs, dirty=True) - assert m.groups['Crv'][0].DeptRef.value == 3000 - assert m.groups['Crv'][0].DeptRef.dirty - assert m.groups['Crv'][0].Pri.value == 3000 - assert m.groups['Crv'][0].Pri.dirty - - # computed - m.groups['Crv'][0].points['DeptRef'].sf_required = True - 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 - d.set_mb(bs, computed=True, dirty=False) - assert m.groups['Crv'][0].DeptRef.value == 3 - assert not m.groups['Crv'][0].DeptRef.dirty - assert m.groups['Crv'][0].Pri.value == 3 - assert not m.groups['Crv'][0].Pri.dirty - - def test_find_mid(self): - d = file_client.FileClientDevice() - m = file_client.FileClientModel() - setattr(m, 'model_id', 'mid_test') - setattr(m, 'gname', 'group_test') - setattr(m, 'mid', 'mid_test') - d.add_model(m) - assert d.find_mid('mid_test') == m - - def test_scan(self): - d = file_client.FileClientDevice('sunspec2/tests/test_data/device_1547.json') - d.scan() - assert d.common - assert d.DERMeasureAC - - -class FileClient: - pass diff --git a/build/lib/sunspec2/tests/test_mb.py b/build/lib/sunspec2/tests/test_mb.py deleted file mode 100644 index 04fdc2e..0000000 --- a/build/lib/sunspec2/tests/test_mb.py +++ /dev/null @@ -1,221 +0,0 @@ -import sunspec2.mb as mb -import pytest - - -def test_create_unimpl_value(): - with pytest.raises(ValueError): - mb.create_unimpl_value(None) - - with pytest.raises(ValueError): - mb.create_unimpl_value('string') - - assert mb.create_unimpl_value('string', len=8) == b'\x00\x00\x00\x00\x00\x00\x00\x00' - assert mb.create_unimpl_value('ipv6addr') == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - assert mb.create_unimpl_value('int16') == b'\x80\x00' - assert mb.create_unimpl_value('uint16') == b'\xff\xff' - assert mb.create_unimpl_value('acc16') == b'\x00\x00' - assert mb.create_unimpl_value('enum16') == b'\xff\xff' - assert mb.create_unimpl_value('bitfield16') == b'\xff\xff' - assert mb.create_unimpl_value('int32') == b'\x80\x00\x00\x00' - assert mb.create_unimpl_value('uint32') == b'\xff\xff\xff\xff' - assert mb.create_unimpl_value('acc32') == b'\x00\x00\x00\x00' - assert mb.create_unimpl_value('enum32') == b'\xff\xff\xff\xff' - assert mb.create_unimpl_value('bitfield32') == b'\xff\xff\xff\xff' - assert mb.create_unimpl_value('ipaddr') == b'\x00\x00\x00\x00' - assert mb.create_unimpl_value('int64') == b'\x80\x00\x00\x00\x00\x00\x00\x00' - assert mb.create_unimpl_value('uint64') == b'\xff\xff\xff\xff\xff\xff\xff\xff' - assert mb.create_unimpl_value('acc64') == b'\x00\x00\x00\x00\x00\x00\x00\x00' - assert mb.create_unimpl_value('float32') == b'N\xff\x80\x00' - assert mb.create_unimpl_value('sunssf') == b'\x80\x00' - assert mb.create_unimpl_value('eui48') == b'\x00\x00\xff\xff\xff\xff\xff\xff' - assert mb.create_unimpl_value('pad') == b'\x00\x00' - - -def test_data_to_s16(): - assert mb.data_to_s16(b'\x13\x88') == 5000 - - -def test_data_to_u16(): - assert mb.data_to_u16(b'\x27\x10') == 10000 - - -def test_data_to_s32(): - assert mb.data_to_s32(b'\x12\x34\x56\x78') == 305419896 - assert mb.data_to_s32(b'\xED\xCB\xA9\x88') == -305419896 - - -def test_data_to_u32(): - assert mb.data_to_u32(b'\x12\x34\x56\x78') == 305419896 - - -def test_data_to_s64(): - assert mb.data_to_s64(b'\x12\x34\x56\x78\x12\x34\x56\x78') == 1311768465173141112 - assert mb.data_to_s64(b'\xED\xCB\xA9\x87\xED\xCB\xA9\x88') == -1311768465173141112 - - -def test_data_to_u64(): - assert mb.data_to_u64(b'\xff\xff\xff\xff\xff\xff\xff\xff') == 18446744073709551615 - - -def test_data_to_ipv6addr(): - assert mb.data_to_ipv6addr(b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34') == '20010DB8:85A30000:00008A2E:03707334' - - -def test_data_to_eui48(): - # need test to test for python 2 - assert mb.data_to_eui48(b'\x00\x00\x12\x34\x56\x78\x90\xAB') == '12:34:56:78:90:AB' - - -def test_data_to_f64(): - assert mb.data_to_f64(b'\x44\x9a\x43\xf3\x00\x00\x00\x00') == 3.1008742600725133e+22 - - -def test_data_to_str(): - assert mb.data_to_str(b'test') == 'test' - assert mb.data_to_str(b'444444') == '444444' - - -def test_s16_to_data(): - assert mb.s16_to_data(5000) == b'\x13\x88' - - -def test_u16_to_data(): - assert mb.u16_to_data(10000) == b'\x27\x10' - - -def test_s32_to_data(): - assert mb.s32_to_data(305419896) == b'\x12\x34\x56\x78' - assert mb.s32_to_data(-305419896) == b'\xED\xCB\xA9\x88' - - -def test_u32_to_data(): - assert mb.u32_to_data(305419896) == b'\x12\x34\x56\x78' - - -def test_s64_to_data(): - assert mb.s64_to_data(1311768465173141112) == b'\x12\x34\x56\x78\x12\x34\x56\x78' - assert mb.s64_to_data(-1311768465173141112) == b'\xED\xCB\xA9\x87\xED\xCB\xA9\x88' - - -def test_u64_to_data(): - assert mb.u64_to_data(18446744073709551615) == b'\xff\xff\xff\xff\xff\xff\xff\xff' - - -def test_ipv6addr_to_data(): - assert mb.ipv6addr_to_data('20010DB8:85A30000:00008A2E:03707334') == \ - b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34' - # need additional test to test for python 2 - - -def test_f32_to_data(): - assert mb.f32_to_data(32500.43359375) == b'F\xfd\xe8\xde' - - -def test_f64_to_data(): - assert mb.f64_to_data(3.1008742600725133e+22) == b'\x44\x9a\x43\xf3\x00\x00\x00\x00' - - -def test_str_to_data(): - assert mb.str_to_data('test') == b'test' - assert mb.str_to_data('444444') == b'444444' - assert mb.str_to_data('test', 5) == b'test\x00' - - -def test_eui48_to_data(): - assert mb.eui48_to_data('12:34:56:78:90:AB') == b'\x00\x00\x12\x34\x56\x78\x90\xAB' - - -def test_is_impl_int16(): - assert not mb.is_impl_int16(-32768) - assert mb.is_impl_int16(1111) - assert mb.is_impl_int16(None) - - -def test_is_impl_uint16(): - assert not mb.is_impl_uint16(0xffff) - assert mb.is_impl_uint16(0x1111) - - -def test_is_impl_acc16(): - assert not mb.is_impl_acc16(0) - assert mb.is_impl_acc16(1111) - - -def test_is_impl_enum16(): - assert not mb.is_impl_enum16(0xffff) - assert mb.is_impl_enum16(0x1111) - - -def test_is_impl_bitfield16(): - assert not mb.is_impl_bitfield16(0xffff) - assert mb.is_impl_bitfield16(0x1111) - - -def test_is_impl_int32(): - assert not mb.is_impl_int32(-2147483648) - assert mb.is_impl_int32(1111111) - - -def test_is_impl_uint32(): - assert not mb.is_impl_uint32(0xffffffff) - assert mb.is_impl_uint32(0x11111111) - - -def test_is_impl_acc32(): - assert not mb.is_impl_acc32(0) - assert mb.is_impl_acc32(1) - - -def test_is_impl_enum32(): - assert not mb.is_impl_enum32(0xffffffff) - assert mb.is_impl_enum32(0x11111111) - - -def test_is_impl_bitfield32(): - assert not mb.is_impl_bitfield32(0xffffffff) - assert mb.is_impl_bitfield32(0x11111111) - - -def test_is_impl_ipaddr(): - assert not mb.is_impl_ipaddr(0) - assert mb.is_impl_ipaddr('192.168.0.1') - - -def test_is_impl_int64(): - assert not mb.is_impl_int64(-9223372036854775808) - assert mb.is_impl_int64(111111111111111) - - -def test_is_impl_uint64(): - assert not mb.is_impl_uint64(0xffffffffffffffff) - assert mb.is_impl_uint64(0x1111111111111111) - - -def test_is_impl_acc64(): - assert not mb.is_impl_acc64(0) - assert mb.is_impl_acc64(1) - - -def test_is_impl_ipv6addr(): - assert not mb.is_impl_ipv6addr('\0') - assert mb.is_impl_ipv6addr(b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34') - - -def test_is_impl_float32(): - assert not mb.is_impl_float32(None) - assert mb.is_impl_float32(0x123456) - - -def test_is_impl_string(): - assert not mb.is_impl_string('\0') - assert mb.is_impl_string(b'\x74\x65\x73\x74') - - -def test_is_impl_sunssf(): - assert not mb.is_impl_sunssf(-32768) - assert mb.is_impl_sunssf(30000) - - -def test_is_impl_eui48(): - assert not mb.is_impl_eui48('FF:FF:FF:FF:FF:FF') - assert mb.is_impl_eui48('00:00:00:00:00:00') diff --git a/build/lib/sunspec2/tests/test_mdef.py b/build/lib/sunspec2/tests/test_mdef.py deleted file mode 100644 index 709b9ac..0000000 --- a/build/lib/sunspec2/tests/test_mdef.py +++ /dev/null @@ -1,307 +0,0 @@ -import sunspec2.mdef as mdef -import json -import copy - - -def test_to_int(): - assert mdef.to_int('4') == 4 - assert isinstance(mdef.to_int('4'), int) - assert isinstance(mdef.to_int(4.0), int) - - -def test_to_str(): - assert mdef.to_str(4) == '4' - assert isinstance(mdef.to_str('4'), str) - - -def test_to_float(): - assert mdef.to_float('4') == 4.0 - assert isinstance(mdef.to_float('4'), float) - assert mdef.to_float('z') is None - - -def test_to_number_type(): - assert mdef.to_number_type('4') == 4 - assert mdef.to_number_type('4.0') == 4.0 - assert mdef.to_number_type('z') == 'z' - - -def test_validate_find_point(): - with open('sunspec2/models/json/model_702.json') as f: - model_json = json.load(f) - - assert mdef.validate_find_point(model_json['group'], 'ID') == model_json['group']['points'][0] - assert mdef.validate_find_point(model_json['group'], 'abc') is None - - -def test_validate_attrs(): - with open('sunspec2/models/json/model_701.json') as f: - model_json = json.load(f) - - # model - assert mdef.validate_attrs(model_json, mdef.model_attr) == '' - - model_unexp_attr_err = copy.deepcopy(model_json) - model_unexp_attr_err['abc'] = 'def' - assert mdef.validate_attrs(model_unexp_attr_err, mdef.model_attr)[0:37] == 'Unexpected model definition attribute' - - model_unexp_type_err = copy.deepcopy(model_json) - model_unexp_type_err['id'] = '701' - assert mdef.validate_attrs(model_unexp_type_err, mdef.model_attr)[0:15] == 'Unexpected type' - - model_attr_missing = copy.deepcopy(model_json) - del model_attr_missing['id'] - assert mdef.validate_attrs(model_attr_missing, mdef.model_attr)[0:27] == 'Mandatory attribute missing' - - # group - assert mdef.validate_attrs(model_json['group'], mdef.group_attr) == '' - group_unexp_attr_err = copy.deepcopy(model_json)['group'] - group_unexp_attr_err['abc'] = 'def' - assert mdef.validate_attrs(group_unexp_attr_err, mdef.group_attr)[0:37] == 'Unexpected model definition attribute' - - group_unexp_type_err = copy.deepcopy(model_json)['group'] - group_unexp_type_err['name'] = 1 - assert mdef.validate_attrs(group_unexp_type_err, mdef.group_attr)[0:15] == 'Unexpected type' - - group_attr_missing = copy.deepcopy(model_json)['group'] - del group_attr_missing['name'] - assert mdef.validate_attrs(group_attr_missing, mdef.group_attr)[0:27] == 'Mandatory attribute missing' - - # point - assert mdef.validate_attrs(model_json['group']['points'][0], mdef.point_attr) == '' - - point_unexp_attr_err = copy.deepcopy(model_json)['group']['points'][0] - point_unexp_attr_err['abc'] = 'def' - assert mdef.validate_attrs(point_unexp_attr_err, mdef.point_attr)[0:37] == 'Unexpected model definition attribute' - - point_unexp_type_err = copy.deepcopy(model_json)['group']['points'][0] - point_unexp_type_err['name'] = 1 - assert mdef.validate_attrs(point_unexp_type_err, mdef.point_attr)[0:15] == 'Unexpected type' - - point_unexp_value_err = copy.deepcopy(model_json)['group']['points'][1] - point_unexp_value_err['access'] = 'z' - assert mdef.validate_attrs(point_unexp_value_err, mdef.point_attr)[0:16] == 'Unexpected value' - - point_attr_missing = copy.deepcopy(model_json)['group']['points'][0] - del point_attr_missing['name'] - assert mdef.validate_attrs(point_attr_missing, mdef.point_attr)[0:27] == 'Mandatory attribute missing' - - # symbol - assert mdef.validate_attrs(model_json['group']['points'][2]['symbols'][0], mdef.symbol_attr) == '' - - symbol_unexp_attr_err = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0] - symbol_unexp_attr_err['abc'] = 'def' - assert mdef.validate_attrs(symbol_unexp_attr_err, mdef.symbol_attr)[0:37] == 'Unexpected model definition attribute' - - symbol_unexp_type_err = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0] - symbol_unexp_type_err['name'] = 1 - assert mdef.validate_attrs(symbol_unexp_type_err, mdef.symbol_attr)[0:15] == 'Unexpected type' - - symbol_attr_missing = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0] - del symbol_attr_missing['name'] - assert mdef.validate_attrs(symbol_attr_missing, mdef.symbol_attr)[0:27] == 'Mandatory attribute missing' - - -def test_validate_group_point_dup(): - with open('sunspec2/models/json/model_704.json') as f: - model_json = json.load(f) - - assert mdef.validate_group_point_dup(model_json['group']) == '' - - dup_group_id_model = copy.deepcopy(model_json) - dup_group_id_group = dup_group_id_model['group'] - dup_group_id_group['groups'][0]['name'] = 'PFWInjRvrt' - assert mdef.validate_group_point_dup(dup_group_id_group)[0:18] == 'Duplicate group id' - - dup_group_point_id_model = copy.deepcopy(model_json) - dup_group_point_id_group = dup_group_point_id_model['group'] - dup_group_point_id_group['groups'][0]['name'] = 'PFWInjEna' - assert mdef.validate_group_point_dup(dup_group_point_id_group)[0:28] == 'Duplicate group and point id' - - mand_attr_miss_model = copy.deepcopy(model_json) - mand_attr_miss_group = mand_attr_miss_model['group'] - del mand_attr_miss_group['groups'][0]['name'] - assert mdef.validate_group_point_dup(mand_attr_miss_group)[0:32] == 'Mandatory name attribute missing' - - dup_point_id_model = copy.deepcopy(model_json) - dup_point_id_group = dup_point_id_model['group'] - dup_point_id_group['points'][1]['name'] = 'ID' - assert mdef.validate_group_point_dup(dup_point_id_group)[0:30] == 'Duplicate point id ID in group' - - mand_attr_miss_point_model = copy.deepcopy(model_json) - mand_attr_miss_point_group = mand_attr_miss_point_model['group'] - del mand_attr_miss_point_group['points'][1]['name'] - assert mdef.validate_group_point_dup(mand_attr_miss_point_group)[0:55] == 'Mandatory attribute missing in point ' \ - 'definition element' - - -def test_validate_symbols(): - symbols = [ - {'name': 'CAT_A', 'value': 1}, - {'name': 'CAT_B', 'value': 2} - ] - assert mdef.validate_symbols(symbols, mdef.symbol_attr) == '' - - -def test_validate_sf(): - with open('sunspec2/models/json/model_702.json') as f: - model_json = json.load(f) - - model_point = model_json['group']['points'][2] - model_group = model_json['group'] - model_group_arr = [model_group, model_group] - assert mdef.validate_sf(model_point, 'W_SF', model_group_arr) == '' - - not_sf_type_model = copy.deepcopy(model_json) - not_sf_type_point = not_sf_type_model['group']['points'][2] - not_sf_type_group = not_sf_type_model['group'] - not_sf_type_group_arr = [not_sf_type_group, not_sf_type_group] - for point in not_sf_type_model['group']['points']: - if point['name'] == 'W_SF': - point['type'] = 'abc' - assert mdef.validate_sf(not_sf_type_point, 'W_SF', not_sf_type_group_arr)[0:60] == 'Scale factor W_SF for point ' \ - 'WMaxRtg is not scale factor ' \ - 'type' - - sf_not_found_model = copy.deepcopy(model_json) - sf_not_found_point = sf_not_found_model['group']['points'][2] - sf_not_found_group = sf_not_found_model['group'] - sf_not_found_group_arr = [sf_not_found_group, sf_not_found_group] - assert mdef.validate_sf(sf_not_found_point, 'ABC', sf_not_found_group_arr)[0:44] == 'Scale factor ABC for point ' \ - 'WMaxRtg not found' - - sf_out_range_model = copy.deepcopy(model_json) - sf_out_range_point = sf_out_range_model['group']['points'][2] - sf_out_range_group = sf_out_range_model['group'] - sf_out_range_group_arr = [sf_out_range_group, sf_out_range_group] - assert mdef.validate_sf(sf_out_range_point, 11, sf_out_range_group_arr)[0:46] == 'Scale factor 11 for point ' \ - 'WMaxRtg out of range' - - sf_invalid_type_model = copy.deepcopy(model_json) - sf_invalid_type_point = sf_invalid_type_model['group']['points'][2] - sf_invalid_type_group = sf_invalid_type_model['group'] - sf_invalid_type_group_arr = [sf_invalid_type_group, sf_invalid_type_group] - assert mdef.validate_sf(sf_invalid_type_point, 4.0, sf_invalid_type_group_arr)[0:51] == 'Scale factor 4.0 for' \ - ' point WMaxRtg has ' \ - 'invalid type' - - -def test_validate_point_def(): - with open('sunspec2/models/json/model_702.json') as f: - model_json = json.load(f) - - model_group = model_json['group'] - group = model_json['group'] - point = model_json['group']['points'][0] - assert mdef.validate_point_def(point, model_group, group) == '' - - unk_point_type_model = copy.deepcopy(model_json) - unk_point_type_model_group = unk_point_type_model['group'] - unk_point_type_group = unk_point_type_model['group'] - unk_point_type_point = unk_point_type_model['group']['points'][0] - unk_point_type_point['type'] = 'abc' - assert mdef.validate_point_def(unk_point_type_point, unk_point_type_model_group, - unk_point_type_group)[0:35] == 'Unknown point type abc for point ID' - - dup_symbol_model = copy.deepcopy(model_json) - dup_symbol_model_group = dup_symbol_model['group'] - dup_symbol_group = dup_symbol_model['group'] - dup_symbol_point = dup_symbol_model['group']['points'][21] - dup_symbol_point['symbols'][0]['name'] = 'CAT_B' - assert mdef.validate_point_def(dup_symbol_point, dup_symbol_model_group, - dup_symbol_group)[0:19] == 'Duplicate symbol id' - - mand_attr_missing = copy.deepcopy(model_json) - mand_attr_missing_model_group = mand_attr_missing['group'] - mand_attr_missing_group = mand_attr_missing['group'] - mand_attr_missing_point = mand_attr_missing['group']['points'][0] - del mand_attr_missing_point['name'] - assert mdef.validate_point_def(mand_attr_missing_point, mand_attr_missing_model_group, - mand_attr_missing_group)[0:27] == 'Mandatory attribute missing' - - -def test_validate_group_def(): - with open('sunspec2/models/json/model_702.json') as f: - model_json = json.load(f) - - assert mdef.validate_group_def(model_json['group'], model_json['group']) == '' - - -def test_validate_model_group_def(): - with open('sunspec2/models/json/model_702.json') as f: - model_json = json.load(f) - - assert mdef.validate_model_group_def(model_json, model_json['group']) == '' - - missing_id_model = copy.deepcopy(model_json) - missing_id_group = missing_id_model['group'] - missing_id_group['points'][0]['name'] = 'abc' - assert mdef.validate_model_group_def(missing_id_model, missing_id_group)[0:41] == 'First point in top-level' \ - ' group must be ID' - - wrong_model_id_model = copy.deepcopy(model_json) - wrong_model_id_group = wrong_model_id_model['group'] - wrong_model_id_group['points'][0]['value'] = 0 - assert mdef.validate_model_group_def(wrong_model_id_model, wrong_model_id_group)[0:42] == 'Model ID does not ' \ - 'match top-level group ID' - - missing_len_model = copy.deepcopy(model_json) - missing_len_group = missing_len_model['group'] - missing_len_group['points'][1]['name'] = 'abc' - assert mdef.validate_model_group_def(missing_len_model, missing_len_group)[0:41] == 'Second point in top-level ' \ - 'group must be L' - - missing_two_p_model = copy.deepcopy(model_json) - missing_two_p_group = missing_two_p_model['group'] - missing_two_p_point = missing_two_p_group['points'][0] - del missing_two_p_group['points'] - missing_two_p_group['points'] = [missing_two_p_point] - assert mdef.validate_model_group_def(missing_two_p_model, missing_two_p_group)[0:48] == 'Top-level group must' \ - ' contain at least two ' \ - 'points' - - missing_p_def_model = copy.deepcopy(model_json) - missing_p_def_group = missing_p_def_model['group'] - del missing_p_def_group['points'] - assert mdef.validate_model_group_def(missing_p_def_model, missing_p_def_group)[0:41] == 'Top-level group' \ - ' missing point definitions' - - -def test_validate_model_def(): - with open('sunspec2/models/json/model_702.json') as f: - model_json = json.load(f) - - assert mdef.validate_model_def(model_json) == '' - - -def test_from_json_str(): - with open('sunspec2/models/json/model_63001.json') as f: - model_json = json.load(f) - model_json_str = json.dumps(model_json) - assert isinstance(mdef.from_json_str(model_json_str), dict) - - -def test_from_json_file(): - assert isinstance(mdef.from_json_file('sunspec2/models/json/model_63001.json'), dict) - - -def test_to_json_str(): - with open('sunspec2/models/json/model_63001.json') as f: - model_json = json.load(f) - assert isinstance(mdef.to_json_str(model_json), str) - - -def test_to_json_filename(): - assert mdef.to_json_filename('63001') == 'model_63001.json' - - -def test_to_json_file(tmp_path): - with open('sunspec2/models/json/model_63001.json') as f: - model_json = json.load(f) - mdef.to_json_file(model_json, filedir=tmp_path) - - with open(tmp_path / 'model_63001.json') as f: - model_json = json.load(f) - assert isinstance(model_json, dict) - - diff --git a/build/lib/sunspec2/tests/test_modbus_client.py b/build/lib/sunspec2/tests/test_modbus_client.py deleted file mode 100644 index daa49ff..0000000 --- a/build/lib/sunspec2/tests/test_modbus_client.py +++ /dev/null @@ -1,731 +0,0 @@ -import sunspec2.modbus.client as client -import pytest -import socket -import sunspec2.tests.mock_socket as MockSocket -import serial -import sunspec2.tests.mock_port as MockPort - - -class TestSunSpecModbusClientPoint: - def test_read(self, monkeypatch): - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - - # tcp - d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) - tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', - b'SunS\x00\x01', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00B', - b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', - b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00~', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00@', - b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', - b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' - b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' - b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\xff\xff'] - d_tcp.client.connect() - d_tcp.client.socket._set_buffer(tcp_buffer) - d_tcp.scan() - assert d_tcp.common[0].SN.value == 'sn-123456789' - assert not d_tcp.common[0].SN.dirty - - d_tcp.common[0].SN.value = 'will be overwritten by read' - assert d_tcp.common[0].SN.value == 'will be overwritten by read' - assert d_tcp.common[0].SN.dirty - - d_tcp.client.socket.clear_buffer() - tcp_p_buffer = [b'\x00\x00\x00\x00\x00#\x01\x03 ', - b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] - d_tcp.client.socket._set_buffer(tcp_p_buffer) - d_tcp.common[0].SN.read() - assert d_tcp.common[0].SN.value == 'sn-123456789' - assert not d_tcp.common[0].SN.dirty - - # rtu - d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") - rtu_buffer = [b'\x01\x03\x06Su', - b'nS\x00\x01\x8d\xe4', - b'\x01\x03\x02\x00B', - b'8u', - b'\x01\x03\x88\x00\x01', - b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x01\x00\x00M\xf9', - b'\x01\x03\x02\x00~', - b'8d', - b'\x01\x03\x02\x00@', - b'\xb9\xb4', - b'\x01\x03\x84\x00~', - b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' - b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', - b'\x01\x03\x02\xff\xff', - b'\xb9\xf4'] - d_rtu.open() - d_rtu.client.serial._set_buffer(rtu_buffer) - d_rtu.scan() - assert d_rtu.common[0].SN.value == 'sn-123456789' - assert not d_rtu.common[0].SN.dirty - - d_rtu.common[0].SN.value = 'will be overwritten by read' - assert d_rtu.common[0].SN.value == 'will be overwritten by read' - assert d_rtu.common[0].SN.dirty - - d_rtu.client.serial.clear_buffer() - tcp_p_buffer = [b'\x01\x03 sn', - b'-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd5\xb8'] - d_rtu.client.serial._set_buffer(tcp_p_buffer) - d_rtu.common[0].SN.read() - assert d_rtu.common[0].SN.value == 'sn-123456789' - assert not d_rtu.common[0].SN.dirty - - def test_write(self, monkeypatch): - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - - # tcp - d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) - tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', - b'SunS\x00\x01', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00B', - b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', - b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00~', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00@', - b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', - b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' - b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' - b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\xff\xff'] - d_tcp.client.connect() - d_tcp.client.socket._set_buffer(tcp_buffer) - d_tcp.scan() - - assert d_tcp.common[0].SN.value == 'sn-123456789' - assert not d_tcp.common[0].SN.dirty - - d_tcp.common[0].SN.value = 'sn-000' - assert d_tcp.common[0].SN.value == 'sn-000' - assert d_tcp.common[0].SN.dirty - - tcp_write_buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', - b't\x00\x10'] - d_tcp.client.socket.clear_buffer() - d_tcp.client.socket._set_buffer(tcp_write_buffer) - d_tcp.common[0].write() - - d_tcp.common[0].SN.value = 'will be overwritten by read' - assert d_tcp.common[0].SN.value == 'will be overwritten by read' - assert d_tcp.common[0].SN.dirty - - tcp_read_buffer = [b'\x00\x00\x00\x00\x00#\x01\x03 ', - b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] - d_tcp.client.socket.clear_buffer() - d_tcp.client.socket._set_buffer(tcp_read_buffer) - d_tcp.common[0].SN.read() - assert d_tcp.common[0].SN.value == 'sn-000' - assert not d_tcp.common[0].SN.dirty - - # rtu - d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") - rtu_buffer = [b'\x01\x03\x06Su', - b'nS\x00\x01\x8d\xe4', - b'\x01\x03\x02\x00B', - b'8u', - b'\x01\x03\x88\x00\x01', - b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x01\x00\x00M\xf9', - b'\x01\x03\x02\x00~', - b'8d', - b'\x01\x03\x02\x00@', - b'\xb9\xb4', - b'\x01\x03\x84\x00~', - b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' - b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', - b'\x01\x03\x02\xff\xff', - b'\xb9\xf4'] - d_rtu.open() - d_rtu.client.serial._set_buffer(rtu_buffer) - d_rtu.scan() - assert d_rtu.common[0].SN.value == 'sn-123456789' - assert not d_rtu.common[0].SN.dirty - - d_rtu.common[0].SN.value = 'sn-000' - assert d_rtu.common[0].SN.value == 'sn-000' - assert d_rtu.common[0].SN.dirty - - rtu_write_buffer = [b'\x01\x10\x9ct\x00', - b'\x10\xaf\x8f'] - d_rtu.client.serial.clear_buffer() - d_rtu.client.serial._set_buffer(rtu_write_buffer) - d_rtu.common[0].write() - - d_rtu.common[0].SN.value = 'will be overwritten by read' - assert d_rtu.common[0].SN.value == 'will be overwritten by read' - assert d_rtu.common[0].SN.dirty - - rtu_read_buffer = [b'\x01\x03 sn', - b'-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\xfb'] - d_rtu.client.serial.clear_buffer() - d_rtu.client.serial._set_buffer(rtu_read_buffer) - d_rtu.common[0].SN.read() - assert d_rtu.common[0].SN.value == 'sn-000' - assert not d_rtu.common[0].SN.dirty - - -class TestSunSpecModbusClientGroup: - def test_read(self, monkeypatch): - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - - # tcp - d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) - tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', - b'SunS\x00\x01', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00B', - b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', - b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00~', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00@', - b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', - b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' - b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' - b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\xff\xff'] - d_tcp.client.connect() - d_tcp.client.socket._set_buffer(tcp_buffer) - d_tcp.scan() - assert d_tcp.common[0].SN.value == "sn-123456789" - assert d_tcp.common[0].Vr.value == "1.2.3" - assert not d_tcp.common[0].SN.dirty - assert not d_tcp.common[0].Vr.dirty - - d_tcp.common[0].SN.value = 'this will overwrite from read' - d_tcp.common[0].Vr.value = 'this will overwrite from read' - assert d_tcp.common[0].SN.value == 'this will overwrite from read' - assert d_tcp.common[0].Vr.value == 'this will overwrite from read' - assert d_tcp.common[0].SN.dirty - assert d_tcp.common[0].Vr.dirty - - tcp_read_buffer = [b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', - b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] - d_tcp.client.socket.clear_buffer() - d_tcp.client.socket._set_buffer(tcp_read_buffer) - d_tcp.common[0].read() - - assert d_tcp.common[0].SN.value == "sn-123456789" - assert d_tcp.common[0].Vr.value == "1.2.3" - assert not d_tcp.common[0].SN.dirty - assert not d_tcp.common[0].Vr.dirty - - # rtu - d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") - rtu_buffer = [b'\x01\x03\x06Su', - b'nS\x00\x01\x8d\xe4', - b'\x01\x03\x02\x00B', - b'8u', - b'\x01\x03\x88\x00\x01', - b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x01\x00\x00M\xf9', - b'\x01\x03\x02\x00~', - b'8d', - b'\x01\x03\x02\x00@', - b'\xb9\xb4', - b'\x01\x03\x84\x00~', - b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' - b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', - b'\x01\x03\x02\xff\xff', - b'\xb9\xf4'] - d_rtu.open() - d_rtu.client.serial._set_buffer(rtu_buffer) - d_rtu.scan() - assert d_rtu.common[0].SN.value == "sn-123456789" - assert d_rtu.common[0].Vr.value == "1.2.3" - assert not d_rtu.common[0].SN.dirty - assert not d_rtu.common[0].Vr.dirty - - d_rtu.common[0].SN.value = 'this will overwrite from read' - d_rtu.common[0].Vr.value = 'this will overwrite from read' - assert d_rtu.common[0].SN.value == 'this will overwrite from read' - assert d_rtu.common[0].Vr.value == 'this will overwrite from read' - assert d_rtu.common[0].SN.dirty - assert d_rtu.common[0].Vr.dirty - - rtu_read_buffer = [b'\x01\x03\x84\x00\x01', - b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00H\xef'] - d_rtu.client.serial.clear_buffer() - d_rtu.client.serial._set_buffer(rtu_read_buffer) - d_rtu.common[0].read() - assert d_rtu.common[0].SN.value == "sn-123456789" - assert d_rtu.common[0].Vr.value == "1.2.3" - assert not d_rtu.common[0].SN.dirty - assert not d_rtu.common[0].Vr.dirty - - def test_write(self, monkeypatch): - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - - # tcp - d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) - tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', - b'SunS\x00\x01', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00B', - b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', - b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00~', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00@', - b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', - b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' - b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' - b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\xff\xff'] - d_tcp.client.connect() - d_tcp.client.socket._set_buffer(tcp_buffer) - d_tcp.scan() - assert d_tcp.common[0].SN.value == "sn-123456789" - assert d_tcp.common[0].Vr.value == "1.2.3" - assert not d_tcp.common[0].SN.dirty - assert not d_tcp.common[0].Vr.dirty - - d_tcp.common[0].SN.value = 'sn-000' - d_tcp.common[0].Vr.value = 'v0.0.0' - assert d_tcp.common[0].SN.value == "sn-000" - assert d_tcp.common[0].Vr.value == "v0.0.0" - assert d_tcp.common[0].SN.dirty - assert d_tcp.common[0].Vr.dirty - - tcp_write_buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', - b'l\x00\x18'] - d_tcp.client.socket.clear_buffer() - d_tcp.client.socket._set_buffer(tcp_write_buffer) - d_tcp.common[0].write() - - d_tcp.common[0].SN.value = 'this will overwrite from read' - d_tcp.common[0].Vr.value = 'this will overwrite from read' - assert d_tcp.common[0].SN.value == 'this will overwrite from read' - assert d_tcp.common[0].Vr.value == 'this will overwrite from read' - assert d_tcp.common[0].SN.dirty - assert d_tcp.common[0].Vr.dirty - - tcp_read_buffer = [b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', - b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' - b'\x00\x00\x00\x00\x00\x00\x00v0.0.0\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00sn-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'] - d_tcp.client.socket.clear_buffer() - d_tcp.client.socket._set_buffer(tcp_read_buffer) - d_tcp.common[0].read() - - assert d_tcp.common[0].SN.value == "sn-000" - assert d_tcp.common[0].Vr.value == "v0.0.0" - assert not d_tcp.common[0].SN.dirty - assert not d_tcp.common[0].Vr.dirty - - # rtu - d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") - rtu_buffer = [b'\x01\x03\x06Su', - b'nS\x00\x01\x8d\xe4', - b'\x01\x03\x02\x00B', - b'8u', - b'\x01\x03\x88\x00\x01', - b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x01\x00\x00M\xf9', - b'\x01\x03\x02\x00~', - b'8d', - b'\x01\x03\x02\x00@', - b'\xb9\xb4', - b'\x01\x03\x84\x00~', - b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' - b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', - b'\x01\x03\x02\xff\xff', - b'\xb9\xf4'] - d_rtu.open() - d_rtu.client.serial._set_buffer(rtu_buffer) - d_rtu.scan() - assert d_rtu.common[0].SN.value == "sn-123456789" - assert d_rtu.common[0].Vr.value == "1.2.3" - assert not d_rtu.common[0].SN.dirty - assert not d_rtu.common[0].Vr.dirty - - d_rtu.common[0].SN.value = 'sn-000' - d_rtu.common[0].Vr.value = 'v0.0.0' - assert d_rtu.common[0].SN.value == "sn-000" - assert d_rtu.common[0].Vr.value == "v0.0.0" - assert d_rtu.common[0].SN.dirty - assert d_rtu.common[0].Vr.dirty - - rtu_write_buffer = [b'\x01\x10\x9cl\x00', - b'\x18.N'] - d_rtu.client.serial.clear_buffer() - d_rtu.client.serial._set_buffer(rtu_write_buffer) - d_rtu.common[0].write() - - d_rtu.common[0].SN.value = 'this will overwrite from read' - d_rtu.common[0].Vr.value = 'this will overwrite from read' - assert d_rtu.common[0].SN.value == 'this will overwrite from read' - assert d_rtu.common[0].Vr.value == 'this will overwrite from read' - assert d_rtu.common[0].SN.dirty - assert d_rtu.common[0].Vr.dirty - - rtu_read_buffer = [b'\x01\x03\x84\x00\x01', - b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00' - b'\x00\x00\x00\x00\x00\x00v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4h'] - d_rtu.client.serial.clear_buffer() - d_rtu.client.serial._set_buffer(rtu_read_buffer) - d_rtu.common[0].read() - assert d_rtu.common[0].SN.value == "sn-000" - assert d_rtu.common[0].Vr.value == "v0.0.0" - assert not d_rtu.common[0].SN.dirty - assert not d_rtu.common[0].Vr.dirty - - def test_write_points(self): - pass - - -class TestSunSpecModbusClientModel: - def test___init__(self): - c = client.SunSpecModbusClientModel(704) - assert c.model_id == 704 - assert c.model_addr == 0 - assert c.model_len == 0 - assert c.model_def['id'] == 704 - assert c.error_info == '' - assert c.gdef['name'] == 'DERCtlAC' - assert c.mid is None - assert c.device is None - assert c.model == c - - def test_error(self): - c = client.SunSpecModbusClientModel(704) - c.add_error('test error') - assert c.error_info == 'test error\n' - - -class TestSunSpecModbusClientDevice: - def test___init__(self): - d = client.SunSpecModbusClientDevice() - assert d.did - assert d.retry_count == 2 - assert d.base_addr_list == [40000, 0, 50000] - assert d.base_addr is None - - def test_connect(self): - pass - - def test_disconnect(self): - pass - - def test_close(self): - pass - - def test_read(self): - pass - - def test_write(self): - pass - - def test_scan(self, monkeypatch): - # tcp scan - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - c_tcp = client.SunSpecModbusClientDeviceTCP() - tcp_req_check = [b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00\x03', - b'\x00\x00\x00\x00\x00\x06\x01\x03\x9cC\x00\x01', - b'\x00\x00\x00\x00\x00\x06\x01\x03\x9cB\x00D', - b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\x86\x00\x01', - b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\x87\x00\x01', - b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\x86\x00B', - b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\xc8\x00\x01'] - tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', - b'SunS\x00\x01', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00B', - b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', - b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00~', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\x00@', - b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', - b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' - b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' - b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', - b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', - b'\xff\xff'] - c_tcp.client.connect() - c_tcp.client.socket._set_buffer(tcp_buffer) - c_tcp.scan() - assert c_tcp.common - assert c_tcp.volt_var - for req in range(len(tcp_req_check)): - assert tcp_req_check[req] == c_tcp.client.socket.request[req] - - # rtu scan - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c_rtu = client.SunSpecModbusClientDeviceRTU(1, "COMM2") - - rtu_req_check = [b'\x01\x03\x9c@\x00\x03*O', - b'\x01\x03\x9cC\x00\x01[\x8e', - b'\x01\x03\x9cB\x00D\xcb\xbd', - b'\x01\x03\x9c\x86\x00\x01K\xb3', - b'\x01\x03\x9c\x87\x00\x01\x1as', - b'\x01\x03\x9c\x86\x00B\nB', - b'\x01\x03\x9c\xc8\x00\x01+\xa4'] - rtu_buffer = [b'\x01\x03\x06Su', - b'nS\x00\x01\x8d\xe4', - b'\x01\x03\x02\x00B', - b'8u', - b'\x01\x03\x88\x00\x01', - b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x01\x00\x00M\xf9', - b'\x01\x03\x02\x00~', - b'8d', - b'\x01\x03\x02\x00@', - b'\xb9\xb4', - b'\x01\x03\x84\x00~', - b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' - b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' - b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' - b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', - b'\x01\x03\x02\xff\xff', - b'\xb9\xf4'] - c_rtu.open() - c_rtu.client.serial._set_buffer(rtu_buffer) - c_rtu.scan() - assert c_rtu.common - assert c_rtu.volt_var - for req in range(len(rtu_req_check)): - assert rtu_req_check[req] == c_rtu.client.serial.request[req] - - -class TestSunSpecModbusClientDeviceTCP: - def test___init__(self): - d = client.SunSpecModbusClientDeviceTCP() - assert d.slave_id == 1 - assert d.ipaddr == '127.0.0.1' - assert d.ipport == 502 - assert d.timeout is None - assert d.ctx is None - assert d.trace_func is None - assert d.max_count == 125 - assert d.client.__class__.__name__ == 'ModbusClientTCP' - - def test_connect(self, monkeypatch): - d = client.SunSpecModbusClientDeviceTCP() - with pytest.raises(Exception) as exc: - d.connect() - - assert 'Connection error' in str(exc.value) - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - - d.connect() - assert d.client.socket is not None - assert d.client.socket.connected is True - assert d.client.socket.ipaddr == '127.0.0.1' - assert d.client.socket.ipport == 502 - assert d.client.socket.timeout == 2 - - def test_disconnect(self, monkeypatch): - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - - d = client.SunSpecModbusClientDeviceTCP() - d.client.connect() - assert d.client.socket - d.client.disconnect() - assert d.client.socket is None - - def test_read(self, monkeypatch): - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - d = client.SunSpecModbusClientDeviceTCP() - buffer = [b'\x00\x00\x00\x00\x00\x8f\x01\x03\x8c', b'SunS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' - b'\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x01\x00\x00'] - check_req = b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00F' - d.client.connect() - d.client.socket._set_buffer(buffer) - assert d.read(40000, 70) == buffer[1] - assert d.client.socket.request[0] == check_req - - def test_write(self, monkeypatch): - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - d = client.SunSpecModbusClientDeviceTCP() - d.client.connect() - - data_to_write = b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - - buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', b't\x00\x10'] - d.client.socket._set_buffer(buffer) - d.client.write(40052, data_to_write) - - 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 d.client.socket.request[0] == check_req - - -class TestSunSpecModbusClientDeviceRTU: - def test___init__(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") - assert d.slave_id == 1 - assert d.name == "COMM2" - assert d.client.__class__.__name__ == "ModbusClientRTU" - assert d.ctx is None - assert d.trace_func is None - assert d.max_count == 125 - - def test_open(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") - d.open() - assert d.client.serial.connected - - def test_close(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") - d.open() - d.close() - assert not d.client.serial.connected - - def test_read(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") - d.open() - in_buff = [b'\x01\x03\x8cSu', b'nS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00' - b'\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x01\x00\x00\xb7d'] - check_req = b'\x01\x03\x9c@\x00F\xeb\xbc' - d.client.serial._set_buffer(in_buff) - check_read = in_buff[0] + in_buff[1] - assert d.read(40000, 70) == check_read[3:-2] - assert d.client.serial.request[0] == check_req - - def test_write(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") - d.open() - data_to_write = b'v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-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' - - buffer = [b'\x01\x10\x9cl\x00', b'\x18.N'] - d.client.serial._set_buffer(buffer) - d.write(40044, data_to_write) - - check_req = b'\x01\x10\x9cl\x00\x180v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00' \ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ - b'\x00\x00\x00\x00\x00\xad\xff' - assert d.client.serial.request[0] == check_req - - -if __name__ == "__main__": - pass - diff --git a/build/lib/sunspec2/tests/test_modbus_modbus.py b/build/lib/sunspec2/tests/test_modbus_modbus.py deleted file mode 100644 index a29132b..0000000 --- a/build/lib/sunspec2/tests/test_modbus_modbus.py +++ /dev/null @@ -1,206 +0,0 @@ -import sunspec2.modbus.modbus as modbus_client -import pytest -import socket -import serial -import sunspec2.tests.mock_socket as MockSocket -import sunspec2.tests.mock_port as MockPort - - -def test_modbus_rtu_client(monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c = modbus_client.modbus_rtu_client('COMM2') - assert c.baudrate == 9600 - assert c.parity == "N" - assert modbus_client.modbus_rtu_clients['COMM2'] - - with pytest.raises(modbus_client.ModbusClientError) as exc1: - c2 = modbus_client.modbus_rtu_client('COMM2', baudrate=99) - assert 'Modbus client baudrate mismatch' in str(exc1.value) - - with pytest.raises(modbus_client.ModbusClientError) as exc2: - c2 = modbus_client.modbus_rtu_client('COMM2', parity='E') - assert 'Modbus client parity mismatch' in str(exc2.value) - - -def test_modbus_rtu_client_remove(monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c = modbus_client.modbus_rtu_client('COMM2') - assert modbus_client.modbus_rtu_clients['COMM2'] - modbus_client.modbus_rtu_client_remove('COMM2') - assert modbus_client.modbus_rtu_clients.get('COMM2') is None - - -def test___generate_crc16_table(): - pass - - -def test_computeCRC(): - pass - - -def test_checkCRC(): - pass - - -class TestModbusClientRTU: - def test___init__(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c = modbus_client.ModbusClientRTU(name="COM2") - assert c.name == "COM2" - assert c.baudrate == 9600 - assert c.parity is None - assert c.serial is not None - assert c.timeout == .5 - assert c.write_timeout == .5 - assert not c.devices - - def test_open(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c = modbus_client.ModbusClientRTU(name="COM2") - c.open() - assert c.serial.connected - - def test_close(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c = modbus_client.ModbusClientRTU(name="COM2") - c.open() - c.close() - assert not c.serial.connected - - def test_add_device(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c = modbus_client.ModbusClientRTU(name="COM2") - c.add_device(1, "1") - assert c.devices.get(1) is not None - assert c.devices[1] == "1" - - def test_remove_device(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c = modbus_client.ModbusClientRTU(name="COM2") - c.add_device(1, "1") - assert c.devices.get(1) is not None - assert c.devices[1] == "1" - c.remove_device(1) - assert c.devices.get(1) is None - - def test__read(self): - pass - - def test_read(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c = modbus_client.ModbusClientRTU(name="COM2") - in_buff = [b'\x01\x03\x8cSu', b'nS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00' - b'\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x01\x00\x00\xb7d'] - check_req = b'\x01\x03\x9c@\x00F\xeb\xbc' - c.open() - c.serial._set_buffer(in_buff) - - check_read = in_buff[0] + in_buff[1] - assert c.read(1, 40000, 70) == check_read[3:-2] - assert c.serial.request[0] == check_req - - def test__write(self): - pass - - def test_write(self, monkeypatch): - monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) - c = modbus_client.ModbusClientRTU(name="COM2") - c.open() - data_to_write = b'v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-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' - - buffer = [b'\x01\x10\x9cl\x00', b'\x18.N'] - c.serial._set_buffer(buffer) - c.write(1, 40044, data_to_write) - - check_req = b'\x01\x10\x9cl\x00\x180v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00' \ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ - b'\x00\x00\x00\x00\x00\xad\xff' - assert c.serial.request[0] == check_req - - -class TestModbusClientTCP: - def test___init__(self): - c = modbus_client.ModbusClientTCP() - assert c.slave_id == 1 - assert c.ipaddr == '127.0.0.1' - assert c.ipport == 502 - assert c.timeout == 2 - assert c.ctx is None - assert c.trace_func is None - assert c.max_count == 125 - - def test_close(self, monkeypatch): - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - - c = modbus_client.ModbusClientTCP() - c.connect() - assert c.socket - c.disconnect() - assert c.socket is None - - def test_connect(self, monkeypatch): - c = modbus_client.ModbusClientTCP() - - with pytest.raises(Exception) as exc: - c.connect() - assert 'Connection error' in str(exc.value) - - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - c.connect() - assert c.socket is not None - assert c.socket.connected is True - assert c.socket.ipaddr == '127.0.0.1' - assert c.socket.ipport == 502 - assert c.socket.timeout == 2 - - def test_disconnect(self, monkeypatch): - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - - c = modbus_client.ModbusClientTCP() - c.connect() - assert c.socket - c.disconnect() - assert c.socket is None - - def test__read(self, monkeypatch): - pass - - def test_read(self, monkeypatch): - c = modbus_client.ModbusClientTCP() - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - in_buff = [b'\x00\x00\x00\x00\x00\x8f\x01\x03\x8c', b'SunS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' - b'\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x01\x00\x00'] - check_req = b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00F' - c.connect() - c.socket._set_buffer(in_buff) - assert c.read(40000, 70) == in_buff[1] - assert c.socket.request[0] == check_req - - def test__write(self, monkeypatch): - pass - - def test_write(self, monkeypatch): - c = modbus_client.ModbusClientTCP() - monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) - c.connect() - data_to_write = b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - - buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', b't\x00\x10'] - c.socket._set_buffer(buffer) - c.write(40052, data_to_write) - - 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 diff --git a/build/lib/sunspec2/tests/test_smdx.py b/build/lib/sunspec2/tests/test_smdx.py deleted file mode 100644 index 0276d14..0000000 --- a/build/lib/sunspec2/tests/test_smdx.py +++ /dev/null @@ -1,250 +0,0 @@ -import sunspec2.smdx as smdx -import sunspec2.mdef as mdef -import xml.etree.ElementTree as ET -import pytest -import copy - - -def test_to_smdx_filename(): - assert smdx.to_smdx_filename(77) == 'smdx_00077.xml' - - -def test_model_filename_to_id(): - assert smdx.model_filename_to_id('smdx_00077.xml') == 77 - assert smdx.model_filename_to_id('smdx_abc.xml') is None - - -def test_from_smdx_file(): - smdx_304 = { - "id": 304, - "group": { - "name": "inclinometer", - "type": "group", - "points": [ - { - "name": "ID", - "value": 304, - "desc": "Model identifier", - "label": "Model ID", - "mandatory": "M", - "static": "S", - "type": "uint16" - }, - { - "name": "L", - "desc": "Model length", - "label": "Model Length", - "mandatory": "M", - "static": "S", - "type": "uint16" - } - ], - "groups": [ - { - "name": "incl", - "type": "group", - "count": 0, - "points": [ - { - "name": "Inclx", - "type": "int32", - "mandatory": "M", - "units": "Degrees", - "sf": -2, - "label": "X", - "desc": "X-Axis inclination" - }, - { - "name": "Incly", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Y", - "desc": "Y-Axis inclination" - }, - { - "name": "Inclz", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Z", - "desc": "Z-Axis inclination" - } - ] - } - ], - "label": "Inclinometer Model", - "desc": "Include to support orientation measurements" - } - } - assert smdx.from_smdx_file('sunspec2/models/smdx/smdx_00304.xml') == smdx_304 - - -def test_from_smdx(): - tree = ET.parse('sunspec2/models/smdx/smdx_00304.xml') - root = tree.getroot() - - mdef_not_found = copy.deepcopy(root) - mdef_not_found.remove(mdef_not_found.find('model')) - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx(mdef_not_found) - - duplicate_fixed_btype_str = ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ''' - duplicate_fixed_btype_xml = ET.fromstring(duplicate_fixed_btype_str) - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx(duplicate_fixed_btype_xml) - - dup_repeating_btype_str = ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - ''' - dup_repeating_btype_xml = ET.fromstring(dup_repeating_btype_str) - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx(dup_repeating_btype_xml) - - invalid_btype_root = copy.deepcopy(root) - invalid_btype_root.find('model').find('block').set('type', 'abc') - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx(invalid_btype_root) - - dup_fixed_p_def_str = ''' - - - - - - - - - - - - - - - - ''' - dup_fixed_p_def_xml = ET.fromstring(dup_fixed_p_def_str) - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx(dup_fixed_p_def_xml) - - dup_repeating_p_def_str = ''' - - - - - - - - - - - - - - - - - - - - - ''' - dup_repeating_p_def_xml = ET.fromstring(dup_repeating_p_def_str) - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx(dup_repeating_p_def_xml) - - -def test_from_smdx_point(): - smdx_point_str = """""" - smdx_point_xml = ET.fromstring(smdx_point_str) - assert smdx.from_smdx_point(smdx_point_xml) == {'name': 'Mn', 'type': 'string', 'size': 16, 'mandatory': 'M'} - - missing_pid_xml = copy.deepcopy(smdx_point_xml) - del missing_pid_xml.attrib['id'] - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx_point(missing_pid_xml) - - missing_ptype = copy.deepcopy(smdx_point_xml) - del missing_ptype.attrib['type'] - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx_point(missing_ptype) - - unk_ptype = copy.deepcopy(smdx_point_xml) - unk_ptype.attrib['type'] = 'abc' - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx_point(unk_ptype) - - missing_len = copy.deepcopy(smdx_point_xml) - del missing_len.attrib['len'] - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx_point(missing_len) - - unk_mand_type = copy.deepcopy(smdx_point_xml) - unk_mand_type.attrib['mandatory'] = 'abc' - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx_point(unk_mand_type) - - unk_access_type = copy.deepcopy(smdx_point_xml) - unk_access_type.attrib['access'] = 'abc' - with pytest.raises(mdef.ModelDefinitionError): - smdx.from_smdx_point(unk_access_type) - - -def test_indent(): - pass diff --git a/build/lib/sunspec2/tests/test_spreadsheet.py b/build/lib/sunspec2/tests/test_spreadsheet.py deleted file mode 100644 index 973711b..0000000 --- a/build/lib/sunspec2/tests/test_spreadsheet.py +++ /dev/null @@ -1,591 +0,0 @@ -import sunspec2.spreadsheet as spreadsheet -import pytest -import csv -import copy -import json - - -def test_idx(): - row = ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', - 'Units', 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'] - - assert spreadsheet.idx(row, 'Address Offset') == 0 - with pytest.raises(ValueError): - del row[0] - assert spreadsheet.idx(row, 'Address Offset', mandatory=True) - - -def test_row_is_empty(): - row = [''] * 10 - assert spreadsheet.row_is_empty(row, 0) - row[0] = 'abc' - assert not spreadsheet.row_is_empty(row, 0) - - -def test_find_name(): - points = [ - { - "name": "Inclx", - "type": "int32", - "mandatory": "M", - "units": "Degrees", - "sf": -2, - "label": "X", - "desc": "X-Axis inclination" - }, - { - "name": "Incly", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Y", - "desc": "Y-Axis inclination" - }, - { - "name": "Inclz", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Z", - "desc": "Z-Axis inclination" - } - ] - assert spreadsheet.find_name(points, 'abc') is None - assert spreadsheet.find_name(points, 'Incly') == points[1] - - -def test_element_type(): - pass - - -def test_from_spreadsheet(): - model_spreadsheet = [ - ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', - 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], - ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', 'Include to support orientation measurements', ''], - [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], - [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], - ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], - ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], - ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], - ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] - ] - model_def = { - "id": 304, - "group": { - "name": "inclinometer", - "type": "group", - "points": [ - { - "name": "ID", - "value": 304, - "desc": "Model identifier", - "label": "Model ID", - "mandatory": "M", - "static": "S", - "type": "uint16" - }, - { - "name": "L", - "desc": "Model length", - "label": "Model Length", - "mandatory": "M", - "static": "S", - "type": "uint16" - } - ], - "groups": [ - { - "name": "incl", - "type": "group", - "count": 0, - "points": [ - { - "name": "Inclx", - "type": "int32", - "mandatory": "M", - "units": "Degrees", - "sf": -2, - "label": "X", - "desc": "X-Axis inclination" - }, - { - "name": "Incly", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Y", - "desc": "Y-Axis inclination" - }, - { - "name": "Inclz", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Z", - "desc": "Z-Axis inclination" - } - ] - } - ], - "label": "Inclinometer Model", - "desc": "Include to support orientation measurements" - } - } - - assert spreadsheet.from_spreadsheet(model_spreadsheet) == model_def - - -def test_to_spreadsheet(): - model_spreadsheet = [ - ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', - 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description'], - ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', 'Include to support orientation measurements'], - [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier'], - [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length'], - ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', ''], - ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination'], - ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination'], - ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination'] - ] - model_def = { - "id": 304, - "group": { - "name": "inclinometer", - "type": "group", - "points": [ - { - "name": "ID", - "value": 304, - "desc": "Model identifier", - "label": "Model ID", - "mandatory": "M", - "static": "S", - "type": "uint16" - }, - { - "name": "L", - "desc": "Model length", - "label": "Model Length", - "mandatory": "M", - "static": "S", - "type": "uint16" - } - ], - "groups": [ - { - "name": "incl", - "type": "group", - "count": 0, - "points": [ - { - "name": "Inclx", - "type": "int32", - "mandatory": "M", - "units": "Degrees", - "sf": -2, - "label": "X", - "desc": "X-Axis inclination" - }, - { - "name": "Incly", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Y", - "desc": "Y-Axis inclination" - }, - { - "name": "Inclz", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Z", - "desc": "Z-Axis inclination" - } - ] - } - ], - "label": "Inclinometer Model", - "desc": "Include to support orientation measurements" - } - } - assert spreadsheet.to_spreadsheet(model_def) == model_spreadsheet - - -def test_to_spreadsheet_group(): - model_def = { - "group": { - "desc": "DER capacity model.", - "label": "DER Capacity", - "name": "DERCapacity", - "points": [ - { - "access": "R", - "desc": "DER capacity model id.", - "label": "DER Capacity Model ID", - "mandatory": "M", - "name": "ID", - "static": "S", - "type": "uint16", - "value": 702 - }, - { - "access": "R", - "desc": "DER capacity name model length.", - "label": "DER Capacity Model Length", - "mandatory": "M", - "name": "L", - "static": "S", - "type": "uint16" - }, - { - "access": "R", - "comments": [ - "Nameplate Ratings - Specifies capacity ratings" - ], - "desc": "Maximum active power rating at unity power factor in watts.", - "label": "Active Power Max Rating", - "mandatory": "O", - "name": "WMaxRtg", - "sf": "W_SF", - "type": "uint16", - "units": "W", - "symbols": [ - { - "name": "CAT_A", - "value": 1 - }, - { - "name": "CAT_B", - "value": 2 - } - ] - } - ], - "type": "group" - }, - "id": 702 - } - ss = [] - spreadsheet.to_spreadsheet_group(ss, model_def['group'], has_notes=False) - assert ss == [ - ['', '', 'DERCapacity', '', '', 'group', '', '', '', '', '', '', 'DER Capacity', 'DER capacity model.'], - ['', 0, 'ID', 702, '', 'uint16', '', '', '', '', 'M', 'S', 'DER Capacity Model ID', - 'DER capacity model id.'], - ['', 1, 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'DER Capacity Model Length', - 'DER capacity name model length.'], - ['Nameplate Ratings - Specifies capacity ratings', '', '', '', '', '', '', '', '', '', '', '', '', ''], - ['', 2, 'WMaxRtg', '', '', 'uint16', '', 'W_SF', 'W', '', '', '', 'Active Power Max Rating', - 'Maximum active power rating at unity power factor in watts.'], - ['', '', 'CAT_A', 1, '', '', '', '', '', '', '', '', '', ''], - ['', '', 'CAT_B', 2, '', '', '', '', '', '', '', '', '', '']] - -def test_to_spreadsheet_point(): - point = { - "access": "R", - "desc": "Abnormal operating performance category as specified in IEEE 1547-2018.", - "label": "Abnormal Operating Category", - "mandatory": "O", - "name": "AbnOpCatRtg", - "symbols": [ - { - "name": "CAT_1", - "value": 1 - }, - { - "name": "CAT_2", - "value": 2 - }, - { - "name": "CAT_3", - "value": 3 - } - ], - "type": "enum16" - } - ss = [] - assert spreadsheet.to_spreadsheet_point(ss, point, has_notes=False) == 1 - - missing_name_p = copy.deepcopy(point) - del missing_name_p['name'] - with pytest.raises(Exception) as exc1: - spreadsheet.to_spreadsheet_point(ss, missing_name_p, has_notes=False) - assert 'Point missing name attribute' in str(exc1.value) - - missing_type_p = copy.deepcopy(point) - del missing_type_p['type'] - with pytest.raises(Exception) as exc2: - spreadsheet.to_spreadsheet_point(ss, missing_type_p, has_notes=False) - assert 'Point AbnOpCatRtg missing type' in str(exc2.value) - - unk_p_type = copy.deepcopy(point) - unk_p_type['type'] = 'abc' - with pytest.raises(Exception) as exc3: - spreadsheet.to_spreadsheet_point(ss, unk_p_type, has_notes=False) - assert 'Unknown point type' in str(exc3.value) - - p_size_not_int = copy.deepcopy(point) - p_size_not_int['type'] = 'string' - p_size_not_int['size'] = 'abc' - with pytest.raises(Exception) as exc4: - spreadsheet.to_spreadsheet_point(ss, p_size_not_int, has_notes=False) - assert 'Point size is for point AbnOpCatRtg not an iteger value' in str(exc4.value) - - -def test_to_spreadsheet_symbol(): - symbol = {"name": "MAX_W", "value": 0} - ss = [] - spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False) - assert ss[0][2] == 'MAX_W' and ss[0][3] == 0 - - ss = [] - del symbol['value'] - with pytest.raises(Exception) as exc1: - spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False) - assert 'Symbol MAX_W missing value' in str(exc1.value) - - ss = [] - del symbol['name'] - with pytest.raises(Exception) as exc2: - spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False) - assert 'Symbol missing name attribute' in str(exc2.value) - - -def test_to_spreadsheet_comment(): - ss = [] - spreadsheet.to_spreadsheet_comment(ss, 'Scaling Factors', has_notes=False) - assert ss[0][0] == 'Scaling Factors' - - -def test_spreadsheet_equal(): - spreadsheet_smdx_304 = [ - ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', - 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], - ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', - 'Include to support orientation measurements', ''], - ['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], - ['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], - ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], - ['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], - ['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], - ['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] - ] - ss_copy = copy.deepcopy(spreadsheet_smdx_304) - - assert spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy) - - with pytest.raises(Exception) as exc1: - ss_copy[0][0] = 'abc' - spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy) - assert 'Line 1 different' in str(exc1.value) - - with pytest.raises(Exception) as exc2: - del ss_copy[0] - spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy) - assert 'Different length' in str(exc2.value) - - -def test_from_csv(): - model_def = { - "id": 304, - "group": { - "name": "inclinometer", - "type": "group", - "points": [ - { - "name": "ID", - "value": 304, - "desc": "Model identifier", - "label": "Model ID", - "mandatory": "M", - "static": "S", - "type": "uint16" - }, - { - "name": "L", - "desc": "Model length", - "label": "Model Length", - "mandatory": "M", - "static": "S", - "type": "uint16" - } - ], - "groups": [ - { - "name": "incl", - "type": "group", - "count": 0, - "points": [ - { - "name": "Inclx", - "type": "int32", - "mandatory": "M", - "units": "Degrees", - "sf": -2, - "label": "X", - "desc": "X-Axis inclination" - }, - { - "name": "Incly", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Y", - "desc": "Y-Axis inclination" - }, - { - "name": "Inclz", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Z", - "desc": "Z-Axis inclination" - } - ] - } - ], - "label": "Inclinometer Model", - "desc": "Include to support orientation measurements" - } - } - assert model_def == spreadsheet.from_csv('sunspec2/tests/test_data/smdx_304.csv') - - -def test_to_csv(tmp_path): - model_def = { - "id": 304, - "group": { - "name": "inclinometer", - "type": "group", - "points": [ - { - "name": "ID", - "value": 304, - "desc": "Model identifier", - "label": "Model ID", - "mandatory": "M", - "static": "S", - "type": "uint16" - }, - { - "name": "L", - "desc": "Model length", - "label": "Model Length", - "mandatory": "M", - "static": "S", - "type": "uint16" - } - ], - "groups": [ - { - "name": "incl", - "type": "group", - "count": 0, - "points": [ - { - "name": "Inclx", - "type": "int32", - "mandatory": "M", - "units": "Degrees", - "sf": -2, - "label": "X", - "desc": "X-Axis inclination" - }, - { - "name": "Incly", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Y", - "desc": "Y-Axis inclination" - }, - { - "name": "Inclz", - "type": "int32", - "units": "Degrees", - "sf": -2, - "label": "Z", - "desc": "Z-Axis inclination" - } - ] - } - ], - "label": "Inclinometer Model", - "desc": "Include to support orientation measurements" - } - } - ss = spreadsheet.to_spreadsheet(model_def) - spreadsheet.to_csv(model_def, filename=tmp_path / 'smdx_304.csv') - - same_data = True - row_num = 0 - idx = 0 - with open(tmp_path / 'smdx_304.csv') as csvfile: - csvreader = csv.reader(csvfile) - for row in csvreader: - idx = 0 - for i in row: - if str(ss[row_num][idx]) != str(i): - same_data = False - idx += 1 - row_num += 1 - assert same_data - - -def test_spreadsheet_from_csv(): - spreadsheet_smdx_304 = [ - ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', - 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], - ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', - 'Include to support orientation measurements', ''], - ['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], - ['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], - ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], - ['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], - ['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], - ['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] - ] - - counter = 0 - for row in spreadsheet.spreadsheet_from_csv('sunspec2/tests/test_data/smdx_304.csv'): - same = True - counter2 = 0 - for i in row: - if i != spreadsheet_smdx_304[counter][counter2]: - same = False - counter2 += 1 - counter += 1 - assert same - - -def test_spreadsheet_to_csv(tmp_path): - spreadsheet_smdx_304 = [ - ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', - 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], - ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', - 'Include to support orientation measurements', ''], - [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], - [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], - ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], - ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], - ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], - ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] - ] - spreadsheet.spreadsheet_to_csv(spreadsheet_smdx_304, filename=tmp_path / 'smdx_304.csv') - - same_data = True - rowNum = 0 - idx = 0 - with open(tmp_path / 'smdx_304.csv') as csvfile: - csvreader = csv.reader(csvfile) - for row in csvreader: - idx = 0 - for i in row: - if str(spreadsheet_smdx_304[rowNum][idx]) != str(i): - same_data = False - idx += 1 - rowNum += 1 - assert same_data - - diff --git a/build/lib/sunspec2/tests/test_xlsx.py b/build/lib/sunspec2/tests/test_xlsx.py deleted file mode 100644 index 2c1c3bf..0000000 --- a/build/lib/sunspec2/tests/test_xlsx.py +++ /dev/null @@ -1,910 +0,0 @@ -import sunspec2.xlsx as xlsx -import pytest -import openpyxl -import openpyxl.styles as styles -import json - - -def test___init__(): - wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') - assert wb.filename == 'sunspec2/tests/test_data/wb_701-705.xlsx' - assert wb.params == {} - - wb2 = xlsx.ModelWorkbook() - assert wb2.filename is None - assert wb2.params == {} - - -def test_get_models(): - wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') - assert wb.get_models() == [701, 702, 703, 704, 705] - wb2 = xlsx.ModelWorkbook() - assert wb2.get_models() == [] - - -def test_save(tmp_path): - wb = xlsx.ModelWorkbook() - wb.save(tmp_path / 'test.xlsx') - wb2 = xlsx.ModelWorkbook(filename=tmp_path / 'test.xlsx') - iter_rows = wb2.xlsx_iter_rows(wb.wb['Index']) - assert next(iter_rows) == ['Model', 'Label', 'Description'] - - -def test_xlsx_iter_rows(): - wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') - iter_rows = wb.xlsx_iter_rows(wb.wb['704']) - assert next(iter_rows) == ['Address Offset', 'Group Offset', 'Name', - 'Value', 'Count', 'Type', 'Size', 'Scale Factor', - 'Units', 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', - 'Label', 'Description', 'Detailed Description'] - assert next(iter_rows) == [None, None, 'DERCtlAC', None, None, 'group', - None, None, None, None, None, None, 'DER AC Controls', - 'DER AC controls model.', None] - - -def test_spreadsheet_from_xlsx(): - wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') - assert wb.spreadsheet_from_xlsx(704)[0:2] == [['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', - 'Type', 'Size', 'Scale Factor', 'Units', 'RW Access (RW)', - 'Mandatory (M)', 'Static (S)', 'Label', 'Description', - 'Detailed Description'], - ['', '', 'DERCtlAC', None, None, 'group', None, None, None, - None, None, None, 'DER AC Controls', 'DER AC controls model.', None]] - - -# need deep diff to compare from_xlsx to json file, right now just compares with its own output -def test_from_xlsx(): - wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') - with open('sunspec2/models/json/model_704.json') as f: - model_json_704 = json.load(f) - from_xlsx_output = { - "group": { - "name": "DERCtlAC", - "type": "group", - "label": "DER AC Controls", - "desc": "DER AC controls model.", - "points": [ - { - "name": "ID", - "type": "uint16", - "mandatory": "M", - "static": "S", - "label": "Model ID", - "desc": "Model name model id.", - "value": 704 - }, - { - "name": "L", - "type": "uint16", - "mandatory": "M", - "static": "S", - "label": "Model Length", - "desc": "Model name model length." - }, - { - "name": "PFWInjEna", - "type": "enum16", - "access": "RW", - "label": "Power Factor Enable (W Inj) Enable", - "desc": "Power factor enable when injecting active power.", - "comments": [ - "Set Power Factor (when injecting active power)" - ], - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "PFWInjEnaRvrt", - "type": "enum16", - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "PFWInjRvrtTms", - "type": "uint32", - "units": "Secs", - "access": "RW", - "label": "PF Reversion Time (W Inj)", - "desc": "Power factor reversion timer when injecting active power." - }, - { - "name": "PFWInjRvrtRem", - "type": "uint32", - "units": "Secs", - "label": "PF Reversion Time Rem (W Inj)", - "desc": "Power factor reversion time remaining when injecting active power." - }, - { - "name": "PFWAbsEna", - "type": "enum16", - "access": "RW", - "label": "Power Factor Enable (W Abs) Enable", - "desc": "Power factor enable when absorbing active power.", - "comments": [ - "Set Power Factor (when absorbing active power)" - ], - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "PFWAbsEnaRvrt", - "type": "enum16", - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "PFWAbsRvrtTms", - "type": "uint32", - "units": "Secs", - "access": "RW", - "label": "PF Reversion Time (W Abs)", - "desc": "Power factor reversion timer when absorbing active power." - }, - { - "name": "PFWAbsRvrtRem", - "type": "uint32", - "units": "Secs", - "label": "PF Reversion Time Rem (W Abs)", - "desc": "Power factor reversion time remaining when absorbing active power." - }, - { - "name": "WMaxLimEna", - "type": "enum16", - "access": "RW", - "label": "Limit Max Active Power Enable", - "desc": "Limit maximum active power enable.", - "comments": [ - "Limit Maximum Active Power Generation" - ], - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "WMaxLim", - "type": "uint16", - "sf": "WMaxLim_SF", - "units": "Pct", - "access": "RW", - "label": "Limit Max Power Setpoint", - "desc": "Limit maximum active power value." - }, - { - "name": "WMaxLimRvrt", - "type": "uint16", - "sf": "WMaxLim_SF", - "units": "Pct", - "access": "RW", - "label": "Reversion Limit Max Power", - "desc": "Reversion limit maximum active power value." - }, - { - "name": "WMaxLimEnaRvrt", - "type": "enum16", - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "WMaxLimRvrtTms", - "type": "uint32", - "units": "Secs", - "access": "RW", - "label": "Limit Max Power Reversion Time", - "desc": "Limit maximum active power reversion time." - }, - { - "name": "WMaxLimRvrtRem", - "type": "uint32", - "units": "Secs", - "label": "Limit Max Power Rev Time Rem", - "desc": "Limit maximum active power reversion time remaining." - }, - { - "name": "WSetEna", - "type": "enum16", - "access": "RW", - "label": "Set Active Power Enable", - "desc": "Set active power enable.", - "comments": [ - "Set Active Power Level (may be negative for charging)" - ], - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "WSetMod", - "type": "enum16", - "access": "RW", - "label": "Set Active Power Mode", - "desc": "Set active power mode.", - "symbols": [ - { - "name": "W_MAX_PCT", - "value": 1, - "label": "Active Power As Max Percent", - "desc": "Active power setting is percentage of maximum active power." - }, - { - "name": "WATTS", - "value": 2, - "label": "Active Power As Watts", - "desc": "Active power setting is in watts." - } - ] - }, - { - "name": "WSet", - "type": "int32", - "sf": "WSet_SF", - "units": "W", - "access": "RW", - "label": "Active Power Setpoint (W)", - "desc": "Active power setting value in watts." - }, - { - "name": "WSetRvrt", - "type": "int32", - "sf": "WSet_SF", - "units": "W", - "access": "RW", - "label": "Reversion Active Power (W)", - "desc": "Reversion active power setting value in watts." - }, - { - "name": "WSetPct", - "type": "int32", - "sf": "WSetPct_SF", - "units": "Pct", - "access": "RW", - "label": "Active Power Setpoint (Pct)", - "desc": "Active power setting value as percent." - }, - { - "name": "WSetPctRvrt", - "type": "int32", - "sf": "WSetPct_SF", - "units": "Pct", - "access": "RW", - "label": "Reversion Active Power (Pct)", - "desc": "Reversion active power setting value as percent." - }, - { - "name": "WSetEnaRvrt", - "type": "enum16", - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "WSetRvrtTms", - "type": "uint32", - "units": "Secs", - "access": "RW", - "label": "Active Power Reversion Time", - "desc": "Set active power reversion time." - }, - { - "name": "WSetRvrtRem", - "type": "uint32", - "units": "Secs", - "label": "Active Power Rev Time Rem", - "desc": "Set active power reversion time remaining." - }, - { - "name": "VarSetEna", - "type": "enum16", - "access": "RW", - "label": "Set Reactive Power Enable", - "desc": "Set reactive power enable.", - "comments": [ - "Set Reacitve Power Level" - ], - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "VarSetMod", - "type": "enum16", - "access": "RW", - "label": "Set Reactive Power Mode", - "desc": "Set reactive power mode.", - "symbols": [ - { - "name": "W_MAX_PCT", - "value": 1, - "label": "Reactive Power as Watt Max Pct", - "desc": "Reactive power setting is percent of maximum active power." - }, - { - "name": "VAR_MAX_PCT", - "value": 2, - "label": "Reactive Power as Var Max Pct", - "desc": "Reactive power setting is percent of maximum reactive power." - }, - { - "name": "VAR_AVAIL_PCT", - "value": 3, - "label": "Reactive Power as Var Avail Pct", - "desc": "Reactive power setting is percent of available reactive power." - }, - { - "name": "VARS", - "value": 4, - "label": "Reactive Power as Vars", - "desc": "Reactive power is in vars." - } - ] - }, - { - "name": "VarSetPri", - "type": "enum16", - "symbols": [ - { - "name": "ACTIVE", - "value": 1, - "label": "Active Power Priority", - "desc": "Active power priority." - }, - { - "name": "REACTIVE", - "value": 2, - "label": "Reactive Power Priority", - "desc": "Reactive power priority." - }, - { - "name": "IEEE_1547", - "value": 3, - "label": "IEEE 1547 Power Priority", - "desc": "IEEE 1547-2018 power priority mode." - }, - { - "name": "PF", - "value": 4, - "label": "PF Power Priority", - "desc": "Track PF setting derived from current active and reactive power settings." - }, - { - "name": "VENDOR", - "value": 5, - "label": "Vendor Power Priority", - "desc": "Power priority is vendor specific mode." - } - ] - }, - { - "name": "VarSet", - "type": "int32", - "sf": "VarSet_SF", - "units": "Var", - "access": "RW", - "label": "Reactive Power Setpoint (Vars)", - "desc": "Reactive power setting value in vars." - }, - { - "name": "VarSetRvrt", - "type": "int32", - "sf": "VarSet_SF", - "units": "Var", - "access": "RW", - "label": "Reversion Reactive Power (Vars)", - "desc": "Reversion reactive power setting value in vars." - }, - { - "name": "VarSetPct", - "type": "int32", - "sf": "VarSetPct_SF", - "units": "Pct", - "access": "RW", - "label": "Reactive Power Setpoint (Pct)", - "desc": "Reactive power setting value as percent." - }, - { - "name": "VarSetPctRvrt", - "type": "enum16", - "sf": "VarSetPct_SF", - "units": "Pct", - "access": "RW", - "label": "Reversion Reactive Power (Pct)", - "desc": "Reversion reactive power setting value as percent.", - "symbols": [ - { - "name": "DISABLED", - "value": 0, - "label": "Disabled", - "desc": "Function is disabled." - }, - { - "name": "ENABLED", - "value": 1, - "label": "Enabled", - "desc": "Function is enabled." - } - ] - }, - { - "name": "VarSetRvrtTms", - "type": "uint32", - "units": "Secs", - "access": "RW", - "label": "Reactive Power Reversion Time", - "desc": "Set reactive power reversion time." - }, - { - "name": "VarSetRvrtRem", - "type": "uint32", - "units": "Secs", - "label": "Reactive Power Rev Time Rem", - "desc": "Set reactive power reversion time remaining." - }, - { - "name": "RGra", - "type": "uint32", - "units": "%WMax/Sec", - "access": "RW", - "label": "Normal Ramp Rate", - "desc": "Ramp rate for increases in active power during normal generation.", - "comments": [ - "Ramp Rate" - ], - "symbols": [ - { - "name": "A_MAX", - "value": 1, - "label": "Max Current Ramp", - "desc": "Ramp based on percent of max current per second." - }, - { - "name": "W_MAX", - "value": 2, - "label": "Max Active Power Ramp", - "desc": "Ramp based on percent of max active power per second." - } - ] - }, - { - "name": "PF_SF", - "type": "sunssf", - "static": "S", - "label": "Power Factor Scale Factor", - "desc": "Power factor scale factor.", - "comments": [ - "Scale Factors" - ] - }, - { - "name": "WMaxLim_SF", - "type": "sunssf", - "static": "S", - "label": "Limit Max Power Scale Factor", - "desc": "Limit maximum power scale factor." - }, - { - "name": "WSet_SF", - "type": "sunssf", - "static": "S", - "label": "Active Power Scale Factor", - "desc": "Active power scale factor." - }, - { - "name": "WSetPct_SF", - "type": "sunssf", - "static": "S", - "label": "Active Power Pct Scale Factor", - "desc": "Active power pct scale factor." - }, - { - "name": "VarSet_SF", - "type": "sunssf", - "static": "S", - "label": "Reactive Power Scale Factor", - "desc": "Reactive power scale factor." - }, - { - "name": "VarSetPct_SF", - "type": "sunssf", - "static": "S", - "label": "Reactive Power Pct Scale Factor", - "desc": "Reactive power pct scale factor." - } - ], - "groups": [ - { - "name": "PFWInj", - "type": "sync", - "label": " ", - "desc": " ", - "comments": [ - "Power Factor Settings" - ], - "points": [ - { - "name": "PF", - "type": "uint16", - "sf": "PF_SF", - "access": "RW", - "label": "Power Factor (W Inj) ", - "desc": "Power factor setpoint when injecting active power." - }, - { - "name": "Ext", - "type": "enum16", - "access": "RW", - "label": "Power Factor Excitation (W Inj)", - "desc": "Power factor excitation setpoint when injecting active power.", - "symbols": [ - { - "name": "OVER_EXCITED", - "value": 0, - "label": "Over-excited", - "desc": "Power factor over-excited excitation." - }, - { - "name": "UNDER_EXCITED", - "value": 1, - "label": "Under-excited", - "desc": "Power factor under-excited excitation." - } - ] - } - ] - }, - { - "name": "PFWInjRvrt", - "type": "sync", - "label": " ", - "desc": " ", - "points": [ - { - "name": "PF", - "type": "uint16", - "sf": "PF_SF", - "access": "RW", - "label": "Reversion Power Factor (W Inj) ", - "desc": "Reversion power factor setpoint when injecting active power." - }, - { - "name": "Ext", - "type": "enum16", - "access": "RW", - "label": "Reversion PF Excitation (W Inj)", - "desc": "Reversion power factor excitation setpoint when injecting active power.", - "symbols": [ - { - "name": "OVER_EXCITED", - "value": 0, - "label": "Over-excited", - "desc": "Power factor over-excited excitation." - }, - { - "name": "UNDER_EXCITED", - "value": 1, - "label": "Under-excited", - "desc": "Power factor under-excited excitation." - } - ] - } - ] - }, - { - "name": "PFWAbs", - "type": "sync", - "label": " ", - "desc": " ", - "points": [ - { - "name": "PF", - "type": "uint16", - "sf": "PF_SF", - "access": "RW", - "label": "Power Factor (W Abs) ", - "desc": "Power factor setpoint when absorbing active power." - }, - { - "name": "Ext", - "type": "enum16", - "access": "RW", - "label": "Power Factor Excitation (W Abs)", - "desc": "Power factor excitation setpoint when absorbing active power.", - "symbols": [ - { - "name": "OVER_EXCITED", - "value": 0, - "label": "Over-excited", - "desc": "Power factor over-excited excitation." - }, - { - "name": "UNDER_EXCITED", - "value": 1, - "label": "Under-excited", - "desc": "Power factor under-excited excitation." - } - ] - } - ] - }, - { - "name": "PFWAbsRvrt", - "type": "sync", - "label": " ", - "desc": " ", - "points": [ - { - "name": "PF", - "type": "uint16", - "sf": "PF_SF", - "access": "RW", - "label": "Reversion Power Factor (W Abs) ", - "desc": "Reversion power factor setpoint when absorbing active power." - }, - { - "name": "Ext", - "type": "enum16", - "access": "RW", - "label": "Reversion PF Excitation (W Abs)", - "desc": "Reversion power factor excitation setpoint when absorbing active power.", - "symbols": [ - { - "name": "OVER_EXCITED", - "value": 0, - "label": "Over-excited", - "desc": "Power factor over-excited excitation." - }, - { - "name": "UNDER_EXCITED", - "value": 1, - "label": "Under-excited", - "desc": "Power factor under-excited excitation." - } - ] - } - ] - } - ] - }, - "id": 704 - } - assert wb.from_xlsx(704) == from_xlsx_output - - -def test_set_cell(): - wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') - with pytest.raises(ValueError) as exc: - wb.set_cell(wb.wb['704'], 1, 2, 3) - assert 'Workbooks opened with existing file are read only' in str(exc.value) - - wb2 = xlsx.ModelWorkbook() - assert wb2.set_cell(wb2.wb['Index'], 2, 1, 3, style='suns_comment').value == 3 - - -def test_set_info(): - wb = xlsx.ModelWorkbook() - values = [''] * 15 - values[14] = 'detail' - values[13] = 'description' - values[12] = 'label' - wb.set_info(wb.wb['Index'], 2, values) - iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) - next(iter_rows) - assert next(iter_rows) == [None, None, None, None, None, None, - None, None, None, None, None, None, 'label', 'description', 'detail'] - - -def test_set_group(): - wb = xlsx.ModelWorkbook() - values = [''] * 15 - values[2] = 'name' - values[5] = 'type' - values[4] = 'count' - values[14] = 'detail' - values[13] = 'description' - values[12] = 'label' - wb.set_group(wb.wb['Index'], 2, values, 2) - iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) - next(iter_rows) - assert next(iter_rows) == ['', '', 'name', '', 'count', 'type', '', '', '', '', '', '', - 'label', 'description', 'detail'] - - -def test_set_point(): - wb = xlsx.ModelWorkbook() - values = [''] * 15 - values[0] = 'addr_offset' - values[1] = 'group_offset' - values[2] = 'name' - values[3] = 'value' - values[4] = 'count' - values[5] = 'type' - values[6] = 'size' - values[7] = 'sf' - values[8] = 'units' - values[9] = 'access' - values[10] = 'mandatory' - values[11] = 'static' - wb.set_point(wb.wb['Index'], 2, values, 1) - iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) - next(iter_rows) - assert next(iter_rows) == ['addr_offset', 'group_offset', 'name', 'value', 'count', - 'type', 'size', 'sf', 'units', 'access', 'mandatory', 'static', '', '', ''] - - -def test_set_symbol(): - wb = xlsx.ModelWorkbook() - values = [''] * 15 - values[2] = 'name' - values[3] = 'value' - values[14] = 'detail' - values[13] = 'description' - values[12] = 'label' - wb.set_symbol(wb.wb['Index'], 2, values) - iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) - next(iter_rows) - assert next(iter_rows) == ['', '', 'name', 'value', '', '', '', - '', '', '', '', '', 'label', 'description', 'detail'] - - -def test_set_comment(): - wb = xlsx.ModelWorkbook() - wb.set_comment(wb.wb['Index'], 2, ['This is a comment']) - iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) - next(iter_rows) - assert next(iter_rows)[0] == 'This is a comment' - - -def test_set_hdr(): - wb = xlsx.ModelWorkbook() - wb.set_hdr(wb.wb['Index'], ['This', 'is', 'a', 'test', 'header']) - iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) - assert next(iter_rows) == ['This', 'is', 'a', 'test', 'header'] - - -def test_spreadsheet_to_xlsx(): - wb = xlsx.ModelWorkbook(filename='sunspec2/tests/test_data/wb_701-705.xlsx') - with pytest.raises(ValueError) as exc: - wb.spreadsheet_to_xlsx(702, []) - assert 'Workbooks opened with existing file are read only' in str(exc.value) - - spreadsheet_smdx_304 = [ - ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', - 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], - ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', - 'Include to support orientation measurements', ''], - ['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], - ['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], - ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], - ['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], - ['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], - ['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] - ] - wb2 = xlsx.ModelWorkbook() - wb2.spreadsheet_to_xlsx(304, spreadsheet_smdx_304) - iter_rows = wb2.xlsx_iter_rows(wb2.wb['304']) - for row in spreadsheet_smdx_304: - assert next(iter_rows) == row - - -def test_to_xlsx(tmp_path): - spreadsheet_smdx_304 = [ - ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', - 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description'], - ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', - 'Include to support orientation measurements'], - [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier'], - [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length'], - ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', ''], - ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination'], - ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination'], - ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination'] - ] - with open('sunspec2/models/json/model_304.json') as f: - m_703 = json.load(f) - wb = xlsx.ModelWorkbook() - wb.to_xlsx(m_703) - iter_rows = wb.xlsx_iter_rows(wb.wb['304']) - for row in spreadsheet_smdx_304: - assert next(iter_rows) == row diff --git a/build/lib/sunspec2/xlsx.py b/build/lib/sunspec2/xlsx.py deleted file mode 100644 index 46c2838..0000000 --- a/build/lib/sunspec2/xlsx.py +++ /dev/null @@ -1,387 +0,0 @@ - -""" - Copyright (C) 2020 SunSpec Alliance - - Permission is hereby granted, free of charge, to any person obtaining a - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. -""" - -import sunspec2.mdef as mdef -import sunspec2.spreadsheet as ss - -models_hdr = [('Model', 0), - ('Label', 30), - ('Description', 60)] - -column_width = { - ss.ADDRESS_OFFSET_IDX: 0, - ss.GROUP_OFFSET_IDX: 0, - ss.NAME_IDX: 25, - ss.VALUE_IDX: 12, - ss.COUNT_IDX: 12, - ss.TYPE_IDX: 12, - ss.SIZE_IDX: 12, - ss.SCALE_FACTOR_IDX: 12, - ss.UNITS_IDX: 12, - ss.ACCESS_IDX: 12, - ss.MANDATORY_IDX: 12, - ss.STATIC_IDX: 12, - ss.LABEL_IDX: 30, - ss.DESCRIPTION_IDX: 60, - ss.NOTES_IDX: 60 -} - -group_styles = { - 'suns_group_1': { - 'group_color': 'b8cce4', # 184, 204, 228 - 'point_color': 'dce6f1', # 220, 230, 241 - }, - 'suns_group_2': { - 'group_color': 'd8e4bc', # 216, 228, 188 - 'point_color': 'ebf1de', # 235, 241, 222 - }, - 'suns_group_3': { - 'group_color': 'ccc0da', # 204, 192, 218 - 'point_color': 'e4dfec', # 228, 223, 236 - }, - 'suns_group_4': { - 'group_color': 'fcd5b4', # 252, 213, 180 - 'point_color': 'fde9d9', # 253, 233, 217 - }, - 'suns_group_5': { - 'group_color': 'e6b8b7', # 230, 184, 183 - 'point_color': 'f2dcdb', # 242, 220, 219 - } -} - -try: - import openpyxl - import openpyxl.styles as styles - - - class ModelWorkbook(object): - def __init__(self, filename=None, model_dir=None, license_summary=False, params=None): - self.wb = None - self.filename = filename - self.params = params - if self.params is None: - self.params = {} - - if filename is not None: - self.wb = openpyxl.load_workbook(filename=filename) - else: - self.wb = openpyxl.Workbook() - - self.ws_models = self.wb.active - self.ws_models.title = 'Index' - - thin = styles.Side(border_style=self.params.get('side_border', 'thin'), - color=self.params.get('side_color', '999999')) - - for i in range(1, len(group_styles) + 1): - key = 'suns_group_%s' % i - name = 'suns_group_entry_%s' % i - style = styles.NamedStyle(name=name) - color = group_styles[key]['group_color'] - # self.params.get('group_color', color) - style.fill = styles.PatternFill('solid', fgColor=color) - style.font = styles.Font() - style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - style.alignment = styles.Alignment(horizontal='center', wrapText=True) - self.wb.add_named_style(style) - - name = 'suns_group_text_%s' % i - style = styles.NamedStyle(name=name) - style.fill = styles.PatternFill('solid', fgColor=color) - style.font = styles.Font() - style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - style.alignment = styles.Alignment(horizontal='left', wrapText=True) - self.wb.add_named_style(style) - - name = 'suns_point_entry_%s' % i - style = styles.NamedStyle(name=name) - color = group_styles[key]['point_color'] - # self.params.get('group_color', color) - style.fill = styles.PatternFill('solid', fgColor=color) - style.font = styles.Font() - style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - style.alignment = styles.Alignment(horizontal='center', wrapText=True) - self.wb.add_named_style(style) - - name = 'suns_point_text_%s' % i - style = styles.NamedStyle(name=name) - style.fill = styles.PatternFill('solid', fgColor=color) - style.font = styles.Font() - style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - style.alignment = styles.Alignment(horizontal='left', wrapText=True) - self.wb.add_named_style(style) - - if 'suns_hdr' not in self.wb.named_styles: - hdr_style = styles.NamedStyle(name='suns_hdr') - hdr_style.fill = styles.PatternFill('solid', fgColor=self.params.get('hdr_color', 'dddddd')) - hdr_style.font = styles.Font(bold=True) - hdr_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - hdr_style.alignment = styles.Alignment(horizontal='center', wrapText=True) - self.wb.add_named_style(hdr_style) - if 'suns_group_entry' not in self.wb.named_styles: - model_entry_style = styles.NamedStyle(name='suns_group_entry') - model_entry_style.fill = styles.PatternFill('solid', - fgColor=self.params.get('group_color', 'fff9e5')) - model_entry_style.font = styles.Font() - model_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - model_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) - self.wb.add_named_style(model_entry_style) - if 'suns_group_text' not in self.wb.named_styles: - model_text_style = styles.NamedStyle(name='suns_group_text') - model_text_style.fill = styles.PatternFill('solid', - fgColor=self.params.get('group_color', 'fff9e5')) - model_text_style.font = styles.Font() - model_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - model_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) - self.wb.add_named_style(model_text_style) - if 'suns_point_entry' not in self.wb.named_styles: - fixed_entry_style = styles.NamedStyle(name='suns_point_entry') - fixed_entry_style.fill = styles.PatternFill('solid', - fgColor=self.params.get('point_color', 'e6f2ff')) - fixed_entry_style.font = styles.Font() - fixed_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - fixed_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) - self.wb.add_named_style(fixed_entry_style) - if 'suns_point_text' not in self.wb.named_styles: - fixed_text_style = styles.NamedStyle(name='suns_point_text') - fixed_text_style.fill = styles.PatternFill('solid', - fgColor=self.params.get('point_color', 'e6f2ff')) - fixed_text_style.font = styles.Font() - fixed_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - fixed_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) - self.wb.add_named_style(fixed_text_style) - if 'suns_point_variable_entry' not in self.wb.named_styles: - fixed_entry_style = styles.NamedStyle(name='suns_point_variable_entry') - fixed_entry_style.fill = styles.PatternFill('solid', - fgColor=self.params.get('point_variable_color', 'ecf9ec')) - fixed_entry_style.font = styles.Font() - fixed_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - fixed_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) - self.wb.add_named_style(fixed_entry_style) - if 'suns_point_variable_text' not in self.wb.named_styles: - fixed_text_style = styles.NamedStyle(name='suns_point_variable_text') - fixed_text_style.fill = styles.PatternFill('solid', - fgColor=self.params.get('point_variable_color', 'ecf9ec')) - fixed_text_style.font = styles.Font() - fixed_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - fixed_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) - self.wb.add_named_style(fixed_text_style) - if 'suns_symbol_entry' not in self.wb.named_styles: - repeating_entry_style = styles.NamedStyle(name='suns_symbol_entry') - repeating_entry_style.fill =styles.PatternFill('solid', - fgColor=self.params.get('symbol_color', 'fafafa')) - repeating_entry_style.font = styles.Font() - repeating_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - repeating_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) - self.wb.add_named_style(repeating_entry_style) - if 'suns_symbol_text' not in self.wb.named_styles: - repeating_text_style = styles.NamedStyle(name='suns_symbol_text') - repeating_text_style.fill = styles.PatternFill('solid', - fgColor=self.params.get('symbol_color', 'fafafa')) - repeating_text_style.font = styles.Font() - repeating_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - repeating_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) - self.wb.add_named_style(repeating_text_style) - if 'suns_comment' not in self.wb.named_styles: - symbol_text_style = styles.NamedStyle(name='suns_comment') - symbol_text_style.fill = styles.PatternFill('solid', - fgColor=self.params.get('comment_color', 'dddddd')) - # fgColor=self.params.get('symbol_color', 'fffcd9')) - symbol_text_style.font = styles.Font() - symbol_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - symbol_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) - self.wb.add_named_style(symbol_text_style) - if 'suns_entry' not in self.wb.named_styles: - entry_style = styles.NamedStyle(name='suns_entry') - entry_style.fill = styles.PatternFill('solid', fgColor='ffffff') - entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) - entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) - self.wb.add_named_style(entry_style) - if 'suns_text' not in self.wb.named_styles: - text_style = styles.NamedStyle(name='suns_text') - text_style.font = styles.Font() - text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) - self.wb.add_named_style(text_style) - if 'suns_hyper' not in self.wb.named_styles: - hyper_style = openpyxl.styles.NamedStyle(name='suns_hyper') - hyper_style.font = openpyxl.styles.Font(color='0000ee', underline='single') - hyper_style.alignment = openpyxl.styles.Alignment(horizontal='left', wrapText=True) - self.wb.add_named_style(hyper_style) - - for i in range(len(models_hdr)): - self.set_cell(self.ws_models, 1, i + 1, models_hdr[i][0], 'suns_hdr') - if models_hdr[i][1]: - self.ws_models.column_dimensions[chr(65 + i)].width = models_hdr[i][1] - - def get_models(self): - models = [] - if self.wb is not None: - for m in self.wb.sheetnames: - try: - mid = int(m) - models.append(mid) - except: - pass - return models - - def save(self, filename): - self.wb.save(filename) - - def xlsx_iter_rows(self, ws): - for row in ws.iter_rows(): - yield [cell.value for cell in row] - - def spreadsheet_from_xlsx(self, mid=None): - spreadsheet = [] - ws = self.wb[str(mid)] - for row in self.xlsx_iter_rows(ws): - # filter out informative offset information from the normative model definition - if row[ss.TYPE_IDX] and row[ss.TYPE_IDX] != ss.TYPE: - row[ss.ADDRESS_OFFSET_IDX] = '' - row[ss.GROUP_OFFSET_IDX] = '' - spreadsheet.append(row) - return spreadsheet - - def from_xlsx(self, mid=None): - return ss.from_spreadsheet(self.spreadsheet_from_xlsx(mid)) - - def set_cell(self, ws, row, col, value, style=None): - if self.filename: - raise ValueError('Workbooks opened with existing file are read only') - cell = ws.cell(row=row, column=col) - cell.value = value - if style: - cell.style = style - return cell - - def set_info(self, ws, row, values, style=None): - self.set_cell(ws, row, ss.LABEL_IDX + 1, values[ss.LABEL_IDX], style=style) - self.set_cell(ws, row, ss.DESCRIPTION_IDX + 1, values[ss.DESCRIPTION_IDX], style=style) - if len(values) > ss.NOTES_IDX: - self.set_cell(ws, row, ss.NOTES_IDX + 1, values[ss.NOTES_IDX], style=style) - - def set_group(self, ws, row, values, level): - for i in range(len(values)): - self.set_cell(ws, row, i + 1, '', 'suns_group_entry_%s' % level) - self.set_cell(ws, row, ss.NAME_IDX + 1, values[ss.NAME_IDX]) - self.set_cell(ws, row, ss.TYPE_IDX + 1, values[ss.TYPE_IDX]) - self.set_cell(ws, row, ss.COUNT_IDX + 1, values[ss.COUNT_IDX]) - self.set_info(ws, row, values, 'suns_group_text_%s' % level) - - def set_point(self, ws, row, values, level): - entry_style = 'suns_point_entry_%s' % level - text_style = 'suns_point_text_%s' % level - self.set_cell(ws, row, ss.ADDRESS_OFFSET_IDX + 1, values[ss.ADDRESS_OFFSET_IDX], entry_style) - self.set_cell(ws, row, ss.GROUP_OFFSET_IDX + 1, values[ss.GROUP_OFFSET_IDX], entry_style) - self.set_cell(ws, row, ss.NAME_IDX + 1, values[ss.NAME_IDX], entry_style) - self.set_cell(ws, row, ss.VALUE_IDX + 1, values[ss.VALUE_IDX], entry_style) - self.set_cell(ws, row, ss.COUNT_IDX + 1, values[ss.COUNT_IDX], entry_style) - self.set_cell(ws, row, ss.TYPE_IDX + 1, values[ss.TYPE_IDX], entry_style) - self.set_cell(ws, row, ss.SIZE_IDX + 1, values[ss.SIZE_IDX], entry_style) - self.set_cell(ws, row, ss.SCALE_FACTOR_IDX + 1, values[ss.SCALE_FACTOR_IDX], entry_style) - self.set_cell(ws, row, ss.UNITS_IDX + 1, values[ss.UNITS_IDX], entry_style) - self.set_cell(ws, row, ss.ACCESS_IDX + 1, values[ss.ACCESS_IDX], entry_style) - self.set_cell(ws, row, ss.MANDATORY_IDX + 1, values[ss.MANDATORY_IDX], entry_style) - self.set_cell(ws, row, ss.STATIC_IDX + 1, values[ss.STATIC_IDX], entry_style) - self.set_info(ws, row, values, text_style) - - def set_symbol(self, ws, row, values): - for i in range(len(values)): - self.set_cell(ws, row, i + 1, '', 'suns_symbol_entry') - self.set_cell(ws, row, ss.NAME_IDX + 1, values[ss.NAME_IDX]) - self.set_cell(ws, row, ss.VALUE_IDX + 1, values[ss.VALUE_IDX]) - self.set_info(ws, row, values, 'suns_symbol_text') - - def set_comment(self, ws, row, values): - ws.merge_cells('A%s:%s%s' % (row, chr(65+len(values)-1), row)) - self.set_cell(ws, row, 1, values[0], 'suns_comment') - - def set_hdr(self, ws, values): - for i in range(len(values)): - self.set_cell(ws, 1, i + 1, values[i], 'suns_hdr') - width = column_width[i] - if width: - ws.column_dimensions[chr(65+i)].width = column_width[i] - - def spreadsheet_to_xlsx(self, mid, spreadsheet): - if self.filename: - raise ValueError('Workbooks opened with existing file are read only') - has_notes = 'Notes' in spreadsheet[0] - info = False - label = None - description = None - notes = None - level = 1 - - ws = self.wb.create_sheet(title=str(mid)) - self.set_hdr(ws, spreadsheet[0]) - row = 2 - for values in spreadsheet[1:]: - # point - has type - etype = values[ss.TYPE_IDX] - if etype: - # group - if etype in mdef.group_types: - level = len(values[ss.NAME_IDX].split('.')) - self.set_group(ws, row, values, level) - if not info: - label = values[ss.LABEL_IDX] - description = values[ss.DESCRIPTION_IDX] - if has_notes: - notes = values[ss.NOTES_IDX] - info = True - # point - elif etype in mdef.point_type_info: - self.set_point(ws, row, values, level) - else: - raise Exception('Unknown element type: %s' % etype) - elif values[ss.NAME_IDX]: - # symbol - has name and value with no type - if values[ss.VALUE_IDX] is not None and values[ss.VALUE_IDX] != '': - self.set_symbol(ws, row, values) - # comment - no name, value, or type - elif values[0]: - self.set_comment(ws, row, values) - row += 1 - - if self.ws_models is not None: - row = self.ws_models.max_row + 1 - self.set_cell(self.ws_models, row, 1, str(mid), 'suns_entry') - cell = self.set_cell(self.ws_models, row, 2, label, 'suns_hyper') - cell.hyperlink = '#%s!%s' % (str(mid), 'A1') - self.set_cell(self.ws_models, row, 3, description, 'suns_text') - - def to_xlsx(self, model_def): - mid = model_def[mdef.ID] - spreadsheet = ss.to_spreadsheet(model_def) - self.spreadsheet_to_xlsx(mid, spreadsheet) - -except: - # provide indication the openpyxl library not available - class ModelWorkbook(object): - def __init__(self, filename=None, model_dir=None, license_summary=False): - raise Exception('openpyxl library not installed, it is required for working with .xlsx files') - -if __name__ == "__main__": - pass