1111
1212import yaml
1313from pydantic import BaseModel , field_validator , model_validator
14- from typeguard import typechecked
1514
1615from bec_lib import messages
1716from bec_lib .alarm_handler import Alarms
@@ -386,7 +385,7 @@ def _update_device_state(self, msg_obj: MessageObject) -> messages.BeamlineState
386385
387386
388387@dataclass (frozen = True )
389- class _ResolvedStateSignal :
388+ class ResolvedStateSignal :
390389 label : str
391390 device_name : str
392391 signal_name : str
@@ -405,7 +404,7 @@ def __init__(self, *args, **kwargs) -> None:
405404 # Mapping from signal updates to affected state labels, used for efficient evaluation when a signal update is received
406405 self ._signal_info_to_labels : dict [tuple [str , SignalSource , str ], set [str ]] = {}
407406 # Mapping from state labels to the list of signal requirements that define that state
408- self ._requirements_for_label : dict [str , list [_ResolvedStateSignal ]] = {}
407+ self ._requirements_for_label : dict [str , list [ResolvedStateSignal ]] = {}
409408 # Set of subscriptions to signal updates
410409 self ._subscriptions : set [tuple [str , SignalSource ]] = set ()
411410 # Cache of the latest signal values
@@ -426,32 +425,39 @@ def _endpoint(device: str, source: SignalSource):
426425 f"Invalid signal source '{ source } ', please use 'readback', 'configuration', or 'limits'."
427426 )
428427
429- def _get_devices (self ):
428+ def _get_device_manager (self ):
430429 if self .device_manager is None :
431430 # pylint: disable=import-outside-toplevel
432431 from bec_lib .client import BECClient
433432
434433 bec = BECClient ()
435- return bec .device_manager . devices
436- return self .device_manager . devices
434+ return bec .device_manager
435+ return self .device_manager
437436
438- def _get_signal_source (self , signal_info : dict [str , Any ]) -> SignalSource :
437+ @staticmethod
438+ def _get_signal_source (signal_info : dict [str , Any ], error_prefix : str ) -> SignalSource :
439439 kind_str = str (signal_info .get ("kind_str" , "" )).lower ()
440440 if "hinted" in kind_str or "normal" in kind_str :
441441 return "readback"
442442 if "config" in kind_str :
443443 return "configuration"
444444 raise ValueError (
445- f"{ self . _error_prefix } Unsupported kind: '{ kind_str } ' for signal : \n { yaml .dump (signal_info , indent = 4 )} "
445+ f"{ error_prefix } Unsupported kind: '{ kind_str } ' for signal : \n { yaml .dump (signal_info , indent = 4 )} "
446446 )
447447
448- @typechecked
449- def _resolve_signal (self , device_name : str , signal_name : str ) -> tuple [str , SignalSource ]:
450- devices = self ._get_devices ()
448+ @staticmethod
449+ def _resolve_signal (
450+ device_name : str , signal_name : str , device_manager : DeviceManagerBase , error_prefix : str
451+ ) -> tuple [str , SignalSource ]:
452+ devices = device_manager .devices
451453 try :
454+ if not isinstance (device_name , str ):
455+ raise ValueError (
456+ f"{ error_prefix } Device name must be a string, got { type (device_name )} "
457+ )
452458 device_obj : DeviceBase = devices [device_name ]
453459 except KeyError :
454- raise ValueError (f"{ self . _error_prefix } Device '{ device_name } ' not found." ) from None
460+ raise ValueError (f"{ error_prefix } Device '{ device_name } ' not found." ) from None
455461
456462 # Special handling for limits, as they are not regular signals.
457463 if signal_name in ["low_limit" , "low_limit_travel" ]:
@@ -469,11 +475,11 @@ def _resolve_signal(self, device_name: str, signal_name: str) -> tuple[str, Sign
469475 signal_obj = devices [signal_name ]
470476 except AttributeError :
471477 raise ValueError (
472- f"{ self . _error_prefix } Signal '{ signal_name } ' not found for device '{ device_name } '."
478+ f"{ error_prefix } Signal '{ signal_name } ' not found for device '{ device_name } '."
473479 ) from None
474480 if signal_obj .parent != device_obj :
475481 raise ValueError (
476- f"{ self . _error_prefix } Signal '{ signal_name } ' does not belong to device '{ device_name } '."
482+ f"{ error_prefix } Signal '{ signal_name } ' does not belong to device '{ device_name } '."
477483 )
478484 signal_component = "." .join (signal_name .split ("." )[1 :])
479485 signal_info = device_obj .root ._info ["signals" ].get (signal_component )
@@ -488,75 +494,119 @@ def _resolve_signal(self, device_name: str, signal_name: str) -> tuple[str, Sign
488494
489495 if signal_info is None :
490496 raise ValueError (
491- f"{ self . _error_prefix } Signal '{ signal_name } ' not found for device '{ device_name } '."
497+ f"{ error_prefix } Signal '{ signal_name } ' not found for device '{ device_name } '."
492498 )
493499
494500 obj_name = signal_info .get ("obj_name" )
495- signal_source = self ._get_signal_source (signal_info )
501+ signal_source = AggregatedState ._get_signal_source (signal_info , error_prefix )
496502 return obj_name , signal_source
497503
498- def _build_rules (self ) -> None :
499- self ._signal_info_to_labels .clear ()
500- self ._requirements_for_label .clear ()
501- self ._subscriptions .clear ()
502- for label , device_configs in self .config .states .items ():
503- state_requirements : list [_ResolvedStateSignal ] = []
504- for device_name , config in device_configs .devices .items ():
505- if isinstance (config , SignalConfig ):
504+ @staticmethod
505+ def get_state_requirements (
506+ label : str ,
507+ state_config : SubDeviceStateConfig ,
508+ device_manager : DeviceManagerBase ,
509+ error_prefix : str ,
510+ ) -> list [ResolvedStateSignal ]:
511+ state_requirements : list [ResolvedStateSignal ] = []
512+ for device_name , config in state_config .devices .items ():
513+ if isinstance (config , SignalConfig ):
514+ state_requirements .append (
515+ AggregatedState ._build_requirement_for_signal (
516+ device_name ,
517+ device_name ,
518+ config .value ,
519+ config .abs_tol ,
520+ label ,
521+ device_manager ,
522+ error_prefix ,
523+ )
524+ )
525+ elif isinstance (config , DeviceConfig ):
526+ # If a value is specified for the device, add it as a requirement
527+ if config .value is not None :
506528 state_requirements .append (
507- self ._build_requirement_for_signal (
508- device_name , device_name , config .value , config .abs_tol , label
529+ AggregatedState ._build_requirement_for_signal (
530+ device_name ,
531+ device_name ,
532+ config .value ,
533+ config .abs_tol ,
534+ label ,
535+ device_manager ,
536+ error_prefix ,
509537 )
510538 )
511- elif isinstance (config , DeviceConfig ):
512- # If a value is specified for the device, add it as a requirement
513- if config .value is not None :
514- state_requirements .append (
515- self ._build_requirement_for_signal (
516- device_name , device_name , config .value , config .abs_tol , label
517- )
518- )
519- if config .low_limit is not None :
520- state_requirements .append (
521- self ._build_requirement_for_signal (
522- device_name ,
523- "low_limit" ,
524- config .low_limit .value ,
525- config .low_limit .abs_tol ,
526- label ,
527- )
539+ if config .low_limit is not None :
540+ state_requirements .append (
541+ AggregatedState ._build_requirement_for_signal (
542+ device_name ,
543+ "low_limit" ,
544+ config .low_limit .value ,
545+ config .low_limit .abs_tol ,
546+ label ,
547+ device_manager ,
548+ error_prefix ,
528549 )
529- if config .high_limit is not None :
530- state_requirements .append (
531- self ._build_requirement_for_signal (
532- device_name ,
533- "high_limit" ,
534- config .high_limit .value ,
535- config .high_limit .abs_tol ,
536- label ,
537- )
550+ )
551+ if config .high_limit is not None :
552+ state_requirements .append (
553+ AggregatedState ._build_requirement_for_signal (
554+ device_name ,
555+ "high_limit" ,
556+ config .high_limit .value ,
557+ config .high_limit .abs_tol ,
558+ label ,
559+ device_manager ,
560+ error_prefix ,
538561 )
539- for signal_name , signal_config in (config .signals or {}).items ():
540- state_requirements .append (
541- self ._build_requirement_for_signal (
542- device_name ,
543- signal_name ,
544- signal_config .value ,
545- signal_config .abs_tol ,
546- label ,
547- )
562+ )
563+ for signal_name , signal_config in (config .signals or {}).items ():
564+ state_requirements .append (
565+ AggregatedState ._build_requirement_for_signal (
566+ device_name ,
567+ signal_name ,
568+ signal_config .value ,
569+ signal_config .abs_tol ,
570+ label ,
571+ device_manager ,
572+ error_prefix ,
548573 )
574+ )
575+ return state_requirements
576+
577+ def _build_rules (self ) -> None :
578+ self ._signal_info_to_labels .clear ()
579+ self ._requirements_for_label .clear ()
580+ self ._subscriptions .clear ()
581+ for label , device_configs in self .config .states .items ():
582+ state_requirements : list [ResolvedStateSignal ] = AggregatedState .get_state_requirements (
583+ label , device_configs , self ._get_device_manager (), self ._error_prefix
584+ )
585+ for requirement in state_requirements :
586+ device_name = requirement .device_name
587+ signal_name = requirement .signal_name
588+ source = requirement .source
589+ self ._subscriptions .add ((device_name , source ))
590+ self ._signal_info_to_labels .setdefault (
591+ (device_name , source , signal_name ), set ()
592+ ).add (label )
549593 self ._requirements_for_label [label ] = state_requirements
550594
595+ @staticmethod
551596 def _build_requirement_for_signal (
552- self , device_name : str , signal_name : str , value : Any , abs_tol : float , label : str
553- ) -> _ResolvedStateSignal :
554- resolved_signal_name , source = self ._resolve_signal (device_name , signal_name )
555- self ._subscriptions .add ((device_name , source ))
556- self ._signal_info_to_labels .setdefault (
557- (device_name , source , resolved_signal_name ), set ()
558- ).add (label )
559- return _ResolvedStateSignal (
597+ device_name : str ,
598+ signal_name : str ,
599+ value : Any ,
600+ abs_tol : float ,
601+ label : str ,
602+ device_manager : DeviceManagerBase ,
603+ error_prefix : str ,
604+ ) -> ResolvedStateSignal :
605+ resolved_signal_name , source = AggregatedState ._resolve_signal (
606+ device_name , signal_name , device_manager , error_prefix
607+ )
608+
609+ return ResolvedStateSignal (
560610 label = label ,
561611 device_name = device_name ,
562612 signal_name = resolved_signal_name ,
@@ -665,7 +715,7 @@ def _label_matches(self, label: str) -> bool:
665715 self ._requirement_matches (requirement ) for requirement in requirements
666716 )
667717
668- def _requirement_matches (self , requirement : _ResolvedStateSignal ) -> bool :
718+ def _requirement_matches (self , requirement : ResolvedStateSignal ) -> bool :
669719 key = (requirement .device_name , requirement .source , requirement .signal_name )
670720 cached_value = self ._signal_value_cache .get (key , None )
671721 if cached_value is None :
0 commit comments