equipment_id for merged configs and routed nodes

Add a new custom module property 'original_id' indicating
the equipment_id the modules originally belongs to.
This property is only given, when distinct from the equipment_id
of the SEC node.
It happens when multiple config files are given, for all modules
but the ones given in the first file, and for routed modules,
when  multiple nodes are routed or own modules are given.

+ fix an issue in router: additional modules were ignore in case
of a single node.

+ small cosmetic changes in config.py reducing IDE complains

Change-Id: If846c47a06158629cef807d22b91f69e4f416563
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/35396
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
2025-01-10 14:20:29 +01:00
parent a25a368491
commit fd43687465
4 changed files with 27 additions and 14 deletions

View File

@ -56,10 +56,12 @@ class Param(dict):
kwds['value'] = value kwds['value'] = value
super().__init__(**kwds) super().__init__(**kwds)
class Group(tuple): class Group(tuple):
def __new__(cls, *args): def __new__(cls, *args):
return super().__new__(cls, args) return super().__new__(cls, args)
class Mod(dict): class Mod(dict):
def __init__(self, name, cls, description, **kwds): def __init__(self, name, cls, description, **kwds):
super().__init__( super().__init__(
@ -70,7 +72,8 @@ class Mod(dict):
# matches name from spec # matches name from spec
if not re.match(r'^[a-zA-Z]\w{0,62}$', name, re.ASCII): 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 # Make parameters out of all keywords
groups = {} groups = {}
for key, val in kwds.items(): for key, val in kwds.items():
@ -85,6 +88,7 @@ class Mod(dict):
for member in members: for member in members:
self[member]['group'] = group self[member]['group'] = group
class Collector: class Collector:
def __init__(self, cls): def __init__(self, cls):
self.list = [] self.list = []
@ -120,12 +124,14 @@ class Config(dict):
def merge_modules(self, other): def merge_modules(self, other):
""" merges only the modules from 'other' into 'self'""" """ merges only the modules from 'other' into 'self'"""
self.ambiguous |= self.module_names & other.module_names self.ambiguous |= self.module_names & other.module_names
equipment_id = other['node']['equipment_id']
for name, mod in other.items(): for name, mod in other.items():
if name == 'node': if name == 'node':
continue continue
if name not in self.module_names: if name not in self.module_names:
self.module_names.add(name) self.module_names.add(name)
self[name] = mod self[name] = mod
mod['original_id'] = equipment_id
def process_file(filename, log): def process_file(filename, log):

View File

@ -319,6 +319,8 @@ class Module(HasAccessibles):
slowinterval = Property('poll interval for other parameters', FloatRange(0.1, 120), default=15) 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', omit_unchanged_within = Property('default for minimum time between updates of unchanged values',
NoneOr(FloatRange(0)), export=False, default=None) 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 enablePoll = True
pollInfo = None pollInfo = None

View File

@ -77,25 +77,29 @@ class SecopClient(frappy.client.SecopClient):
class Router(frappy.protocol.dispatcher.Dispatcher): class Router(frappy.protocol.dispatcher.Dispatcher):
singlenode = None
def __init__(self, name, logger, options, srv): def __init__(self, name, logger, options, srv):
"""initialize router """initialize router
Use the option node = <uri> for a single node or Use the option node = <uri> for a single node or
nodes = ["<uri1>", "<uri2>" ...] for multiple nodes. nodes = ["<uri1>", "<uri2>" ...] 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. else the description property is a merge from all client node properties.
""" """
uri = options.pop('node', None) uri = options.pop('node', None)
uris = options.pop('nodes', None) uris = options.pop('nodes', None)
if uri and uris: try:
raise frappy.errors.ConfigError('can not specify node _and_ nodes') if uris is not None:
super().__init__(name, logger, options, srv) if isinstance(uris, str) or not all(isinstance(v, str) for v in uris) or uri:
if uri: raise TypeError()
self.nodes = [SecopClient(uri, logger.getChild('routed'), self)] elif isinstance(uri, str):
self.singlenode = self.nodes[0] uris = [uri]
else: 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)
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 # register callbacks
for node in self.nodes: for node in self.nodes:
@ -127,8 +131,8 @@ class Router(frappy.protocol.dispatcher.Dispatcher):
logger.warning('can not connect to node %r', node.nodename) logger.warning('can not connect to node %r', node.nodename)
def handle_describe(self, conn, specifier, data): def handle_describe(self, conn, specifier, data):
if self.singlenode: if len(self.nodes) == 1 and not self.secnode.modules:
return DESCRIPTIONREPLY, specifier, self.singlenode.descriptive_data return DESCRIPTIONREPLY, specifier, self.nodes[0].descriptive_data
reply = super().handle_describe(conn, specifier, data) reply = super().handle_describe(conn, specifier, data)
result = reply[2] result = reply[2]
allmodules = result.get('modules', {}) allmodules = result.get('modules', {})
@ -144,6 +148,7 @@ class Router(frappy.protocol.dispatcher.Dispatcher):
self.log.info('module %r is already present', modname) self.log.info('module %r is already present', modname)
else: else:
allmodules[modname] = moddesc allmodules[modname] = moddesc
moddesc.setdefault('original_id', equipment_id)
result['modules'] = allmodules result['modules'] = allmodules
result['description'] = '\n\n'.join(node_description) result['description'] = '\n\n'.join(node_description)
return DESCRIPTIONREPLY, specifier, result return DESCRIPTIONREPLY, specifier, result

View File

@ -243,7 +243,7 @@ def test_ModuleMagic():
'export', 'group', 'description', 'features', 'export', 'group', 'description', 'features',
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop', 'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2', '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()) == { assert set(cfg['value'].keys()) == {
'group', 'export', 'relative_resolution', 'group', 'export', 'relative_resolution',
'visibility', 'unit', 'default', 'value', 'datatype', 'fmtstr', 'visibility', 'unit', 'default', 'value', 'datatype', 'fmtstr',