From 6c8ef8feb184f4fc30ef3939b0515855b8403e20 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Thu, 11 Dec 2025 17:25:25 +0100 Subject: [PATCH 1/6] feat: add needs field to device model --- bec_lib/bec_lib/atlas_models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bec_lib/bec_lib/atlas_models.py b/bec_lib/bec_lib/atlas_models.py index c72ca5cef..1dbb62bed 100644 --- a/bec_lib/bec_lib/atlas_models.py +++ b/bec_lib/bec_lib/atlas_models.py @@ -43,6 +43,7 @@ class _DeviceModelCore(BaseModel): connectionTimeout: float = 5.0 description: str = "" deviceTags: set[str] = set() + needs: list[str] = [] onFailure: Literal["buffer", "retry", "raise"] = "retry" readOnly: bool = False softwareTrigger: bool = False From 74ca8e04ff8e6d56ac8d6a1a483211d8a53b6135 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sun, 7 Dec 2025 10:40:22 +0100 Subject: [PATCH 2/6] feat(device manager): add device dependency resolution --- bec_lib/bec_lib/config_helper.py | 4 + .../devices/config_update_handler.py | 44 ++++++++- .../device_server/devices/devicemanager.py | 68 ++++++++++++- .../test_device_manager_ds.py | 96 +++++++++++++++++++ 4 files changed, 210 insertions(+), 2 deletions(-) diff --git a/bec_lib/bec_lib/config_helper.py b/bec_lib/bec_lib/config_helper.py index 4281f9b94..832a304d4 100644 --- a/bec_lib/bec_lib/config_helper.py +++ b/bec_lib/bec_lib/config_helper.py @@ -542,6 +542,7 @@ def _save_config_to_file(self, file_path: str, raise_on_error: bool = True) -> b '# - description: A string description of the device. Default is "" (empty string).\n' "# - deviceConfig: A dictionary of configuration parameters specific to the device class. Default is None.\n" "# - deviceTags: A list/set of tags associated with the device. Default is an empty list/set.\n" + "# - needs: A list of other device names that this device depends on. Default is [].\n" '# - onFailure: The action to take on failure. Possible values are ["buffer", "retry", "raise"]. Default is "retry".\n' "# - readOnly: A boolean indicating if the device is read-only. Default is false.\n" "# - softwareTrigger: A boolean indicating if the device uses software triggering. Default is false.\n" @@ -563,6 +564,9 @@ def _save_config_to_file(self, file_path: str, raise_on_error: bool = True) -> b "# enabled: true\n" "# connectionTimeout: 20\n" "# readoutPriority: baseline\n" + "# needs:\n" + "# - device2\n" + "# - device3\n" "# onFailure: retry\n" "# readOnly: false\n" "# softwareTrigger: false\n" diff --git a/bec_server/bec_server/device_server/devices/config_update_handler.py b/bec_server/bec_server/device_server/devices/config_update_handler.py index b7d8fe385..805149583 100644 --- a/bec_server/bec_server/device_server/devices/config_update_handler.py +++ b/bec_server/bec_server/device_server/devices/config_update_handler.py @@ -146,7 +146,11 @@ def parse_config_request( self._remove_config(msg, cancel_event) case _: pass - + # After any config change, resolve dependencies. It will raise if dependencies are not met. + self.update_session_config(msg) + self.device_manager.resolve_device_dependencies( + self.device_manager.current_session["devices"] + ) except CancelledError: error_msg = "Request was cancelled" accepted = False @@ -154,6 +158,7 @@ def parse_config_request( f"Config request {msg.metadata.get('RID')} was cancelled. The config will be flushed." ) self._flush_config() + except Exception: error_msg = traceback.format_exc() accepted = False @@ -290,6 +295,43 @@ def _remove_config( self.device_manager.reset_device(device) self.device_manager.devices.pop(dev) + def update_session_config(self, msg: messages.DeviceConfigMessage) -> None: + """ + Updates the current session config with the new config from the message. + + Args: + msg (BECMessage.DeviceConfigMessage): Config message containing the new config + + """ + action = msg.action + match action: + case "update": + # Update the session config + for dev in msg.content["config"]: + dev_config = self.device_manager.devices[dev]._config + session_device_config = next( + ( + d + for d in self.device_manager.current_session["devices"] + if d["name"] == dev + ), + None, + ) + if session_device_config: + session_device_config.update(dev_config) + case "add": + # Add new devices to the session config + for dev, dev_config in msg.content["config"].items(): + self.device_manager.current_session["devices"].append(dev_config) + case "remove": + # Remove devices from the session config + for dev in msg.content["config"]: + self.device_manager.current_session["devices"] = [ + d + for d in self.device_manager.current_session["devices"] + if d["name"] != dev + ] + def handle_failed_device_inits(self): if self.device_manager.failed_devices: msg = messages.DeviceConfigMessage( diff --git a/bec_server/bec_server/device_server/devices/devicemanager.py b/bec_server/bec_server/device_server/devices/devicemanager.py index 639c746c2..4da8e1f6b 100644 --- a/bec_server/bec_server/device_server/devices/devicemanager.py +++ b/bec_server/bec_server/device_server/devices/devicemanager.py @@ -10,6 +10,7 @@ import threading import time import traceback +from collections import deque from typing import TYPE_CHECKING, Callable import numpy as np @@ -145,6 +146,7 @@ def __init__( self.config_update_handler = None self.failed_devices = {} self._bec_message_handler = BECMessageHandler(self) + self._device_order_map = {} def initialize(self, bootstrap_server) -> None: self.config_update_handler = ( @@ -176,8 +178,9 @@ def _load_session(self, *_args, cancel_event: threading.Event | None = None, **_ progress = DeviceProgress(self.connector, self._session["devices"]) try: + devices = self.resolve_device_dependencies(self.current_session["devices"]) self.failed_devices = {} - for dev in self._session["devices"]: + for dev in devices: if cancel_event and cancel_event.is_set(): raise CancelledError("Device initialization cancelled.") name = dev.get("name") @@ -231,6 +234,69 @@ def _load_session(self, *_args, cancel_event: threading.Event | None = None, **_ f"Failed to initialize device: {dev}: {content}. The config will be reset." ) from exc + def resolve_device_dependencies(self, devices: list[dict]) -> list[dict]: + """ + Resolve device dependencies and return a sorted list of devices. It uses + the device's "needs" field of the config to determine dependencies. + + Using Kahn's algorithm for topological sorting. + + Args: + devices (list[dict]): List of device config dictionaries + Returns: + list[dict]: Sorted list of device config dictionaries + """ + device_dict = {dev["name"]: dev for dev in devices} + in_degree = {dev["name"]: 0 for dev in devices} + adj_list = {dev["name"]: [] for dev in devices} + + for dev in devices: + needs = dev.get("needs", []) + for dep in needs: + if dep not in device_dict: + raise DeviceConfigError(f"Device {dev['name']} needs unknown device {dep}.") + adj_list[dep].append(dev["name"]) + in_degree[dev["name"]] += 1 + + queue = deque([name for name, degree in in_degree.items() if degree == 0]) + sorted_devices = [] + + while queue: + current = queue.popleft() + sorted_devices.append(device_dict[current]) + for neighbor in adj_list[current]: + in_degree[neighbor] -= 1 + if in_degree[neighbor] == 0: + queue.append(neighbor) + + cyclic_devices = [name for name, degree in in_degree.items() if degree > 0] + if cyclic_devices: + raise DeviceConfigError(f"Cyclic dependency detected among devices: {cyclic_devices}") + + self._device_order_map = {dev["name"]: idx for idx, dev in enumerate(sorted_devices)} + + return sorted_devices + + def get_device_order(self, device_names: list[str]) -> list[str]: + """ + Get the device names sorted by their initialization order. + + Args: + device_names (list[str]): List of device names to sort. + + Returns: + list[str]: Sorted list of device names. + + Raises: + RuntimeError: If the device order map is not initialized. + """ + if not self._device_order_map: + raise RuntimeError("Device order map is not initialized.") + return sorted( + device_names, + key=lambda name: self._device_order_map.get(name.split(".")[0], float("inf")), + ) + def initialize_delayed_devices(self, dev: dict, config: dict, obj: OphydObject) -> None: """Initialize delayed device after all other devices have been initialized.""" name = dev.get("name") diff --git a/bec_server/tests/tests_device_server/test_device_manager_ds.py b/bec_server/tests/tests_device_server/test_device_manager_ds.py index 0b7ee9975..1fb18e0de 100644 --- a/bec_server/tests/tests_device_server/test_device_manager_ds.py +++ b/bec_server/tests/tests_device_server/test_device_manager_ds.py @@ -7,6 +7,7 @@ from ophyd_devices.devices.psi_motor import EpicsMotor from bec_lib import messages +from bec_lib.bec_errors import DeviceConfigError from bec_lib.endpoints import MessageEndpoints from bec_server.device_server.devices.devicemanager import DeviceManagerDS @@ -493,3 +494,98 @@ def test_initialize_device(dm_with_devices, epics_motor, epics_motor_config, tim mock_high_subscribe.assert_called_once_with( dm_with_devices._obj_callback_limit_change, run=False ) + + +@pytest.mark.parametrize("device_manager_class", [DeviceManagerDS]) +def test_device_dependency_resolution(dm_with_devices): + devices = [ + {"name": "dev1", "deviceClass": "SomeClass", "deviceConfig": {}, "enabled": True}, + { + "name": "dev2", + "deviceClass": "SomeClass", + "deviceConfig": {}, + "needs": ["dev1"], + "enabled": True, + }, + { + "name": "dev3", + "deviceClass": "SomeClass", + "deviceConfig": {}, + "needs": ["dev2"], + "enabled": True, + }, + ] + + sorted_devices = dm_with_devices.resolve_device_dependencies(devices) + sorted_names = [dev["name"] for dev in sorted_devices] + assert sorted_names == ["dev1", "dev2", "dev3"] + + +@pytest.mark.parametrize("device_manager_class", [DeviceManagerDS]) +def test_device_dependency_resolution_with_unknown_dependency(dm_with_devices): + devices = [ + {"name": "dev1", "deviceClass": "SomeClass", "deviceConfig": {}, "enabled": True}, + { + "name": "dev2", + "deviceClass": "SomeClass", + "deviceConfig": {}, + "needs": ["unknown_dev"], + "enabled": True, + }, + ] + + with pytest.raises(DeviceConfigError, match="needs unknown device"): + dm_with_devices.resolve_device_dependencies(devices) + + +@pytest.mark.parametrize("device_manager_class", [DeviceManagerDS]) +def test_device_dependency_resolution_with_cyclic_dependency(dm_with_devices): + devices = [ + { + "name": "dev1", + "deviceClass": "SomeClass", + "deviceConfig": {}, + "needs": ["dev3"], + "enabled": True, + }, + { + "name": "dev2", + "deviceClass": "SomeClass", + "deviceConfig": {}, + "needs": ["dev1"], + "enabled": True, + }, + { + "name": "dev3", + "deviceClass": "SomeClass", + "deviceConfig": {}, + "needs": ["dev2"], + "enabled": True, + }, + ] + + with pytest.raises(DeviceConfigError, match="Cyclic dependency detected"): + dm_with_devices.resolve_device_dependencies(devices) + + +@pytest.mark.parametrize("device_manager_class", [DeviceManagerDS]) +def test_get_device_order(dm_with_devices): + device_manager = dm_with_devices + devices = ["samx", "eiger"] + ordered_devices = device_manager.get_device_order(devices) + assert len(ordered_devices) == len(devices) + assert ordered_devices == ["eiger", "samx"] + + devices = ["samx.readback", "eiger"] + ordered_devices = device_manager.get_device_order(devices) + assert len(ordered_devices) == len(devices) + assert ordered_devices == ["eiger", "samx.readback"] + + +@pytest.mark.parametrize("device_manager_class", [DeviceManagerDS]) +def test_get_device_order_without_order_map(dm_with_devices): + device_manager = dm_with_devices + device_manager._device_order_map = {} + devices = ["samx", "eiger"] + with pytest.raises(RuntimeError, match="Device order map is not initialized"): + device_manager.get_device_order(devices) From c4f0ff3486fb8d15a3859b9bc9ca19afbe4fc602 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sun, 7 Dec 2025 11:26:04 +0100 Subject: [PATCH 3/6] feat(device server): run ophyd method according to device order --- .../bec_server/device_server/device_server.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bec_server/bec_server/device_server/device_server.py b/bec_server/bec_server/device_server/device_server.py index b46686261..0bafea763 100644 --- a/bec_server/bec_server/device_server/device_server.py +++ b/bec_server/bec_server/device_server/device_server.py @@ -575,6 +575,7 @@ def _trigger_device(self, instr: messages.DeviceInstructionMessage) -> None: devices = instr.content["device"] if not isinstance(devices, list): devices = [devices] + devices = self.device_manager.get_device_order(devices) self.requests_handler.add_request(instr, num_status_objects=len(devices)) for dev in devices: obj = self.device_manager.devices.get(dev) @@ -611,6 +612,8 @@ def _complete_device(self, instr: messages.DeviceInstructionMessage) -> None: if not isinstance(devices, list): devices = [devices] + devices = self.device_manager.get_device_order(devices) + self.requests_handler.add_request(instr, num_status_objects=len(devices)) num_status_objects = 0 for dev in devices: @@ -662,6 +665,8 @@ def _pre_scan(self, instr: messages.DeviceInstructionMessage) -> None: if not isinstance(devices, list): devices = [devices] + devices = self.device_manager.get_device_order(devices) + self.requests_handler.add_request(instr, num_status_objects=len(devices)) num_status_objects = 0 for dev in devices: @@ -745,6 +750,8 @@ def _read_device(self, instr: messages.DeviceInstructionMessage, new_status=True if not isinstance(devices, list): devices = [devices] + devices = self.device_manager.get_device_order(devices) + if not new_status: self._read_and_update_devices(devices, instr.metadata) return @@ -757,6 +764,7 @@ def _read_and_update_devices(self, devices: list[str], metadata: dict) -> list: start = time.time() pipe = self.connector.pipeline() signal_container = [] + devices = self.device_manager.get_device_order(devices) for dev in devices: device_root = dev.split(".")[0] self.device_manager.devices.get(device_root).metadata = metadata @@ -788,6 +796,7 @@ def _read_config_and_update_devices(self, devices: list[str], metadata: dict) -> start = time.time() pipe = self.connector.pipeline() signal_container = [] + devices = self.device_manager.get_device_order(devices) for dev in devices: self.device_manager.devices.get(dev).metadata = metadata obj = self.device_manager.devices.get(dev).obj @@ -853,6 +862,8 @@ def _stage_device( if not isinstance(devices, list): devices = [devices] + devices = self.device_manager.get_device_order(devices) + self.requests_handler.add_request(instr, num_status_objects=len(devices)) num_status_objects = 0 @@ -913,6 +924,8 @@ def _unstage_device(self, instr: messages.DeviceInstructionMessage) -> None: if not isinstance(devices, list): devices = [devices] + devices = self.device_manager.get_device_order(devices) + self.requests_handler.add_request(instr, num_status_objects=len(devices)) num_status_objects = 0 for dev in devices: From fab188f317ea38c813f460e94f2b79055eefe41d Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Mon, 15 Dec 2025 19:17:21 +0100 Subject: [PATCH 4/6] fix: adjust hash calculation for new device field 'needs' --- bec_lib/bec_lib/atlas_models.py | 1 + bec_lib/tests/test_device_hashing.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bec_lib/bec_lib/atlas_models.py b/bec_lib/bec_lib/atlas_models.py index 1dbb62bed..3aee8dd9e 100644 --- a/bec_lib/bec_lib/atlas_models.py +++ b/bec_lib/bec_lib/atlas_models.py @@ -120,6 +120,7 @@ class DeviceHashModel(BaseModel, frozen=True): connectionTimeout: HashInclusion = HashInclusion.EXCLUDE deviceConfig: DictHashInclusion = DictHashInclusion(field_inclusion=HashInclusion.VARIANT) deviceTags: HashInclusion = HashInclusion.EXCLUDE + needs: HashInclusion = HashInclusion.VARIANT readoutPriority: HashInclusion = HashInclusion.EXCLUDE description: HashInclusion = HashInclusion.EXCLUDE readOnly: HashInclusion = HashInclusion.EXCLUDE diff --git a/bec_lib/tests/test_device_hashing.py b/bec_lib/tests/test_device_hashing.py index ee283e721..a1834d85c 100644 --- a/bec_lib/tests/test_device_hashing.py +++ b/bec_lib/tests/test_device_hashing.py @@ -18,6 +18,7 @@ "deviceClass": "TestDeviceClass", "readoutPriority": "baseline", "enabled": True, + "needs": [], } @@ -246,11 +247,12 @@ def test_device_equality_according_to_model(hash_model: DeviceHashModel, equal: @pytest.mark.parametrize( "hash_model, expected", [ - (DeviceHashModel(), {"deviceConfig": {"a": "b", "c": "d", "l": "m"}}), - (DeviceHashModel(deviceConfig=DictHashInclusion()), {}), + (DeviceHashModel(), {"deviceConfig": {"a": "b", "c": "d", "l": "m"}, "needs": []}), + (DeviceHashModel(deviceConfig=DictHashInclusion(), needs=HashInclusion.EXCLUDE), {}), ( DeviceHashModel( deviceConfig=DictHashInclusion(), + needs=HashInclusion.EXCLUDE, enabled=HashInclusion.VARIANT, softwareTrigger=HashInclusion.VARIANT, ), @@ -259,6 +261,7 @@ def test_device_equality_according_to_model(hash_model: DeviceHashModel, equal: ( DeviceHashModel( deviceConfig=DictHashInclusion(), + needs=HashInclusion.EXCLUDE, userParameter=DictHashInclusion( field_inclusion=HashInclusion.INCLUDE, inclusion_keys={"a"}, @@ -275,7 +278,7 @@ def test_device_equality_according_to_model(hash_model: DeviceHashModel, equal: remainder_inclusion=HashInclusion.EXCLUDE, ) ), - {"deviceConfig": {"a": "b", "c": "d", "l": "m"}}, + {"deviceConfig": {"a": "b", "c": "d", "l": "m"}, "needs": []}, ), ], ) @@ -426,5 +429,6 @@ def test_hashable_device_set_or_adds_variants(): combined_device: HashableDevice = combined.pop() assert combined_device.variants != set() assert HashableDevice.model_validate(combined_device.variants.pop())._variant_info() == { - "deviceConfig": {"param": "other_value"} + "deviceConfig": {"param": "other_value"}, + "needs": [], } From 267b9dc0ad58570382c603ceb7a54ead615af323 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 7 Jan 2026 11:24:53 +0100 Subject: [PATCH 5/6] refactor(device init): simplified initialization loop --- .../device_server/devices/devicemanager.py | 79 +++++++++++-------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/bec_server/bec_server/device_server/devices/devicemanager.py b/bec_server/bec_server/device_server/devices/devicemanager.py index 4da8e1f6b..3762d09ff 100644 --- a/bec_server/bec_server/device_server/devices/devicemanager.py +++ b/bec_server/bec_server/device_server/devices/devicemanager.py @@ -170,6 +170,37 @@ def _get_device_class(dev_type: str) -> type: """Get the device class from the device type""" return plugin_helper.get_plugin_class(dev_type, [opd, ophyd]) + def _init_device(self, device_info: dict, delayed: bool, progress: DeviceProgress) -> None: + """ + Initialize a device from its configuration dictionary. + + Args: + device_info (dict): Device configuration dictionary. + delayed (bool): Whether to initialize the device in delayed mode. + progress (DeviceProgress): DeviceProgress instance to track initialization progress. + """ + + name = device_info.get("name") + success = True + progress.update_progress(device_name=name, finished=False, success=success) + + obj, config = self.construct_device_obj(device_info, device_manager=self) + try: + if delayed: + self.initialize_delayed_devices(device_info, config, obj) + else: + self.initialize_device(device_info, config, obj) + # pylint: disable=broad-except + except Exception: + if name not in self.devices: + raise + msg = traceback.format_exc() + logger.warning(f"Failed to initialize device {name}: {msg}") + self.failed_devices[name] = msg + success = False + finally: + progress.update_progress(device_name=name, finished=True, success=success) + def _load_session(self, *_args, cancel_event: threading.Event | None = None, **_kwargs): delayed_init = [] if not self._is_config_valid(): @@ -177,49 +208,35 @@ def _load_session(self, *_args, cancel_event: threading.Event | None = None, **_ return progress = DeviceProgress(self.connector, self._session["devices"]) + current_device_name = None try: devices = self.resolve_device_dependencies(self.current_session["devices"]) self.failed_devices = {} for dev in devices: - if cancel_event and cancel_event.is_set(): - raise CancelledError("Device initialization cancelled.") name = dev.get("name") enabled = dev.get("enabled") + + if cancel_event and cancel_event.is_set(): + raise CancelledError("Device initialization cancelled.") logger.info(f"Adding device {name}: {'ENABLED' if enabled else 'DISABLED'}") + current_device_name = name + dev_cls = self._get_device_class(dev.get("deviceClass")) if issubclass(dev_cls, (opd.DeviceProxy, opd.ComputedSignal)): delayed_init.append(dev) continue - success = True - progress.update_progress(device_name=name, finished=False, success=success) - obj, config = self.construct_device_obj(dev, device_manager=self) - try: - self.initialize_device(dev, config, obj) - # pylint: disable=broad-except - except Exception: - if name not in self.devices: - raise - msg = traceback.format_exc() - logger.warning(f"Failed to initialize device {name}: {msg}") - self.failed_devices[name] = msg - success = False - - progress.update_progress(device_name=name, finished=True, success=success) + + self._init_device(dev, delayed=False, progress=progress) + current_device_name = None for dev in delayed_init: - success = True name = dev.get("name") - progress.update_progress(device_name=name, finished=False, success=success) - obj, config = self.construct_device_obj(dev, device_manager=self) - try: - self.initialize_delayed_devices(dev, config, obj) - # pylint: disable=broad-except - except Exception: - msg = traceback.format_exc() - logger.warning(f"Failed to initialize device {name}: {msg}") - self.failed_devices[name] = msg - success = False - progress.update_progress(device_name=name, finished=True, success=success) + if cancel_event and cancel_event.is_set(): + raise CancelledError("Device initialization cancelled.") + current_device_name = name + self._init_device(dev, delayed=True, progress=progress) + current_device_name = None + self.config_update_handler.handle_failed_device_inits() except CancelledError: self._reset_config() @@ -227,11 +244,11 @@ def _load_session(self, *_args, cancel_event: threading.Event | None = None, **_ except Exception as exc: content = traceback.format_exc() logger.error( - f"Failed to initialize device: {dev}: {content}. The config will be reset." + f"Failed to initialize device: {current_device_name}: {content}. The config will be reset." ) self._reset_config() raise DeviceConfigError( - f"Failed to initialize device: {dev}: {content}. The config will be reset." + f"Failed to initialize device: {current_device_name}: {content}. The config will be reset." ) from exc def resolve_device_dependencies(self, devices: list[dict]) -> list[dict]: From 5fe5b53823d4d31b042df8cddb4ce24a5416c1fa Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 7 Jan 2026 12:49:06 +0100 Subject: [PATCH 6/6] feat(device manager): add alarm for dependencies on disabled devices --- .../bec_server/device_server/devices/devicemanager.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bec_server/bec_server/device_server/devices/devicemanager.py b/bec_server/bec_server/device_server/devices/devicemanager.py index 3762d09ff..f00efbcf5 100644 --- a/bec_server/bec_server/device_server/devices/devicemanager.py +++ b/bec_server/bec_server/device_server/devices/devicemanager.py @@ -22,6 +22,7 @@ from typeguard import typechecked from bec_lib import messages, plugin_helper +from bec_lib.alarm_handler import Alarms from bec_lib.bec_errors import DeviceConfigError from bec_lib.bec_service import BECService from bec_lib.device import DeviceBaseWithConfig @@ -272,6 +273,16 @@ def resolve_device_dependencies(self, devices: list[dict]) -> list[dict]: for dep in needs: if dep not in device_dict: raise DeviceConfigError(f"Device {dev['name']} needs unknown device {dep}.") + if device_dict[dep].get("enabled") is False: + self.connector.raise_alarm( + Alarms.WARNING, + messages.ErrorInfo( + error_message=f"Device {dev['name']} depends on disabled device {dep}.", + compact_error_message=f"Dependency on disabled device {dep}.", + exception_type="Warning", + device=dev["name"], + ), + ) adj_list[dep].append(dev["name"]) in_degree[dev["name"]] += 1