diff --git a/frappy/config.py b/frappy/config.py index f157fcc..85586c1 100644 --- a/frappy/config.py +++ b/frappy/config.py @@ -56,10 +56,12 @@ class Param(dict): kwds['value'] = value super().__init__(**kwds) + class Group(tuple): def __new__(cls, *args): return super().__new__(cls, args) + class Mod(dict): def __init__(self, name, cls, description, **kwds): super().__init__( @@ -70,7 +72,8 @@ class Mod(dict): # matches name from spec if not re.match(r'^[a-zA-Z]\w{0,62}$', name, re.ASCII): - raise ConfigError(f'Not a valid SECoP Module name: "{name}". Does it only contain letters, numbers and underscores?') + raise ConfigError(f'Not a valid SECoP Module name: "{name}".' + ' Does it only contain letters, numbers and underscores?') # Make parameters out of all keywords groups = {} for key, val in kwds.items(): @@ -85,6 +88,7 @@ class Mod(dict): for member in members: self[member]['group'] = group + class Collector: def __init__(self, cls): self.list = [] @@ -120,12 +124,14 @@ class Config(dict): def merge_modules(self, other): """ merges only the modules from 'other' into 'self'""" self.ambiguous |= self.module_names & other.module_names + equipment_id = other['node']['equipment_id'] for name, mod in other.items(): if name == 'node': continue if name not in self.module_names: self.module_names.add(name) self[name] = mod + mod['original_id'] = equipment_id def process_file(filename, log): diff --git a/frappy/modulebase.py b/frappy/modulebase.py index d572dcd..b0154be 100644 --- a/frappy/modulebase.py +++ b/frappy/modulebase.py @@ -319,6 +319,8 @@ class Module(HasAccessibles): slowinterval = Property('poll interval for other parameters', FloatRange(0.1, 120), default=15) omit_unchanged_within = Property('default for minimum time between updates of unchanged values', NoneOr(FloatRange(0)), export=False, default=None) + original_id = Property('original equipment_id\n\ngiven only if different from equipment_id of node', + NoneOr(StringType()), default=None, export=True) # exported as custom property _original_id enablePoll = True pollInfo = None diff --git a/frappy/protocol/router.py b/frappy/protocol/router.py index 40f99f3..55d20ea 100644 --- a/frappy/protocol/router.py +++ b/frappy/protocol/router.py @@ -77,26 +77,30 @@ class SecopClient(frappy.client.SecopClient): class Router(frappy.protocol.dispatcher.Dispatcher): - singlenode = None - def __init__(self, name, logger, options, srv): """initialize router Use the option node = for a single node or nodes = ["", "" ...] for multiple nodes. - If a single node is given, the node properties are forwarded transparently, + If a single node is given, and no more additional modules are given, + the node properties are forwarded transparently, else the description property is a merge from all client node properties. """ uri = options.pop('node', None) uris = options.pop('nodes', None) - if uri and uris: - raise frappy.errors.ConfigError('can not specify node _and_ nodes') + try: + if uris is not None: + if isinstance(uris, str) or not all(isinstance(v, str) for v in uris) or uri: + raise TypeError() + elif isinstance(uri, str): + uris = [uri] + else: + raise TypeError() + except Exception as e: + raise frappy.errors.ConfigError("a router needs either 'node' as a string'" + "' or 'nodes' as a list of strings") from e super().__init__(name, logger, options, srv) - if uri: - self.nodes = [SecopClient(uri, logger.getChild('routed'), self)] - self.singlenode = self.nodes[0] - else: - self.nodes = [SecopClient(uri, logger.getChild(f'routed{i}'), self) for i, uri in enumerate(uris)] + self.nodes = [SecopClient(uri, logger.getChild(f'routed{i}'), self) for i, uri in enumerate(uris)] # register callbacks for node in self.nodes: node.register_callback(None, node.updateEvent, node.descriptiveDataChange, node.nodeStateChange) @@ -127,8 +131,8 @@ class Router(frappy.protocol.dispatcher.Dispatcher): logger.warning('can not connect to node %r', node.nodename) def handle_describe(self, conn, specifier, data): - if self.singlenode: - return DESCRIPTIONREPLY, specifier, self.singlenode.descriptive_data + if len(self.nodes) == 1 and not self.secnode.modules: + return DESCRIPTIONREPLY, specifier, self.nodes[0].descriptive_data reply = super().handle_describe(conn, specifier, data) result = reply[2] allmodules = result.get('modules', {}) @@ -144,6 +148,7 @@ class Router(frappy.protocol.dispatcher.Dispatcher): self.log.info('module %r is already present', modname) else: allmodules[modname] = moddesc + moddesc.setdefault('original_id', equipment_id) result['modules'] = allmodules result['description'] = '\n\n'.join(node_description) return DESCRIPTIONREPLY, specifier, result diff --git a/test/test_modules.py b/test/test_modules.py index 9b865cd..b9069ba 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -243,7 +243,7 @@ def test_ModuleMagic(): 'export', 'group', 'description', 'features', 'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop', 'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2', - 'cmd2', 'value', 'a1', 'omit_unchanged_within'} + 'cmd2', 'value', 'a1', 'omit_unchanged_within', 'original_id'} assert set(cfg['value'].keys()) == { 'group', 'export', 'relative_resolution', 'visibility', 'unit', 'default', 'value', 'datatype', 'fmtstr',