Skip to content

Commit f6d7977

Browse files
committed
w
1 parent f182538 commit f6d7977

3 files changed

Lines changed: 218 additions & 96 deletions

File tree

bec_lib/bec_lib/bl_states.py

Lines changed: 120 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
import yaml
1313
from pydantic import BaseModel, field_validator, model_validator
14-
from typeguard import typechecked
1514

1615
from bec_lib import messages
1716
from 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:

bec_lib/tests/test_beamline_states.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ def test_aggregated_state_requirement_matches(
496496
redis_connector=connected_connector,
497497
device_manager=dm_with_devices,
498498
)
499-
requirement = bl_states._ResolvedStateSignal(
499+
requirement = bl_states.ResolvedStateSignal(
500500
label="alignment",
501501
device_name="bpm4i",
502502
signal_name="bpm4i",

0 commit comments

Comments
 (0)