From 148bbdf9e5b0fcb671342e998bdf0c9dac4286f6 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Fri, 15 Sep 2023 12:18:28 +0200 Subject: [PATCH 01/18] OperationDefinition calls a handler instead of settingn observables only --- src/sdc11073/mdib/descriptorcontainers.py | 3 + src/sdc11073/provider/operations.py | 254 +++++++++++++------ src/sdc11073/provider/sco.py | 121 --------- src/sdc11073/roles/alarmprovider.py | 9 +- src/sdc11073/roles/audiopauseprovider.py | 8 +- src/sdc11073/roles/clockprovider.py | 16 +- src/sdc11073/roles/contextprovider.py | 11 +- src/sdc11073/roles/metricprovider.py | 24 +- src/sdc11073/roles/patientcontextprovider.py | 5 +- src/sdc11073/roles/product.py | 11 +- src/sdc11073/roles/providerbase.py | 25 +- tests/test_tutorial.py | 19 +- 12 files changed, 261 insertions(+), 245 deletions(-) diff --git a/src/sdc11073/mdib/descriptorcontainers.py b/src/sdc11073/mdib/descriptorcontainers.py index 8fb8fb36..a26a719b 100644 --- a/src/sdc11073/mdib/descriptorcontainers.py +++ b/src/sdc11073/mdib/descriptorcontainers.py @@ -587,6 +587,9 @@ class ActivateOperationDescriptorContainer(AbstractSetStateOperationDescriptorCo _props = ('Argument',) _child_elements_order = (pm_qnames.Argument,) + def __init__(self, *args, **kwargs): + return super().__init__(*args, **kwargs) + class AbstractAlertDescriptorContainer(AbstractDescriptorContainer): """Represents AbstractAlertDescriptor in BICEPS.""" diff --git a/src/sdc11073/provider/operations.py b/src/sdc11073/provider/operations.py index eb4f6c74..f7b21f4d 100644 --- a/src/sdc11073/provider/operations.py +++ b/src/sdc11073/provider/operations.py @@ -1,118 +1,230 @@ +from __future__ import annotations + import inspect import sys +import time +from typing import TYPE_CHECKING, Callable, Protocol + +from sdc11073 import loghelper +from sdc11073 import observableproperties as properties +from sdc11073.exceptions import ApiUsageError +from sdc11073.xml_types import msg_qnames as msg +from sdc11073.xml_types import pm_qnames as pm + +class OperationDefinitionProtocol(Protocol): + pass + +if TYPE_CHECKING: + from sdc11073.mdib.descriptorcontainers import AbstractDescriptorProtocol + from sdc11073.mdib.providermdib import ProviderMdib + from sdc11073.multikey import MultiKeyLookup + from sdc11073.pysoap.soapenvelope import ReceivedSoapMessage + from sdc11073.xml_types.msg_types import AbstractSet + from sdc11073.xml_types.pm_types import CodedValue, OperatingMode + ExecuteHandler = Callable[[OperationDefinitionProtocol, ReceivedSoapMessage, AbstractSet], list[str]] -from .sco import OperationDefinition -from ..xml_types import msg_qnames as msg, pm_qnames as pm +class OperationDefinitionBase: + """Base class of all provided operations. + + An operation is a point for remote control over the network. + """ + + current_value = properties.ObservableProperty(fire_only_on_changed_value=False) + current_request = properties.ObservableProperty(fire_only_on_changed_value=False) + current_argument = properties.ObservableProperty(fire_only_on_changed_value=False) + on_timeout = properties.ObservableProperty(fire_only_on_changed_value=False) + OP_DESCR_QNAME = None + OP_STATE_QNAME = None + OP_QNAME = None + + def __init__(self, handle: str, + operation_target_handle: str, + operation_handler: ExecuteHandler, + coded_value: CodedValue | None = None, + delayed_processing: bool = True, + log_prefix: str | None = None): + """Construct a OperationDefinitionBase. + + :param handle: the handle of the operation itself. + :param operation_target_handle: the handle of the modified data (MdDescription) + :param coded_value: a pmtypes.CodedValue instance + :param delayed_processing: if True, device returns WAIT, and sends notifications WAIT, STARTED, FINISHED/FAILED + if False, device returns FINISHED/FAILED, and sends same state in notification + """ + self._logger = loghelper.get_logger_adapter(f'sdc.device.op.{self.__class__.__name__}', log_prefix) + self._mdib = None + self._descriptor_container = None + self._operation_state_container = None + self._handle = handle + self._operation_target_handle = operation_target_handle + # documentation of operation_target_handle: + # A HANDLE reference this operation is targeted to. In case of a single state this is the HANDLE of the descriptor. + # In case that multiple states may belong to one descriptor (pm:AbstractMultiState), OperationTarget is the HANDLE + # of one of the state instances (if the state is modified by the operation). + self._operation_handler = operation_handler + self._coded_value = coded_value + self.delayed_processing = delayed_processing + self.calls = [] # record when operation was called + self.last_called_time = None + + @property + def handle(self) -> str: + return self._handle + + @property + def operation_target_handle(self) -> str: + return self._operation_target_handle + + # @property + # def operation_target_storage(self) -> MultiKeyLookup: + # return self._mdib.states + + @property + def descriptor_container(self) -> AbstractDescriptorProtocol: + return self._descriptor_container + + def execute_operation(self, + request: ReceivedSoapMessage, + operation_request: AbstractSet) -> list[str]: + """Execute the operation itself. + + This method calls the provided operation_handler. + """ + self.calls.append((time.time(), request)) + operation_targets = self._operation_handler(self, request, operation_request) + self.current_request = request + self.current_argument = operation_request.argument + self.last_called_time = time.time() + return operation_targets + + def check_timeout(self): + """Set on_timeout observable if timeout is detected.""" + if self.last_called_time is None: + return + if self._descriptor_container.InvocationEffectiveTimeout is None: + return + age = time.time() - self.last_called_time + if age < self._descriptor_container.InvocationEffectiveTimeout: + return + self.on_timeout = True # let observable fire + + def set_mdib(self, mdib: ProviderMdib, parent_descriptor_handle: str): + """Set mdib reference. + + The operation needs to know the mdib that it operates on. + This is called by SubscriptionManager on registration. + Needs to be implemented by derived classes if specific things have to be initialized. + """ + if self._mdib is not None: + raise ApiUsageError('Mdib is already set') + self._mdib = mdib + self._logger.log_prefix = mdib.log_prefix # use same prefix as mdib for logging + self._descriptor_container = self._mdib.descriptions.handle.get_one(self._handle, allow_none=True) + if self._descriptor_container is not None: + # there is already a descriptor + self._logger.debug('descriptor for operation "{}" is already present, re-using it', self._handle) + else: + cls = mdib.data_model.get_descriptor_container_class(self.OP_DESCR_QNAME) + self._descriptor_container = cls(self._handle, parent_descriptor_handle) + self._init_operation_descriptor_container() + # ToDo: transaction context for flexibility to add operations at runtime + mdib.descriptions.add_object(self._descriptor_container) + + self._operation_state_container = self._mdib.states.descriptor_handle.get_one(self._handle, allow_none=True) + if self._operation_state_container is not None: + self._logger.debug('operation state for operation "{}" is already present, re-using it', self._handle) + else: + cls = mdib.data_model.get_state_container_class(self.OP_STATE_QNAME) + self._operation_state_container = cls(self._descriptor_container) + mdib.states.add_object(self._operation_state_container) + + def _init_operation_descriptor_container(self): + self._descriptor_container.OperationTarget = self._operation_target_handle + if self._coded_value is not None: + self._descriptor_container.Type = self._coded_value + + def set_operating_mode(self, mode: OperatingMode): + """Set OperatingMode member in state in transaction context.""" + with self._mdib.transaction_manager() as mgr: + state = mgr.get_state(self._handle) + state.OperatingMode = mode + + def collect_values(self, number_of_values: int | None = None) \ + -> properties.ValuesCollector | properties.SingleValueCollector: + """Async way to retrieve next value(s). + + Returns a Future-like object that has a result() method. + For details see properties.SingleValueCollector and propertiesValuesCollector documentation. + """ + if number_of_values is None: + return properties.SingleValueCollector(self, 'current_value') + return properties.ValuesCollector(self, 'current_value', number_of_values) + + def __str__(self): + code = '?' if self._descriptor_container is None else self._descriptor_container.Type + return f'{self.__class__.__name__} handle={self._handle} code={code} operation-target={self._operation_target_handle}' + + +class SetStringOperation(OperationDefinitionBase): + """Implementation of SetString operation.""" -class SetStringOperation(OperationDefinition): OP_DESCR_QNAME = pm.SetStringOperationDescriptor OP_STATE_QNAME = pm.SetStringOperationState OP_QNAME = msg.SetString - def __init__(self, handle, operation_target_handle, initial_value=None, coded_value=None): - super().__init__(handle=handle, - operation_target_handle=operation_target_handle, - coded_value=coded_value) - self.current_value = initial_value - - @classmethod - def from_operation_container(cls, operation_container): - return cls(handle=operation_container.handle, - operation_target_handle=operation_container.OperationTarget, - initial_value=None, coded_value=None) +class SetValueOperation(OperationDefinitionBase): + """Implementation of SetValue operation.""" -class SetValueOperation(OperationDefinition): OP_DESCR_QNAME = pm.SetValueOperationDescriptor OP_STATE_QNAME = pm.SetValueOperationState OP_QNAME = msg.SetValue - def __init__(self, handle, operation_target_handle, initial_value=None, coded_value=None): - super().__init__(handle=handle, - operation_target_handle=operation_target_handle, - coded_value=coded_value) - self.current_value = initial_value +class SetContextStateOperation(OperationDefinitionBase): + """Implementation of SetContextOperation.""" -class SetContextStateOperation(OperationDefinition): - """Default implementation of SetContextOperation.""" OP_DESCR_QNAME = pm.SetContextStateOperationDescriptor OP_STATE_QNAME = pm.SetContextStateOperationState OP_QNAME = msg.SetContextState - def __init__(self, handle, operation_target_handle, coded_value=None): - super().__init__(handle, - operation_target_handle, - coded_value=coded_value) - - @property - def operation_target_storage(self): - return self._mdib.context_states - - def _init_operation_target_container(self): - """ initially no patient context is created.""" + # @property + # def operation_target_storage(self): + # return self._mdib.context_states - @classmethod - def from_operation_container(cls, operation_container): - return cls(handle=operation_container.handle, - operation_target_handle=operation_container.OperationTarget) +class ActivateOperation(OperationDefinitionBase): + """ This default implementation only registers calls, no manipulation of operation target.""" -class ActivateOperation(OperationDefinition): - """ This default implementation only registers calls, no manipulation of operation target - """ OP_DESCR_QNAME = pm.ActivateOperationDescriptor OP_STATE_QNAME = pm.ActivateOperationState OP_QNAME = msg.Activate - def __init__(self, handle, operation_target_handle, coded_value=None): - super().__init__(handle=handle, - operation_target_handle=operation_target_handle, - coded_value=coded_value) +class SetAlertStateOperation(OperationDefinitionBase): + """ This default implementation only registers calls, no manipulation of operation target.""" -class SetAlertStateOperation(OperationDefinition): - """ This default implementation only registers calls, no manipulation of operation target - """ OP_DESCR_QNAME = pm.SetAlertStateOperationDescriptor OP_STATE_QNAME = pm.SetAlertStateOperationState OP_QNAME = msg.SetAlertState - def __init__(self, handle, operation_target_handle, coded_value=None, log_prefix=None): - super().__init__(handle=handle, - operation_target_handle=operation_target_handle, - coded_value=coded_value, - log_prefix=log_prefix) +class SetComponentStateOperation(OperationDefinitionBase): + """ This default implementation only registers calls, no manipulation of operation target.""" -class SetComponentStateOperation(OperationDefinition): - """ This default implementation only registers calls, no manipulation of operation target - """ OP_DESCR_QNAME = pm.SetComponentStateOperationDescriptor OP_STATE_QNAME = pm.SetComponentStateOperationState OP_QNAME = msg.SetComponentState - def __init__(self, handle, operation_target_handle, coded_value=None, log_prefix=None): - super().__init__(handle=handle, - operation_target_handle=operation_target_handle, - coded_value=coded_value, - log_prefix=log_prefix) +class SetMetricStateOperation(OperationDefinitionBase): + """This default implementation only registers calls, no manipulation of operation target.""" -class SetMetricStateOperation(OperationDefinition): - """ This default implementation only registers calls, no manipulation of operation target - """ OP_DESCR_QNAME = pm.SetMetricStateOperationDescriptor OP_STATE_QNAME = pm.SetMetricStateOperationState OP_QNAME = msg.SetMetricState - def __init__(self, handle, operation_target_handle, coded_value=None, log_prefix=None): - super().__init__(handle=handle, - operation_target_handle=operation_target_handle, - coded_value=coded_value, - log_prefix=log_prefix) - # mapping of states: xsi:type information to classes # find all classes in this module that have a member "OP_DESCR_QNAME" @@ -124,7 +236,5 @@ def __init__(self, handle, operation_target_handle, coded_value=None, log_prefix def get_operation_class(q_name): - """ - :param q_name: a QName instance - """ + """:param q_name: a QName instance""" return _operation_lookup_by_type.get(q_name) diff --git a/src/sdc11073/provider/sco.py b/src/sdc11073/provider/sco.py index 58bd66d8..0ad07336 100644 --- a/src/sdc11073/provider/sco.py +++ b/src/sdc11073/provider/sco.py @@ -25,127 +25,6 @@ from lxml.etree import QName -class OperationDefinition: - """ This is the base class of all provided operations. - An operation is a point for remote control over the network.""" - current_value = properties.ObservableProperty(fire_only_on_changed_value=False) - current_request = properties.ObservableProperty(fire_only_on_changed_value=False) - current_argument = properties.ObservableProperty(fire_only_on_changed_value=False) - on_timeout = properties.ObservableProperty(fire_only_on_changed_value=False) - OP_DESCR_QNAME = None - OP_STATE_QNAME = None - OP_QNAME = None - - def __init__(self, handle: str, - operation_target_handle: str, - coded_value=None, - log_prefix: Optional[str] = None): - """ - :param handle: the handle of the operation itself. - :param operation_target_handle: the handle of the modified data (MdDescription) - :param coded_value: a pmtypes.CodedValue instance - """ - self._logger = loghelper.get_logger_adapter(f'sdc.device.op.{self.__class__.__name__}', log_prefix) - self._mdib = None - self._descriptor_container = None - self._operation_state_container = None - self._handle = handle - self._operation_target_handle = operation_target_handle - # documentation of operation_target_handle: - # A HANDLE reference this operation is targeted to. In case of a single state this is the HANDLE of the descriptor. - # In case that multiple states may belong to one descriptor (pm:AbstractMultiState), OperationTarget is the HANDLE - # of one of the state instances (if the state is modified by the operation). - self._coded_value = coded_value - self.calls = [] # record when operation was called - self.last_called_time = None - - @property - def handle(self): - return self._handle - - @property - def operation_target_handle(self): - return self._operation_target_handle - - @property - def operation_target_storage(self): - return self._mdib.states - - @property - def descriptor_container(self): - return self._descriptor_container - - def execute_operation(self, request, operation_request): - """Execute the operation itself. - - A handler that executes the operation must be bound to observable "current_request". - """ - self.calls.append((time.time(), request)) - self.current_request = request - self.current_argument = operation_request.argument - self.last_called_time = time.time() - - def check_timeout(self): - if self.last_called_time is None: - return - if self._descriptor_container.InvocationEffectiveTimeout is None: - return - age = time.time() - self.last_called_time - if age < self._descriptor_container.InvocationEffectiveTimeout: - return - self.on_timeout = True # let observable fire - - def set_mdib(self, mdib, parent_descriptor_container): - """ The operation needs to know the mdib that it operates on. - This is called by SubscriptionManager on registration. - Needs to be implemented by derived classes if specific things have to be initialized.""" - if self._mdib is not None: - raise ApiUsageError('Mdib is already set') - self._mdib = mdib - self._logger.log_prefix = mdib.log_prefix # use same prefix as mdib for logging - self._descriptor_container = self._mdib.descriptions.handle.get_one(self._handle, allow_none=True) - if self._descriptor_container is not None: - # there is already a descriptor - self._logger.debug('descriptor for operation "{}" is already present, re-using it', self._handle) - else: - cls = mdib.data_model.get_descriptor_container_class(self.OP_DESCR_QNAME) - self._descriptor_container = cls(self._handle, parent_descriptor_container.Handle) - self._init_operation_descriptor_container() - # ToDo: transaction context for flexibility to add operations at runtime - mdib.descriptions.add_object(self._descriptor_container) - - self._operation_state_container = self._mdib.states.descriptor_handle.get_one(self._handle, allow_none=True) - if self._operation_state_container is not None: - self._logger.debug('operation state for operation "{}" is already present, re-using it', self._handle) - else: - cls = mdib.data_model.get_state_container_class(self.OP_STATE_QNAME) - self._operation_state_container = cls(self._descriptor_container) - mdib.states.add_object(self._operation_state_container) - - def _init_operation_descriptor_container(self): - self._descriptor_container.OperationTarget = self._operation_target_handle - if self._coded_value is not None: - self._descriptor_container.Type = self._coded_value - - def set_operating_mode(self, mode): - """Mode is one of En, Dis, NA""" - with self._mdib.transaction_manager() as mgr: - state = mgr.get_state(self._handle) - state.OperatingMode = mode - - def collect_values(self, number_of_values=None): - """Async way to retrieve next value(s). - - Returns a Future-like object that has a result() method. - For details see properties.SingleValueCollector and propertiesValuesCollector documentation. - """ - if number_of_values is None: - return properties.SingleValueCollector(self, 'current_value') - return properties.ValuesCollector(self, 'current_value', number_of_values) - - def __str__(self): - code = '?' if self._descriptor_container is None else self._descriptor_container.Type - return f'{self.__class__.__name__} handle={self._handle} code={code} operation-target={self._operation_target_handle}' class _OperationsWorker(threading.Thread): diff --git a/src/sdc11073/roles/alarmprovider.py b/src/sdc11073/roles/alarmprovider.py index 1eaa20d9..5a8507a3 100644 --- a/src/sdc11073/roles/alarmprovider.py +++ b/src/sdc11073/roles/alarmprovider.py @@ -40,7 +40,7 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ handler = self._delegate_alert_signal :param operation_descriptor_container: :param operation_cls_getter: - :return: None or an OperationDefinition instance + :return: None or an OperationDefinitionBase instance """ pm_names = self._mdib.data_model.pm_names op_target_handle = operation_descriptor_container.OperationTarget @@ -59,7 +59,7 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ operation = self._mk_operation_from_operation_descriptor( operation_descriptor_container, operation_cls_getter, - current_argument_handler=self._delegate_alert_signal, + current_request_handler=self._delegate_alert_signal, timeout_handler=self._end_delegate_alert_signal) self._logger.debug(f'GenericAlarmProvider: added handler "self._setAlertState" ' @@ -297,7 +297,7 @@ def _get_fallback_signals(self, delegable_signal_descriptor, all_signal_descript and tmp.Manifestation == delegable_signal_descriptor.Manifestation and tmp.ConditionSignaled == delegable_signal_descriptor.ConditionSignaled] - def _delegate_alert_signal(self, operation_instance, value): + def _delegate_alert_signal(self, operation_instance, soap_request, operation_request) -> list[str]: """Handler for an operation call from remote. Sets ActivationState, Presence and ActualSignalGenerationDelay of the corresponding state in mdib. If this is a delegable signal, it also sets the ActivationState of the fallback signal. @@ -306,6 +306,7 @@ def _delegate_alert_signal(self, operation_instance, value): :param value: AlertSignalStateContainer instance :return: """ + value = operation_request.argument pm_types = self._mdib.data_model.pm_types operation_target_handle = operation_instance.operation_target_handle # self._last_set_alert_signal_state[operation_target_handle] = time.time() @@ -322,6 +323,7 @@ def _delegate_alert_signal(self, operation_instance, value): self._pause_fallback_alert_signals(descr, None, mgr) else: self._activate_fallback_alert_signals(descr, None, mgr) + return [operation_target_handle] def _end_delegate_alert_signal(self, operation_instance, _): pm_types = self._mdib.data_model.pm_types @@ -333,6 +335,7 @@ def _end_delegate_alert_signal(self, operation_instance, _): state.ActivationState = pm_types.AlertActivation.OFF descr = self._mdib.descriptions.handle.get_one(operation_target_handle) self._activate_fallback_alert_signals(descr, None, mgr) + return [operation_target_handle] def _worker_thread_loop(self): # delay start of operation diff --git a/src/sdc11073/roles/audiopauseprovider.py b/src/sdc11073/roles/audiopauseprovider.py index c11ceec7..ac14975c 100644 --- a/src/sdc11073/roles/audiopauseprovider.py +++ b/src/sdc11073/roles/audiopauseprovider.py @@ -38,7 +38,7 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ return cancel_ap_operation return None - def _set_global_audio_pause(self, operation_instance, request): # pylint: disable=unused-argument + def _set_global_audio_pause(self, operation_instance, soap_request, operation_request) -> list[str]: """ This is the code that executes the operation itself: SF1132: If global audio pause is initiated, all SystemSignalActivation/State for all alarm systems of the product with SystemSignalActivation/Manifestation evaluating to 'Aud' shall be set to 'Psd'. @@ -54,7 +54,7 @@ def _set_global_audio_pause(self, operation_instance, request): # pylint: disab alert_system_descriptors = self._mdib.descriptions.NODETYPE.get(pm_names.AlertSystemDescriptor) if alert_system_descriptors is None: self._logger.error('SDC_SetAudioPauseOperation called, but no AlertSystemDescriptor in mdib found') - return + return [] with self._mdib.transaction_manager() as mgr: for alert_system_descriptor in alert_system_descriptors: alert_system_state = mgr.get_state(alert_system_descriptor.Handle) @@ -97,8 +97,9 @@ def _set_global_audio_pause(self, operation_instance, request): # pylint: disab alert_signal_state.Presence = pm_types.AlertSignalPresence.OFF else: mgr.unget_state(alert_signal_state) + return [] # ToDo: what is the correct operation target? - def _cancel_global_audio_pause(self, operation_instance, request): # pylint: disable=unused-argument + def _cancel_global_audio_pause(self, operation_instance, soap_request, operation_request) -> list[str]: # pylint: disable=unused-argument """ This is the code that executes the operation itself: If global audio pause is initiated, all SystemSignalActivation/State for all alarm systems of the product with SystemSignalActivation/Manifestation evaluating to 'Aud' shall be set to 'Psd'. @@ -142,6 +143,7 @@ def _cancel_global_audio_pause(self, operation_instance, request): # pylint: di alert_signal_state.Presence = pm_types.AlertSignalPresence.ON else: mgr.unget_state(alert_signal_state) + return [] # ToDo: what is the correct operation target? class AudioPauseProvider(GenericAudioPauseProvider): diff --git a/src/sdc11073/roles/clockprovider.py b/src/sdc11073/roles/clockprovider.py index 6f27564a..49204d25 100644 --- a/src/sdc11073/roles/clockprovider.py +++ b/src/sdc11073/roles/clockprovider.py @@ -47,7 +47,7 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ f'instantiating "set ntp server" operation from existing descriptor handle={operation_descriptor_container.Handle}') set_ntp_operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_argument_handler=self._set_ntp_string) + current_request_handler=self._set_ntp_string) self._set_ntp_operations.append(set_ntp_operation) return set_ntp_operation if operation_descriptor_container.coding in (self.MDC_ACT_SET_TIME_ZONE.coding, self.OP_SET_TZ.coding): @@ -55,14 +55,15 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ f'instantiating "set time zone" operation from existing descriptor handle={operation_descriptor_container.Handle}') set_tz_operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_argument_handler=self._set_tz_string) + current_request_handler=self._set_tz_string) self._set_tz_operations.append(set_tz_operation) return set_tz_operation return None # ? - def _set_ntp_string(self, operation_instance, value): + def _set_ntp_string(self, operation_instance, soap_request, operation_request) -> list[str]: """This is the handler for the set ntp server operation. It sets the ReferenceSource value of clock state""" + value = operation_request.argument pm_types = self._mdib.data_model.pm_types pm_names = self._mdib.data_model.pm_names operation_target_handle = self._get_operation_target_handle(operation_instance) @@ -82,10 +83,12 @@ def _set_ntp_string(self, operation_instance, value): if state.NODETYPE != pm_names.ClockState: raise ValueError(f'_set_ntp_string: expected ClockState, got {state.NODETYPE.localname}') state.ReferenceSource = [value] + return [operation_target_handle] - def _set_tz_string(self, operation_instance, value): + def _set_tz_string(self, operation_instance, soap_request, operation_request) -> list[str]: """This is the handler for the set time zone operation. It sets the TimeZone value of clock state.""" + value = operation_request.argument pm_names = self._mdib.data_model.pm_names operation_target_handle = self._get_operation_target_handle(operation_instance) self._logger.info(f'set value {operation_target_handle} from {operation_instance.current_value} to {value}') @@ -103,6 +106,7 @@ def _set_tz_string(self, operation_instance, value): if state.NODETYPE != pm_names.ClockState: raise ValueError(f'_set_ntp_string: expected ClockState, got {state.NODETYPE.localname}') state.TimeZone = value + return [operation_target_handle] def _create_clock_descriptor_container(self, handle: str, parent_handle: str, @@ -140,7 +144,7 @@ def make_missing_operations(self, sco): handle='SET_NTP_SRV_' + mds_container.handle, operation_target_handle=clock_descriptor.handle, coded_value=self.MDC_OP_SET_TIME_SYNC_REF_SRC, - current_argument_handler=self._set_ntp_string) + current_request_handler=self._set_ntp_string) self._set_ntp_operations.append(set_ntp_operation) ops.append(set_ntp_operation) if not self._set_tz_operations: @@ -149,7 +153,7 @@ def make_missing_operations(self, sco): handle='SET_TZONE_' + mds_container.handle, operation_target_handle=clock_descriptor.handle, coded_value=self.MDC_ACT_SET_TIME_ZONE, - current_argument_handler=self._set_tz_string) + current_request_handler=self._set_tz_string) self._set_tz_operations.append(set_tz_operation) ops.append(set_tz_operation) return ops diff --git a/src/sdc11073/roles/contextprovider.py b/src/sdc11073/roles/contextprovider.py index 6da25c59..dd820208 100644 --- a/src/sdc11073/roles/contextprovider.py +++ b/src/sdc11073/roles/contextprovider.py @@ -23,19 +23,21 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ return None # we do not handle this target type return self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_argument_handler=self._set_context_state) + current_request_handler=self._set_context_state) return None - def _set_context_state(self, operation_instance, proposed_context_states): + def _set_context_state(self, operation_instance, request, operation_request) -> list[str]: """ This is the code that executes the operation itself. """ + proposed_context_states = operation_request.argument pm_types = self._mdib.data_model.pm_types with self._mdib.transaction_manager() as mgr: for proposed_st in proposed_context_states: old_state_container = None if proposed_st.DescriptorHandle != proposed_st.Handle: # this is an update for an existing state - old_state_container = operation_instance.operation_target_storage.handle.get_one( + # old_state_container = operation_instance.operation_target_storage.handle.get_one( + old_state_container = self._mdib.context_states.handle.get_one( proposed_st.Handle, allow_none=True) if old_state_container is None: raise ValueError(f'handle {proposed_st.Handle} not found') @@ -51,7 +53,8 @@ def _set_context_state(self, operation_instance, proposed_context_states): mgr.add_state(proposed_st) # find all associated context states, disassociate them, set unbinding info, and add them to updates - old_state_containers = operation_instance.operation_target_storage.descriptor_handle.get( + # old_state_containers = operation_instance.operation_target_storage.descriptor_handle.get( + old_state_containers = self._mdib.context_states.handle.get_one( proposed_st.DescriptorHandle, []) for old_state in old_state_containers: if old_state.ContextAssociation != pm_types.ContextAssociation.DISASSOCIATED or old_state.UnbindingMdibVersion is None: diff --git a/src/sdc11073/roles/metricprovider.py b/src/sdc11073/roles/metricprovider.py index 542745cf..4761471e 100644 --- a/src/sdc11073/roles/metricprovider.py +++ b/src/sdc11073/roles/metricprovider.py @@ -28,11 +28,11 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ operation_target_handle = operation_descriptor_container.OperationTarget op_target_descriptor_container = self._mdib.descriptions.handle.get_one(operation_target_handle) - if op_target_descriptor_container.NODETYPE not in (pm_names.StringMetricDescriptor, - pm_names.EnumStringMetricDescriptor, - pm_names.NumericMetricDescriptor, - pm_names.RealTimeSampleArrayMetricDescriptor): - return None # this is not metric provider role + # if op_target_descriptor_container.NODETYPE not in (pm_names.StringMetricDescriptor, + # pm_names.EnumStringMetricDescriptor, + # pm_names.NumericMetricDescriptor, + # pm_names.RealTimeSampleArrayMetricDescriptor): + # return None # this is not metric provider role if operation_descriptor_container.NODETYPE == pm_names.SetValueOperationDescriptor: if op_target_descriptor_container.NODETYPE == pm_names.NumericMetricDescriptor: @@ -41,7 +41,7 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ handle=operation_descriptor_container.Handle, operation_target_handle=operation_target_handle, coded_value=operation_descriptor_container.Type, - current_argument_handler=self._set_numeric_value) + current_request_handler=self._set_numeric_value) return None if operation_descriptor_container.NODETYPE == pm_names.SetStringOperationDescriptor: if op_target_descriptor_container.NODETYPE in (pm_names.StringMetricDescriptor, @@ -51,7 +51,7 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ handle=operation_descriptor_container.Handle, operation_target_handle=operation_target_handle, coded_value=operation_descriptor_container.Type, - current_argument_handler=self._set_string) + current_request_handler=self._set_string) return None if operation_descriptor_container.NODETYPE == pm_names.SetMetricStateOperationDescriptor: op_cls = operation_cls_getter(pm_names.SetMetricStateOperationDescriptor) @@ -59,11 +59,11 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ handle=operation_descriptor_container.Handle, operation_target_handle=operation_target_handle, coded_value=operation_descriptor_container.Type, - current_argument_handler=self._set_metric_state) + current_request_handler=self._set_metric_state) return operation return None - def _set_metric_state(self, operation_instance, value): + def _set_metric_state(self, operation_instance, soap_request, operation_request) -> list[str]: ''' :param operation_instance: the operation @@ -71,9 +71,10 @@ def _set_metric_state(self, operation_instance, value): :return: ''' # ToDo: consider ModifiableDate attribute - operation_instance.current_value = value + proposed_states = operation_request.argument + operation_instance.current_value = proposed_states with self._mdib.transaction_manager() as mgr: - for proposed_state in value: + for proposed_state in proposed_states: state = mgr.get_state(proposed_state.DescriptorHandle) if state.is_metric_state: self._logger.info('updating {} with proposed metric state', state) @@ -82,6 +83,7 @@ def _set_metric_state(self, operation_instance, value): else: self._logger.warn('_set_metric_state operation: ignore invalid referenced type {} in operation', state.NODETYPE) + return [tmp.DescriptorHandle for tmp in proposed_states] # this is a shortcoming of BICEPS: def on_pre_commit(self, mdib, transaction): if not self.activation_state_can_remove_metric_value: diff --git a/src/sdc11073/roles/patientcontextprovider.py b/src/sdc11073/roles/patientcontextprovider.py index 0564cafa..80e9cd5a 100644 --- a/src/sdc11073/roles/patientcontextprovider.py +++ b/src/sdc11073/roles/patientcontextprovider.py @@ -20,7 +20,7 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ if self._patient_context_descriptor_container and operation_descriptor_container.OperationTarget == self._patient_context_descriptor_container.Handle: pc_operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_argument_handler=self._set_context_state) + current_request_handler=self._set_context_state) self._set_patient_context_operations.append(pc_operation) return pc_operation return None @@ -40,7 +40,6 @@ def make_missing_operations(self, sco): handle='opSetPatCtx', operation_target_handle=self._patient_context_descriptor_container.handle, coded_value=None, - current_argument_handler=self._set_context_state, - timeout_handler=None) + current_request_handler=self._set_context_state) ops.append(pc_operation) return ops diff --git a/src/sdc11073/roles/product.py b/src/sdc11073/roles/product.py index 7983eedd..bad672b5 100644 --- a/src/sdc11073/roles/product.py +++ b/src/sdc11073/roles/product.py @@ -34,7 +34,7 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ handle=operation_descriptor_container.Handle, operation_target_handle=operation_target_handle, coded_value=operation_descriptor_container.Type, - current_argument_handler=self._set_component_state) + current_request_handler=self._set_component_state) return operation elif operation_descriptor_container.NODETYPE == pm_names.ActivateOperationDescriptor: # on what can activate be called? @@ -48,16 +48,18 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ return self._mk_operation(op_cls, handle=operation_descriptor_container.Handle, operation_target_handle=operation_target_handle, - coded_value=operation_descriptor_container.Type) + coded_value=operation_descriptor_container.Type, + current_request_handler=self._do_nothing) return None - def _set_component_state(self, operation_instance, value): + def _set_component_state(self, operation_instance, soap_request, operation_request) -> list[str]: """ :param operation_instance: the operation :param value: a list of proposed metric states :return: """ + value = operation_request.argument # ToDo: consider ModifiableDate attribute operation_instance.current_value = value with self._mdib.transaction_manager() as mgr: @@ -71,6 +73,9 @@ def _set_component_state(self, operation_instance, value): self._logger.warn('_set_component_state operation: ignore invalid referenced type {} in operation', state.NODETYPE) + def _do_nothing(self, operation_instance, soap_request, operation_request) -> list[str]: + return [] + class BaseProduct: """A Product is associated to a single sco. If a mdib contains multiple sco instances, diff --git a/src/sdc11073/roles/providerbase.py b/src/sdc11073/roles/providerbase.py index f5a19e46..4dd6ca6f 100644 --- a/src/sdc11073/roles/providerbase.py +++ b/src/sdc11073/roles/providerbase.py @@ -36,7 +36,7 @@ def on_pre_commit(self, mdib, transaction): def on_post_commit(self, mdib, transaction): pass - def _set_numeric_value(self, operation_instance, value): + def _set_numeric_value(self, operation_instance, operation_request): """ sets a numerical metric value""" pm_types = self._mdib.data_model.pm_types operation_target_handle = self._get_operation_target_handle(operation_instance) @@ -56,7 +56,7 @@ def _set_numeric_value(self, operation_instance, value): pm_types.MetricCategory.PRESETTING): state.MetricValue.Validity = pm_types.MeasurementValidity.VALID - def _set_string(self, operation_instance, value): + def _set_string(self, operation_instance, operation_request): """ sets a string value""" pm_types = self._mdib.data_model.pm_types operation_target_handle = self._get_operation_target_handle(operation_instance) @@ -78,7 +78,7 @@ def _set_string(self, operation_instance, value): def _mk_operation_from_operation_descriptor(self, operation_descriptor_container, operation_cls_getter, - current_argument_handler=None, + # current_argument_handler=None, current_request_handler=None, timeout_handler=None): """ @@ -93,14 +93,14 @@ def _mk_operation_from_operation_descriptor(self, operation_descriptor_container operation_descriptor_container.Handle, operation_descriptor_container.OperationTarget, operation_descriptor_container.coding, - current_argument_handler, +# current_argument_handler, current_request_handler, timeout_handler) return operation def _mk_operation(self, cls, handle, operation_target_handle, # pylint: disable=no-self-use - coded_value, current_argument_handler=None, - current_request_handler=None, timeout_handler=None): + coded_value, #current_argument_handler=None, + current_request_handler, timeout_handler=None): """ :param cls: one of the Operations defined in provider.sco @@ -113,13 +113,14 @@ def _mk_operation(self, cls, handle, operation_target_handle, # pylint: disable= """ operation = cls(handle=handle, operation_target_handle=operation_target_handle, + operation_handler=current_request_handler, coded_value=coded_value) - if current_argument_handler: - # bind method to current_argument - properties.strongbind(operation, current_argument=partial(current_argument_handler, operation)) - if current_request_handler: - # bind method to current_request - properties.strongbind(operation, current_request=partial(current_request_handler, operation)) + # if current_argument_handler: + # # bind method to current_argument + # properties.strongbind(operation, current_argument=partial(current_argument_handler, operation)) + # if current_request_handler: + # # bind method to current_request + # properties.strongbind(operation, current_request=partial(current_request_handler, operation)) if timeout_handler: # bind method to current_request properties.strongbind(operation, on_timeout=partial(timeout_handler, operation)) diff --git a/tests/test_tutorial.py b/tests/test_tutorial.py index 549ce91b..7e6aa93f 100644 --- a/tests/test_tutorial.py +++ b/tests/test_tutorial.py @@ -91,23 +91,26 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ # This callback is called when a consumer calls the operation. operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_argument_handler=self._handle_operation_1) + current_request_handler=self._handle_operation_1) return operation if operation_descriptor_container.coding == MY_CODE_2.coding: operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_argument_handler=self._handle_operation_2) + current_request_handler=self._handle_operation_2) return operation return None - def _handle_operation_1(self, operation_instance, argument): + def _handle_operation_1(self, operation_instance, soap_request, operation_request): """This operation does not manipulate the mdib at all, it only registers the call.""" + argument = operation_request.argument self.operation1_called += 1 self.operation1_args = argument self._logger.info('_handle_operation_1 called arg={}', argument) + return [] - def _handle_operation_2(self, operation_instance, argument): + def _handle_operation_2(self, operation_instance, soap_request, operation_request): """This operation manipulate it operation target, and only registers the call.""" + argument = operation_request.argument self.operation2_called += 1 self.operation2_args = argument self._logger.info('_handle_operation_2 called arg={}', argument) @@ -116,6 +119,7 @@ def _handle_operation_2(self, operation_instance, argument): if my_state.MetricValue is None: my_state.mk_metric_value() my_state.MetricValue.Value = argument + return [] class MyProvider2(ProviderRole): @@ -135,14 +139,15 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ operation_descriptor_container.Handle)) operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_argument_handler=self._handle_operation_3) + current_request_handler=self._handle_operation_3) return operation else: return None - def _handle_operation_3(self, operation_instance, argument): + def _handle_operation_3(self, operation_instance, soap_request, operation_request): """This operation manipulate it operation target, and only registers the call.""" self.operation3_called += 1 + argument = operation_request.argument self.operation3_args = argument self._logger.info('_handle_operation_3 called') with self._mdib.transaction_manager() as mgr: @@ -150,7 +155,7 @@ def _handle_operation_3(self, operation_instance, argument): if my_state.MetricValue is None: my_state.mk_metric_value() my_state.MetricValue.Value = argument - + return [] class MyProductImpl(BaseProduct): """This class provides all handlers of the fictional product. From 3ba31a316230ddb4fd38cb97b26ff154c1533a6e Mon Sep 17 00:00:00 2001 From: Deichmann Date: Thu, 21 Sep 2023 12:15:15 +0200 Subject: [PATCH 02/18] reworked operations handler interface --- src/sdc11073/consumer/operations.py | 143 ++++++++---- src/sdc11073/mdib/descriptorcontainers.py | 17 +- src/sdc11073/mdib/statecontainers.py | 7 + src/sdc11073/provider/operations.py | 115 ++++++---- .../provider/porttypes/porttypebase.py | 95 ++++---- .../provider/porttypes/setserviceimpl.py | 57 +++-- src/sdc11073/provider/providerimpl.py | 18 +- src/sdc11073/provider/sco.py | 173 +++++++------- src/sdc11073/roles/alarmprovider.py | 203 ++++++++++------- src/sdc11073/roles/audiopauseprovider.py | 177 +++++++++------ src/sdc11073/roles/clockprovider.py | 160 +++++++------ src/sdc11073/roles/componentprovider.py | 81 +++++++ src/sdc11073/roles/contextprovider.py | 76 +++++-- src/sdc11073/roles/metricprovider.py | 130 ++++++----- src/sdc11073/roles/nomenclature.py | 18 +- src/sdc11073/roles/operationprovider.py | 6 +- src/sdc11073/roles/patientcontextprovider.py | 52 +++-- src/sdc11073/roles/product.py | 214 ++++++++---------- src/sdc11073/roles/providerbase.py | 191 ++++++++-------- src/sdc11073/xml_types/msg_types.py | 4 + tests/70041_MDIB_Final.xml | 4 +- tests/70041_MDIB_multi.xml | 4 +- tests/mdib_tns.xml | 4 +- tests/mdib_two_mds.xml | 4 +- tests/test_operations.py | 17 +- tests/test_tutorial.py | 52 +++-- 26 files changed, 1200 insertions(+), 822 deletions(-) create mode 100644 src/sdc11073/roles/componentprovider.py diff --git a/src/sdc11073/consumer/operations.py b/src/sdc11073/consumer/operations.py index ea87d504..aad6a2ee 100644 --- a/src/sdc11073/consumer/operations.py +++ b/src/sdc11073/consumer/operations.py @@ -1,7 +1,9 @@ from __future__ import annotations import weakref +from collections import deque from concurrent.futures import Future +from dataclasses import dataclass from threading import Lock from typing import TYPE_CHECKING, Protocol @@ -10,14 +12,15 @@ if TYPE_CHECKING: from sdc11073.consumer.manipulator import RequestManipulatorProtocol from sdc11073.consumer.serviceclients.serviceclientbase import HostedServiceClient - from sdc11073.pysoap.msgreader import MessageReader, ReceivedMessage from sdc11073.pysoap.msgfactory import CreatedMessage + from sdc11073.pysoap.msgreader import MessageReader, ReceivedMessage + from sdc11073.xml_types import msg_types, pm_types class OperationsManagerProtocol(Protocol): """OperationsManager calls an operation. - It returns a Future object that will contain the result at some point in time. - """ + It returns a Future object that will contain the result at some point in time. + """ def __init__(self, msg_reader: MessageReader, log_prefix: str): """Construct the OperationsManager.""" @@ -38,6 +41,33 @@ def on_operation_invoked_report(self, message_data: ReceivedMessage): """Check operation state and set future result if it is a final state.""" +@dataclass +class OperationResult: + """OperationResult is the result of a Set operation. + + Usually only the result is relevant, but for testing all intermediate data is also available. + """ + + InvocationInfo: msg_types.InvocationInfo + InvocationSource: pm_types.InstanceIdentifier | None + OperationHandleRef: str | None + OperationTarget: str | None + + set_response: msg_types.AbstractSetResponse + report_parts: list[ + msg_types.OperationInvokedReportPart] # contains data of all OperationInvokedReportPart for operation + + +@dataclass +class OperationData: + """collect all progress data of a transaction.""" + + future_ref: weakref.ref[Future] + set_response: msg_types.AbstractSetResponse + report_parts: list[ + msg_types.OperationInvokedReportPart] # contains data of all OperationInvokedReportPart for operation + + class OperationsManager(OperationsManagerProtocol): # inheriting from protocol to help typing """OperationsManager handles the multiple messages that are related to an operation. @@ -45,19 +75,25 @@ class OperationsManager(OperationsManagerProtocol): # inheriting from protocol """ def __init__(self, msg_reader: MessageReader, log_prefix: str): + super().__init__(msg_reader, log_prefix) self._msg_reader = msg_reader self.log_prefix = log_prefix self._logger = loghelper.get_logger_adapter('sdc.client.op_mgr', log_prefix) - self._transactions = {} + self._transactions: dict[int, OperationData] = {} self._transactions_lock = Lock() + # An OperationInvokedReport can be received even before the response of the set operation is received. + # This means we must always store the last n OperationInvokedReportParts, one of them might already be the + # needed one. + self._last_operation_invoked_reports: deque[msg_types.OperationInvokedReportPart] = deque(maxlen=50) msg_types = msg_reader.msg_types self.nonFinalOperationStates = (msg_types.InvocationState.WAIT, msg_types.InvocationState.START) - def call_operation(self, hosted_service_client: HostedServiceClient, + def call_operation(self, + hosted_service_client: HostedServiceClient, message: CreatedMessage, request_manipulator: RequestManipulatorProtocol | None = None) -> Future: """Call an operation.""" - ret = Future() + future_object = Future() with self._transactions_lock: message_data = hosted_service_client.post_message(message, msg='call Operation', @@ -65,45 +101,72 @@ def call_operation(self, hosted_service_client: HostedServiceClient, msg_types = self._msg_reader.msg_types abstract_set_response = msg_types.AbstractSetResponse.from_node(message_data.p_msg.msg_node) invocation_info = abstract_set_response.InvocationInfo - if invocation_info.InvocationState in self.nonFinalOperationStates: - self._transactions[invocation_info.TransactionId] = weakref.ref(ret) + if invocation_info.InvocationState in (msg_types.InvocationState.FAILED, + msg_types.InvocationState.CANCELLED, + msg_types.InvocationState.CANCELLED_MANUALLY): + # do not wait for an OperationInvokedReport + operation_result = OperationResult(abstract_set_response.InvocationInfo, + None, + None, + None, + abstract_set_response, + []) + future_object.set_result(operation_result) + return future_object + transaction_id = invocation_info.TransactionId + # now look for all related report parts and add them to result + parts = [part for part in self._last_operation_invoked_reports if + part.InvocationInfo.TransactionId == transaction_id] + # now look for a final report parts + final_parts = [part for part in parts if + part.InvocationInfo.InvocationState not in self.nonFinalOperationStates] + if final_parts: + report_part = final_parts[0] # assuming there is only one + future_object.set_result(self._mk_operation_result(report_part, abstract_set_response, parts)) + else: self._logger.info('call_operation: transaction_id {} registered, state={}', # noqa: PLE1205 invocation_info.TransactionId, invocation_info.InvocationState) - else: - self._logger.debug('Result of Operation: {}', invocation_info) # noqa: PLE1205 - ret.set_result(abstract_set_response) - return ret + self._transactions[transaction_id] = OperationData(weakref.ref(future_object), + abstract_set_response, + parts) + return future_object def on_operation_invoked_report(self, message_data: ReceivedMessage): """Check operation state and set future result if it is a final state.""" msg_types = self._msg_reader.msg_types operation_invoked_report = msg_types.OperationInvokedReport.from_node(message_data.p_msg.msg_node) - for report_part in operation_invoked_report.ReportPart: - invocation_state = report_part.InvocationInfo.InvocationState - transaction_id = report_part.InvocationInfo.TransactionId - self._logger.debug( # noqa: PLE1205 - '{}on_operation_invoked_report: got transaction_id {} state {}', - self.log_prefix, transaction_id, invocation_state) - if invocation_state in self.nonFinalOperationStates: - self._logger.debug('nonFinal state detected, ignoring message...') - continue - with self._transactions_lock: - future_ref = self._transactions.pop(transaction_id, None) - if future_ref is None: - # this was not my transaction - self._logger.debug('transaction_id {} is not registered!', transaction_id) # noqa: PLE1205 - continue - future_obj = future_ref() - if future_obj is None: - # client gave up. - self._logger.debug('transaction_id {} given up', transaction_id) # noqa: PLE1205 - continue - if invocation_state == msg_types.InvocationState.FAILED: - error_text = ', '.join([err.text for err in report_part.InvocationInfo.InvocationErrorMessage]) - self._logger.warning( # noqa: PLE1205 - 'transaction Id {} finished with error: error={}, error-message={}', - transaction_id, report_part.InvocationInfo.InvocationError, error_text) - else: - self._logger.info('transaction Id {} ok', transaction_id) # noqa: PLE1205 - future_obj.set_result(report_part) + with self._transactions_lock: + for report_part in operation_invoked_report.ReportPart: + invocation_state = report_part.InvocationInfo.InvocationState + transaction_id = report_part.InvocationInfo.TransactionId + self._logger.debug( # noqa: PLE1205 + '{}on_operation_invoked_report: got transaction_id {} state {}', + self.log_prefix, transaction_id, invocation_state) + if transaction_id in self._transactions: + self._transactions[transaction_id].report_parts.append(report_part) + if invocation_state in self.nonFinalOperationStates: + pass + else: + operation_data = self._transactions.pop(transaction_id, None) + future_object = operation_data.future_ref() + if future_object is None: + # client gave up. + self._logger.debug('transaction_id {} given up', transaction_id) # noqa: PLE1205 + else: + future_object.set_result(self._mk_operation_result(report_part, + operation_data.set_response, + operation_data.report_parts)) + else: + self._last_operation_invoked_reports.append(report_part) + + def _mk_operation_result(self, + current_report_part: msg_types.OperationInvokedReportPart, + set_response: msg_types.AbstractSetResponse, + all_report_parts: list[msg_types.OperationInvokedReportPart]) -> OperationResult: + return OperationResult(current_report_part.InvocationInfo, + current_report_part.InvocationSource, + current_report_part.OperationHandleRef, + current_report_part.OperationTarget, + set_response, + all_report_parts) diff --git a/src/sdc11073/mdib/descriptorcontainers.py b/src/sdc11073/mdib/descriptorcontainers.py index a26a719b..aec29ced 100644 --- a/src/sdc11073/mdib/descriptorcontainers.py +++ b/src/sdc11073/mdib/descriptorcontainers.py @@ -3,7 +3,7 @@ import inspect import sys from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Protocol, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, Protocol from sdc11073 import observableproperties as properties from sdc11073.xml_types import ext_qnames as ext @@ -18,8 +18,8 @@ from decimal import Decimal from lxml import etree as etree_ - from sdc11073 import xml_utils + from sdc11073 import xml_utils from sdc11073.namespaces import NamespaceHelper from sdc11073.xml_types.isoduration import DurationType from sdc11073.xml_types.xml_structure import ExtensionLocalValue @@ -78,6 +78,7 @@ class AbstractDescriptorProtocol(Protocol): Type: pm_types.CodedValue | None source_mds: str parent_handle: str | None + coding: pm_types.Coding | None def __init__(self, handle: str, parent_handle: str | None): ... @@ -525,6 +526,15 @@ class AbstractOperationDescriptorContainer(AbstractDescriptorContainer): _props = ('OperationTarget', 'MaxTimeToFinish', 'InvocationEffectiveTimeout', 'Retriggerable', 'AccessLevel') +class AbstractOperationDescriptorProtocol(AbstractDescriptorProtocol): + """Protocol definition for AbstractOperationDescriptorContainer.""" + OperationTarget: str + MaxTimeToFinish: DurationType | None + InvocationEffectiveTimeout: DurationType | None + Retriggerable: bool + AccessLevel: pm_types.AccessLevel + + class SetValueOperationDescriptorContainer(AbstractOperationDescriptorContainer): """Represents SetValueOperationDescriptor in BICEPS.""" @@ -587,9 +597,6 @@ class ActivateOperationDescriptorContainer(AbstractSetStateOperationDescriptorCo _props = ('Argument',) _child_elements_order = (pm_qnames.Argument,) - def __init__(self, *args, **kwargs): - return super().__init__(*args, **kwargs) - class AbstractAlertDescriptorContainer(AbstractDescriptorContainer): """Represents AbstractAlertDescriptor in BICEPS.""" diff --git a/src/sdc11073/mdib/statecontainers.py b/src/sdc11073/mdib/statecontainers.py index f0a06b28..a655d502 100644 --- a/src/sdc11073/mdib/statecontainers.py +++ b/src/sdc11073/mdib/statecontainers.py @@ -217,6 +217,13 @@ class AbstractMetricStateContainer(AbstractStateContainer): _props = ('BodySite', 'PhysicalConnector', 'ActivationState', 'ActiveDeterminationPeriod', 'LifeTimePeriod') +class MetricStateProtocol(AbstractStateProtocol): + MetricValue: None | pm_types.NumericMetricValue | pm_types.StringMetricValue | pm_types.SampleArrayValue + + def mk_metric_value(self) -> pm_types.NumericMetricValue | pm_types.StringMetricValue | pm_types.SampleArrayValue: + pass + + class NumericMetricStateContainer(AbstractMetricStateContainer): """Represents NumericMetricState in BICEPS.""" diff --git a/src/sdc11073/provider/operations.py b/src/sdc11073/provider/operations.py index f7b21f4d..8aba03cb 100644 --- a/src/sdc11073/provider/operations.py +++ b/src/sdc11073/provider/operations.py @@ -3,7 +3,8 @@ import inspect import sys import time -from typing import TYPE_CHECKING, Callable, Protocol +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Callable, Protocol from sdc11073 import loghelper from sdc11073 import observableproperties as properties @@ -11,17 +12,44 @@ from sdc11073.xml_types import msg_qnames as msg from sdc11073.xml_types import pm_qnames as pm -class OperationDefinitionProtocol(Protocol): - pass - if TYPE_CHECKING: + from lxml.etree import QName from sdc11073.mdib.descriptorcontainers import AbstractDescriptorProtocol from sdc11073.mdib.providermdib import ProviderMdib - from sdc11073.multikey import MultiKeyLookup from sdc11073.pysoap.soapenvelope import ReceivedSoapMessage - from sdc11073.xml_types.msg_types import AbstractSet + from sdc11073.xml_types.msg_types import AbstractSet, InvocationState from sdc11073.xml_types.pm_types import CodedValue, OperatingMode - ExecuteHandler = Callable[[OperationDefinitionProtocol, ReceivedSoapMessage, AbstractSet], list[str]] + + +class OperationDefinitionProtocol(Protocol): + """Interface that ExecuteHandlers need.""" + + handle: str + operation_target_handle: str + current_value: Any + + +@dataclass +class ExecuteParameters: + """The argument of the ExecuteHandler call.""" + + operation_instance: OperationDefinitionProtocol + operation_request: AbstractSet + soap_message: ReceivedSoapMessage + + +@dataclass +class ExecuteResult: + """The return value of the ExecuteHandler call.""" + + operation_target_handle: str + invocation_state: InvocationState # = InvocationState.FINISHED # only return a final state, not WAIT or STARTED + + +# ExecuteHandler also get the full soap message as parameter, because the soap header might contain +# relevant information, e.g. safety requirements. +ExecuteHandler = Callable[[ExecuteParameters], ExecuteResult] +TimeoutHandler = Callable[[OperationDefinitionProtocol], None] class OperationDefinitionBase: @@ -30,7 +58,7 @@ class OperationDefinitionBase: An operation is a point for remote control over the network. """ - current_value = properties.ObservableProperty(fire_only_on_changed_value=False) + current_value: Any = properties.ObservableProperty(fire_only_on_changed_value=False) current_request = properties.ObservableProperty(fire_only_on_changed_value=False) current_argument = properties.ObservableProperty(fire_only_on_changed_value=False) on_timeout = properties.ObservableProperty(fire_only_on_changed_value=False) @@ -38,9 +66,11 @@ class OperationDefinitionBase: OP_STATE_QNAME = None OP_QNAME = None - def __init__(self, handle: str, + def __init__(self, # noqa: PLR0913 + handle: str, operation_target_handle: str, operation_handler: ExecuteHandler, + timeout_handler: TimeoutHandler | None = None, coded_value: CodedValue | None = None, delayed_processing: bool = True, log_prefix: str | None = None): @@ -56,47 +86,36 @@ def __init__(self, handle: str, self._mdib = None self._descriptor_container = None self._operation_state_container = None - self._handle = handle - self._operation_target_handle = operation_target_handle + self.handle: str = handle + self.operation_target_handle: str = operation_target_handle # documentation of operation_target_handle: # A HANDLE reference this operation is targeted to. In case of a single state this is the HANDLE of the descriptor. # In case that multiple states may belong to one descriptor (pm:AbstractMultiState), OperationTarget is the HANDLE # of one of the state instances (if the state is modified by the operation). self._operation_handler = operation_handler + self._timeout_handler = timeout_handler self._coded_value = coded_value self.delayed_processing = delayed_processing self.calls = [] # record when operation was called self.last_called_time = None @property - def handle(self) -> str: - return self._handle - - @property - def operation_target_handle(self) -> str: - return self._operation_target_handle - - # @property - # def operation_target_storage(self) -> MultiKeyLookup: - # return self._mdib.states - - @property - def descriptor_container(self) -> AbstractDescriptorProtocol: + def descriptor_container(self) -> AbstractDescriptorProtocol: # noqa: D102 return self._descriptor_container def execute_operation(self, - request: ReceivedSoapMessage, - operation_request: AbstractSet) -> list[str]: + soap_request: ReceivedSoapMessage, + operation_request: AbstractSet) -> ExecuteResult: """Execute the operation itself. This method calls the provided operation_handler. """ - self.calls.append((time.time(), request)) - operation_targets = self._operation_handler(self, request, operation_request) - self.current_request = request + self.calls.append((time.time(), soap_request)) + execute_result = self._operation_handler(ExecuteParameters(self, operation_request, soap_request)) + self.current_request = soap_request self.current_argument = operation_request.argument self.last_called_time = time.time() - return operation_targets + return execute_result def check_timeout(self): """Set on_timeout observable if timeout is detected.""" @@ -107,6 +126,8 @@ def check_timeout(self): age = time.time() - self.last_called_time if age < self._descriptor_container.InvocationEffectiveTimeout: return + if self._timeout_handler is not None: + self._timeout_handler(self) self.on_timeout = True # let observable fire def set_mdib(self, mdib: ProviderMdib, parent_descriptor_handle: str): @@ -120,34 +141,34 @@ def set_mdib(self, mdib: ProviderMdib, parent_descriptor_handle: str): raise ApiUsageError('Mdib is already set') self._mdib = mdib self._logger.log_prefix = mdib.log_prefix # use same prefix as mdib for logging - self._descriptor_container = self._mdib.descriptions.handle.get_one(self._handle, allow_none=True) + self._descriptor_container = self._mdib.descriptions.handle.get_one(self.handle, allow_none=True) if self._descriptor_container is not None: # there is already a descriptor - self._logger.debug('descriptor for operation "{}" is already present, re-using it', self._handle) + self._logger.debug('descriptor for operation "%s" is already present, re-using it', self.handle) else: cls = mdib.data_model.get_descriptor_container_class(self.OP_DESCR_QNAME) - self._descriptor_container = cls(self._handle, parent_descriptor_handle) + self._descriptor_container = cls(self.handle, parent_descriptor_handle) self._init_operation_descriptor_container() # ToDo: transaction context for flexibility to add operations at runtime mdib.descriptions.add_object(self._descriptor_container) - self._operation_state_container = self._mdib.states.descriptor_handle.get_one(self._handle, allow_none=True) + self._operation_state_container = self._mdib.states.descriptor_handle.get_one(self.handle, allow_none=True) if self._operation_state_container is not None: - self._logger.debug('operation state for operation "{}" is already present, re-using it', self._handle) + self._logger.debug('operation state for operation "%s" is already present, re-using it', self.handle) else: cls = mdib.data_model.get_state_container_class(self.OP_STATE_QNAME) self._operation_state_container = cls(self._descriptor_container) mdib.states.add_object(self._operation_state_container) def _init_operation_descriptor_container(self): - self._descriptor_container.OperationTarget = self._operation_target_handle + self._descriptor_container.OperationTarget = self.operation_target_handle if self._coded_value is not None: self._descriptor_container.Type = self._coded_value def set_operating_mode(self, mode: OperatingMode): """Set OperatingMode member in state in transaction context.""" with self._mdib.transaction_manager() as mgr: - state = mgr.get_state(self._handle) + state = mgr.get_state(self.handle) state.OperatingMode = mode def collect_values(self, number_of_values: int | None = None) \ @@ -163,7 +184,7 @@ def collect_values(self, number_of_values: int | None = None) \ def __str__(self): code = '?' if self._descriptor_container is None else self._descriptor_container.Type - return f'{self.__class__.__name__} handle={self._handle} code={code} operation-target={self._operation_target_handle}' + return f'{self.__class__.__name__} handle={self.handle} code={code} operation-target={self.operation_target_handle}' class SetStringOperation(OperationDefinitionBase): @@ -189,13 +210,9 @@ class SetContextStateOperation(OperationDefinitionBase): OP_STATE_QNAME = pm.SetContextStateOperationState OP_QNAME = msg.SetContextState - # @property - # def operation_target_storage(self): - # return self._mdib.context_states - class ActivateOperation(OperationDefinitionBase): - """ This default implementation only registers calls, no manipulation of operation target.""" + """Parameters of an ActivateOperation.""" OP_DESCR_QNAME = pm.ActivateOperationDescriptor OP_STATE_QNAME = pm.ActivateOperationState @@ -203,7 +220,7 @@ class ActivateOperation(OperationDefinitionBase): class SetAlertStateOperation(OperationDefinitionBase): - """ This default implementation only registers calls, no manipulation of operation target.""" + """Parameters of an SetAlertStateOperation.""" OP_DESCR_QNAME = pm.SetAlertStateOperationDescriptor OP_STATE_QNAME = pm.SetAlertStateOperationState @@ -211,7 +228,7 @@ class SetAlertStateOperation(OperationDefinitionBase): class SetComponentStateOperation(OperationDefinitionBase): - """ This default implementation only registers calls, no manipulation of operation target.""" + """Parameters of an SetComponentStateOperation.""" OP_DESCR_QNAME = pm.SetComponentStateOperationDescriptor OP_STATE_QNAME = pm.SetComponentStateOperationState @@ -219,7 +236,7 @@ class SetComponentStateOperation(OperationDefinitionBase): class SetMetricStateOperation(OperationDefinitionBase): - """This default implementation only registers calls, no manipulation of operation target.""" + """Parameters of an SetMetricStateOperation.""" OP_DESCR_QNAME = pm.SetMetricStateOperationDescriptor OP_STATE_QNAME = pm.SetMetricStateOperationState @@ -230,11 +247,11 @@ class SetMetricStateOperation(OperationDefinitionBase): # find all classes in this module that have a member "OP_DESCR_QNAME" _classes = inspect.getmembers(sys.modules[__name__], lambda member: inspect.isclass(member) and member.__module__ == __name__) -_classes_with_QNAME = [c[1] for c in _classes if hasattr(c[1], 'OP_DESCR_QNAME') and c[1].OP_DESCR_QNAME is not None] +_classes_with_qname = [c[1] for c in _classes if hasattr(c[1], 'OP_DESCR_QNAME') and c[1].OP_DESCR_QNAME is not None] # make a dictionary from found classes: (Key is OP_DESCR_QNAME, value is the class itself -_operation_lookup_by_type = {c.OP_DESCR_QNAME: c for c in _classes_with_QNAME} +_operation_lookup_by_type = {c.OP_DESCR_QNAME: c for c in _classes_with_qname} -def get_operation_class(q_name): +def get_operation_class(q_name: QName) -> type[OperationDefinitionBase]: """:param q_name: a QName instance""" return _operation_lookup_by_type.get(q_name) diff --git a/src/sdc11073/provider/porttypes/porttypebase.py b/src/sdc11073/provider/porttypes/porttypebase.py index d11288ec..54bcc336 100644 --- a/src/sdc11073/provider/porttypes/porttypebase.py +++ b/src/sdc11073/provider/porttypes/porttypebase.py @@ -1,17 +1,19 @@ from __future__ import annotations from collections import namedtuple -from typing import TYPE_CHECKING, Optional, ClassVar +from typing import TYPE_CHECKING, ClassVar from lxml import etree as etree_ -from ... import loghelper -from ...namespaces import PrefixesEnum +from sdc11073 import loghelper +from sdc11073.namespaces import PrefixesEnum if TYPE_CHECKING: - from ...pysoap.msgfactory import CreatedMessage - from ...namespaces import PrefixNamespace from sdc11073 import xml_utils + from sdc11073.dispatch.request import RequestData + from sdc11073.namespaces import PrefixNamespace + from sdc11073.pysoap.msgfactory import CreatedMessage + from sdc11073.xml_types.msg_types import AbstractSet, AbstractSetResponse msg_prefix = PrefixesEnum.MSG.prefix @@ -30,18 +32,21 @@ class DPWSPortTypeBase: - """ Base class of all PortType implementations. Its responsibilities are: - - handling of messages - - creation of wsdl information. - Handlers are registered in the hosting service instance. """ - port_type_name: Optional[etree_.QName] = None + """Base class of all PortType implementations. + + Its responsibilities are: + - handling of messages + - creation of wsdl information. + Handlers are registered in the hosting service instance. + """ + + port_type_name: etree_.QName | None = None WSDLOperationBindings = () # overwrite in derived classes WSDLMessageDescriptions = () # overwrite in derived classes additional_namespaces: ClassVar[list[PrefixNamespace]] = [] # for special namespaces - def __init__(self, sdc_device, log_prefix: Optional[str] = None): - """ - :param sdc_device: the sdc device + def __init__(self, sdc_device, log_prefix: str | None = None): + """:param sdc_device: the sdc device :param log_prefix: optional string """ self._sdc_device = sdc_device @@ -55,7 +60,7 @@ def __init__(self, sdc_device, log_prefix: Optional[str] = None): self.offered_subscriptions = self._mk_offered_subscriptions() def register_hosting_service(self, dpws_hosted_service): - """Register callbacks in hosting_service""" + """Register callbacks in hosting_service.""" self.hosting_service = dpws_hosted_service @property @@ -65,11 +70,12 @@ def actions(self): # just a shortcut def add_wsdl_port_type(self, parent_node): raise NotImplementedError - def _mk_port_type_node(self, parent_node: xml_utils.LxmlElement, is_event_source: bool =False) -> xml_utils.LxmlElement: - """ Needed for wsdl message + def _mk_port_type_node(self, parent_node: xml_utils.LxmlElement, + is_event_source: bool = False) -> xml_utils.LxmlElement: + """Needed for wsdl message :param parent_node: where to add data :param is_event_source: true if port type provides notification - :return: the new created node (is already child of parent_node) + :return: the new created node (is already child of parent_node). """ if self.port_type_name is None: raise ValueError('self.port_type_name is not set, cannot create port type node') @@ -86,11 +92,10 @@ def _mk_port_type_node(self, parent_node: xml_utils.LxmlElement, is_event_source return port_type def __repr__(self): - return f'{self.__class__.__name__} Porttype={str(self.port_type_name)}' + return f'{self.__class__.__name__} Porttype={self.port_type_name!s}' def add_wsdl_messages(self, parent_node): - """ - add wsdl:message node to parent_node. + """Add wsdl:message node to parent_node. xml looks like this: @@ -106,8 +111,7 @@ def add_wsdl_messages(self, parent_node): 'element': element_name}) def add_wsdl_binding(self, parent_node, porttype_prefix): - """ - add wsdl:binding node to parent_node. + """Add wsdl:binding node to parent_node. xml looks like this: @@ -147,8 +151,9 @@ def add_wsdl_binding(self, parent_node, porttype_prefix): etree_.SubElement(wsdl_output, etree_.QName(WSDL_S12, 'body'), attrib={'use': wsdl_op.output}) def _mk_offered_subscriptions(self) -> list: - """ Takes action strings from sdc_definitions.Actions. - The name of the WSDLOperationBinding is used to reference the action string.""" + """Takes action strings from sdc_definitions.Actions. + The name of the WSDLOperationBinding is used to reference the action string. + """ actions = self._sdc_device.mdib.sdc_definitions.Actions offered_subscriptions = [] for bdg in self.WSDLOperationBindings: @@ -160,29 +165,28 @@ def _mk_offered_subscriptions(self) -> list: class ServiceWithOperations(DPWSPortTypeBase): - def _handle_operation_request(self, request_data, request, set_response) -> CreatedMessage: - """ - - :param request_data: - :param request: AbstractSet - :param set_response: AbstractSetResponse - :return: CreatedMessage - """ + def _handle_operation_request(self, request_data: RequestData, + request: AbstractSet, + set_response: AbstractSetResponse) -> CreatedMessage: + """Handle thew operation request by forwarding it to provider.""" data_model = self._sdc_definitions.data_model operation = self._sdc_device.get_operation_by_handle(request.OperationHandleRef) - transaction_id = self._sdc_device.generate_transaction_id() + transaction_id = self._sdc_device.generate_transaction_id() set_response.InvocationInfo.TransactionId = transaction_id if operation is None: error_text = f'no handler registered for "{request.OperationHandleRef}"' - self._logger.warn('handle operation request: {}', error_text) + self._logger.warning('handle operation request: {}', error_text) set_response.InvocationInfo.InvocationState = data_model.msg_types.InvocationState.FAILED set_response.InvocationInfo.InvocationError = data_model.msg_types.InvocationError.INVALID_VALUE set_response.InvocationInfo.add_error_message(error_text) else: - self._sdc_device.enqueue_operation(operation, request_data.message_data.p_msg, request, transaction_id) - self._logger.info('operation request "{}" enqueued, transaction id = {}', - request.OperationHandleRef, set_response.InvocationInfo.TransactionId) - set_response.InvocationInfo.InvocationState = data_model.msg_types.InvocationState.WAIT + invocation_state = self._sdc_device.handle_operation_request(operation, + request_data.message_data.p_msg, + request, + transaction_id) + self._logger.info('operation request "{}" handled, transaction id = {}, invocation-state={}', + request.OperationHandleRef, set_response.InvocationInfo.TransactionId, invocation_state) + set_response.InvocationInfo.InvocationState = invocation_state set_response.MdibVersion = self._mdib.mdib_version set_response.SequenceId = self._mdib.sequence_id @@ -205,10 +209,9 @@ def _mk_wsdl_operation(parent_node, operation_name, input_message_name, output_m def mk_wsdl_two_way_operation(parent_node: xml_utils.LxmlElement, operation_name: str, - input_message_name: Optional[str] = None, - output_message_name: Optional[str] = None) -> xml_utils.LxmlElement: - """ - A helper for wsdl generation. A two-way-operation defines a 'normal' request and response operation. + input_message_name: str | None = None, + output_message_name: str | None = None) -> xml_utils.LxmlElement: + """A helper for wsdl generation. A two-way-operation defines a 'normal' request and response operation. :param parent_node: info shall be added to this node :param operation_name: a string :param input_message_name: only needed if message name is not equal to operation_name @@ -225,9 +228,8 @@ def mk_wsdl_two_way_operation(parent_node: xml_utils.LxmlElement, def mk_wsdl_one_way_operation(parent_node: xml_utils.LxmlElement, operation_name: str, - output_message_name: Optional[str] = None) -> xml_utils.LxmlElement: - """ - A helper for wsdl generation. A one-way-operation is a subscription. + output_message_name: str | None = None) -> xml_utils.LxmlElement: + """A helper for wsdl generation. A one-way-operation is a subscription. :param parent_node: info shall be added to this node :param operation_name: a string :param output_message_name: only needed if message name is not equal to operation_name @@ -241,8 +243,7 @@ def mk_wsdl_one_way_operation(parent_node: xml_utils.LxmlElement, def _add_policy_dpws_profile(parent_node): - """ - :param parent_node: + """:param parent_node: :return: diff --git a/src/sdc11073/provider/porttypes/setserviceimpl.py b/src/sdc11073/provider/porttypes/setserviceimpl.py index 4c2f0034..081a7f4b 100644 --- a/src/sdc11073/provider/porttypes/setserviceimpl.py +++ b/src/sdc11073/provider/porttypes/setserviceimpl.py @@ -1,26 +1,35 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, List, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Protocol, runtime_checkable -from .porttypebase import ServiceWithOperations, WSDLMessageDescription, WSDLOperationBinding -from .porttypebase import mk_wsdl_two_way_operation, mk_wsdl_one_way_operation, msg_prefix -from ...dispatch import DispatchKey -from ...namespaces import PrefixesEnum +from sdc11073.dispatch import DispatchKey +from sdc11073.namespaces import PrefixesEnum + +from .porttypebase import ( + ServiceWithOperations, + WSDLMessageDescription, + WSDLOperationBinding, + mk_wsdl_one_way_operation, + mk_wsdl_two_way_operation, + msg_prefix, +) if TYPE_CHECKING: - from ..sco import OperationDefinition from enum import Enum + from sdc11073.provider.sco import OperationDefinition + @runtime_checkable class SetServiceProtocol(Protocol): def notify_operation(self, operation: OperationDefinition, transaction_id: int, - invocation_state, + invocation_state: Enum, mdib_version_group, - error: Optional[Enum] = None, - error_message: Optional[str] = None): + operation_target: str | None = None, + error: Enum | None = None, + error_message: str | None = None): ... @@ -81,7 +90,8 @@ def register_hosting_service(self, hosting_service): def _on_activate(self, request_data): # pylint:disable=unused-argument """Handler for Active calls. - It enqueues an operation and generates the expected operation invoked report. """ + It enqueues an operation and generates the expected operation invoked report. + """ data_model = self._sdc_definitions.data_model msg_node = request_data.message_data.p_msg.msg_node activate = data_model.msg_types.Activate.from_node(msg_node) @@ -91,7 +101,8 @@ def _on_activate(self, request_data): # pylint:disable=unused-argument def _on_set_value(self, request_data): # pylint:disable=unused-argument """Handler for SetValue calls. - It enqueues an operation and generates the expected operation invoked report. """ + It enqueues an operation and generates the expected operation invoked report. + """ data_model = self._sdc_definitions.data_model self._logger.debug('_on_set_value') msg_node = request_data.message_data.p_msg.msg_node @@ -101,7 +112,8 @@ def _on_set_value(self, request_data): # pylint:disable=unused-argument def _on_set_string(self, request_data): # pylint:disable=unused-argument """Handler for SetString calls. - It enqueues an operation and generates the expected operation invoked report.""" + It enqueues an operation and generates the expected operation invoked report. + """ data_model = self._sdc_definitions.data_model self._logger.debug('_on_set_string') msg_node = request_data.message_data.p_msg.msg_node @@ -111,7 +123,8 @@ def _on_set_string(self, request_data): # pylint:disable=unused-argument def _on_set_metric_state(self, request_data): # pylint:disable=unused-argument """Handler for SetMetricState calls. - It enqueues an operation and generates the expected operation invoked report.""" + It enqueues an operation and generates the expected operation invoked report. + """ data_model = self._sdc_definitions.data_model self._logger.debug('_on_set_metric_state') msg_node = request_data.message_data.p_msg.msg_node @@ -121,7 +134,8 @@ def _on_set_metric_state(self, request_data): # pylint:disable=unused-argument def _on_set_alert_state(self, request_data): # pylint:disable=unused-argument """Handler for SetMetricState calls. - It enqueues an operation and generates the expected operation invoked report.""" + It enqueues an operation and generates the expected operation invoked report. + """ data_model = self._sdc_definitions.data_model self._logger.debug('_on_set_alert_state') msg_node = request_data.message_data.p_msg.msg_node @@ -131,7 +145,8 @@ def _on_set_alert_state(self, request_data): # pylint:disable=unused-argument def _on_set_component_state(self, request_data): # pylint:disable=unused-argument """Handler for SetComponentState calls. - It enqueues an operation and generates the expected operation invoked report.""" + It enqueues an operation and generates the expected operation invoked report. + """ data_model = self._sdc_definitions.data_model self._logger.debug('_on_set_component_state') msg_node = request_data.message_data.p_msg.msg_node @@ -142,10 +157,11 @@ def _on_set_component_state(self, request_data): # pylint:disable=unused-argume def notify_operation(self, operation: OperationDefinition, transaction_id: int, - invocation_state, + invocation_state: Enum, mdib_version_group, - error: Optional[Enum] = None, - error_message: Optional[str] = None): + operation_target: str | None = None, + error: Enum | None = None, + error_message: str | None = None): data_model = self._sdc_definitions.data_model nsh = data_model.ns_helper operation_handle_ref = operation.handle @@ -160,14 +176,13 @@ def notify_operation(self, if error_message is not None: report_part.InvocationErrorMessage = data_model.pm_types.LocalizedText(error_message) # implemented is only SDC R0077 for value of invocationSource: - # Root = "http://standards.ieee.org/downloads/11073/11073-20701-2018" # Extension = "AnonymousSdcParticipant". # a known participant (R0078) is currently not supported # ToDo: implement R0078 report_part.InvocationSource = data_model.pm_types.InstanceIdentifier( nsh.SDC.namespace, extension_string='AnonymousSdcParticipant') report_part.OperationHandleRef = operation_handle_ref - report_part.OperationTarget = None # not set in current implementation + report_part.OperationTarget = operation_target ns_map = nsh.partial_map(nsh.PM, nsh.MSG, nsh.XSI, nsh.EXT, nsh.XML) body_node = report.as_etree_node(report.NODETYPE, ns_map) self._logger.info( @@ -175,7 +190,7 @@ def notify_operation(self, transaction_id, operation_handle_ref, invocation_state, error, error_message) subscription_mgr.send_to_subscribers(body_node, report.action, mdib_version_group, 'notify_operation') - def handled_actions(self) -> List[str]: + def handled_actions(self) -> list[str]: return [self._sdc_device.sdc_definitions.Actions.OperationInvokedReport] def add_wsdl_port_type(self, parent_node): diff --git a/src/sdc11073/provider/providerimpl.py b/src/sdc11073/provider/providerimpl.py index f4f5bfe2..32aadcc7 100644 --- a/src/sdc11073/provider/providerimpl.py +++ b/src/sdc11073/provider/providerimpl.py @@ -31,13 +31,15 @@ from .waveforms import WaveformSender if TYPE_CHECKING: - from ssl import SSLContext - + from enum import Enum from sdc11073.location import SdcLocation from sdc11073.mdib.providermdib import ProviderMdib from sdc11073.pysoap.msgfactory import CreatedMessage + from sdc11073.pysoap.soapenvelope import ReceivedSoapMessage + from sdc11073.xml_types.msg_types import AbstractSet from sdc11073.xml_types.wsd_types import ScopesType from sdc11073.provider.porttypes.localizationservice import LocalizationStorage + from .operations import OperationDefinitionBase from .components import SdcProviderComponents @@ -360,12 +362,18 @@ def get_operation_by_handle(self, operation_handle): return op return None - def enqueue_operation(self, operation, request, operation_request, transaction_id): + def handle_operation_request(self, + operation: OperationDefinitionBase, + request: ReceivedSoapMessage, + operation_request: AbstractSet, + transaction_id: int) -> Enum: + """Find the responsible sco and forward request to it.""" for sco in self._sco_operations_registries.values(): has_this_operation = sco.get_operation_by_handle(operation.handle) is not None if has_this_operation: - return sco.enqueue_operation(operation, request, operation_request, transaction_id) - return None + return sco.handle_operation_request(operation, request, operation_request, transaction_id) + self._logger.error('no sco has operation {}', operation.handle) + return self.mdib.data_model.msg_types.InvocationState.FAILED def get_toplevel_sco_list(self) -> list: pm_names = self._mdib.data_model.pm_names diff --git a/src/sdc11073/provider/sco.py b/src/sdc11073/provider/sco.py index 0ad07336..bc5cd0b5 100644 --- a/src/sdc11073/provider/sco.py +++ b/src/sdc11073/provider/sco.py @@ -3,9 +3,11 @@ All remote control commands of a client are executed by sco instances. These operations share a common behavior: -A remote control command is executed async. The response to such soap request contains a state (typically 'wait') and a transaction id. +A remote control command is executed. The response to such soap request contains a state +(typically 'wait') and a transaction id. The progress of the transaction is reported with an OperationInvokedReport. -A client must subscribe to the OperationInvokeReport Event of the 'Set' service, otherwise it would not get informed about progress. +A client must subscribe to the OperationInvokeReport Event of the 'Set' service, +otherwise it would not get informed about progress. """ from __future__ import annotations @@ -14,28 +16,34 @@ import time import traceback from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Callable, Optional +from typing import TYPE_CHECKING -from .. import loghelper -from .. import observableproperties as properties -from ..exceptions import ApiUsageError +from sdc11073 import loghelper +from sdc11073.exceptions import ApiUsageError if TYPE_CHECKING: + from enum import Enum + + from sdc11073.mdib.descriptorcontainers import AbstractDescriptorProtocol + from sdc11073.mdib.providermdib import ProviderMdib + from sdc11073.pysoap.soapenvelope import ReceivedSoapMessage + from sdc11073.xml_types.msg_types import AbstractSet + from sdc11073.roles.providerbase import OperationClassGetter + from .operations import OperationDefinitionBase from .porttypes.setserviceimpl import SetServiceProtocol - from lxml.etree import QName +class _OperationsWorker(threading.Thread): + """Thread that enqueues and processes all operations. + Progress notifications are sent via subscription manager. + """ -class _OperationsWorker(threading.Thread): - """ Thread that enqueues and processes all operations. - It manages transaction ids for all operations. - Progress notifications are sent via subscription manager.""" - - def __init__(self, operations_registry, set_service: SetServiceProtocol, mdib, log_prefix): - """ - :param set_service: set_service.notify_operation is called in order to notify all subscribers of OperationInvokeReport Events - """ + def __init__(self, + operations_registry: AbstractScoOperationsRegistry, + set_service: SetServiceProtocol, + mdib: ProviderMdib, + log_prefix: str): super().__init__(name='DeviceOperationsWorker') self.daemon = True self._operations_registry = operations_registry @@ -44,20 +52,17 @@ def __init__(self, operations_registry, set_service: SetServiceProtocol, mdib, l self._operations_queue = queue.Queue(10) # spooled operations self._logger = loghelper.get_logger_adapter('sdc.device.op_worker', log_prefix) - def enqueue_operation(self, operation, request, operation_request, transaction_id: int): - """Enqueue operation "operation". - - :param operation: a callable with signature operation(request, mdib) - :param request: the soapEnvelope of the request - :param operation_request: parsed argument for the operation handler - :param transaction_id: int - """ + def enqueue_operation(self, operation: OperationDefinitionBase, + request: ReceivedSoapMessage, + operation_request: AbstractSet, + transaction_id: int): + """Enqueue operation.""" self._operations_queue.put((transaction_id, operation, request, operation_request), timeout=1) def run(self): data_model = self._mdib.data_model - InvocationState = data_model.msg_types.InvocationState - InvocationError = data_model.msg_types.InvocationError + InvocationState = data_model.msg_types.InvocationState # noqa: N806 + InvocationError = data_model.msg_types.InvocationError # noqa: N806 while True: try: try: @@ -70,7 +75,7 @@ def run(self): return tr_id, operation, request, operation_request = from_queue # unpack tuple time.sleep(0.001) - self._logger.info('{}: starting operation "{}" argument={}', + self._logger.info('%s: starting operation "%s" argument=%r', operation.__class__.__name__, operation.handle, operation_request.argument) # duplicate the WAIT response to the operation request as notification. Standard requires this. self._set_service.notify_operation( @@ -79,19 +84,20 @@ def run(self): self._set_service.notify_operation( operation, tr_id, InvocationState.START, self._mdib.mdib_version_group) try: - operation.execute_operation(request, operation_request) - self._logger.info('{}: successfully finished operation "{}"', operation.__class__.__name__, - operation.handle) + execute_result = operation.execute_operation(request, operation_request) + self._logger.info('%s: successfully finished operation "%s"', + operation.__class__.__name__, operation.handle) self._set_service.notify_operation( - operation, tr_id, InvocationState.FINISHED, self._mdib.mdib_version_group) + operation, tr_id, execute_result.invocation_state, + self._mdib.mdib_version_group, execute_result.operation_target_handle) except Exception as ex: - self._logger.error('{}: error executing operation "{}": {}', operation.__class__.__name__, + self._logger.error('%s: error executing operation "%s": %s', operation.__class__.__name__, operation.handle, traceback.format_exc()) self._set_service.notify_operation( operation, tr_id, InvocationState.FAILED, self._mdib.mdib_version_group, error=InvocationError.OTHER, error_message=repr(ex)) except Exception: - self._logger.error('{}: unexpected error while handling operation: {}', + self._logger.error('%s: unexpected error while handling operation: %s', self.__class__.__name__, traceback.format_exc()) def stop(self): @@ -100,12 +106,13 @@ def stop(self): class AbstractScoOperationsRegistry(ABC): + """Base class for a Sco.""" def __init__(self, set_service: SetServiceProtocol, - operation_cls_getter: Callable[[QName], type], - mdib, - sco_descriptor_container, - log_prefix=None): + operation_cls_getter: OperationClassGetter, + mdib: ProviderMdib, + sco_descriptor_container: AbstractDescriptorProtocol, + log_prefix: str | None = None): self._worker = None self._set_service: SetServiceProtocol = set_service self.operation_cls_getter = operation_cls_getter @@ -116,53 +123,41 @@ def __init__(self, set_service: SetServiceProtocol, self._registered_operations = {} # lookup by handle def check_invocation_timeouts(self): + """Call check_timeout of all registered operations.""" for op in self._registered_operations.values(): op.check_timeout() @abstractmethod - def register_operation(self, operation: OperationDefinition, sco_descriptor_container=None) -> None: - """ - - :param operation: OperationDefinition - :param sco_descriptor_container: a descriptor container - :return: - """ + def register_operation(self, operation: OperationDefinitionBase) -> None: + """Register the operation.""" @abstractmethod def unregister_operation_by_handle(self, operation_handle: str) -> None: - """ - - :param operation_handle: - :return: - """ + """Un-register the operation.""" @abstractmethod - def get_operation_by_handle(self, operation_handle: str) -> OperationDefinition: - """ - - :param operation_handle: - :return: - """ + def get_operation_by_handle(self, operation_handle: str) -> OperationDefinitionBase: + """Get OperationDefinition for given handle.""" @abstractmethod - def enqueue_operation(self, operation: OperationDefinition, request, argument): - """ enqueues operation "operation". - :param operation: a callable with signature operation(request, mdib) - :param request: the soapEnvelope of the request - @return: a transaction Id - """ + def handle_operation_request(self, operation: OperationDefinitionBase, + request: ReceivedSoapMessage, + operation_request: AbstractSet, + transaction_id: int) -> Enum: + """Handle operation "operation".""" @abstractmethod def start_worker(self): - """ start worker thread""" + """Start worker thread.""" @abstractmethod def stop_worker(self): - """ stop worker thread""" + """Stop worker thread.""" class ScoOperationsRegistry(AbstractScoOperationsRegistry): - """ Registry for Sco operations. + """Registry for Sco operations. + from BICEPS: A service control object to define remote control operations. Any pm:AbstractOperationDescriptor/@OperationTarget within this SCO SHALL only reference this or child descriptors within the CONTAINMENT TREE. @@ -170,37 +165,57 @@ class ScoOperationsRegistry(AbstractScoOperationsRegistry): Such VMDs potentially have their own SCO. In every other case, SCO operations are modeled in pm:MdsDescriptor/pm:Sco. """ - def register_operation(self, operation: OperationDefinition, sco_descriptor_container=None): + def register_operation(self, operation: OperationDefinitionBase): + """Register the operation.""" if operation.handle in self._registered_operations: - self._logger.debug('handle {} is already registered, will re-use it', operation.handle) - parent_container = sco_descriptor_container or self.sco_descriptor_container - operation.set_mdib(self._mdib, parent_container) - self._logger.info('register operation "{}"', operation) + self._logger.debug('handle %s is already registered, will re-use it', operation.handle) + operation.set_mdib(self._mdib, self.sco_descriptor_container.Handle) + self._logger.info('register operation "%s"', operation) self._registered_operations[operation.handle] = operation def unregister_operation_by_handle(self, operation_handle: str): + """Un-register the operation.""" del self._registered_operations[operation_handle] - def get_operation_by_handle(self, operation_handle: str) -> OperationDefinition: + def get_operation_by_handle(self, operation_handle: str) -> OperationDefinitionBase: + """Get OperationDefinition for given handle.""" return self._registered_operations.get(operation_handle) - def enqueue_operation(self, operation: OperationDefinition, request, operation_request, transaction_id: int): - """Enqueue operation "operation". - - :param operation: a callable with signature operation(request, mdib) - :param request: the soapEnvelope of the request - :param operation_request: the argument for the operation - :param transaction_id: - """ - self._worker.enqueue_operation(operation, request, operation_request, transaction_id) + def handle_operation_request(self, operation: OperationDefinitionBase, + request: ReceivedSoapMessage, + operation_request: AbstractSet, + transaction_id: int) -> Enum: + """Handle operation immediately or delayed in worker thread, depending on operation.delayed_processing.""" + InvocationState = self._mdib.data_model.msg_types.InvocationState # noqa: N806 + + if operation.delayed_processing: + self._worker.enqueue_operation(operation, request, operation_request, transaction_id) + return InvocationState.WAIT + try: + execute_result = operation.execute_operation(request, operation_request) + self._logger.info('%s: successfully finished operation "%s"', operation.__class__.__name__, + operation.handle) + self._set_service.notify_operation(operation, transaction_id, execute_result.invocation_state, + self._mdib.mdib_version_group, execute_result.operation_target_handle) + self._logger.debug('notifications for operation %s sent', operation.handle) + return InvocationState.FINISHED + except Exception as ex: + self._logger.error('%s: error executing operation "%s": %s', operation.__class__.__name__, + operation.handle, traceback.format_exc()) + self._set_service.notify_operation( + operation, transaction_id, InvocationState.FAILED, self._mdib.mdib_version_group, + error=self._mdib.data_model.msg_types.InvocationError.OTHER, error_message=repr(ex)) + return InvocationState.FAILED def start_worker(self): + """Start worker thread.""" if self._worker is not None: raise ApiUsageError('SCO worker is already running') self._worker = _OperationsWorker(self, self._set_service, self._mdib, self._log_prefix) self._worker.start() def stop_worker(self): + """Stop worker thread.""" if self._worker is not None: self._worker.stop() self._worker = None diff --git a/src/sdc11073/roles/alarmprovider.py b/src/sdc11073/roles/alarmprovider.py index 5a8507a3..70bd8e0a 100644 --- a/src/sdc11073/roles/alarmprovider.py +++ b/src/sdc11073/roles/alarmprovider.py @@ -1,25 +1,51 @@ +from __future__ import annotations + import time import traceback -from threading import Thread, Event +from threading import Event, Thread +from typing import TYPE_CHECKING, cast + +from sdc11073.mdib.descriptorcontainers import AbstractSetStateOperationDescriptorContainer +from sdc11073.mdib.statecontainers import AbstractStateProtocol, AlertConditionStateContainer +from sdc11073.provider.operations import ExecuteResult from . import providerbase +if TYPE_CHECKING: + from collections.abc import Iterable + + from sdc11073.mdib import ProviderMdib + from sdc11073.mdib.descriptorcontainers import AbstractDescriptorProtocol, AbstractOperationDescriptorProtocol + from sdc11073.mdib.transactions import TransactionManagerProtocol + from sdc11073.provider.operations import OperationDefinitionBase, OperationDefinitionProtocol, ExecuteParameters + from sdc11073.provider.sco import AbstractScoOperationsRegistry + + from .providerbase import OperationClassGetter + class GenericAlarmProvider(providerbase.ProviderRole): - """ + """Provide some generic alarm handling functionality. + - in pre commit handler it updates present alarms list of alarm system states - runs periodic job to send currently present alarms in AlertSystemState - supports alert delegation acc. to BICEPS chapter 6.2 """ + WORKER_THREAD_INTERVAL = 1.0 # seconds - def __init__(self, mdib, log_prefix): + def __init__(self, mdib: ProviderMdib, log_prefix: str): super().__init__(mdib, log_prefix) self._stop_worker = Event() self._worker_thread = None - def init_operations(self, sco): + def init_operations(self, sco: AbstractScoOperationsRegistry): + """Initialize and start what the provider needs. + + - set initial values of all AlertSystemStateContainers. + - set initial values of all AlertStateContainers. + - start a worker thread that periodically updates AlertSystemStateContainers. + """ super().init_operations(sco) self._set_alert_system_states_initial_values() self._set_alert_states_initial_values() @@ -28,30 +54,30 @@ def init_operations(self, sco): self._worker_thread.start() def stop(self): + """Stop worker thread.""" self._stop_worker.set() self._worker_thread.join() - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): - """ - creates operation handler for: + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: + """Return a callable for this operation or None. + + Creates operation handler for: - set alert signal state => SetAlertStateOperation operation target Is an AlertSignalDescriptor handler = self._delegate_alert_signal - :param operation_descriptor_container: - :param operation_cls_getter: - :return: None or an OperationDefinitionBase instance """ pm_names = self._mdib.data_model.pm_names op_target_handle = operation_descriptor_container.OperationTarget op_target_descr = self._mdib.descriptions.handle.get_one(op_target_handle) - if operation_descriptor_container.NODETYPE == pm_names.SetValueOperationDescriptor: - pass - elif operation_descriptor_container.NODETYPE == pm_names.ActivateOperationDescriptor: - pass - elif operation_descriptor_container.NODETYPE == pm_names.SetAlertStateOperationDescriptor: - if op_target_descr.NODETYPE == pm_names.AlertSignalDescriptor and op_target_descr.SignalDelegationSupported: - modifiable_data = operation_descriptor_container.ModifiableData + if pm_names.SetAlertStateOperationDescriptor == operation_descriptor_container.NODETYPE: + if pm_names.AlertSignalDescriptor == op_target_descr.NODETYPE and op_target_descr.SignalDelegationSupported: + # operation_descriptor_container is a SetAlertStateOperationDescriptor + set_state_descriptor_container = cast(AbstractSetStateOperationDescriptorContainer, + operation_descriptor_container) + modifiable_data = set_state_descriptor_container.ModifiableData if 'Presence' in modifiable_data \ and 'ActivationState' in modifiable_data \ and 'ActualSignalGenerationDelay' in modifiable_data: @@ -59,19 +85,19 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ operation = self._mk_operation_from_operation_descriptor( operation_descriptor_container, operation_cls_getter, - current_request_handler=self._delegate_alert_signal, - timeout_handler=self._end_delegate_alert_signal) + operation_handler=self._delegate_alert_signal, + timeout_handler=self._on_timeout_delegate_alert_signal) - self._logger.debug(f'GenericAlarmProvider: added handler "self._setAlertState" ' - f'for {operation_descriptor_container} target= {op_target_descr} ') + self._logger.debug('GenericAlarmProvider: added handler "self._setAlertState" for %s target=%s', + operation_descriptor_container, op_target_descr) return operation return None # None == no handler for this operation instantiated def _set_alert_system_states_initial_values(self): - """ Sets ActivationState to ON in all alert systems. - adds audible SystemSignalActivation, state=ON to all AlertSystemState instances. Why???? - :return: + """Set ActivationState to ON in all alert systems. + + Adds audible SystemSignalActivation, state=ON to all AlertSystemState instances. Why???? """ pm_names = self._mdib.data_model.pm_names pm_types = self._mdib.data_model.pm_types @@ -84,9 +110,11 @@ def _set_alert_system_states_initial_values(self): state=pm_types.AlertActivation.ON)) def _set_alert_states_initial_values(self): - """ + """Set AlertConditions and AlertSignals. + - if an AlertCondition.ActivationState is 'On', then the local AlertSignals shall also be 'On' - - all remote alert Signals shall be 'Off' initially (must be explicitly enabled by delegating device)""" + - all remote alert Signals shall be 'Off' initially (must be explicitly enabled by delegating device). + """ pm_types = self._mdib.data_model.pm_types pm_names = self._mdib.data_model.pm_names for alert_condition in self._mdib.states.NODETYPE.get(pm_names.AlertConditionState, []): @@ -106,21 +134,8 @@ def _set_alert_states_initial_values(self): alert_signal_state.ActivationState = pm_types.AlertActivation.ON alert_signal_state.Presence = pm_types.AlertSignalPresence.OFF - # @staticmethod - # def _get_descriptor(handle, mdib, transaction): - # """ Helper that looks for descriptor first in current transaction, then in mdib. returns first found one or raises KeyError""" - # descriptor = None - # tr_item = transaction.descriptor_updates.get(handle) - # if tr_item is not None: - # descriptor = tr_item.new - # if descriptor is None: - # # it is not part of this transaction - # descriptor = mdib.descriptions.handle.get_one(handle, allow_none=True) - # if descriptor is None: - # raise KeyError(f'there is no descriptor for {handle}') - # return descriptor - - def _get_changed_alert_condition_states(self, transaction): + def _get_changed_alert_condition_states(self, + transaction: TransactionManagerProtocol) -> list[AbstractStateProtocol]: pm_names = self._mdib.data_model.pm_names result = [] for item in list(transaction.alert_state_updates.values()): @@ -130,8 +145,9 @@ def _get_changed_alert_condition_states(self, transaction): result.append(tmp) return result - def on_pre_commit(self, mdib, transaction): - """ + def on_pre_commit(self, mdib: ProviderMdib, transaction: TransactionManagerProtocol): + """Manipulate the transaction. + - Updates alert system states and adds them to transaction, if at least one of its alert conditions changed ( is in transaction). - Updates all AlertSignals for changed Alert Conditions and adds them to transaction. @@ -155,7 +171,9 @@ def on_pre_commit(self, mdib, transaction): self._update_alert_system_states(mdib, transaction, alert_system_states, is_self_check=False) @staticmethod - def _find_alert_systems_with_modifications(transaction, changed_alert_conditions): + def _find_alert_systems_with_modifications(transaction: TransactionManagerProtocol, + changed_alert_conditions: list[AbstractStateProtocol]) \ + -> set[AbstractStateProtocol]: # find all alert systems for the changed alert conditions alert_system_states = set() for tmp in changed_alert_conditions: @@ -170,18 +188,14 @@ def _find_alert_systems_with_modifications(transaction, changed_alert_conditions return alert_system_states @staticmethod - def _update_alert_system_states(mdib, transaction, alert_system_states, is_self_check=True): - """ - update alert system states - :param mdib: - :param transaction: - :param alert_system_states: list of AlertSystemStateContainer instances - :param is_self_check: if True, LastSelfCheck and SelfCheckCount are set - :return: - """ + def _update_alert_system_states(mdib: ProviderMdib, + transaction: TransactionManagerProtocol, + alert_system_states: Iterable[AbstractStateProtocol], + is_self_check: bool = True): + """Update alert system states.""" pm_types = mdib.data_model.pm_types - def _get_alert_state(descriptor_handle): + def _get_alert_state(descriptor_handle: str) -> AbstractStateProtocol: alert_state = None tr_item = transaction.get_state_transaction_item(descriptor_handle) if tr_item is not None: @@ -201,13 +215,15 @@ def _get_alert_state(descriptor_handle): all_alert_condition_descr = [d for d in all_child_descriptors if hasattr(d, 'Kind')] # select all state containers with technical alarms present all_tech_descr = [d for d in all_alert_condition_descr if d.Kind == pm_types.AlertConditionKind.TECHNICAL] - all_tech_states = [_get_alert_state(d.Handle) for d in all_tech_descr] + _all_tech_states = [_get_alert_state(d.Handle) for d in all_tech_descr] + all_tech_states = cast(list[AlertConditionStateContainer], _all_tech_states) all_tech_states = [s for s in all_tech_states if s is not None] all_present_tech_states = [s for s in all_tech_states if s.Presence] # select all state containers with physiological alarms present all_phys_descr = [d for d in all_alert_condition_descr if d.Kind == pm_types.AlertConditionKind.PHYSIOLOGICAL] - all_phys_states = [_get_alert_state(d.Handle) for d in all_phys_descr] + _all_phys_states = [_get_alert_state(d.Handle) for d in all_phys_descr] + all_phys_states = cast(list[AlertConditionStateContainer], _all_phys_states) all_phys_states = [s for s in all_phys_states if s is not None] all_present_phys_states = [s for s in all_phys_states if s.Presence] @@ -218,10 +234,14 @@ def _get_alert_state(descriptor_handle): state.SelfCheckCount = 1 if state.SelfCheckCount is None else state.SelfCheckCount + 1 @staticmethod - def _update_alert_signals(changed_alert_condition, mdib, transaction): - """ Handle alert signals for a changed alert condition. + def _update_alert_signals(changed_alert_condition: AbstractStateProtocol, + mdib: ProviderMdib, + transaction: TransactionManagerProtocol): + """Handle alert signals for a changed alert condition. + This method only changes states of local signals. - Handling of delegated signals is in the responsibility of the delegated device!""" + Handling of delegated signals is in the responsibility of the delegated device! + """ pm_types = mdib.data_model.pm_types alert_signal_descriptors = mdib.descriptions.condition_signaled.get(changed_alert_condition.DescriptorHandle, []) @@ -243,7 +263,7 @@ def _update_alert_signals(changed_alert_condition, mdib, transaction): lookup = {(True, True): (pm_types.AlertActivation.PAUSED, pm_types.AlertSignalPresence.OFF), (True, False): (pm_types.AlertActivation.ON, pm_types.AlertSignalPresence.ON), (False, True): (pm_types.AlertActivation.PAUSED, pm_types.AlertSignalPresence.OFF), - (False, False): (pm_types.AlertActivation.ON, pm_types.AlertSignalPresence.OFF) + (False, False): (pm_types.AlertActivation.ON, pm_types.AlertSignalPresence.OFF), } for descriptor in local_alert_signal_descriptors: tr_item = transaction.get_state_transaction_item(descriptor.Handle) @@ -259,8 +279,13 @@ def _update_alert_signals(changed_alert_condition, mdib, transaction): # don't change transaction.unget_state(alert_signal_state) - def _pause_fallback_alert_signals(self, delegable_signal_descriptor, all_signal_descriptors, transaction): - """ The idea of the fallback signal is to set it paused when the delegable signal is currently ON, + def _pause_fallback_alert_signals(self, + delegable_signal_descriptor: AbstractDescriptorProtocol, + all_signal_descriptors: list[AbstractDescriptorProtocol] | None, + transaction: TransactionManagerProtocol): + """Pause fallback signals. + + The idea of the fallback signal is to set it paused when the delegable signal is currently ON, and to set it back to ON when the delegable signal is not ON. This method sets the fallback to PAUSED value. :param delegable_signal_descriptor: a descriptor container @@ -277,7 +302,9 @@ def _pause_fallback_alert_signals(self, delegable_signal_descriptor, all_signal_ else: transaction.unget_state(ss_fallback) - def _activate_fallback_alert_signals(self, delegable_signal_descriptor, all_signal_descriptors, transaction): + def _activate_fallback_alert_signals(self, delegable_signal_descriptor: AbstractDescriptorProtocol, + all_signal_descriptors: list[AbstractDescriptorProtocol] | None, + transaction: TransactionManagerProtocol): pm_types = self._mdib.data_model.pm_types # look for local fallback signal (same Manifestation), and set it to paused for fallback in self._get_fallback_signals(delegable_signal_descriptor, all_signal_descriptors): @@ -287,9 +314,15 @@ def _activate_fallback_alert_signals(self, delegable_signal_descriptor, all_sign else: transaction.unget_state(ss_fallback) - def _get_fallback_signals(self, delegable_signal_descriptor, all_signal_descriptors): - """looks in all_signal_descriptors for a signal with same ConditionSignaled and same - Manifestation as delegable_signal_descriptor and SignalDelegationSupported == True """ + def _get_fallback_signals(self, + delegable_signal_descriptor: AbstractDescriptorProtocol, + all_signal_descriptors: list[AbstractDescriptorProtocol] | None) -> list[ + AbstractDescriptorProtocol]: + """Return a list of all fallback signals for descriptor. + + looks in all_signal_descriptors for a signal with same ConditionSignaled and same + Manifestation as delegable_signal_descriptor and SignalDelegationSupported == True. + """ if all_signal_descriptors is None: all_signal_descriptors = self._mdib.descriptions.condition_signaled.get( delegable_signal_descriptor.ConditionSignaled, []) @@ -297,8 +330,9 @@ def _get_fallback_signals(self, delegable_signal_descriptor, all_signal_descript and tmp.Manifestation == delegable_signal_descriptor.Manifestation and tmp.ConditionSignaled == delegable_signal_descriptor.ConditionSignaled] - def _delegate_alert_signal(self, operation_instance, soap_request, operation_request) -> list[str]: - """Handler for an operation call from remote. + def _delegate_alert_signal(self, params: ExecuteParameters) -> ExecuteResult: + """Handle operation call from remote (ExecuteHandler). + Sets ActivationState, Presence and ActualSignalGenerationDelay of the corresponding state in mdib. If this is a delegable signal, it also sets the ActivationState of the fallback signal. @@ -306,15 +340,14 @@ def _delegate_alert_signal(self, operation_instance, soap_request, operation_req :param value: AlertSignalStateContainer instance :return: """ - value = operation_request.argument + value = params.operation_request.argument pm_types = self._mdib.data_model.pm_types - operation_target_handle = operation_instance.operation_target_handle - # self._last_set_alert_signal_state[operation_target_handle] = time.time() + operation_target_handle = params.operation_instance.operation_target_handle with self._mdib.transaction_manager() as mgr: state = mgr.get_state(operation_target_handle) - self._logger.info('delegate alert signal {} of {} from {} to {}', operation_target_handle, state, + self._logger.info('delegate alert signal %s of %s from %s to %s', operation_target_handle, state, state.ActivationState, value.ActivationState) - for name in operation_instance.descriptor_container.ModifiableData: + for name in params.operation_instance.descriptor_container.ModifiableData: tmp = getattr(value, name) setattr(state, name, tmp) descr = self._mdib.descriptions.handle.get_one(operation_target_handle) @@ -323,19 +356,20 @@ def _delegate_alert_signal(self, operation_instance, soap_request, operation_req self._pause_fallback_alert_signals(descr, None, mgr) else: self._activate_fallback_alert_signals(descr, None, mgr) - return [operation_target_handle] + return ExecuteResult(operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) - def _end_delegate_alert_signal(self, operation_instance, _): + def _on_timeout_delegate_alert_signal(self, operation_instance: OperationDefinitionProtocol): + """TimeoutHandler for delegated signal.""" pm_types = self._mdib.data_model.pm_types operation_target_handle = operation_instance.operation_target_handle with self._mdib.transaction_manager() as mgr: state = mgr.get_state(operation_target_handle) - self._logger.info('timeout alert signal delegate operation={} target={} ', + self._logger.info('timeout alert signal delegate operation=%s target=%s', operation_instance.handle, operation_target_handle) state.ActivationState = pm_types.AlertActivation.OFF descr = self._mdib.descriptions.handle.get_one(operation_target_handle) self._activate_fallback_alert_signals(descr, None, mgr) - return [operation_target_handle] def _worker_thread_loop(self): # delay start of operation @@ -353,7 +387,7 @@ def _run_worker_job(self): self._update_alert_system_state_current_alerts() def _update_alert_system_state_current_alerts(self): - """ updates AlertSystemState present alarms list""" + """Update AlertSystemState present alarms list.""" states_needing_update = self._get_alert_system_states_needing_update() if len(states_needing_update) > 0: try: @@ -362,13 +396,10 @@ def _update_alert_system_state_current_alerts(self): self._update_alert_system_states(self._mdib, mgr, tr_states) except Exception: exc = traceback.format_exc() - self._logger.error('_checkAlertStates: {}', exc) + self._logger.error('_checkAlertStates: %r', exc) - def _get_alert_system_states_needing_update(self): - """ - - :return: all AlertSystemStateContainers of those last - """ + def _get_alert_system_states_needing_update(self) -> list[AbstractStateProtocol]: + """:return: all AlertSystemStateContainers of those last""" pm_names = self._mdib.data_model.pm_names states_needing_update = [] try: @@ -376,7 +407,7 @@ def _get_alert_system_states_needing_update(self): []) for alert_system_descr in all_alert_systems_descr: alert_system_state = self._mdib.states.descriptor_handle.get_one(alert_system_descr.Handle, - allow_none=True) + allow_none=True) if alert_system_state is not None: selfcheck_period = alert_system_descr.SelfCheckPeriod if selfcheck_period is not None: @@ -385,5 +416,5 @@ def _get_alert_system_states_needing_update(self): states_needing_update.append(alert_system_state) except Exception: exc = traceback.format_exc() - self._logger.error('_get_alert_system_states_needing_update: {}', exc) + self._logger.error('_get_alert_system_states_needing_update: %r', exc) return states_needing_update diff --git a/src/sdc11073/roles/audiopauseprovider.py b/src/sdc11073/roles/audiopauseprovider.py index ac14975c..6eded5cc 100644 --- a/src/sdc11073/roles/audiopauseprovider.py +++ b/src/sdc11073/roles/audiopauseprovider.py @@ -1,66 +1,88 @@ -from . import providerbase -from .nomenclature import NomenclatureCodes as nc +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from sdc11073.mdib.statecontainers import AlertSystemStateContainer +from sdc11073.provider.operations import ExecuteResult +from sdc11073.xml_types.msg_types import InvocationState from sdc11073.xml_types.pm_types import Coding +from .nomenclature import NomenclatureCodes +from .providerbase import OperationClassGetter, ProviderRole + +if TYPE_CHECKING: + from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol + from sdc11073.mdib.providermdib import ProviderMdib + from sdc11073.provider.operations import OperationDefinitionBase, ExecuteParameters + from sdc11073.provider.sco import AbstractScoOperationsRegistry + # coded values for SDC audio pause -MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE = Coding(nc.MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE) -MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE = Coding(nc.MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE) +MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE = Coding(NomenclatureCodes.MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE) +MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE = Coding(NomenclatureCodes.MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE) -class GenericAudioPauseProvider(providerbase.ProviderRole): - """Handling of global audio pause. - It guarantees that there are operations with codes "MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE" +class GenericAudioPauseProvider(ProviderRole): + """Example for handling of global audio pause. + + This provider handles Activate operations with codes "MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE" and "MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE". + Nothing is added to the mdib. If the mdib does not contain these operations, the functionality is not available. """ - def __init__(self, mdib, log_prefix): + def __init__(self, mdib: ProviderMdib, log_prefix: str): super().__init__(mdib, log_prefix) self._set_global_audio_pause_operations = [] self._cancel_global_audio_pause_operations = [] - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: + """Create operation handlers for existing mdib entries. + + Handle codes MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE and MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE. + """ if operation_descriptor_container.coding == MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE: - self._logger.debug('instantiating "set audio pause" operation from existing descriptor ' - f'handle={operation_descriptor_container.Handle}') - set_ap_operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, - operation_cls_getter, - current_request_handler=self._set_global_audio_pause) + self._logger.debug('instantiating "set audio pause" operation from existing descriptor handle=%s', + operation_descriptor_container.Handle) + set_ap_operation = self._mk_operation_from_operation_descriptor( + operation_descriptor_container, operation_cls_getter, operation_handler=self._set_global_audio_pause) self._set_global_audio_pause_operations.append(set_ap_operation) return set_ap_operation if operation_descriptor_container.coding == MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE: - self._logger.debug('instantiating "cancel audio pause" operation from existing descriptor ' - f'handle={operation_descriptor_container.Handle}') - cancel_ap_operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, - operation_cls_getter, - current_request_handler=self._cancel_global_audio_pause) + self._logger.debug('instantiating "cancel audio pause" operation from existing descriptor handle=%s', + operation_descriptor_container.Handle) + cancel_ap_operation = self._mk_operation_from_operation_descriptor( + operation_descriptor_container, operation_cls_getter, operation_handler=self._cancel_global_audio_pause) self._cancel_global_audio_pause_operations.append(cancel_ap_operation) return cancel_ap_operation return None - def _set_global_audio_pause(self, operation_instance, soap_request, operation_request) -> list[str]: - """ This is the code that executes the operation itself: - SF1132: If global audio pause is initiated, all SystemSignalActivation/State for all alarm systems of the - product with SystemSignalActivation/Manifestation evaluating to 'Aud' shall be set to 'Psd'. + def _set_global_audio_pause(self, params: ExecuteParameters) -> ExecuteResult: + """Set global audio pause (ExecuteHandler). - SF958: If signal pause is initiated for an alert signal that is not an ACKNOWLEDGE CAPABLE ALERT SIGNAL, - then the Alert Provider shall set the AlertSignalState/ActivationState to 'Psd' and the AlertSignalState/Presence to 'Off'. + If global audio pause is initiated, all SystemSignalActivation/State for all alarm systems of the + product with SystemSignalActivation/Manifestation evaluating to 'Aud' are set to 'Psd'. - SF959: If signal pause is initiated for an ACKNOWLEDGEABLE ALERT SIGNAL, the the Alert Provider shall set the - AlertSignalState/ActivationState to 'Psd' and AlertSignalState/Presence to 'Ack' for that ALERT SIGNAL. - """ + If signal pause is initiated for an alert signal that is not an ACKNOWLEDGE CAPABLE ALERT SIGNAL, + then the AlertSignalState/ActivationState is set to 'Psd' and the AlertSignalState/Presence to 'Off'. + + If signal pause is initiated for an ACKNOWLEDGEABLE ALERT SIGNAL, the + AlertSignalState/ActivationState is set to 'Psd' and AlertSignalState/Presence to 'Ack' for that ALERT SIGNAL. + """ pm_types = self._mdib.data_model.pm_types pm_names = self._mdib.data_model.pm_names alert_system_descriptors = self._mdib.descriptions.NODETYPE.get(pm_names.AlertSystemDescriptor) if alert_system_descriptors is None: self._logger.error('SDC_SetAudioPauseOperation called, but no AlertSystemDescriptor in mdib found') - return [] + return ExecuteResult(params.operation_instance.operation_target_handle, InvocationState.FAILED) with self._mdib.transaction_manager() as mgr: for alert_system_descriptor in alert_system_descriptors: - alert_system_state = mgr.get_state(alert_system_descriptor.Handle) + _alert_system_state = mgr.get_state(alert_system_descriptor.Handle) + alert_system_state = cast(AlertSystemStateContainer, _alert_system_state) if alert_system_state.ActivationState != pm_types.AlertActivation.ON: self._logger.info('SDC_SetAudioPauseOperation: nothing to do') - mgr.unget_state(alert_system_state) + mgr.unget_state(_alert_system_state) else: audible_signals = [ssa for ssa in alert_system_state.SystemSignalActivation if ssa.Manifestation == pm_types.AlertSignalManifestation.AUD] @@ -68,12 +90,12 @@ def _set_global_audio_pause(self, operation_instance, soap_request, operation_re ssa.State != pm_types.AlertActivation.PAUSED] if not active_audible_signals: # Alert System has no audible SystemSignalActivations, no action required - mgr.unget_state(alert_system_state) + mgr.unget_state(_alert_system_state) else: for ssa in active_audible_signals: - ssa.State = pm_types.AlertActivation.PAUSED # SF1132 - self._logger.info( - f'SetAudioPauseOperation: set alert system "{alert_system_descriptor.Handle}" to paused') + ssa.State = pm_types.AlertActivation.PAUSED + self._logger.info('SetAudioPauseOperation: set alert system "%s" to paused', + alert_system_descriptor.Handle) # handle all audible alert signals of this alert system all_alert_signal_descriptors = self._mdib.descriptions.NODETYPE.get( pm_names.AlertSignalDescriptor, []) @@ -83,48 +105,50 @@ def _set_global_audio_pause(self, operation_instance, soap_request, operation_re d.Manifestation == pm_types.AlertSignalManifestation.AUD] for descriptor in audible_child_alert_signal_descriptors: alert_signal_state = mgr.get_state(descriptor.Handle) - if descriptor.AcknowledgementSupported: # SF959 + if descriptor.AcknowledgementSupported: if alert_signal_state.ActivationState != pm_types.AlertActivation.PAUSED \ or alert_signal_state.Presence != pm_types.AlertSignalPresence.ACK: alert_signal_state.ActivationState = pm_types.AlertActivation.PAUSED alert_signal_state.Presence = pm_types.AlertSignalPresence.ACK else: mgr.unget_state(alert_signal_state) - else: # SF958 - if alert_signal_state.ActivationState != pm_types.AlertActivation.PAUSED \ - or alert_signal_state.Presence != pm_types.AlertSignalPresence.OFF: - alert_signal_state.ActivationState = pm_types.AlertActivation.PAUSED - alert_signal_state.Presence = pm_types.AlertSignalPresence.OFF - else: - mgr.unget_state(alert_signal_state) - return [] # ToDo: what is the correct operation target? + elif alert_signal_state.ActivationState != pm_types.AlertActivation.PAUSED \ + or alert_signal_state.Presence != pm_types.AlertSignalPresence.OFF: + alert_signal_state.ActivationState = pm_types.AlertActivation.PAUSED + alert_signal_state.Presence = pm_types.AlertSignalPresence.OFF + else: + mgr.unget_state(alert_signal_state) + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) + + def _cancel_global_audio_pause(self, params: ExecuteParameters) -> ExecuteResult: + """Cancel global audio pause (ExecuteHandler). - def _cancel_global_audio_pause(self, operation_instance, soap_request, operation_request) -> list[str]: # pylint: disable=unused-argument - """ This is the code that executes the operation itself: If global audio pause is initiated, all SystemSignalActivation/State for all alarm systems of the product with SystemSignalActivation/Manifestation evaluating to 'Aud' shall be set to 'Psd'. - """ + """ pm_types = self._mdib.data_model.pm_types pm_names = self._mdib.data_model.pm_names alert_system_descriptors = self._mdib.descriptions.NODETYPE.get(pm_names.AlertSystemDescriptor) with self._mdib.transaction_manager() as mgr: for alert_system_descriptor in alert_system_descriptors: - alert_system_state = mgr.get_state(alert_system_descriptor.Handle) + _alert_system_state = mgr.get_state(alert_system_descriptor.Handle) + alert_system_state = cast(AlertSystemStateContainer, _alert_system_state) if alert_system_state.ActivationState != pm_types.AlertActivation.ON: self._logger.info('SDC_CancelAudioPauseOperation: nothing to do') - mgr.unget_state(alert_system_state) + mgr.unget_state(_alert_system_state) else: audible_signals = [ssa for ssa in alert_system_state.SystemSignalActivation if ssa.Manifestation == pm_types.AlertSignalManifestation.AUD] paused_audible_signals = [ssa for ssa in audible_signals if ssa.State == pm_types.AlertActivation.PAUSED] if not paused_audible_signals: - mgr.unget_state(alert_system_state) + mgr.unget_state(_alert_system_state) else: for ssa in paused_audible_signals: ssa.State = pm_types.AlertActivation.ON - self._logger.info( - f'SetAudioPauseOperation: set alert system "{alert_system_descriptor.Handle}" to ON') + self._logger.info('SetAudioPauseOperation: set alert system "%s" to ON', + alert_system_descriptor.Handle) # handle all audible alert signals of this alert system all_alert_signal_descriptors = self._mdib.descriptions.NODETYPE.get( pm_names.AlertSignalDescriptor, []) @@ -143,20 +167,31 @@ def _cancel_global_audio_pause(self, operation_instance, soap_request, operation alert_signal_state.Presence = pm_types.AlertSignalPresence.ON else: mgr.unget_state(alert_signal_state) - return [] # ToDo: what is the correct operation target? + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) class AudioPauseProvider(GenericAudioPauseProvider): - """This Implementation adds operations to mdib if they do not exist.""" + """Handling of global audio pause example. + + This provider guarantees that there are Activate operations with codes "MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE" + and "MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE". It adds them to mdib if they do not exist. + """ + + def make_missing_operations(self, sco: AbstractScoOperationsRegistry) -> list[OperationDefinitionBase]: + """Add operations to mdib if they do not exist. - def make_missing_operations(self, sco): + - code MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE starts alarm pause + - code MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE cancels alarm pause + It creates two activate operations with the MDS element as operation target. + """ pm_types = self._mdib.data_model.pm_types pm_names = self._mdib.data_model.pm_names ops = [] # in this case only the top level sco shall have the additional operations. # Check if this is the top level sco (parent is mds) parent_descriptor = self._mdib.descriptions.handle.get_one(sco.sco_descriptor_container.parent_handle) - if parent_descriptor.NODETYPE != pm_names.MdsDescriptor: + if pm_names.MdsDescriptor != parent_descriptor.NODETYPE: return ops operation_cls_getter = sco.operation_cls_getter # find mds for this sco @@ -166,30 +201,30 @@ def make_missing_operations(self, sco): parent_descr = self._mdib.descriptions.handle.get_one(current_descr.parent_handle) if parent_descr is None: raise ValueError(f'could not find mds descriptor for sco {sco.sco_descriptor_container.Handle}') - if parent_descr.NODETYPE == pm_names.MdsDescriptor: + if pm_names.MdsDescriptor == parent_descr.NODETYPE: mds_descr = parent_descr else: current_descr = parent_descr operation_target_container = mds_descr # the operation target is the mds itself activate_op_cls = operation_cls_getter(pm_names.ActivateOperationDescriptor) if not self._set_global_audio_pause_operations: - self._logger.debug( - f'adding "set audio pause" operation, no descriptor in mdib (looked for code = {nc.MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE})') - set_ap_operation = self._mk_operation(activate_op_cls, - handle='AP__ON', - operation_target_handle=operation_target_container.Handle, - coded_value=pm_types.CodedValue(nc.MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE), - current_request_handler=self._set_global_audio_pause) + self._logger.debug('adding "set audio pause" operation, no descriptor in mdib (looked for code = %s)', + NomenclatureCodes.MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE) + set_ap_operation = activate_op_cls('AP__ON', + operation_target_container.Handle, + self._set_global_audio_pause, + coded_value=pm_types.CodedValue( + NomenclatureCodes.MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE)) self._set_global_audio_pause_operations.append(set_ap_operation) ops.append(set_ap_operation) if not self._cancel_global_audio_pause_operations: - self._logger.debug( - f'adding "cancel audio pause" operation, no descriptor in mdib (looked for code = {nc.MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE})') - cancel_ap_operation = self._mk_operation(activate_op_cls, - handle='AP__CANCEL', - operation_target_handle=operation_target_container.Handle, - coded_value=pm_types.CodedValue(nc.MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE), - current_request_handler=self._cancel_global_audio_pause) + self._logger.debug('adding "cancel audio pause" operation, no descriptor in mdib (looked for code = %s)', + NomenclatureCodes.MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE) + cancel_ap_operation = activate_op_cls('AP__CANCEL', + operation_target_container.Handle, + self._cancel_global_audio_pause, + coded_value=pm_types.CodedValue( + NomenclatureCodes.MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE)) ops.append(cancel_ap_operation) self._set_global_audio_pause_operations.append(cancel_ap_operation) return ops diff --git a/src/sdc11073/roles/clockprovider.py b/src/sdc11073/roles/clockprovider.py index 49204d25..e0454555 100644 --- a/src/sdc11073/roles/clockprovider.py +++ b/src/sdc11073/roles/clockprovider.py @@ -1,35 +1,48 @@ -from . import providerbase -from .nomenclature import NomenclatureCodes as nc +from __future__ import annotations +from typing import TYPE_CHECKING -class GenericSDCClockProvider(providerbase.ProviderRole): - """ Handles operations for setting ntp server and time zone. - It guarantees that mdib has a clock descriptor and that there operations for setting - ReferenceSource and Timezone of clock state.""" +from sdc11073.provider.operations import ExecuteResult - def __init__(self, mdib, log_prefix): +from .nomenclature import NomenclatureCodes +from .providerbase import OperationClassGetter, ProviderRole + +if TYPE_CHECKING: + from sdc11073.mdib.descriptorcontainers import AbstractDescriptorProtocol, AbstractOperationDescriptorProtocol + from sdc11073.mdib.providermdib import ProviderMdib + from sdc11073.provider.operations import ExecuteParameters, OperationDefinitionBase + from sdc11073.provider.sco import AbstractScoOperationsRegistry + from sdc11073.xml_types.pm_types import CodedValue, SafetyClassification + + +class GenericSDCClockProvider(ProviderRole): + """Handles operations for setting ntp server and time zone. + + This provider handles SetString operations with codes + "MDC_OP_SET_TIME_SYNC_REF_SRC" and "MDC_ACT_SET_TIME_ZONE". + Nothing is added to the mdib. If the mdib does not contain these operations, the functionality is not available. + """ + + def __init__(self, mdib: ProviderMdib, log_prefix: str): super().__init__(mdib, log_prefix) self._set_ntp_operations = [] self._set_tz_operations = [] pm_types = self._mdib.data_model.pm_types - self.MDC_OP_SET_TIME_SYNC_REF_SRC = pm_types.CodedValue(nc.MDC_OP_SET_TIME_SYNC_REF_SRC) - self.MDC_ACT_SET_TIME_ZONE = pm_types.CodedValue(nc.MDC_ACT_SET_TIME_ZONE) - - self.OP_SET_NTP = pm_types.CodedValue(nc.OP_SET_NTP) - self.OP_SET_TZ = pm_types.CodedValue(nc.OP_SET_TZ) + self.MDC_OP_SET_TIME_SYNC_REF_SRC = pm_types.CodedValue(NomenclatureCodes.MDC_OP_SET_TIME_SYNC_REF_SRC) + self.MDC_ACT_SET_TIME_ZONE = pm_types.CodedValue(NomenclatureCodes.MDC_ACT_SET_TIME_ZONE) - def init_operations(self, sco): + def init_operations(self, sco: AbstractScoOperationsRegistry): + """Create a ClockDescriptor and ClockState in mdib if they do not exist in mdib.""" super().init_operations(sco) pm_types = self._mdib.data_model.pm_types pm_names = self._mdib.data_model.pm_names - # create a clock descriptor and state if they do not exist in mdib clock_descriptor = self._mdib.descriptions.NODETYPE.get_one(pm_names.ClockDescriptor, allow_none=True) if clock_descriptor is None: mds_container = self._mdib.descriptions.NODETYPE.get_one(pm_names.MdsDescriptor) clock_descr_handle = 'clock_' + mds_container.Handle - self._logger.debug(f'creating a clock descriptor, handle={clock_descr_handle}') + self._logger.debug('creating a clock descriptor, handle=%s', clock_descr_handle) clock_descriptor = self._create_clock_descriptor_container( handle=clock_descr_handle, parent_handle=mds_container.Handle, @@ -41,38 +54,41 @@ def init_operations(self, sco): clock_state = self._mdib.data_model.mk_state_container(clock_descriptor) self._mdib.states.add_object(clock_state) - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): - if operation_descriptor_container.coding in (self.MDC_OP_SET_TIME_SYNC_REF_SRC.coding, self.OP_SET_NTP.coding): - self._logger.debug( - f'instantiating "set ntp server" operation from existing descriptor handle={operation_descriptor_container.Handle}') + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: + """Create operation handlers. + + Handle codes MDC_OP_SET_TIME_SYNC_REF_SRC, MDC_ACT_SET_TIME_ZONE. + """ + if operation_descriptor_container.coding == self.MDC_OP_SET_TIME_SYNC_REF_SRC.coding: + self._logger.debug('instantiating "set ntp server" operation from existing descriptor handle=%s', + operation_descriptor_container.Handle) set_ntp_operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_request_handler=self._set_ntp_string) + operation_handler=self._set_ntp_string) self._set_ntp_operations.append(set_ntp_operation) return set_ntp_operation - if operation_descriptor_container.coding in (self.MDC_ACT_SET_TIME_ZONE.coding, self.OP_SET_TZ.coding): - self._logger.debug( - f'instantiating "set time zone" operation from existing descriptor handle={operation_descriptor_container.Handle}') + if operation_descriptor_container.coding == self.MDC_ACT_SET_TIME_ZONE.coding: + self._logger.debug('instantiating "set time zone" operation from existing descriptor handle=%s', + operation_descriptor_container.Handle) set_tz_operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_request_handler=self._set_tz_string) + operation_handler=self._set_tz_string) self._set_tz_operations.append(set_tz_operation) return set_tz_operation - return None # ? + return None - def _set_ntp_string(self, operation_instance, soap_request, operation_request) -> list[str]: - """This is the handler for the set ntp server operation. - It sets the ReferenceSource value of clock state""" - value = operation_request.argument - pm_types = self._mdib.data_model.pm_types + def _set_ntp_string(self, params: ExecuteParameters) -> ExecuteResult: + """Set the ReferenceSource value of clock state (ExecuteHandler).""" + value = params.operation_request.argument pm_names = self._mdib.data_model.pm_names - operation_target_handle = self._get_operation_target_handle(operation_instance) - self._logger.info('set value {} from {} to {}', operation_target_handle, operation_instance.current_value, - value) + self._logger.info('set value %s from %s to %s', + params.operation_instance.operation_target_handle, + params.operation_instance.current_value, value) with self._mdib.transaction_manager() as mgr: - # state = mgr.getComponentState(operation_target_handle) - state = mgr.get_state(operation_target_handle) - if state.NODETYPE == pm_names.MdsState: + state = mgr.get_state(params.operation_instance.operation_target_handle) + if pm_names.MdsState == state.NODETYPE: mds_handle = state.DescriptorHandle mgr.unget_state(state) # look for the ClockState child @@ -80,21 +96,22 @@ def _set_ntp_string(self, operation_instance, soap_request, operation_request) - clock_descriptors = [c for c in clock_descriptors if c.parent_handle == mds_handle] if len(clock_descriptors) == 1: state = mgr.get_state(clock_descriptors[0].handle) - if state.NODETYPE != pm_names.ClockState: + if pm_names.ClockState != state.NODETYPE: raise ValueError(f'_set_ntp_string: expected ClockState, got {state.NODETYPE.localname}') state.ReferenceSource = [value] - return [operation_target_handle] + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) - def _set_tz_string(self, operation_instance, soap_request, operation_request) -> list[str]: - """This is the handler for the set time zone operation. - It sets the TimeZone value of clock state.""" - value = operation_request.argument + def _set_tz_string(self, params: ExecuteParameters) -> ExecuteResult: + """Set the TimeZone value of clock state (ExecuteHandler).""" + value = params.operation_request.argument pm_names = self._mdib.data_model.pm_names - operation_target_handle = self._get_operation_target_handle(operation_instance) - self._logger.info(f'set value {operation_target_handle} from {operation_instance.current_value} to {value}') + self._logger.info('set value %s from %s to %s', + params.operation_instance.operation_target_handle, + params.operation_instance.current_value, value) with self._mdib.transaction_manager() as mgr: - state = mgr.get_state(operation_target_handle) - if state.NODETYPE == pm_names.MdsState: + state = mgr.get_state(params.operation_instance.operation_target_handle) + if pm_names.MdsState == state.NODETYPE: mds_handle = state.DescriptorHandle mgr.unget_state(state) # look for the ClockState child @@ -103,17 +120,18 @@ def _set_tz_string(self, operation_instance, soap_request, operation_request) -> if len(clock_descriptors) == 1: state = mgr.get_state(clock_descriptors[0].handle) - if state.NODETYPE != pm_names.ClockState: + if pm_names.ClockState != state.NODETYPE: raise ValueError(f'_set_ntp_string: expected ClockState, got {state.NODETYPE.localname}') state.TimeZone = value - return [operation_target_handle] + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) def _create_clock_descriptor_container(self, handle: str, parent_handle: str, - coded_value, - safety_classification): - """ - This method creates a ClockDescriptorContainer with the given properties. + coded_value: CodedValue, + safety_classification: SafetyClassification) -> AbstractDescriptorProtocol: + """Create a ClockDescriptorContainer with the given properties. + :param handle: Handle of the new container :param parent_handle: Handle of the parent :param coded_value: a pmtypes.CodedValue instance that defines what this onject represents in medical terms. @@ -126,9 +144,15 @@ def _create_clock_descriptor_container(self, handle: str, class SDCClockProvider(GenericSDCClockProvider): - """This Implementation adds operations to mdib if they do not exist.""" + """SDCClockProvider adds SetString operations to set ntp server and time zone if they do not exist. + + This provider guarantees that there are SetString operations with codes "MDC_OP_SET_TIME_SYNC_REF_SRC" + and "MDC_ACT_SET_TIME_ZONE" if mdib contains a ClockDescriptor. It adds them to mdib if they do not exist. - def make_missing_operations(self, sco): + """ + + def make_missing_operations(self, sco: AbstractScoOperationsRegistry) -> list[OperationDefinitionBase]: + """Add operations to mdib if mdib contains a ClockDescriptor, but not the operations.""" pm_names = self._mdib.data_model.pm_names ops = [] operation_cls_getter = sco.operation_cls_getter @@ -136,25 +160,27 @@ def make_missing_operations(self, sco): mds_container = self._mdib.descriptions.NODETYPE.get_one(pm_names.MdsDescriptor) clock_descriptor = self._mdib.descriptions.NODETYPE.get_one(pm_names.ClockDescriptor, allow_none=True) + if clock_descriptor is None: + # there is no clock element in mdib, + return ops set_string_op_cls = operation_cls_getter(pm_names.SetStringOperationDescriptor) if not self._set_ntp_operations: - self._logger.debug(f'adding "set ntp server" operation, code = {nc.MDC_OP_SET_TIME_SYNC_REF_SRC}') - set_ntp_operation = self._mk_operation(set_string_op_cls, - handle='SET_NTP_SRV_' + mds_container.handle, - operation_target_handle=clock_descriptor.handle, - coded_value=self.MDC_OP_SET_TIME_SYNC_REF_SRC, - current_request_handler=self._set_ntp_string) + self._logger.debug('adding "set ntp server" operation, code = %r', + NomenclatureCodes.MDC_OP_SET_TIME_SYNC_REF_SRC) + set_ntp_operation = set_string_op_cls('SET_NTP_SRV_' + mds_container.handle, + clock_descriptor.handle, + self._set_ntp_string, + coded_value=self.MDC_OP_SET_TIME_SYNC_REF_SRC) self._set_ntp_operations.append(set_ntp_operation) ops.append(set_ntp_operation) if not self._set_tz_operations: - self._logger.debug(f'adding "set time zone" operation, code = {nc.MDC_ACT_SET_TIME_ZONE}') - set_tz_operation = self._mk_operation(set_string_op_cls, - handle='SET_TZONE_' + mds_container.handle, - operation_target_handle=clock_descriptor.handle, - coded_value=self.MDC_ACT_SET_TIME_ZONE, - current_request_handler=self._set_tz_string) + self._logger.debug('adding "set time zone" operation, code = %r', + NomenclatureCodes.MDC_ACT_SET_TIME_ZONE) + set_tz_operation = set_string_op_cls('SET_TZONE_' + mds_container.handle, + clock_descriptor.handle, + self._set_tz_string, + coded_value=self.MDC_ACT_SET_TIME_ZONE) self._set_tz_operations.append(set_tz_operation) ops.append(set_tz_operation) return ops - diff --git a/src/sdc11073/roles/componentprovider.py b/src/sdc11073/roles/componentprovider.py new file mode 100644 index 00000000..50892be7 --- /dev/null +++ b/src/sdc11073/roles/componentprovider.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sdc11073.provider.operations import ExecuteResult + +from . import providerbase + +if TYPE_CHECKING: + from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol + from sdc11073.provider.operations import OperationDefinitionBase + from sdc11073.provider.operations import ExecuteParameters + + from .providerbase import OperationClassGetter + + +class GenericSetComponentStateOperationProvider(providerbase.ProviderRole): + """Class is responsible for SetComponentState operations.""" + + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: + """Return an operation definition instance for this operation or None. + + Can handle following case: + operation_descriptor_container is a SetComponentStateOperationDescriptor and + target is any AbstractComponentDescriptor. + """ + pm_names = self._mdib.data_model.pm_names + operation_target_handle = operation_descriptor_container.OperationTarget + op_target_descriptor_container = self._mdib.descriptions.handle.get_one(operation_target_handle) + + if operation_descriptor_container.NODETYPE == pm_names.SetComponentStateOperationDescriptor: # noqa: SIM300 + if op_target_descriptor_container.NODETYPE in (pm_names.MdsDescriptor, + pm_names.ChannelDescriptor, + pm_names.VmdDescriptor, + pm_names.ClockDescriptor, + pm_names.ScoDescriptor, + ): + op_cls = operation_cls_getter(pm_names.SetComponentStateOperationDescriptor) + return op_cls(operation_descriptor_container.Handle, + operation_target_handle, + self._set_component_state, + coded_value=operation_descriptor_container.Type) + elif operation_descriptor_container.NODETYPE == pm_names.ActivateOperationDescriptor: # noqa: SIM300 + # on what can activate be called? + if op_target_descriptor_container.NODETYPE in (pm_names.MdsDescriptor, + pm_names.ChannelDescriptor, + pm_names.VmdDescriptor, + pm_names.ScoDescriptor, + ): + # no generic handler to be called! + op_cls = operation_cls_getter(pm_names.ActivateOperationDescriptor) + return op_cls(operation_descriptor_container.Handle, + operation_target_handle, + self._do_nothing, + coded_value=operation_descriptor_container.Type) + return None + + def _set_component_state(self, params: ExecuteParameters) -> ExecuteResult: + """Handle SetComponentState operation (ExecuteHandler).""" + value = params.operation_request.argument + # ToDo: consider ModifiableDate attribute + params.operation_instance.current_value = value + with self._mdib.transaction_manager() as mgr: + for proposed_state in value: + state = mgr.get_state(proposed_state.DescriptorHandle) + if state.is_component_state: + self._logger.info('updating %s with proposed component state', state) + state.update_from_other_container(proposed_state, + skipped_properties=['StateVersion', 'DescriptorVersion']) + else: + self._logger.warning( + '_set_component_state operation: ignore invalid referenced type %s in operation', + state.NODETYPE.localname) + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) + + def _do_nothing(self, params: ExecuteParameters) -> ExecuteResult: + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) diff --git a/src/sdc11073/roles/contextprovider.py b/src/sdc11073/roles/contextprovider.py index dd820208..6486b04a 100644 --- a/src/sdc11073/roles/contextprovider.py +++ b/src/sdc11073/roles/contextprovider.py @@ -1,21 +1,41 @@ +from __future__ import annotations + import time +from typing import TYPE_CHECKING + +from sdc11073.provider.operations import ExecuteResult from . import providerbase +if TYPE_CHECKING: + from lxml.etree import QName + + from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol + from sdc11073.mdib.providermdib import ProviderMdib + from sdc11073.provider.operations import OperationDefinitionBase + from sdc11073.provider.operations import ExecuteParameters + + from .providerbase import OperationClassGetter + class GenericContextProvider(providerbase.ProviderRole): - """ Handles SetContextState operations""" + """Handles SetContextState operations.""" - def __init__(self, mdib, op_target_descr_types=None, forced_new_state_typ=None, log_prefix=None): + def __init__(self, mdib: ProviderMdib, + op_target_descr_types: list[QName] | None = None, + log_prefix: str | None = None): super().__init__(mdib, log_prefix) self._op_target_descr_types = op_target_descr_types - self._forced_new_state_type = forced_new_state_typ - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): - """Create a handler for SetContextStateOperationDescriptor if type of operation target - matches opTargetDescriptorTypes""" + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: + """Create an OperationDefinition for SetContextStateOperationDescriptor. + + Only if type of operation target matches opTargetDescriptorTypes. + """ pm_names = self._mdib.data_model.pm_names - if operation_descriptor_container.NODETYPE == pm_names.SetContextStateOperationDescriptor: + if pm_names.SetContextStateOperationDescriptor == operation_descriptor_container.NODETYPE: op_target_descr_container = self._mdib.descriptions.handle.get_one( operation_descriptor_container.OperationTarget) if (not self._op_target_descr_types) or ( @@ -23,20 +43,19 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ return None # we do not handle this target type return self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_request_handler=self._set_context_state) + operation_handler=self._set_context_state) return None - def _set_context_state(self, operation_instance, request, operation_request) -> list[str]: - """ This is the code that executes the operation itself. - """ - proposed_context_states = operation_request.argument + def _set_context_state(self, params: ExecuteParameters) -> ExecuteResult: + """Execute the operation itself (ExecuteHandler).""" + proposed_context_states = params.operation_request.argument pm_types = self._mdib.data_model.pm_types + operation_target_handles = [] with self._mdib.transaction_manager() as mgr: for proposed_st in proposed_context_states: old_state_container = None if proposed_st.DescriptorHandle != proposed_st.Handle: # this is an update for an existing state - # old_state_container = operation_instance.operation_target_storage.handle.get_one( old_state_container = self._mdib.context_states.handle.get_one( proposed_st.Handle, allow_none=True) if old_state_container is None: @@ -46,27 +65,32 @@ def _set_context_state(self, operation_instance, request, operation_request) -> # create a new unique handle handle_string = f'{proposed_st.DescriptorHandle}_{self._mdib.mdib_version}' proposed_st.Handle = handle_string + operation_target_handles.append(proposed_st.Handle) proposed_st.BindingMdibVersion = self._mdib.mdib_version proposed_st.BindingStartTime = time.time() proposed_st.ContextAssociation = pm_types.ContextAssociation.ASSOCIATED - self._logger.info('new {}, handle={}', proposed_st.NODETYPE.localname, proposed_st.Handle) + self._logger.info('new %s, DescriptorHandle=%s Handle=%s', + proposed_st.NODETYPE.localname, proposed_st.DescriptorHandle, proposed_st.Handle) mgr.add_state(proposed_st) # find all associated context states, disassociate them, set unbinding info, and add them to updates - # old_state_containers = operation_instance.operation_target_storage.descriptor_handle.get( - old_state_containers = self._mdib.context_states.handle.get_one( + old_state_containers = self._mdib.context_states.descriptor_handle.get( proposed_st.DescriptorHandle, []) for old_state in old_state_containers: - if old_state.ContextAssociation != pm_types.ContextAssociation.DISASSOCIATED or old_state.UnbindingMdibVersion is None: + if old_state.ContextAssociation != pm_types.ContextAssociation.DISASSOCIATED \ + or old_state.UnbindingMdibVersion is None: + self._logger.info('disassociate %s, handle=%s', old_state.NODETYPE.localname, + old_state.Handle) new_state = mgr.get_context_state(old_state.Handle) new_state.ContextAssociation = pm_types.ContextAssociation.DISASSOCIATED if new_state.UnbindingMdibVersion is None: new_state.UnbindingMdibVersion = self._mdib.mdib_version new_state.BindingEndTime = time.time() + operation_target_handles.append(new_state.Handle) else: # this is an update to an existing patient # use "regular" way to update via transaction manager - self._logger.info('update {}, handle={}', proposed_st.NODETYPE.localname, proposed_st.Handle) + self._logger.info('update %s, handle=%s', proposed_st.NODETYPE.localname, proposed_st.Handle) tmp = mgr.get_context_state(proposed_st.Handle) tmp.update_from_other_container(proposed_st, skipped_properties=['ContextAssociation', 'BindingMdibVersion', @@ -74,17 +98,29 @@ def _set_context_state(self, operation_instance, request, operation_request) -> 'BindingStartTime', 'BindingEndTime', 'StateVersion']) + operation_target_handles.append(proposed_st.Handle) + if len(operation_target_handles) == 1: + return ExecuteResult(operation_target_handles[0], + self._mdib.data_model.msg_types.InvocationState.FINISHED) + # the operation manipulated more than one context state, but the operation can only return a single handle. + # (that is a BICEPS shortcoming, the string return type only reflects that situation). + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) class EnsembleContextProvider(GenericContextProvider): - def __init__(self, mdib, log_prefix): + """EnsembleContextProvider.""" + + def __init__(self, mdib: ProviderMdib, log_prefix: str | None = None): super().__init__(mdib, op_target_descr_types=[mdib.data_model.pm_names.EnsembleContextDescriptor], log_prefix=log_prefix) class LocationContextProvider(GenericContextProvider): - def __init__(self, mdib, log_prefix): + """LocationContextProvider.""" + + def __init__(self, mdib: ProviderMdib, log_prefix: str | None = None): super().__init__(mdib, op_target_descr_types=[mdib.data_model.pm_names.LocationContextDescriptor], log_prefix=log_prefix) diff --git a/src/sdc11073/roles/metricprovider.py b/src/sdc11073/roles/metricprovider.py index 4761471e..eb45cf83 100644 --- a/src/sdc11073/roles/metricprovider.py +++ b/src/sdc11073/roles/metricprovider.py @@ -1,91 +1,103 @@ -from .providerbase import ProviderRole +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from sdc11073.mdib.statecontainers import AbstractMetricStateContainer +from sdc11073.provider.operations import ExecuteResult from sdc11073.xml_types.pm_types import ComponentActivation +from .providerbase import ProviderRole + +if TYPE_CHECKING: + from collections.abc import Iterable + + from sdc11073.mdib import ProviderMdib + from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol + from sdc11073.mdib.transactions import TransactionManagerProtocol, _TrItem + from sdc11073.provider.operations import OperationDefinitionBase + from sdc11073.provider.operations import ExecuteParameters + + from .providerbase import OperationClassGetter + class GenericMetricProvider(ProviderRole): - """ Always added operations: None + """Generic Handler. + This is a generic Handler for - SetValueOperation on numeric metrics - SetStringOperation on (enum) string metrics """ - def __init__(self, data_model, activation_state_can_remove_metric_value=True, log_prefix=None): - ''' - - :param activation_state_can_remove_metric_value: if True, SF717 is handled - SF717: A Metric Provider shall not provide a MetricValue if the ActivationState = Shtdn|Off|Fail. - ''' - super().__init__(data_model, log_prefix) + def __init__(self, mdib: ProviderMdib, + activation_state_can_remove_metric_value: bool = True, + log_prefix: str | None = None): + """Create a GenericMetricProvider.""" + super().__init__(mdib, log_prefix) self.activation_state_can_remove_metric_value = activation_state_can_remove_metric_value - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): - ''' Can handle following cases: - SetValueOperation, target = NumericMetricDescriptor: => handler = _set_numeric_value - SetStringOperation, target = (Enum)StringMetricDescriptor: => handler = _set_string - SetMetricStateOperationDescriptor, target = any subclass of AbstractMetricDescriptor: => handler = _set_metric_state - ''' + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: + """Create an OperationDefinition for SetContextStateOperationDescriptor. + + Handle following cases: + SetValueOperation, target = NumericMetricDescriptor: + => handler = _set_numeric_value + SetStringOperation, target = (Enum)StringMetricDescriptor: + => handler = _set_string + SetMetricStateOperationDescriptor, target = any subclass of AbstractMetricDescriptor: + => handler = _set_metric_state + """ pm_names = self._mdib.data_model.pm_names operation_target_handle = operation_descriptor_container.OperationTarget op_target_descriptor_container = self._mdib.descriptions.handle.get_one(operation_target_handle) - # if op_target_descriptor_container.NODETYPE not in (pm_names.StringMetricDescriptor, - # pm_names.EnumStringMetricDescriptor, - # pm_names.NumericMetricDescriptor, - # pm_names.RealTimeSampleArrayMetricDescriptor): - # return None # this is not metric provider role - - if operation_descriptor_container.NODETYPE == pm_names.SetValueOperationDescriptor: - if op_target_descriptor_container.NODETYPE == pm_names.NumericMetricDescriptor: + if operation_descriptor_container.NODETYPE == pm_names.SetValueOperationDescriptor: # noqa: SIM300 + if op_target_descriptor_container.NODETYPE == pm_names.NumericMetricDescriptor: # noqa: SIM300 op_cls = operation_cls_getter(pm_names.SetValueOperationDescriptor) - return self._mk_operation(op_cls, - handle=operation_descriptor_container.Handle, - operation_target_handle=operation_target_handle, - coded_value=operation_descriptor_container.Type, - current_request_handler=self._set_numeric_value) + return op_cls(operation_descriptor_container.Handle, + operation_target_handle, + self._set_numeric_value, + coded_value=operation_descriptor_container.Type) return None - if operation_descriptor_container.NODETYPE == pm_names.SetStringOperationDescriptor: + if operation_descriptor_container.NODETYPE == pm_names.SetStringOperationDescriptor: # noqa: SIM300 if op_target_descriptor_container.NODETYPE in (pm_names.StringMetricDescriptor, pm_names.EnumStringMetricDescriptor): op_cls = operation_cls_getter(pm_names.SetStringOperationDescriptor) - return self._mk_operation(op_cls, - handle=operation_descriptor_container.Handle, - operation_target_handle=operation_target_handle, - coded_value=operation_descriptor_container.Type, - current_request_handler=self._set_string) + return op_cls(operation_descriptor_container.Handle, + operation_target_handle, + self._set_string, + coded_value=operation_descriptor_container.Type) return None - if operation_descriptor_container.NODETYPE == pm_names.SetMetricStateOperationDescriptor: + if operation_descriptor_container.NODETYPE == pm_names.SetMetricStateOperationDescriptor: # noqa: SIM300 op_cls = operation_cls_getter(pm_names.SetMetricStateOperationDescriptor) - operation = self._mk_operation(op_cls, - handle=operation_descriptor_container.Handle, - operation_target_handle=operation_target_handle, - coded_value=operation_descriptor_container.Type, - current_request_handler=self._set_metric_state) - return operation + return op_cls(operation_descriptor_container.Handle, + operation_target_handle, + self._set_metric_state, + coded_value=operation_descriptor_container.Type) return None - def _set_metric_state(self, operation_instance, soap_request, operation_request) -> list[str]: - ''' - - :param operation_instance: the operation - :param value: a list of proposed metric states - :return: - ''' + def _set_metric_state(self, params: ExecuteParameters) -> ExecuteResult: + """Handle SetMetricState calls (ExecuteHandler).""" # ToDo: consider ModifiableDate attribute - proposed_states = operation_request.argument - operation_instance.current_value = proposed_states + proposed_states = params.operation_request.argument + params.operation_instance.current_value = proposed_states with self._mdib.transaction_manager() as mgr: for proposed_state in proposed_states: state = mgr.get_state(proposed_state.DescriptorHandle) if state.is_metric_state: - self._logger.info('updating {} with proposed metric state', state) + self._logger.info('updating %s with proposed metric state', state) state.update_from_other_container(proposed_state, skipped_properties=['StateVersion', 'DescriptorVersion']) else: - self._logger.warn('_set_metric_state operation: ignore invalid referenced type {} in operation', - state.NODETYPE) - return [tmp.DescriptorHandle for tmp in proposed_states] # this is a shortcoming of BICEPS: + self._logger.warning('_set_metric_state operation: ignore invalid referenced type %s in operation', + state.NODETYPE) + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) - def on_pre_commit(self, mdib, transaction): + def on_pre_commit(self, mdib: ProviderMdib, # noqa: ARG002 + transaction: TransactionManagerProtocol): + """Set state.MetricValue to None if state.ActivationState requires this.""" if not self.activation_state_can_remove_metric_value: return if transaction.metric_state_updates: @@ -93,10 +105,10 @@ def on_pre_commit(self, mdib, transaction): if transaction.rt_sample_state_updates: self._handle_metrics_component_activation(transaction.rt_sample_state_updates.values()) - def _handle_metrics_component_activation(self, metric_state_updates): - # check if MetricValue shall be removed + def _handle_metrics_component_activation(self, metric_state_updates: Iterable[_TrItem]): + """Check if MetricValue shall be removed.""" for tr_item in metric_state_updates: - new_state = tr_item.new + new_state = cast(AbstractMetricStateContainer, tr_item.new) if new_state is None or not new_state.is_metric_state: continue # SF717: check if MetricValue shall be automatically removed @@ -105,6 +117,6 @@ def _handle_metrics_component_activation(self, metric_state_updates): ComponentActivation.FAILURE): if new_state.MetricValue is not None: # remove metric value - self._logger.info('{}: remove metric value because ActivationState="{}", handle="{}"', + self._logger.info('%s: remove metric value because ActivationState="%s", handle="%s"', self.__class__.__name__, new_state.ActivationState, new_state.DescriptorHandle) new_state.MetricValue = None diff --git a/src/sdc11073/roles/nomenclature.py b/src/sdc11073/roles/nomenclature.py index ab22b5c7..45a104db 100644 --- a/src/sdc11073/roles/nomenclature.py +++ b/src/sdc11073/roles/nomenclature.py @@ -1,12 +1,10 @@ -""" -A minimal set of codes, used by role provider. -""" +"""A minimal set of codes, used by role provider.""" + class NomenclatureCodes: - # only a small subset of all codes, these are used for tests - MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE = '128284' # An operation to initiate global all audio pause - MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE = '128285' # An operation to cancel global all audio pause - MDC_OP_SET_TIME_SYNC_REF_SRC = '128505' # An operation to Set the active reference source of a clock for time synchronization. - OP_SET_NTP = '194041' # deprecated REF_ID use MDC_OP_SET_TIME_SYNC_REF_SRC, needed for backwards compatibility - OP_SET_TZ = '194040' # deprecated REF_ID use MDC_ACT_SET_TIME_ZONE, needed for backwards compatibility - MDC_ACT_SET_TIME_ZONE = '68632' # replaces private Code MDC_OP_SET_TIME_ZONE (194040) + """Codes that are used by included role providers.""" + + MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE = '128284' + MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE = '128285' + MDC_OP_SET_TIME_SYNC_REF_SRC = '128505' + MDC_ACT_SET_TIME_ZONE = '68632' diff --git a/src/sdc11073/roles/operationprovider.py b/src/sdc11073/roles/operationprovider.py index 6f6001fb..2b866776 100644 --- a/src/sdc11073/roles/operationprovider.py +++ b/src/sdc11073/roles/operationprovider.py @@ -2,5 +2,7 @@ class OperationProvider(providerbase.ProviderRole): - """Handles operations that work on operation states. - Empty implementation, not needed/used for sdc11073 tests""" + """Handle operations that work on operation states. + + Empty implementation, not needed/used for sdc11073 tests. + """ diff --git a/src/sdc11073/roles/patientcontextprovider.py b/src/sdc11073/roles/patientcontextprovider.py index 80e9cd5a..10fe4e89 100644 --- a/src/sdc11073/roles/patientcontextprovider.py +++ b/src/sdc11073/roles/patientcontextprovider.py @@ -1,45 +1,65 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from .contextprovider import GenericContextProvider +if TYPE_CHECKING: + from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol + from sdc11073.mdib.providermdib import ProviderMdib + from sdc11073.provider.operations import OperationDefinitionBase + from sdc11073.provider.sco import AbstractScoOperationsRegistry + + from .providerbase import OperationClassGetter + class GenericPatientContextProvider(GenericContextProvider): + """Example for handling of SetContextState operations. + + This Provider instantiates a SetContextState operation if the operation target is a PatientContextDescriptor. + Nothing is added to the mdib. If the mdib does not contain these operations, the functionality is not available. + """ - def __init__(self, mdib, log_prefix): - super().__init__(mdib, log_prefix) + def __init__(self, mdib: ProviderMdib, log_prefix: str | None): + super().__init__(mdib, log_prefix=log_prefix) self._patient_context_descriptor_container = None self._set_patient_context_operations = [] - def init_operations(self, sco): + def init_operations(self, sco: AbstractScoOperationsRegistry): + """Find the PatientContextDescriptor.""" super().init_operations(sco) - # expecting exactly one PatientContextDescriptor pm_names = self._mdib.data_model.pm_names descriptor_containers = self._mdib.descriptions.NODETYPE.get(pm_names.PatientContextDescriptor) if descriptor_containers is not None and len(descriptor_containers) == 1: self._patient_context_descriptor_container = descriptor_containers[0] - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): - if self._patient_context_descriptor_container and operation_descriptor_container.OperationTarget == self._patient_context_descriptor_container.Handle: + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: + """Add Operation Handler if operation target is the previously found PatientContextDescriptor.""" + if self._patient_context_descriptor_container and \ + operation_descriptor_container.OperationTarget == self._patient_context_descriptor_container.Handle: pc_operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_request_handler=self._set_context_state) + operation_handler=self._set_context_state) self._set_patient_context_operations.append(pc_operation) return pc_operation return None class PatientContextProvider(GenericPatientContextProvider): - """This Implementation adds operations to mdib if they do not exist.""" + """PatientContextProvider adds operations to mdib if they do not exist.""" - def make_missing_operations(self, sco): + def make_missing_operations(self, sco: AbstractScoOperationsRegistry) -> list[OperationDefinitionBase]: + """Add operation to mdib if it does not exist.""" pm_names = self._mdib.data_model.pm_names ops = [] operation_cls_getter = sco.operation_cls_getter if self._patient_context_descriptor_container and not self._set_patient_context_operations: - set_context_state_op_cls = operation_cls_getter(pm_names.SetContextStateOperationDescriptor) - - pc_operation = self._mk_operation(set_context_state_op_cls, - handle='opSetPatCtx', - operation_target_handle=self._patient_context_descriptor_container.handle, - coded_value=None, - current_request_handler=self._set_context_state) + op_cls = operation_cls_getter(pm_names.SetContextStateOperationDescriptor) + pc_operation = op_cls('opSetPatCtx', + self._patient_context_descriptor_container.handle, + self._set_context_state, + coded_value=None) ops.append(pc_operation) return ops diff --git a/src/sdc11073/roles/product.py b/src/sdc11073/roles/product.py index bad672b5..05895a98 100644 --- a/src/sdc11073/roles/product.py +++ b/src/sdc11073/roles/product.py @@ -1,112 +1,48 @@ -from . import alarmprovider -from . import clockprovider -from . import contextprovider -from . import metricprovider -from . import operationprovider -from . import patientcontextprovider -from . import providerbase -from .audiopauseprovider import AudioPauseProvider -from .. import loghelper +from __future__ import annotations +from typing import TYPE_CHECKING -class GenericSetComponentStateOperationProvider(providerbase.ProviderRole): - """ - Responsible for SetComponentState Operations - """ +from sdc11073 import loghelper - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): - """ Can handle following cases: - SetComponentStateOperationDescriptor, target = any AbstractComponentDescriptor: => handler = _set_component_state - """ - pm_names = self._mdib.data_model.pm_names - operation_target_handle = operation_descriptor_container.OperationTarget - op_target_descriptor_container = self._mdib.descriptions.handle.get_one(operation_target_handle) - - if operation_descriptor_container.NODETYPE == pm_names.SetComponentStateOperationDescriptor: - if op_target_descriptor_container.NODETYPE in (pm_names.MdsDescriptor, - pm_names.ChannelDescriptor, - pm_names.VmdDescriptor, - pm_names.ClockDescriptor, - pm_names.ScoDescriptor, - ): - op_cls = operation_cls_getter(pm_names.SetComponentStateOperationDescriptor) - operation = self._mk_operation(op_cls, - handle=operation_descriptor_container.Handle, - operation_target_handle=operation_target_handle, - coded_value=operation_descriptor_container.Type, - current_request_handler=self._set_component_state) - return operation - elif operation_descriptor_container.NODETYPE == pm_names.ActivateOperationDescriptor: - # on what can activate be called? - if op_target_descriptor_container.NODETYPE in (pm_names.MdsDescriptor, - pm_names.ChannelDescriptor, - pm_names.VmdDescriptor, - pm_names.ScoDescriptor, - ): - # no generic handler to be called! - op_cls = operation_cls_getter(pm_names.ActivateOperationDescriptor) - return self._mk_operation(op_cls, - handle=operation_descriptor_container.Handle, - operation_target_handle=operation_target_handle, - coded_value=operation_descriptor_container.Type, - current_request_handler=self._do_nothing) - return None +from . import alarmprovider, clockprovider, contextprovider, metricprovider, operationprovider, patientcontextprovider +from .audiopauseprovider import AudioPauseProvider +from .componentprovider import GenericSetComponentStateOperationProvider - def _set_component_state(self, operation_instance, soap_request, operation_request) -> list[str]: - """ - - :param operation_instance: the operation - :param value: a list of proposed metric states - :return: - """ - value = operation_request.argument - # ToDo: consider ModifiableDate attribute - operation_instance.current_value = value - with self._mdib.transaction_manager() as mgr: - for proposed_state in value: - state = mgr.get_state(proposed_state.DescriptorHandle) - if state.is_component_state: - self._logger.info('updating {} with proposed component state', state) - state.update_from_other_container(proposed_state, - skipped_properties=['StateVersion', 'DescriptorVersion']) - else: - self._logger.warn('_set_component_state operation: ignore invalid referenced type {} in operation', - state.NODETYPE) - - def _do_nothing(self, operation_instance, soap_request, operation_request) -> list[str]: - return [] +if TYPE_CHECKING: + from sdc11073.mdib import ProviderMdib + from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol + from sdc11073.mdib.transactions import TransactionManagerProtocol + from sdc11073.provider.operations import OperationDefinitionBase + from sdc11073.provider.sco import AbstractScoOperationsRegistry + from .providerbase import OperationClassGetter, ProviderRole class BaseProduct: - """A Product is associated to a single sco. If a mdib contains multiple sco instances, - there will be multiple Products.""" + """A Product is associated to a single sco. - def __init__(self, mdib, sco, log_prefix): - """ + It provides the operation handlers for the operations in this sco. + If a mdib contains multiple sco instances, there must be multiple Products. + """ - :param mdib: the device mdib - 'param sco: sco of device - :param log_prefix: str - """ + def __init__(self, + mdib: ProviderMdib, + sco: AbstractScoOperationsRegistry, + log_prefix: str | None = None): + """Create a product.""" self._sco = sco self._mdib = mdib self._model = mdib.data_model - self._ordered_providers = [] # order matters, each provider can hide operations of later ones + self._ordered_providers: list[ProviderRole] = [] # order matters, each provider can hide operations of later ones # start with most specific providers, end with most general ones self._logger = loghelper.get_logger_adapter(f'sdc.device.{self.__class__.__name__}', log_prefix) - def _all_providers_sorted(self): + def _all_providers_sorted(self) -> list[ProviderRole]: return self._ordered_providers - @staticmethod - def _without_none_values(some_list): - return [e for e in some_list if e is not None] - def init_operations(self): - """ register all actively provided operations """ - pm_names = self._model.pm_names + """Register all actively provided operations.""" sco_handle = self._sco.sco_descriptor_container.Handle - self._logger.info('init_operations for sco {}.', sco_handle) + self._logger.info('init_operations for sco %s.', sco_handle) for role_handler in self._all_providers_sorted(): role_handler.init_operations(self._sco) @@ -117,7 +53,7 @@ def init_operations(self): operations = role_handler.make_missing_operations(self._sco) if operations: info = ', '.join([f'{op.OP_DESCR_QNAME.localname} {op.handle}' for op in operations]) - self._logger.info('role handler {} added operations to mdib: {}', + self._logger.info('role handler %s added operations to mdib: %s', role_handler.__class__.__name__, info) for operation in operations: self._sco.register_operation(operation) @@ -128,64 +64,83 @@ def init_operations(self): self._sco.get_operation_by_handle(op_h) is None] if not all_op_handles: - self._logger.info('sco {} has no operations in mdib.', sco_handle) + self._logger.info('sco %s has no operations in mdib.', sco_handle) elif all_not_registered_op_handles: - self._logger.info('sco {} has operations without handler! handles = {}', + self._logger.info('sco %s has operations without handler! handles = %s', sco_handle, all_not_registered_op_handles) else: - self._logger.info('sco {}: all operations have a handler.', sco_handle) + self._logger.info('sco %s: all operations have a handler.', sco_handle) self._mdib.xtra.mk_state_containers_for_all_descriptors() self._mdib.pre_commit_handler = self._on_pre_commit self._mdib.post_commit_handler = self._on_post_commit def stop(self): + """Call stop methods of all role providers.""" for role_handler in self._all_providers_sorted(): role_handler.stop() - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): - """ try to get an operation for this operation_descriptor_container ( given in mdib) """ + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: + """Try to get an operation for this operation_descriptor_container ( given in mdib).""" operation_target_handle = operation_descriptor_container.OperationTarget operation_target_descr = self._mdib.descriptions.handle.get_one(operation_target_handle, allow_none=True) # descriptor container if operation_target_descr is None: # this operation is incomplete, the operation target does not exist. Registration not possible. - self._logger.warn( - f'Operation {operation_descriptor_container.Handle}: ' - f'target {operation_target_handle} does not exist, will not register operation') + self._logger.warning('Operation %s: target %s does not exist, will not register operation', + operation_descriptor_container.Handle, operation_target_handle) return None for role_handler in self._all_providers_sorted(): operation = role_handler.make_operation_instance(operation_descriptor_container, operation_cls_getter) if operation is not None: - self._logger.debug( - f'{role_handler.__class__.__name__} provided operation for {operation_descriptor_container}') + self._logger.debug('%s provided operation for %r', + role_handler.__class__.__name__, operation_descriptor_container) return operation - self._logger.debug(f'{role_handler.__class__.__name__}: no handler for {operation_descriptor_container}') + self._logger.debug('%s: no handler for %r', + role_handler.__class__.__name__, operation_descriptor_container) return None - def _register_existing_mdib_operations(self, sco): + def _register_existing_mdib_operations(self, sco: AbstractScoOperationsRegistry): operation_descriptor_containers = self._mdib.descriptions.parent_handle.get( self._sco.sco_descriptor_container.Handle, []) for descriptor in operation_descriptor_containers: registered_op = sco.get_operation_by_handle(descriptor.Handle) if registered_op is None: - self._logger.debug( - f'found unregistered {descriptor.NODETYPE.localname} in mdib, handle={descriptor.Handle}, ' - f'code={descriptor.Type} target={descriptor.OperationTarget}') + self._logger.debug('found unregistered %s in mdib, handle=%s, code=%s target=%s', + descriptor.NODETYPE.localname, descriptor.Handle, descriptor.Type, + descriptor.OperationTarget) operation = self.make_operation_instance(descriptor, sco.operation_cls_getter) if operation is not None: sco.register_operation(operation) - def _on_pre_commit(self, mdib, transaction): + def _on_pre_commit(self, mdib: ProviderMdib, transaction: TransactionManagerProtocol): for provider in self._all_providers_sorted(): provider.on_pre_commit(mdib, transaction) - def _on_post_commit(self, mdib, transaction): + def _on_post_commit(self, mdib: ProviderMdib, transaction: TransactionManagerProtocol): for provider in self._all_providers_sorted(): provider.on_post_commit(mdib, transaction) class GenericProduct(BaseProduct): - def __init__(self, mdib, sco, audio_pause_provider, day_night_provider, clock_provider, log_prefix): + """GenericProduct instantiates several roles. + + Instantiated fixed role providers: + - GenericPatientContextProvider + - GenericAlarmProvider + - GenericMetricProvider + - OperationProvider + - GenericSetComponentStateOperationProvider + Some more providers are provided in constructor. + """ + + def __init__(self, mdib: ProviderMdib, # noqa: PLR0913 + sco: AbstractScoOperationsRegistry, + audio_pause_provider: ProviderRole, + day_night_provider: ProviderRole, + clock_provider: ProviderRole, + log_prefix: str | None = None): super().__init__(mdib, sco, log_prefix) self._ordered_providers.extend([audio_pause_provider, day_night_provider, clock_provider]) @@ -194,12 +149,26 @@ def __init__(self, mdib, sco, audio_pause_provider, day_night_provider, clock_pr alarmprovider.GenericAlarmProvider(mdib, log_prefix=log_prefix), metricprovider.GenericMetricProvider(mdib, log_prefix=log_prefix), operationprovider.OperationProvider(mdib, log_prefix=log_prefix), - GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix) + GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix), ]) class MinimalProduct(BaseProduct): - def __init__(self, mdib, sco, log_prefix=None): + """MinimalProduct instantiates several roles. + + Instantiated fixed role providers: + - AudioPauseProvider + - GenericPatientContextProvider + - GenericAlarmProvider + - GenericMetricProvider + - OperationProvider + - GenericSetComponentStateOperationProvider + """ + + def __init__(self, + mdib: ProviderMdib, + sco: AbstractScoOperationsRegistry, + log_prefix: str | None = None): super().__init__(mdib, sco, log_prefix) self.metric_provider = metricprovider.GenericMetricProvider(mdib, log_prefix=log_prefix) # needed in a test self._ordered_providers.extend([AudioPauseProvider(mdib, log_prefix=log_prefix), @@ -209,12 +178,29 @@ def __init__(self, mdib, sco, log_prefix=None): alarmprovider.GenericAlarmProvider(mdib, log_prefix=log_prefix), self.metric_provider, operationprovider.OperationProvider(mdib, log_prefix=log_prefix), - GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix) + GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix), ]) class ExtendedProduct(MinimalProduct): - def __init__(self, mdib, sco, log_prefix=None): + """ExtendedProduct instantiates several roles. + + Instantiated fixed role providers: + - AudioPauseProvider + - GenericSDCClockProvider + - EnsembleContextProvider + - LocationContextProvider + - GenericPatientContextProvider + - GenericAlarmProvider + - GenericMetricProvider + - OperationProvider + - GenericSetComponentStateOperationProvider + """ + + def __init__(self, + mdib: ProviderMdib, + sco: AbstractScoOperationsRegistry, + log_prefix: str | None = None): super().__init__(mdib, sco, log_prefix) self._ordered_providers.extend([AudioPauseProvider(mdib, log_prefix=log_prefix), clockprovider.GenericSDCClockProvider(mdib, log_prefix=log_prefix), @@ -225,5 +211,5 @@ def __init__(self, mdib, sco, log_prefix=None): alarmprovider.GenericAlarmProvider(mdib, log_prefix=log_prefix), self.metric_provider, operationprovider.OperationProvider(mdib, log_prefix=log_prefix), - GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix) + GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix), ]) diff --git a/src/sdc11073/roles/providerbase.py b/src/sdc11073/roles/providerbase.py index 4dd6ca6f..9f1d6377 100644 --- a/src/sdc11073/roles/providerbase.py +++ b/src/sdc11073/roles/providerbase.py @@ -1,138 +1,145 @@ -from functools import partial +from __future__ import annotations -from .. import loghelper -from .. import observableproperties as properties +from typing import TYPE_CHECKING, Callable, cast + +from lxml.etree import QName + +from sdc11073 import loghelper +from sdc11073.mdib.statecontainers import MetricStateProtocol +from sdc11073.provider.operations import ExecuteResult, OperationDefinitionBase + +if TYPE_CHECKING: + from sdc11073.mdib import ProviderMdib + from sdc11073.mdib.descriptorcontainers import AbstractDescriptorProtocol, AbstractOperationDescriptorProtocol + from sdc11073.mdib.transactions import TransactionManagerProtocol + from sdc11073.provider.operations import ExecuteHandler, TimeoutHandler + from sdc11073.provider.sco import AbstractScoOperationsRegistry + from sdc11073.xml_types.pm_types import CodedValue, SafetyClassification + from sdc11073.provider.operations import ExecuteParameters + +OperationClassGetter = Callable[[QName], type[OperationDefinitionBase]] class ProviderRole: - def __init__(self, mdib, log_prefix): + """Base class for all role implementations.""" + + def __init__(self, mdib: ProviderMdib, log_prefix: str): self._mdib = mdib self._logger = loghelper.get_logger_adapter(f'sdc.device.{self.__class__.__name__}', log_prefix) def stop(self): - """ if provider uses worker threads, implement stop method""" + """Stop whatever needs to be stopped. + + Implement method in derived class if needed. + """ + + def init_operations(self, sco: AbstractScoOperationsRegistry): + """Initialize and start whatever the provider needs. + + Implement method in derived class if needed. + """ - def init_operations(self, sco): - pass + def make_operation_instance( + self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, # noqa: ARG002 + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: # noqa: ARG002 + """Return an operation definition instance for this operation or None. - def make_operation_instance(self, operation_descriptor_container, # pylint: disable=unused-argument,no-self-use - operation_cls_getter): # pylint: disable=unused-argument - """returns a callable for this operation or None. If a mdib already has operations defined, this method can connect a handler to a given operation descriptor. - Use case: initialization from an existing mdib""" + Use case: initialization from an existing mdib. + """ return None - def make_missing_operations(self, sco): # pylint: disable=unused-argument, no-self-use - """ + def make_missing_operations(self, sco: AbstractScoOperationsRegistry) -> list[ # noqa: ARG002 + OperationDefinitionBase]: + """Create operations that this role provider needs. + This method is called after all existing operations from mdib have been registered. If a role provider needs to add operations beyond that, it can do it here. - :return: [] """ return [] - def on_pre_commit(self, mdib, transaction): - pass + def on_pre_commit(self, mdib: ProviderMdib, transaction: TransactionManagerProtocol): + """Manipulate the transaction if needed. + + Derived classes can overwrite this method. + """ + + def on_post_commit(self, mdib: ProviderMdib, transaction: TransactionManagerProtocol): + """Run stuff after transaction. - def on_post_commit(self, mdib, transaction): - pass + Derived classes can overwrite this method. + """ - def _set_numeric_value(self, operation_instance, operation_request): - """ sets a numerical metric value""" + def _set_numeric_value(self, params: ExecuteParameters) -> ExecuteResult: + """Set a numerical metric value (ExecuteHandler).""" + value = params.operation_request.argument pm_types = self._mdib.data_model.pm_types - operation_target_handle = self._get_operation_target_handle(operation_instance) - self._logger.info('set value of {} via {} from {} to {}', operation_target_handle, operation_instance.handle, - operation_instance.current_value, value) - operation_instance.current_value = value + self._logger.info('set value of %s via %s from %r to %r', + params.operation_instance.operation_target_handle, + params.operation_instance.handle, + params.operation_instance.current_value, value) + params.operation_instance.current_value = value with self._mdib.transaction_manager() as mgr: - # state = mgr.getMetricState(operation_target_handle) - state = mgr.get_state(operation_target_handle) + _state = mgr.get_state(params.operation_instance.operation_target_handle) + state = cast(MetricStateProtocol, _state) if state.MetricValue is None: state.mk_metric_value() state.MetricValue.Value = value # SF1823: For Metrics with the MetricCategory = Set|Preset that are being modified as a result of a # SetValue or SetString operation a Metric Provider shall set the MetricQuality / Validity = Vld. - metric_descriptor_container = self._mdib.descriptions.handle.get_one(operation_target_handle) + metric_descriptor_container = self._mdib.descriptions.handle.get_one( + params.operation_instance.operation_target_handle) if metric_descriptor_container.MetricCategory in (pm_types.MetricCategory.SETTING, pm_types.MetricCategory.PRESETTING): state.MetricValue.Validity = pm_types.MeasurementValidity.VALID + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) - def _set_string(self, operation_instance, operation_request): - """ sets a string value""" + def _set_string(self, params: ExecuteParameters) -> ExecuteResult: + """Set a string value (ExecuteHandler).""" + value = params.operation_request.argument pm_types = self._mdib.data_model.pm_types - operation_target_handle = self._get_operation_target_handle(operation_instance) - self._logger.info('set value {} from {} to {}', operation_target_handle, operation_instance.current_value, - value) - operation_instance.current_value = value + self._logger.info('set value %s from %s to %s', + params.operation_instance.operation_target_handle, + params.operation_instance.current_value, value) + params.operation_instance.current_value = value with self._mdib.transaction_manager() as mgr: - # state = mgr.getMetricState(operation_target_handle) - state = mgr.get_state(operation_target_handle) + _state = mgr.get_state(params.operation_instance.operation_target_handle) + state = cast(MetricStateProtocol, _state) if state.MetricValue is None: state.mk_metric_value() state.MetricValue.Value = value # SF1823: For Metrics with the MetricCategory = Set|Preset that are being modified as a result of a # SetValue or SetString operation a Metric Provider shall set the MetricQuality / Validity = Vld. - metric_descriptor_container = self._mdib.descriptions.handle.get_one(operation_target_handle) + metric_descriptor_container = self._mdib.descriptions.handle.get_one( + params.operation_instance.operation_target_handle) if metric_descriptor_container.MetricCategory in (pm_types.MetricCategory.SETTING, pm_types.MetricCategory.PRESETTING): state.MetricValue.Validity = pm_types.MeasurementValidity.VALID - - def _mk_operation_from_operation_descriptor(self, operation_descriptor_container, - operation_cls_getter, - # current_argument_handler=None, - current_request_handler=None, - timeout_handler=None): - """ - :param operation_descriptor_container: the operation container for which this operation Handler shall be created - :param current_argument_handler: the handler that shall be called by operation - :param current_request_handler: the handler that shall be called by operation - :para timeout_handler: callable when timeout is detected (InvocationEffectiveTimeout) - :return: instance of cls - """ - cls = operation_cls_getter(operation_descriptor_container.NODETYPE) - operation = self._mk_operation(cls, - operation_descriptor_container.Handle, - operation_descriptor_container.OperationTarget, - operation_descriptor_container.coding, -# current_argument_handler, - current_request_handler, - timeout_handler) - return operation - - def _mk_operation(self, cls, handle, operation_target_handle, # pylint: disable=no-self-use - coded_value, #current_argument_handler=None, - current_request_handler, timeout_handler=None): - """ - - :param cls: one of the Operations defined in provider.sco - :param handle: the handle of this operation - :param operation_target_handle: the handle of the operation target - :param coded_value: the CodedValue for the Operation ( can be None) - :param current_argument_handler: the handler that shall be called by operation - :param current_request_handler: the handler that shall be called by operation - :return: instance of cls - """ - operation = cls(handle=handle, - operation_target_handle=operation_target_handle, - operation_handler=current_request_handler, - coded_value=coded_value) - # if current_argument_handler: - # # bind method to current_argument - # properties.strongbind(operation, current_argument=partial(current_argument_handler, operation)) - # if current_request_handler: - # # bind method to current_request - # properties.strongbind(operation, current_request=partial(current_request_handler, operation)) - if timeout_handler: - # bind method to current_request - properties.strongbind(operation, on_timeout=partial(timeout_handler, operation)) - return operation - - def _get_operation_target_handle(self, operation_instance): - operation_descriptor_handle = operation_instance.handle - operation_descriptor_container = self._mdib.descriptions.handle.get_one(operation_descriptor_handle) - return operation_descriptor_container.OperationTarget + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) + + def _mk_operation_from_operation_descriptor(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter, + operation_handler: ExecuteHandler, + timeout_handler: TimeoutHandler | None = None) \ + -> OperationDefinitionBase: + """Create an OperationDefinition for the operation_descriptor_container.""" + op_cls = operation_cls_getter(operation_descriptor_container.NODETYPE) + return op_cls(operation_descriptor_container.Handle, + operation_descriptor_container.OperationTarget, + operation_handler=operation_handler, + timeout_handler=timeout_handler, + coded_value=operation_descriptor_container.Type) @staticmethod - def _create_descriptor_container(container_cls, handle, parent_handle, coded_value, safety_classification): + def _create_descriptor_container(container_cls: type[AbstractDescriptorProtocol], + handle: str, + parent_handle: str, + coded_value: CodedValue, + safety_classification: SafetyClassification) -> AbstractDescriptorProtocol: obj = container_cls(handle=handle, parent_handle=parent_handle) obj.SafetyClassification = safety_classification obj.Type = coded_value diff --git a/src/sdc11073/xml_types/msg_types.py b/src/sdc11073/xml_types/msg_types.py index 59b07365..931a4574 100644 --- a/src/sdc11073/xml_types/msg_types.py +++ b/src/sdc11073/xml_types/msg_types.py @@ -384,6 +384,10 @@ class AbstractSet(MessageType): OperationHandleRef = cp.NodeStringProperty(msg.OperationHandleRef) _props = ('OperationHandleRef',) + @property + def argument(self): + return None + class AbstractGetResponse(MessageType): MdibVersion = cp.IntegerAttributeProperty('MdibVersion', implied_py_value=0) diff --git a/tests/70041_MDIB_Final.xml b/tests/70041_MDIB_Final.xml index 54c8472b..2aeb8652 100644 --- a/tests/70041_MDIB_Final.xml +++ b/tests/70041_MDIB_Final.xml @@ -105,7 +105,7 @@ - + An operation to Set the active reference source of a clock for time synchronization. @@ -116,7 +116,7 @@ - + An operation to set the time zone of a clock. diff --git a/tests/70041_MDIB_multi.xml b/tests/70041_MDIB_multi.xml index 0657199e..16a449fa 100644 --- a/tests/70041_MDIB_multi.xml +++ b/tests/70041_MDIB_multi.xml @@ -105,7 +105,7 @@ - + An operation to Set the active reference source of a clock for time synchronization. @@ -116,7 +116,7 @@ - + An operation to set the time zone of a clock. diff --git a/tests/mdib_tns.xml b/tests/mdib_tns.xml index e2d795cf..3f51eccb 100644 --- a/tests/mdib_tns.xml +++ b/tests/mdib_tns.xml @@ -51,11 +51,11 @@ MdibVersion="5795" SequenceId="urn:uuid:4ed313b2-f925-418a-8476-6f3b4d06ee3e" In - + - + diff --git a/tests/mdib_two_mds.xml b/tests/mdib_two_mds.xml index ea60cd09..faa67691 100644 --- a/tests/mdib_two_mds.xml +++ b/tests/mdib_two_mds.xml @@ -50,11 +50,11 @@ MdibVersion="5795" SequenceId="urn:uuid:4ed313b2-f925-418a-8476-6f3b4d06ee3e" In - + - + diff --git a/tests/test_operations.py b/tests/test_operations.py index c88c0d9e..aef43433 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -8,7 +8,6 @@ from sdc11073 import loghelper from sdc11073 import observableproperties from sdc11073.xml_types import pm_types, msg_types, pm_qnames as pm -from sdc11073.location import SdcLocation from sdc11073.loghelper import basic_logging_setup from sdc11073.mdib import ConsumerMdib from sdc11073.mdib.providerwaveform import Annotator @@ -143,6 +142,7 @@ def test_set_patient_context_operation(self): result = future.result(timeout=SET_TIMEOUT) state = result.InvocationInfo.InvocationState self.assertEqual(state, msg_types.InvocationState.FAILED) + self.assertIsNone(result.OperationTarget) self.log_watcher.setPaused(False) # insert a new patient with correct handle, this shall succeed @@ -153,6 +153,7 @@ def test_set_patient_context_operation(self): self.assertEqual(state, msg_types.InvocationState.FINISHED) self.assertIsNone(result.InvocationInfo.InvocationError) self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) + self.assertIsNotNone(result.OperationTarget) # check client side patient context, this shall have been set via notification patient_context_state_container = client_mdib.context_states.NODETYPE.get_one(pm.PatientContextState) @@ -178,6 +179,7 @@ def test_set_patient_context_operation(self): result = future.result(timeout=SET_TIMEOUT) state = result.InvocationInfo.InvocationState self.assertEqual(state, msg_types.InvocationState.FINISHED) + self.assertEqual(result.OperationTarget, proposed_context.Handle) patient_context_state_container = client_mdib.context_states.handle.get_one( patient_context_state_container.Handle) self.assertEqual(patient_context_state_container.CoreData.Givenname, 'Karla') @@ -198,9 +200,10 @@ def test_set_patient_context_operation(self): proposed_context.CoreData.Race = pm_types.CodedValue('somerace') future = context.set_context_state(operation_handle, [proposed_context]) result = future.result(timeout=SET_TIMEOUT) - state = result.InvocationInfo.InvocationState - self.assertEqual(state, msg_types.InvocationState.FINISHED) + invocation_state = result.InvocationInfo.InvocationState + self.assertEqual(invocation_state, msg_types.InvocationState.FINISHED) self.assertIsNone(result.InvocationInfo.InvocationError) + self.assertIsNotNone(result.OperationTarget) self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) patient_context_state_containers = client_mdib.context_states.NODETYPE.get(pm.PatientContextState, []) # sort by BindingMdibVersion @@ -395,10 +398,6 @@ def test_set_ntp_server(self): client_mdib.init_mdib() coding = pm_types.Coding(NomenclatureCodes.MDC_OP_SET_TIME_SYNC_REF_SRC) my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) - if my_operation_descriptor is None: - # try old code: - coding = pm_types.Coding(NomenclatureCodes.OP_SET_NTP) - my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding) operation_handle = my_operation_descriptor.Handle for value in ('169.254.0.199', '169.254.0.199:1234'): @@ -427,10 +426,6 @@ def test_set_time_zone(self): coding = pm_types.Coding(NomenclatureCodes.MDC_ACT_SET_TIME_ZONE) my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) - if my_operation_descriptor is None: - # use old code: - coding = pm_types.Coding(NomenclatureCodes.OP_SET_TZ) - my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding) operation_handle = my_operation_descriptor.Handle for value in ('+03:00', '-03:00'): # are these correct values? diff --git a/tests/test_tutorial.py b/tests/test_tutorial.py index 7e6aa93f..f3bf538f 100644 --- a/tests/test_tutorial.py +++ b/tests/test_tutorial.py @@ -1,28 +1,37 @@ +from __future__ import annotations + import os import unittest import uuid from decimal import Decimal +from typing import TYPE_CHECKING from sdc11073 import network from sdc11073.consumer import SdcConsumer from sdc11073.definitions_base import ProtocolsRegistry from sdc11073.definitions_sdc import SDC_v1_Definitions -from sdc11073.location import SdcLocation from sdc11073.loghelper import basic_logging_setup, get_logger_adapter from sdc11073.mdib import ProviderMdib from sdc11073.mdib.consumermdib import ConsumerMdib from sdc11073.provider import SdcProvider from sdc11073.provider.components import SdcProviderComponents +from sdc11073.provider.operations import ExecuteResult from sdc11073.roles.product import BaseProduct from sdc11073.roles.providerbase import ProviderRole from sdc11073.wsdiscovery import WSDiscovery, WSDiscoverySingleAdapter from sdc11073.xml_types import msg_types, pm_types from sdc11073.xml_types import pm_qnames as pm from sdc11073.xml_types.dpws_types import ThisDeviceType, ThisModelType +from sdc11073.xml_types.msg_types import InvocationState from sdc11073.xml_types.pm_types import CodedValue from sdc11073.xml_types.wsd_types import ScopesType from tests import utils +if TYPE_CHECKING: + from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol + from sdc11073.provider.operations import ExecuteParameters, OperationDefinitionBase + from sdc11073.roles.providerbase import OperationClassGetter + loopback_adapter = next(adapter for adapter in network.get_adapters() if adapter.is_loopback) SEARCH_TIMEOUT = 2 # in real world applications this timeout is too short, 10 seconds is a good value. @@ -78,7 +87,9 @@ def __init__(self, mdib, log_prefix): self.operation2_called = 0 self.operation2_args = None - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: """If the role provider is responsible for handling of calls to this operation_descriptor_container, it creates an operation instance and returns it, otherwise it returns None. """ @@ -91,35 +102,35 @@ def make_operation_instance(self, operation_descriptor_container, operation_cls_ # This callback is called when a consumer calls the operation. operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_request_handler=self._handle_operation_1) + self._handle_operation_1) return operation if operation_descriptor_container.coding == MY_CODE_2.coding: operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_request_handler=self._handle_operation_2) + self._handle_operation_2) return operation return None - def _handle_operation_1(self, operation_instance, soap_request, operation_request): + def _handle_operation_1(self, params: ExecuteParameters) -> ExecuteResult: """This operation does not manipulate the mdib at all, it only registers the call.""" - argument = operation_request.argument + argument = params.operation_request.argument self.operation1_called += 1 self.operation1_args = argument self._logger.info('_handle_operation_1 called arg={}', argument) - return [] + return ExecuteResult(params.operation_instance.operation_target_handle, InvocationState.FINISHED) - def _handle_operation_2(self, operation_instance, soap_request, operation_request): + def _handle_operation_2(self, params: ExecuteParameters) -> ExecuteResult: """This operation manipulate it operation target, and only registers the call.""" - argument = operation_request.argument + argument = params.operation_request.argument self.operation2_called += 1 self.operation2_args = argument self._logger.info('_handle_operation_2 called arg={}', argument) with self._mdib.transaction_manager() as mgr: - my_state = mgr.get_state(operation_instance.operation_target_handle) + my_state = mgr.get_state(params.operation_instance.operation_target_handle) if my_state.MetricValue is None: my_state.mk_metric_value() my_state.MetricValue.Value = argument - return [] + return ExecuteResult(params.operation_instance.operation_target_handle, InvocationState.FINISHED) class MyProvider2(ProviderRole): @@ -132,30 +143,34 @@ def __init__(self, mdib, log_prefix): self.operation3_args = None self.operation3_called = 0 - def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): + def make_operation_instance(self, + operation_descriptor_container: AbstractOperationDescriptorProtocol, + operation_cls_getter: OperationClassGetter) -> OperationDefinitionBase | None: + if operation_descriptor_container.coding == MY_CODE_3.coding: self._logger.info( 'instantiating operation 3 from existing descriptor handle={}'.format( operation_descriptor_container.Handle)) operation = self._mk_operation_from_operation_descriptor(operation_descriptor_container, operation_cls_getter, - current_request_handler=self._handle_operation_3) + self._handle_operation_3) return operation else: return None - def _handle_operation_3(self, operation_instance, soap_request, operation_request): + def _handle_operation_3(self, params: ExecuteParameters) -> ExecuteResult: """This operation manipulate it operation target, and only registers the call.""" self.operation3_called += 1 - argument = operation_request.argument + argument = params.operation_request.argument self.operation3_args = argument self._logger.info('_handle_operation_3 called') with self._mdib.transaction_manager() as mgr: - my_state = mgr.get_state(operation_instance.operation_target_handle) + my_state = mgr.get_state(params.operation_instance.operation_target_handle) if my_state.MetricValue is None: my_state.mk_metric_value() my_state.MetricValue.Value = argument - return [] + return ExecuteResult(params.operation_instance.operation_target_handle, InvocationState.FINISHED) + class MyProductImpl(BaseProduct): """This class provides all handlers of the fictional product. @@ -235,9 +250,6 @@ def test_searchDevice(self): # without specifying a type and a location, every WsDiscovery compatible device will be detected # (that can even be printers). # TODO: enable this step once https://github.com/Draegerwerk/sdc11073/issues/223 has been fixed - # services = my_client_ws_discovery.search_services(timeout=SEARCH_TIMEOUT) - # self.assertTrue(len(self.my_location.matching_services(services)), 1) - # self.assertTrue(len(self.my_location2.matching_services(services)), 1) # now search only for devices in my_location2 services = my_client_ws_discovery.search_services(scopes=ScopesType(self.my_location2.scope_string), From 12afe3e4f0fab4e0cbe173ab345bed4afadcb9d8 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Thu, 21 Sep 2023 15:39:26 +0200 Subject: [PATCH 03/18] fix deadlock in consumer operations handling. --- src/sdc11073/consumer/operations.py | 46 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/sdc11073/consumer/operations.py b/src/sdc11073/consumer/operations.py index aad6a2ee..15d93d2c 100644 --- a/src/sdc11073/consumer/operations.py +++ b/src/sdc11073/consumer/operations.py @@ -117,7 +117,7 @@ def call_operation(self, # now look for all related report parts and add them to result parts = [part for part in self._last_operation_invoked_reports if part.InvocationInfo.TransactionId == transaction_id] - # now look for a final report parts + # now look for a final report part final_parts = [part for part in parts if part.InvocationInfo.InvocationState not in self.nonFinalOperationStates] if final_parts: @@ -136,29 +136,29 @@ def on_operation_invoked_report(self, message_data: ReceivedMessage): msg_types = self._msg_reader.msg_types operation_invoked_report = msg_types.OperationInvokedReport.from_node(message_data.p_msg.msg_node) - with self._transactions_lock: - for report_part in operation_invoked_report.ReportPart: - invocation_state = report_part.InvocationInfo.InvocationState - transaction_id = report_part.InvocationInfo.TransactionId - self._logger.debug( # noqa: PLE1205 - '{}on_operation_invoked_report: got transaction_id {} state {}', - self.log_prefix, transaction_id, invocation_state) - if transaction_id in self._transactions: - self._transactions[transaction_id].report_parts.append(report_part) - if invocation_state in self.nonFinalOperationStates: - pass - else: - operation_data = self._transactions.pop(transaction_id, None) - future_object = operation_data.future_ref() - if future_object is None: - # client gave up. - self._logger.debug('transaction_id {} given up', transaction_id) # noqa: PLE1205 - else: - future_object.set_result(self._mk_operation_result(report_part, - operation_data.set_response, - operation_data.report_parts)) + for report_part in operation_invoked_report.ReportPart: + invocation_state = report_part.InvocationInfo.InvocationState + transaction_id = report_part.InvocationInfo.TransactionId + self._logger.debug( # noqa: PLE1205 + '{}on_operation_invoked_report: got transaction_id {} state {}', + self.log_prefix, transaction_id, invocation_state) + if transaction_id in self._transactions: + self._transactions[transaction_id].report_parts.append(report_part) + if invocation_state in self.nonFinalOperationStates: + pass else: - self._last_operation_invoked_reports.append(report_part) + with self._transactions_lock: + operation_data = self._transactions.pop(transaction_id, None) + future_object = operation_data.future_ref() + if future_object is None: + # client gave up. + self._logger.debug('transaction_id {} given up', transaction_id) # noqa: PLE1205 + else: + future_object.set_result(self._mk_operation_result(report_part, + operation_data.set_response, + operation_data.report_parts)) + else: + self._last_operation_invoked_reports.append(report_part) def _mk_operation_result(self, current_report_part: msg_types.OperationInvokedReportPart, From a955ae1f99127977de318b97cbc68d1ed9850dc1 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Thu, 21 Sep 2023 15:54:48 +0200 Subject: [PATCH 04/18] removed what parameter from send_to_subscribers --- .../provider/porttypes/contextserviceimpl.py | 6 +-- .../porttypes/descriptioneventserviceimpl.py | 2 +- .../provider/porttypes/setserviceimpl.py | 2 +- .../porttypes/stateeventserviceimpl.py | 24 ++++------ .../provider/porttypes/waveformserviceimpl.py | 2 +- .../provider/subscriptionmgr_async.py | 10 ++-- src/sdc11073/provider/subscriptionmgr_base.py | 9 ++-- tests/test_operations.py | 46 +++++++++++++++++++ 8 files changed, 67 insertions(+), 34 deletions(-) diff --git a/src/sdc11073/provider/porttypes/contextserviceimpl.py b/src/sdc11073/provider/porttypes/contextserviceimpl.py index 6483b638..8b5cbff2 100644 --- a/src/sdc11073/provider/porttypes/contextserviceimpl.py +++ b/src/sdc11073/provider/porttypes/contextserviceimpl.py @@ -113,8 +113,7 @@ def send_episodic_context_report(self, states: List[AbstractStateContainer], body_node = report.as_etree_node(report.NODETYPE, ns_map) self._logger.debug('sending episodic context report {}', states) - subscription_mgr.send_to_subscribers(body_node, report.action, mdib_version_group, - 'send_episodic_context_report') + subscription_mgr.send_to_subscribers(body_node, report.action, mdib_version_group) def send_periodic_context_report(self, periodic_states_list: List[PeriodicStates], mdib_version_group): @@ -127,5 +126,4 @@ def send_periodic_context_report(self, periodic_states_list: List[PeriodicStates fill_periodic_report_body(report, periodic_states_list) self._logger.debug('sending periodic context report, contains last {} episodic updates', len(periodic_states_list)) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, - 'send_periodic_context_report') + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) diff --git a/src/sdc11073/provider/porttypes/descriptioneventserviceimpl.py b/src/sdc11073/provider/porttypes/descriptioneventserviceimpl.py index 6e83b5fe..c2591b4a 100644 --- a/src/sdc11073/provider/porttypes/descriptioneventserviceimpl.py +++ b/src/sdc11073/provider/porttypes/descriptioneventserviceimpl.py @@ -36,7 +36,7 @@ def send_descriptor_updates(self, updated: List[AbstractDescriptorContainer], body_node = self.mk_description_modification_report_body( mdib_version_group, updated, created, deleted, updated_states) self._logger.debug('sending DescriptionModificationReport upd={} crt={} del={}', updated, created, deleted) - subscription_mgr.send_to_subscribers(body_node, action, mdib_version_group, 'send_descriptor_updates') + subscription_mgr.send_to_subscribers(body_node, action, mdib_version_group) def mk_description_modification_report_body(self, mdib_version_group, updated, created, deleted, updated_states) -> xml_utils.LxmlElement: diff --git a/src/sdc11073/provider/porttypes/setserviceimpl.py b/src/sdc11073/provider/porttypes/setserviceimpl.py index 081a7f4b..4335f591 100644 --- a/src/sdc11073/provider/porttypes/setserviceimpl.py +++ b/src/sdc11073/provider/porttypes/setserviceimpl.py @@ -188,7 +188,7 @@ def notify_operation(self, self._logger.info( 'notify_operation transaction={} operation_handle_ref={}, operationState={}, error={}, errorMessage={}', transaction_id, operation_handle_ref, invocation_state, error, error_message) - subscription_mgr.send_to_subscribers(body_node, report.action, mdib_version_group, 'notify_operation') + subscription_mgr.send_to_subscribers(body_node, report.action, mdib_version_group) def handled_actions(self) -> list[str]: return [self._sdc_device.sdc_definitions.Actions.OperationInvokedReport] diff --git a/src/sdc11073/provider/porttypes/stateeventserviceimpl.py b/src/sdc11073/provider/porttypes/stateeventserviceimpl.py index 8020f1f0..e9ae6217 100644 --- a/src/sdc11073/provider/porttypes/stateeventserviceimpl.py +++ b/src/sdc11073/provider/porttypes/stateeventserviceimpl.py @@ -66,8 +66,7 @@ def send_episodic_metric_report(self, states: List[AbstractStateContainer], report.set_mdib_version_group(mdib_version_group) fill_episodic_report_body(report, states) self._logger.debug('sending episodic metric report {}', states) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, - 'send_episodic_metric_report') + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) def send_periodic_metric_report(self, periodic_states_list: List[PeriodicStates], mdib_version_group): @@ -78,8 +77,7 @@ def send_periodic_metric_report(self, periodic_states_list: List[PeriodicStates] fill_periodic_report_body(report, periodic_states_list) self._logger.debug('sending periodic metric report, contains last {} episodic updates', len(periodic_states_list)) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, - 'send_periodic_metric_report') + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) def send_episodic_alert_report(self, states: List[AbstractStateContainer], mdib_version_group): @@ -89,8 +87,7 @@ def send_episodic_alert_report(self, states: List[AbstractStateContainer], report.set_mdib_version_group(mdib_version_group) fill_episodic_report_body(report, states) self._logger.debug('sending episodic alert report {}', states) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, - 'send_episodic_alert_report') + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) def send_periodic_alert_report(self, periodic_states_list: List[PeriodicStates], mdib_version_group): @@ -101,8 +98,7 @@ def send_periodic_alert_report(self, periodic_states_list: List[PeriodicStates], fill_periodic_report_body(report, periodic_states_list) self._logger.debug('sending periodic alert report, contains last {} episodic updates', len(periodic_states_list)) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, - 'send_periodic_alert_report') + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) def send_episodic_operational_state_report(self, states: List[AbstractStateContainer], mdib_version_group): @@ -112,8 +108,7 @@ def send_episodic_operational_state_report(self, states: List[AbstractStateConta report.set_mdib_version_group(mdib_version_group) fill_episodic_report_body(report, states) self._logger.debug('sending episodic operational state report {}', states) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, - 'send_episodic_operational_state_report') + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) def send_periodic_operational_state_report(self, periodic_states_list: List[PeriodicStates], mdib_version_group): @@ -124,8 +119,7 @@ def send_periodic_operational_state_report(self, periodic_states_list: List[Peri fill_periodic_report_body(report, periodic_states_list) self._logger.debug('sending periodic operational state report, contains last {} episodic updates', len(periodic_states_list)) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, - 'send_periodic_operational_state_report') + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) def send_episodic_component_state_report(self, states: List[AbstractStateContainer], mdib_version_group): @@ -135,8 +129,7 @@ def send_episodic_component_state_report(self, states: List[AbstractStateContain report.set_mdib_version_group(mdib_version_group) fill_episodic_report_body(report, states) self._logger.debug('sending episodic component report {}', states) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, - 'send_episodic_component_state_report') + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) def send_periodic_component_state_report(self, periodic_states_list: List[PeriodicStates], mdib_version_group): @@ -147,8 +140,7 @@ def send_periodic_component_state_report(self, periodic_states_list: List[Period fill_periodic_report_body(report, periodic_states_list) self._logger.debug('sending periodic component report, contains last {} episodic updates', len(periodic_states_list)) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, - 'send_periodic_component_state_report') + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) def fill_episodic_report_body(report, states): diff --git a/src/sdc11073/provider/porttypes/waveformserviceimpl.py b/src/sdc11073/provider/porttypes/waveformserviceimpl.py index 045bcf88..104ca1bc 100644 --- a/src/sdc11073/provider/porttypes/waveformserviceimpl.py +++ b/src/sdc11073/provider/porttypes/waveformserviceimpl.py @@ -32,4 +32,4 @@ def send_realtime_samples_report(self, realtime_sample_states: List[AbstractStat report.set_mdib_version_group(mdib_version_group) report.State.extend(realtime_sample_states) self._logger.debug('sending real time samples report {}', realtime_sample_states) - subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group, None) + subscription_mgr.send_to_subscribers(report, report.action, mdib_version_group) diff --git a/src/sdc11073/provider/subscriptionmgr_async.py b/src/sdc11073/provider/subscriptionmgr_async.py index 0186a4a1..481c3cb7 100644 --- a/src/sdc11073/provider/subscriptionmgr_async.py +++ b/src/sdc11073/provider/subscriptionmgr_async.py @@ -197,8 +197,7 @@ def _end_all_subscriptions(self, send_subscription_end: bool): def send_to_subscribers(self, payload: MessageType | xml_utils.LxmlElement, action: str, - mdib_version_group: MdibVersionGroup, - what: str): + mdib_version_group: MdibVersionGroup): """Send payload to all subscribers.""" with self._subscriptions.lock: if not self._async_send_thread.running: @@ -219,9 +218,8 @@ def send_to_subscribers(self, payload: MessageType | xml_utils.LxmlElement, for subscriber in subscribers: tasks.append(self._async_send_notification_report(subscriber, body_node, action)) - if what: - self._logger.debug('{}: sending report to {}', # noqa: PLE1205 - what, [s.notify_to_address for s in subscribers]) + self._logger.debug('sending report %s to %r', + action, [s.notify_to_address for s in subscribers]) result = self._async_send_thread.run_coro(self._coro_send_to_subscribers(tasks)) if result is None: self._logger.info('could not send notifications, async send loop is not running.') @@ -229,7 +227,7 @@ def send_to_subscribers(self, payload: MessageType | xml_utils.LxmlElement, for counter, element in enumerate(result): if isinstance(element, Exception): self._logger.warning( # noqa: PLE1205 - '{}: _send_to_subscribers {} returned {}', what, subscribers[counter], element) + '{}: _send_to_subscribers {} returned {}', action, subscribers[counter], element) async def _async_send_notification_report(self, subscription: BicepsSubscriptionAsync, body_node: xml_utils.LxmlElement, diff --git a/src/sdc11073/provider/subscriptionmgr_base.py b/src/sdc11073/provider/subscriptionmgr_base.py index 203a2c6d..1187f11e 100644 --- a/src/sdc11073/provider/subscriptionmgr_base.py +++ b/src/sdc11073/provider/subscriptionmgr_base.py @@ -26,6 +26,7 @@ from sdc11073.definitions_base import BaseDefinitions from sdc11073.dispatch import RequestData + from sdc11073.mdib.mdibbase import MdibVersionGroup from sdc11073.pysoap.msgfactory import CreatedMessage, MessageFactory from sdc11073.pysoap.soapclientpool import SoapClientPool @@ -202,7 +203,7 @@ def send_notification_end_message(self, code='SourceShuttingDown', self._logger.info('could not send subscription end message, error = {}', ex) # noqa: PLE1205 def close_by_subscription_manager(self): - self._logger.info('close subscription id={} to {}', # noqa: PLE1205 + self._logger.info('close subscription id={} to {}', # noqa: PLE1205 self.identifier_uuid, self.notify_to_address) self._is_closed = True self._soap_client_pool.forget_usr(self.notify_to_url.netloc, self) @@ -434,8 +435,7 @@ def _get_subscription_for_request(self, request_data: RequestData) -> Subscripti def send_to_subscribers(self, payload: MessageType | xml_utils.LxmlElement, action: str, - mdib_version_group, - what: str): + mdib_version_group: MdibVersionGroup): subscribers = self._get_subscriptions_for_action(action) nsh = self.sdc_definitions.data_model.ns_helper # convert to element tree only once for all subscribers @@ -449,8 +449,7 @@ def send_to_subscribers(self, payload: MessageType | xml_utils.LxmlElement, self.sent_to_subscribers = (action, mdib_version_group, body_node) # update observable for subscriber in subscribers: - if what: - self._logger.debug('{}: sending report to {}', what, subscriber.notify_to_address) + self._logger.debug('{}: sending report to {}', action, subscriber.notify_to_address) try: self._send_notification_report(subscriber, body_node, action) except: diff --git a/tests/test_operations.py b/tests/test_operations.py index aef43433..1ad3dcf4 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -545,3 +545,49 @@ def test_operation_without_handler(self): future2 = set_service.set_string(operation_handle=operation_handle, requested_string=value) result2 = future2.result(timeout=SET_TIMEOUT) self.assertGreater(result2.InvocationInfo.TransactionId, result.InvocationInfo.TransactionId) + + def test_delayed_processing(self): + """Verify that flag 'delayed_processing' changes responses as expected.""" + logging.getLogger('sdc.client.op_mgr').setLevel(logging.DEBUG) + logging.getLogger('sdc.device.op_reg').setLevel(logging.DEBUG) + logging.getLogger('sdc.device.SetService').setLevel(logging.DEBUG) + logging.getLogger('sdc.device.subscrMgr').setLevel(logging.DEBUG) + set_service = self.sdc_client.client('Set') + client_mdib = ConsumerMdib(self.sdc_client) + client_mdib.init_mdib() + coding = pm_types.Coding(NomenclatureCodes.MDC_OP_SET_TIME_SYNC_REF_SRC) + my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) + + operation_handle = my_operation_descriptor.Handle + operation = self.sdc_device.get_operation_by_handle(operation_handle) + for value in ('169.254.0.199', '169.254.0.199:1234'): + self._logger.info('ntp server = %s', value) + operation.delayed_processing = True # first OperationInvokedReport shall have InvocationState.WAIT + coll = observableproperties.SingleValueCollector(self.sdc_client, 'operation_invoked_report') + future = set_service.set_string(operation_handle=operation_handle, requested_string=value) + result = future.result(timeout=SET_TIMEOUT) + received_message = coll.result(timeout=5) + msg_types = received_message.msg_reader.msg_types + operation_invoked_report = msg_types.OperationInvokedReport.from_node(received_message.p_msg.msg_node) + self.assertEqual(operation_invoked_report.ReportPart[0].InvocationInfo.InvocationState, + msg_types.InvocationState.WAIT) + state = result.InvocationInfo.InvocationState + self.assertEqual(state, msg_types.InvocationState.FINISHED) + self.assertIsNone(result.InvocationInfo.InvocationError) + self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) + time.sleep(0.5) + # disable delayed processing + self._logger.info("disable delayed processing") + operation.delayed_processing = False # first OperationInvokedReport shall have InvocationState.FINISHED + coll = observableproperties.SingleValueCollector(self.sdc_client, 'operation_invoked_report') + future = set_service.set_string(operation_handle=operation_handle, requested_string=value) + result = future.result(timeout=SET_TIMEOUT) + received_message = coll.result(timeout=5) + msg_types = received_message.msg_reader.msg_types + operation_invoked_report = msg_types.OperationInvokedReport.from_node(received_message.p_msg.msg_node) + self.assertEqual(operation_invoked_report.ReportPart[0].InvocationInfo.InvocationState, + msg_types.InvocationState.FINISHED) + state = result.InvocationInfo.InvocationState + self.assertEqual(state, msg_types.InvocationState.FINISHED) + self.assertIsNone(result.InvocationInfo.InvocationError) + self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) From cd9365a50e74af27a5c1b8734bab8ab627575c9e Mon Sep 17 00:00:00 2001 From: Deichmann Date: Thu, 21 Sep 2023 15:59:23 +0200 Subject: [PATCH 05/18] Changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84383f18..3ddc3ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- added a way to proccess operations sync (directly send FINISHED) + +### Changed +- The final OperationInvokedReport has OperationTargetRef parameter set. + This required refactoring of Operations handling. + ## [2.0.0a6] - 2023-09-11 ### Added From d04695739c44d887f5096d58ec4c9f506a36b9db Mon Sep 17 00:00:00 2001 From: Deichmann Date: Tue, 26 Sep 2023 11:16:17 +0200 Subject: [PATCH 06/18] fix basic_logging_setup interfering with new commlog. --- CHANGELOG.md | 3 +++ src/sdc11073/loghelper.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ddc3ddd..e5ce1a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - added a way to proccess operations sync (directly send FINISHED) +### Fixed +- basic_logging_setup only handles sdc logger, no more side effect due to calling logging.basicConfig. + ### Changed - The final OperationInvokedReport has OperationTargetRef parameter set. This required refactoring of Operations handling. diff --git a/src/sdc11073/loghelper.py b/src/sdc11073/loghelper.py index a95b7312..44b61dd3 100644 --- a/src/sdc11073/loghelper.py +++ b/src/sdc11073/loghelper.py @@ -24,24 +24,30 @@ def ensure_log_stream(): def reset_log_levels(root_logger_name='sdc'): + sub_logger_name = root_logger_name + '.' for name in logging.Logger.manager.loggerDict: - if name.startswith(root_logger_name): + if name.startswith(sub_logger_name) or name == root_logger_name: logging.getLogger(name).setLevel(logging.NOTSET) def reset_handlers(root_logger_name='sdc'): + sub_logger_name = root_logger_name + '.' for name in logging.Logger.manager.loggerDict: - if name.startswith(root_logger_name): + if name.startswith(sub_logger_name) or name == root_logger_name: logger = logging.getLogger(name) for handler in logger.handlers: logger.removeHandler(handler) def basic_logging_setup(root_logger_name='sdc', level=logging.INFO, log_file_name=None): - logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=level) reset_log_levels(root_logger_name) reset_handlers(root_logger_name) + logger = logging.getLogger(root_logger_name) + logger.setLevel(level) formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) if log_file_name: file_handler = logging_handlers.RotatingFileHandler(log_file_name, maxBytes=5000000, From 5ad0498010a49ac845d8995db809db7ae7cf05ee Mon Sep 17 00:00:00 2001 From: Deichmann Date: Tue, 26 Sep 2023 15:24:16 +0200 Subject: [PATCH 07/18] temporarily fix test test_basic_connection_with_different_ssl_contexts --- tests/test_client_device.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/test_client_device.py b/tests/test_client_device.py index cf0a2183..cf3fcf22 100644 --- a/tests/test_client_device.py +++ b/tests/test_client_device.py @@ -396,8 +396,8 @@ def ssl_context_init_side_effect(*args, **kwargs): @staticmethod def _run_client_with_device(ssl_context_container): - log_watcher = loghelper.LogWatcher(logging.getLogger('sdc'), level=logging.ERROR) basic_logging_setup() + log_watcher = loghelper.LogWatcher(logging.getLogger('sdc'), level=logging.ERROR) wsd = WSDiscovery('127.0.0.1') wsd.start() location = SdcLocation(fac='fac1', poc='CU1', bed='Bed') @@ -428,7 +428,7 @@ def _run_client_with_device(ssl_context_container): sdc_client.stop_all() wsd.stop() - log_watcher.check() + # log_watcher.check() def test_mk_ssl_raises_file_not_found_error(self): """Verify that a FileNotFoundError is raised if a cypher file is specified but not found.""" @@ -748,9 +748,6 @@ def test_instance_id(self): sdc_client.stop_all() def test_metric_report(self): - logging.getLogger('sdc.device.subscrMgr').setLevel(logging.DEBUG) - logging.getLogger('sdc.client.subscrMgr').setLevel(logging.DEBUG) - logging.getLogger('sdc.client.subscr').setLevel(logging.DEBUG) runtest_metric_reports(self, self.sdc_device, self.sdc_client, self.logger) def test_roundtrip_times(self): @@ -967,7 +964,6 @@ def test_realtime_samples(self): def test_description_modification(self): descriptor_handle = '0x34F00100' - logging.getLogger('sdc.device').setLevel(logging.DEBUG) # set value of a metric first_value = Decimal(12) with self.sdc_device.mdib.transaction_manager() as mgr: @@ -1391,9 +1387,6 @@ def test_realtime_samples_common(self): runtest_realtime_samples(self, self.sdc_device_2, self.sdc_client_2) def test_metric_report_common(self): - logging.getLogger('sdc.device.subscrMgr').setLevel(logging.DEBUG) - logging.getLogger('sdc.client.subscrMgr').setLevel(logging.DEBUG) - logging.getLogger('sdc.client.subscr').setLevel(logging.DEBUG) runtest_metric_reports(self, self.sdc_device_1, self.sdc_client_1, self.logger, test_periodic_reports=False) runtest_metric_reports(self, self.sdc_device_2, self.sdc_client_2, self.logger, test_periodic_reports=False) @@ -1607,7 +1600,4 @@ def test_realtime_samples_sync(self): runtest_realtime_samples(self, self.sdc_device, self.sdc_client) def test_metric_report_sync(self): - logging.getLogger('sdc.device.subscrMgr').setLevel(logging.DEBUG) - logging.getLogger('sdc.client.subscrMgr').setLevel(logging.DEBUG) - logging.getLogger('sdc.client.subscr').setLevel(logging.DEBUG) runtest_metric_reports(self, self.sdc_device, self.sdc_client, self.logger) From 443c8639c7e0d2f599ebc7ce1872e4940d22293d Mon Sep 17 00:00:00 2001 From: Deichmann Date: Tue, 26 Sep 2023 15:41:33 +0200 Subject: [PATCH 08/18] fix test test_basic_connection_with_different_ssl_contexts --- tests/test_client_device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_client_device.py b/tests/test_client_device.py index cf3fcf22..c54e0cdd 100644 --- a/tests/test_client_device.py +++ b/tests/test_client_device.py @@ -305,6 +305,8 @@ def socket_init_side_effect(*args, **kwargs): m.branches = list() m.family = s.family + m.proto = s.proto + m.type = s.type return m @@ -428,7 +430,7 @@ def _run_client_with_device(ssl_context_container): sdc_client.stop_all() wsd.stop() - # log_watcher.check() + log_watcher.check() def test_mk_ssl_raises_file_not_found_error(self): """Verify that a FileNotFoundError is raised if a cypher file is specified but not found.""" From 8fc74b9862b0830cf453bfe10bfdf5deca7071f0 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Fri, 29 Sep 2023 15:24:07 +0200 Subject: [PATCH 09/18] review remarks --- src/sdc11073/provider/operations.py | 18 +++++---- src/sdc11073/roles/metricprovider.py | 54 +++++++++++++++++++++++-- src/sdc11073/roles/product.py | 60 ++++++++++++++-------------- src/sdc11073/roles/providerbase.py | 55 +------------------------ 4 files changed, 94 insertions(+), 93 deletions(-) diff --git a/src/sdc11073/provider/operations.py b/src/sdc11073/provider/operations.py index 8aba03cb..fdf14ea4 100644 --- a/src/sdc11073/provider/operations.py +++ b/src/sdc11073/provider/operations.py @@ -62,9 +62,9 @@ class OperationDefinitionBase: current_request = properties.ObservableProperty(fire_only_on_changed_value=False) current_argument = properties.ObservableProperty(fire_only_on_changed_value=False) on_timeout = properties.ObservableProperty(fire_only_on_changed_value=False) - OP_DESCR_QNAME = None - OP_STATE_QNAME = None - OP_QNAME = None + OP_DESCR_QNAME: QName | None = None # to be defined in derived classes + OP_STATE_QNAME: QName | None = None # to be defined in derived classes + OP_QNAME: QName | None = None # to be defined in derived classes def __init__(self, # noqa: PLR0913 handle: str, @@ -89,9 +89,10 @@ def __init__(self, # noqa: PLR0913 self.handle: str = handle self.operation_target_handle: str = operation_target_handle # documentation of operation_target_handle: - # A HANDLE reference this operation is targeted to. In case of a single state this is the HANDLE of the descriptor. - # In case that multiple states may belong to one descriptor (pm:AbstractMultiState), OperationTarget is the HANDLE - # of one of the state instances (if the state is modified by the operation). + # A HANDLE reference this operation is targeted to. In case of a single state this is the HANDLE of the + # descriptor. + # In case that multiple states may belong to one descriptor (pm:AbstractMultiState), + # OperationTarget is the HANDLE of one of the state instances (if the state is modified by the operation). self._operation_handler = operation_handler self._timeout_handler = timeout_handler self._coded_value = coded_value @@ -183,8 +184,9 @@ def collect_values(self, number_of_values: int | None = None) \ return properties.ValuesCollector(self, 'current_value', number_of_values) def __str__(self): - code = '?' if self._descriptor_container is None else self._descriptor_container.Type - return f'{self.__class__.__name__} handle={self.handle} code={code} operation-target={self.operation_target_handle}' + code = None if self._descriptor_container is None else self._descriptor_container.Type + return (f'{self.__class__.__name__} handle={self.handle} code={code} ' + f'operation-target={self.operation_target_handle}') class SetStringOperation(OperationDefinitionBase): diff --git a/src/sdc11073/roles/metricprovider.py b/src/sdc11073/roles/metricprovider.py index eb45cf83..dc54e5c3 100644 --- a/src/sdc11073/roles/metricprovider.py +++ b/src/sdc11073/roles/metricprovider.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, cast -from sdc11073.mdib.statecontainers import AbstractMetricStateContainer +from sdc11073.mdib.statecontainers import AbstractMetricStateContainer, MetricStateProtocol from sdc11073.provider.operations import ExecuteResult from sdc11073.xml_types.pm_types import ComponentActivation @@ -14,8 +14,7 @@ from sdc11073.mdib import ProviderMdib from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol from sdc11073.mdib.transactions import TransactionManagerProtocol, _TrItem - from sdc11073.provider.operations import OperationDefinitionBase - from sdc11073.provider.operations import ExecuteParameters + from sdc11073.provider.operations import ExecuteParameters, OperationDefinitionBase from .providerbase import OperationClassGetter @@ -120,3 +119,52 @@ def _handle_metrics_component_activation(self, metric_state_updates: Iterable[_T self._logger.info('%s: remove metric value because ActivationState="%s", handle="%s"', self.__class__.__name__, new_state.ActivationState, new_state.DescriptorHandle) new_state.MetricValue = None + + def _set_numeric_value(self, params: ExecuteParameters) -> ExecuteResult: + """Set a numerical metric value (ExecuteHandler).""" + value = params.operation_request.argument + pm_types = self._mdib.data_model.pm_types + self._logger.info('set value of %s via %s from %r to %r', + params.operation_instance.operation_target_handle, + params.operation_instance.handle, + params.operation_instance.current_value, value) + params.operation_instance.current_value = value + with self._mdib.transaction_manager() as mgr: + _state = mgr.get_state(params.operation_instance.operation_target_handle) + state = cast(MetricStateProtocol, _state) + if state.MetricValue is None: + state.mk_metric_value() + state.MetricValue.Value = value + # SF1823: For Metrics with the MetricCategory = Set|Preset that are being modified as a result of a + # SetValue or SetString operation a Metric Provider shall set the MetricQuality / Validity = Vld. + metric_descriptor_container = self._mdib.descriptions.handle.get_one( + params.operation_instance.operation_target_handle) + if metric_descriptor_container.MetricCategory in (pm_types.MetricCategory.SETTING, + pm_types.MetricCategory.PRESETTING): + state.MetricValue.Validity = pm_types.MeasurementValidity.VALID + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) + + def _set_string(self, params: ExecuteParameters) -> ExecuteResult: + """Set a string value (ExecuteHandler).""" + value = params.operation_request.argument + pm_types = self._mdib.data_model.pm_types + self._logger.info('set value %s from %s to %s', + params.operation_instance.operation_target_handle, + params.operation_instance.current_value, value) + params.operation_instance.current_value = value + with self._mdib.transaction_manager() as mgr: + _state = mgr.get_state(params.operation_instance.operation_target_handle) + state = cast(MetricStateProtocol, _state) + if state.MetricValue is None: + state.mk_metric_value() + state.MetricValue.Value = value + # SF1823: For Metrics with the MetricCategory = Set|Preset that are being modified as a result of a + # SetValue or SetString operation a Metric Provider shall set the MetricQuality / Validity = Vld. + metric_descriptor_container = self._mdib.descriptions.handle.get_one( + params.operation_instance.operation_target_handle) + if metric_descriptor_container.MetricCategory in (pm_types.MetricCategory.SETTING, + pm_types.MetricCategory.PRESETTING): + state.MetricValue.Validity = pm_types.MeasurementValidity.VALID + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) diff --git a/src/sdc11073/roles/product.py b/src/sdc11073/roles/product.py index 12217d96..94a3fd09 100644 --- a/src/sdc11073/roles/product.py +++ b/src/sdc11073/roles/product.py @@ -4,24 +4,27 @@ from sdc11073 import loghelper -from . import alarmprovider, clockprovider, contextprovider, metricprovider, operationprovider, patientcontextprovider +from .alarmprovider import GenericAlarmProvider from .audiopauseprovider import AudioPauseProvider +from .clockprovider import GenericSDCClockProvider from .componentprovider import GenericSetComponentStateOperationProvider +from .contextprovider import EnsembleContextProvider, LocationContextProvider +from .metricprovider import GenericMetricProvider +from .operationprovider import OperationProvider +from .patientcontextprovider import GenericPatientContextProvider if TYPE_CHECKING: from sdc11073.mdib import ProviderMdib - from sdc11073.mdib.descriptorcontainers import AbstractOperationDescriptorProtocol - from sdc11073.mdib.transactions import TransactionManagerProtocol - from sdc11073.provider.operations import OperationDefinitionBase from sdc11073.provider.sco import AbstractScoOperationsRegistry - from .providerbase import OperationClassGetter, ProviderRole + + from .providerbase import ProviderRole class BaseProduct: """A Product is associated to a single sco. It provides the operation handlers for the operations in this sco. - If a mdib contains multiple sco instances, there must be multiple Products. + If a mdib contains multiple sco instances, there must be multiple Products. """ def __init__(self, @@ -32,7 +35,8 @@ def __init__(self, self._sco = sco self._mdib = mdib self._model = mdib.data_model - self._ordered_providers: list[ProviderRole] = [] # order matters, each provider can hide operations of later ones + self._ordered_providers: list[ + ProviderRole] = [] # order matters, each provider can hide operations of later ones # start with most specific providers, end with most general ones self._logger = loghelper.get_logger_adapter(f'sdc.device.{self.__class__.__name__}', log_prefix) @@ -79,13 +83,13 @@ def stop(self): role_handler.stop() def make_operation_instance(self, operation_descriptor_container, operation_cls_getter): - """ try to get an operation for this operation_descriptor_container ( given in mdib) """ + """Try to get an operation for this operation_descriptor_container ( given in mdib).""" operation_target_handle = operation_descriptor_container.OperationTarget operation_target_descr = self._mdib.descriptions.handle.get_one(operation_target_handle, allow_none=True) # descriptor container if operation_target_descr is None: # this operation is incomplete, the operation target does not exist. Registration not possible. - self._logger.warn( + self._logger.warning( f'Operation {operation_descriptor_container.Handle}: ' f'target {operation_target_handle} does not exist, will not register operation') return None @@ -126,26 +130,25 @@ def __init__(self, mdib, sco, audio_pause_provider, day_night_provider, clock_pr self._ordered_providers.extend([audio_pause_provider, day_night_provider, clock_provider]) self._ordered_providers.extend( - [patientcontextprovider.GenericPatientContextProvider(mdib, log_prefix=log_prefix), - alarmprovider.GenericAlarmProvider(mdib, log_prefix=log_prefix), - metricprovider.GenericMetricProvider(mdib, log_prefix=log_prefix), - operationprovider.OperationProvider(mdib, log_prefix=log_prefix), - GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix) + [GenericPatientContextProvider(mdib, log_prefix=log_prefix), + GenericAlarmProvider(mdib, log_prefix=log_prefix), + GenericMetricProvider(mdib, log_prefix=log_prefix), + OperationProvider(mdib, log_prefix=log_prefix), + GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix), ]) class MinimalProduct(BaseProduct): def __init__(self, mdib, sco, log_prefix=None): super().__init__(mdib, sco, log_prefix) - self.metric_provider = metricprovider.GenericMetricProvider(mdib, log_prefix=log_prefix) # needed in a test + self.metric_provider = GenericMetricProvider(mdib, log_prefix=log_prefix) # needed in a test self._ordered_providers.extend([AudioPauseProvider(mdib, log_prefix=log_prefix), - clockprovider.GenericSDCClockProvider(mdib, log_prefix=log_prefix), - patientcontextprovider.GenericPatientContextProvider(mdib, - log_prefix=log_prefix), - alarmprovider.GenericAlarmProvider(mdib, log_prefix=log_prefix), + GenericSDCClockProvider(mdib, log_prefix=log_prefix), + GenericPatientContextProvider(mdib, log_prefix=log_prefix), + GenericAlarmProvider(mdib, log_prefix=log_prefix), self.metric_provider, - operationprovider.OperationProvider(mdib, log_prefix=log_prefix), - GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix) + OperationProvider(mdib, log_prefix=log_prefix), + GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix), ]) @@ -153,13 +156,12 @@ class ExtendedProduct(MinimalProduct): def __init__(self, mdib, sco, log_prefix=None): super().__init__(mdib, sco, log_prefix) self._ordered_providers.extend([AudioPauseProvider(mdib, log_prefix=log_prefix), - clockprovider.GenericSDCClockProvider(mdib, log_prefix=log_prefix), - contextprovider.EnsembleContextProvider(mdib, log_prefix=log_prefix), - contextprovider.LocationContextProvider(mdib, log_prefix=log_prefix), - patientcontextprovider.GenericPatientContextProvider(mdib, - log_prefix=log_prefix), - alarmprovider.GenericAlarmProvider(mdib, log_prefix=log_prefix), + GenericSDCClockProvider(mdib, log_prefix=log_prefix), + EnsembleContextProvider(mdib, log_prefix=log_prefix), + LocationContextProvider(mdib, log_prefix=log_prefix), + GenericPatientContextProvider(mdib, log_prefix=log_prefix), + GenericAlarmProvider(mdib, log_prefix=log_prefix), self.metric_provider, - operationprovider.OperationProvider(mdib, log_prefix=log_prefix), - GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix) + OperationProvider(mdib, log_prefix=log_prefix), + GenericSetComponentStateOperationProvider(mdib, log_prefix=log_prefix), ]) diff --git a/src/sdc11073/roles/providerbase.py b/src/sdc11073/roles/providerbase.py index 9f1d6377..941a5b8f 100644 --- a/src/sdc11073/roles/providerbase.py +++ b/src/sdc11073/roles/providerbase.py @@ -1,12 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, cast +from typing import TYPE_CHECKING, Callable from lxml.etree import QName from sdc11073 import loghelper -from sdc11073.mdib.statecontainers import MetricStateProtocol -from sdc11073.provider.operations import ExecuteResult, OperationDefinitionBase +from sdc11073.provider.operations import OperationDefinitionBase if TYPE_CHECKING: from sdc11073.mdib import ProviderMdib @@ -15,7 +14,6 @@ from sdc11073.provider.operations import ExecuteHandler, TimeoutHandler from sdc11073.provider.sco import AbstractScoOperationsRegistry from sdc11073.xml_types.pm_types import CodedValue, SafetyClassification - from sdc11073.provider.operations import ExecuteParameters OperationClassGetter = Callable[[QName], type[OperationDefinitionBase]] @@ -71,55 +69,6 @@ def on_post_commit(self, mdib: ProviderMdib, transaction: TransactionManagerProt Derived classes can overwrite this method. """ - def _set_numeric_value(self, params: ExecuteParameters) -> ExecuteResult: - """Set a numerical metric value (ExecuteHandler).""" - value = params.operation_request.argument - pm_types = self._mdib.data_model.pm_types - self._logger.info('set value of %s via %s from %r to %r', - params.operation_instance.operation_target_handle, - params.operation_instance.handle, - params.operation_instance.current_value, value) - params.operation_instance.current_value = value - with self._mdib.transaction_manager() as mgr: - _state = mgr.get_state(params.operation_instance.operation_target_handle) - state = cast(MetricStateProtocol, _state) - if state.MetricValue is None: - state.mk_metric_value() - state.MetricValue.Value = value - # SF1823: For Metrics with the MetricCategory = Set|Preset that are being modified as a result of a - # SetValue or SetString operation a Metric Provider shall set the MetricQuality / Validity = Vld. - metric_descriptor_container = self._mdib.descriptions.handle.get_one( - params.operation_instance.operation_target_handle) - if metric_descriptor_container.MetricCategory in (pm_types.MetricCategory.SETTING, - pm_types.MetricCategory.PRESETTING): - state.MetricValue.Validity = pm_types.MeasurementValidity.VALID - return ExecuteResult(params.operation_instance.operation_target_handle, - self._mdib.data_model.msg_types.InvocationState.FINISHED) - - def _set_string(self, params: ExecuteParameters) -> ExecuteResult: - """Set a string value (ExecuteHandler).""" - value = params.operation_request.argument - pm_types = self._mdib.data_model.pm_types - self._logger.info('set value %s from %s to %s', - params.operation_instance.operation_target_handle, - params.operation_instance.current_value, value) - params.operation_instance.current_value = value - with self._mdib.transaction_manager() as mgr: - _state = mgr.get_state(params.operation_instance.operation_target_handle) - state = cast(MetricStateProtocol, _state) - if state.MetricValue is None: - state.mk_metric_value() - state.MetricValue.Value = value - # SF1823: For Metrics with the MetricCategory = Set|Preset that are being modified as a result of a - # SetValue or SetString operation a Metric Provider shall set the MetricQuality / Validity = Vld. - metric_descriptor_container = self._mdib.descriptions.handle.get_one( - params.operation_instance.operation_target_handle) - if metric_descriptor_container.MetricCategory in (pm_types.MetricCategory.SETTING, - pm_types.MetricCategory.PRESETTING): - state.MetricValue.Validity = pm_types.MeasurementValidity.VALID - return ExecuteResult(params.operation_instance.operation_target_handle, - self._mdib.data_model.msg_types.InvocationState.FINISHED) - def _mk_operation_from_operation_descriptor(self, operation_descriptor_container: AbstractOperationDescriptorProtocol, operation_cls_getter: OperationClassGetter, From 214ac469c59d8b0cc8d7a2167b3ac4c14e6c5701 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 4 Oct 2023 10:26:09 +0200 Subject: [PATCH 10/18] removed OP_QNAME constants --- src/sdc11073/provider/operations.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/sdc11073/provider/operations.py b/src/sdc11073/provider/operations.py index fdf14ea4..7eaee370 100644 --- a/src/sdc11073/provider/operations.py +++ b/src/sdc11073/provider/operations.py @@ -64,7 +64,6 @@ class OperationDefinitionBase: on_timeout = properties.ObservableProperty(fire_only_on_changed_value=False) OP_DESCR_QNAME: QName | None = None # to be defined in derived classes OP_STATE_QNAME: QName | None = None # to be defined in derived classes - OP_QNAME: QName | None = None # to be defined in derived classes def __init__(self, # noqa: PLR0913 handle: str, @@ -194,7 +193,6 @@ class SetStringOperation(OperationDefinitionBase): OP_DESCR_QNAME = pm.SetStringOperationDescriptor OP_STATE_QNAME = pm.SetStringOperationState - OP_QNAME = msg.SetString class SetValueOperation(OperationDefinitionBase): @@ -202,7 +200,6 @@ class SetValueOperation(OperationDefinitionBase): OP_DESCR_QNAME = pm.SetValueOperationDescriptor OP_STATE_QNAME = pm.SetValueOperationState - OP_QNAME = msg.SetValue class SetContextStateOperation(OperationDefinitionBase): @@ -210,7 +207,6 @@ class SetContextStateOperation(OperationDefinitionBase): OP_DESCR_QNAME = pm.SetContextStateOperationDescriptor OP_STATE_QNAME = pm.SetContextStateOperationState - OP_QNAME = msg.SetContextState class ActivateOperation(OperationDefinitionBase): @@ -218,7 +214,6 @@ class ActivateOperation(OperationDefinitionBase): OP_DESCR_QNAME = pm.ActivateOperationDescriptor OP_STATE_QNAME = pm.ActivateOperationState - OP_QNAME = msg.Activate class SetAlertStateOperation(OperationDefinitionBase): @@ -226,7 +221,6 @@ class SetAlertStateOperation(OperationDefinitionBase): OP_DESCR_QNAME = pm.SetAlertStateOperationDescriptor OP_STATE_QNAME = pm.SetAlertStateOperationState - OP_QNAME = msg.SetAlertState class SetComponentStateOperation(OperationDefinitionBase): @@ -234,7 +228,6 @@ class SetComponentStateOperation(OperationDefinitionBase): OP_DESCR_QNAME = pm.SetComponentStateOperationDescriptor OP_STATE_QNAME = pm.SetComponentStateOperationState - OP_QNAME = msg.SetComponentState class SetMetricStateOperation(OperationDefinitionBase): @@ -242,7 +235,6 @@ class SetMetricStateOperation(OperationDefinitionBase): OP_DESCR_QNAME = pm.SetMetricStateOperationDescriptor OP_STATE_QNAME = pm.SetMetricStateOperationState - OP_QNAME = msg.SetMetricState # mapping of states: xsi:type information to classes From d6937f62049bd6867c8f61696d43d4dfad2a04df Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 4 Oct 2023 11:14:24 +0200 Subject: [PATCH 11/18] improve test coverage --- tests/70041_MDIB_Final.xml | 2796 ++++++++++++++++++------------------ 1 file changed, 1398 insertions(+), 1398 deletions(-) diff --git a/tests/70041_MDIB_Final.xml b/tests/70041_MDIB_Final.xml index 2aeb8652..fe290a2c 100644 --- a/tests/70041_MDIB_Final.xml +++ b/tests/70041_MDIB_Final.xml @@ -1,1398 +1,1398 @@ - - - - - - - - - - - - Anesthesia System - - - - Private Coding Semantics - - urn:oid:1.3.6.1.4.1.3592.2.1.2.1 - - - - Device Specialization - - 1.3.6.1.4.1.3592.2.601.13.1.1 - - - - - - - - - - - - - - - - - Apnea by different or undefined sources - - 3569 - - - - - - - - - - - Normal - - - - - - - - - - - - - - - - - - - - - An operation to initiate global all audio pause - - - - - - - - - - - - An operation to cancel global all audio pause - - - - - - - - - - - An Operation to associate the not-associated, pre-assoiciated or disassociated Patient of a System Context. - - - - - - - - - - - An operation to Set the active reference source of a clock for time synchronization. - - - - - - - - - - - An operation to set the time zone of a clock. - - - - - - device - humanreadable - - - Drägerwerk AG & Co. KGaA - A3XX - 01.00 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - None - - - NTPv3 - - - EBWW - - - - - - - - - - - - - Patient-related device-specific settings - - - - - - - - - - - - - - - - - An operation to select a mode - - - - - - - - - - - - Patient related device specific settings channel - - - - - - - - - - Age - - - y - - - - - - - - - - - - Body weight - - - kg - - - - - - - - - - - - Height - - - cm - - - - - - - - - - - - Patient category - - - no unit - - - ADULT - - ADULT - - - - PEDIATRIC - - PEDIATRIC - - - - NEONATE - - NEONATE - - - - - - - - - - - - Gender - - - no unit - - - FEMALE - - FEMALE - - - - MALE - - MALE - - - - UNKNOWN - - UNKNOWN - - - - UNSPECIFIED - - UNSPECIFIED - - - - - - - - - - - - - - - - - - Gas concentration Monitor - - - - - - - - - - - - - - - - - - - Inspiratory halothane concentration > high limit - - 0x34F00150 - - - - - - - - - - Inspiratory oxygen concentration < low limit - - 0x34F001F0 - - - - - - - - - - - - - - - - - - - - - gas concentrations - - - - Device Specialization - - 1.3.6.1.4.1.3592.2.601.55.1.1 - - - - - - - - - - inHal - - - kPa - - - - - - - - - - - - etHal - - - kPa - - - - - - - - - - - - inEnf - - - kPa - - - - - - - - - - - - etEnf - - - kPa - - - - - - - - - - - - inIso - - - kPa - - - - - - - - - - - - etIso - - - kPa - - - - - - - - - - - - inDes - - - kPa - - - - - - - - - - - - etDes - - - kPa - - - - - - - - - - - - inSev - - - kPa - - - - - - - - - - - - etSev - - - kPa - - - - - - - - - - - - inVA 1 - - - kPa - - - - - - - - - - - - etPrimVA - - - kPa - - - - - - - - - - - - inVA 2 - - - kPa - - - - - - - - - - - - etSecVA - - - kPa - - - - - - - - - - - - inDes - - - % - - - - - - - - - - - - etDes - - - % - - - - - - - - - - - - inSev - - - % - - - - - - - - - - - - etSev - - - % - - - - - - - - - - - - ΔO2 - - - % - - - - - - - - - - - - RRc - - - /min - - - - - - - - - - - - inCO2 - - - % - - - - - - - - - - - - etCO2 - - - % - - - - - - - - - - - - etCO2 - - - kPa - - - - - - - - - - - - inCO2 - - - mmHg - - - - - - - - - - - - etCO2 - - - mmHg - - - - - - - - - - - - inVA 1 - - - % - - - - - - - - - - - - etPrimVA - - - % - - - - - - - - - - - - inVA 2 - - - % - - - - - - - - - - - - etSecVA - - - % - - - - - - - - - - - - etO2 - - - % - - - - - - - - - - - - FiO2 - - - % - - - - - - - - - - - - inHal - - - % - - - - - - - - - - - - etHal - - - % - - - - - - - - - - - - inEnf - - - % - - - - - - - - - - - - etEnf - - - % - - - - - - - - - - - - inIso - - - % - - - - - - - - - - - - etIso - - - % - - - - - - - - - - - - inN2O - - - % - - - - - - - - - - - - etN2O - - - % - - - - - - - - - - - - inCO2 - - - kPa - - - - - - - - - - - - O2 - - - % - - - - - - - - - - - - CO2 - - - mmHg - - - - - - - - - - - - CO2 - - - kPa - - - - - - - - - - - - CO2 - - - % - - - - - - - - - - - - Concentration of primary agent (inspiratory/expiratory) - - - % - - - - - - - - - - - - Hal - - - % - - - - - - - - - - - - Enf - - - % - - - - - - - - - - - - Iso - - - % - - - - - - - - - - - - Des - - - % - - - - - - - - - - - - Sev - - - % - - - - - - - - - - - - Concentration of primary agent (inspiratory/expiratory) - - - kPa - - - - - - - - - - - - Hal - - - kPa - - - - - - - - - - - - Enf - - - kPa - - - - - - - - - - - - Iso - - - kPa - - - - - - - - - - - - Des - - - kPa - - - - - - - - - - - - Sev - - - kPa - - - - - - - - - - - - - - - - Airway Parameter Monitor - - - - - - - - - - - - - - - - - - - - - away pressure - - - - - - - - - - Paw - - - mbar - - - - - - - - - - - - - - Flow - - - - - - - - - - Flow - - - L/min - - - - - - - - - - - - - - Lung - - - - - - - - - - - - - - - Calculated parameters - - - - - - - - - - - - - - - - - - Misc - - - - Device Specialization - - 1.3.6.1.4.1.3592.2.601.55.1.1 - - - - - - - - - - Air consumpt - - - L - - - - - - - - - - - + + + + + + + + + + + + Anesthesia System + + + + Private Coding Semantics + + urn:oid:1.3.6.1.4.1.3592.2.1.2.1 + + + + Device Specialization + + 1.3.6.1.4.1.3592.2.601.13.1.1 + + + + + + + + + + + + + + + + + Apnea by different or undefined sources + + 3569 + + + + + + + + + + + Normal + + + + + + + + + + + + + + + + + + + + + An operation to initiate global all audio pause + + + + + + + + + + + + An operation to cancel global all audio pause + + + + + + + + + + + An Operation to associate the not-associated, pre-assoiciated or disassociated Patient of a System Context. + + + + + + + + + + + An operation to Set the active reference source of a clock for time synchronization. + + + + + + + + + + + An operation to set the time zone of a clock. + + + + + + device + humanreadable + + + Drägerwerk AG & Co. KGaA + A3XX + 01.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + None + + + NTPv3 + + + EBWW + + + + + + + + + + + + + Patient-related device-specific settings + + + + + + + + + + + + + + + + + An operation to select a mode + + + + + + + + + + + + Patient related device specific settings channel + + + + + + + + + + Age + + + y + + + + + + + + + + + + Body weight + + + kg + + + + + + + + + + + + Height + + + cm + + + + + + + + + + + + Patient category + + + no unit + + + ADULT + + ADULT + + + + PEDIATRIC + + PEDIATRIC + + + + NEONATE + + NEONATE + + + + + + + + + + + + Gender + + + no unit + + + FEMALE + + FEMALE + + + + MALE + + MALE + + + + UNKNOWN + + UNKNOWN + + + + UNSPECIFIED + + UNSPECIFIED + + + + + + + + + + + + + + + + + + Gas concentration Monitor + + + + + + + + + + + + + + + + + + + Inspiratory halothane concentration > high limit + + 0x34F00150 + + + + + + + + + + Inspiratory oxygen concentration < low limit + + 0x34F001F0 + + + + + + + + + + + + + + + + + + + + + gas concentrations + + + + Device Specialization + + 1.3.6.1.4.1.3592.2.601.55.1.1 + + + + + + + + + + inHal + + + kPa + + + + + + + + + + + + etHal + + + kPa + + + + + + + + + + + + inEnf + + + kPa + + + + + + + + + + + + etEnf + + + kPa + + + + + + + + + + + + inIso + + + kPa + + + + + + + + + + + + etIso + + + kPa + + + + + + + + + + + + inDes + + + kPa + + + + + + + + + + + + etDes + + + kPa + + + + + + + + + + + + inSev + + + kPa + + + + + + + + + + + + etSev + + + kPa + + + + + + + + + + + + inVA 1 + + + kPa + + + + + + + + + + + + etPrimVA + + + kPa + + + + + + + + + + + + inVA 2 + + + kPa + + + + + + + + + + + + etSecVA + + + kPa + + + + + + + + + + + + inDes + + + % + + + + + + + + + + + + etDes + + + % + + + + + + + + + + + + inSev + + + % + + + + + + + + + + + + etSev + + + % + + + + + + + + + + + + ΔO2 + + + % + + + + + + + + + + + + RRc + + + /min + + + + + + + + + + + + inCO2 + + + % + + + + + + + + + + + + etCO2 + + + % + + + + + + + + + + + + etCO2 + + + kPa + + + + + + + + + + + + inCO2 + + + mmHg + + + + + + + + + + + + etCO2 + + + mmHg + + + + + + + + + + + + inVA 1 + + + % + + + + + + + + + + + + etPrimVA + + + % + + + + + + + + + + + + inVA 2 + + + % + + + + + + + + + + + + etSecVA + + + % + + + + + + + + + + + + etO2 + + + % + + + + + + + + + + + + FiO2 + + + % + + + + + + + + + + + + inHal + + + % + + + + + + + + + + + + etHal + + + % + + + + + + + + + + + + inEnf + + + % + + + + + + + + + + + + etEnf + + + % + + + + + + + + + + + + inIso + + + % + + + + + + + + + + + + etIso + + + % + + + + + + + + + + + + inN2O + + + % + + + + + + + + + + + + etN2O + + + % + + + + + + + + + + + + inCO2 + + + kPa + + + + + + + + + + + + O2 + + + % + + + + + + + + + + + + CO2 + + + mmHg + + + + + + + + + + + + CO2 + + + kPa + + + + + + + + + + + + CO2 + + + % + + + + + + + + + + + + Concentration of primary agent (inspiratory/expiratory) + + + % + + + + + + + + + + + + Hal + + + % + + + + + + + + + + + + Enf + + + % + + + + + + + + + + + + Iso + + + % + + + + + + + + + + + + Des + + + % + + + + + + + + + + + + Sev + + + % + + + + + + + + + + + + Concentration of primary agent (inspiratory/expiratory) + + + kPa + + + + + + + + + + + + Hal + + + kPa + + + + + + + + + + + + Enf + + + kPa + + + + + + + + + + + + Iso + + + kPa + + + + + + + + + + + + Des + + + kPa + + + + + + + + + + + + Sev + + + kPa + + + + + + + + + + + + + + + + Airway Parameter Monitor + + + + + + + + + + + + + + + + + + + + + away pressure + + + + + + + + + + Paw + + + mbar + + + + + + + + + + + + + + Flow + + + + + + + + + + Flow + + + L/min + + + + + + + + + + + + + + Lung + + + + + + + + + + + + + + + Calculated parameters + + + + + + + + + + + + + + + + + + Misc + + + + Device Specialization + + 1.3.6.1.4.1.3592.2.601.55.1.1 + + + + + + + + + + Air consumpt + + + L + + + + + + + + + + + From 1a148d9a033f14766df58d534cfc4d6a1875c0cb Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 4 Oct 2023 12:10:20 +0200 Subject: [PATCH 12/18] improved code coverage --- tests/70041_MDIB_Final.xml | 26 +++++++++++++++++++++++++- tests/test_operations.py | 8 +++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/70041_MDIB_Final.xml b/tests/70041_MDIB_Final.xml index fe290a2c..2116b07a 100644 --- a/tests/70041_MDIB_Final.xml +++ b/tests/70041_MDIB_Final.xml @@ -54,7 +54,31 @@ Normal - + + + + + + + + + + Normal + + + + + + + + + + + + Normal + + + diff --git a/tests/test_operations.py b/tests/test_operations.py index 6ba49362..6d84f69a 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -293,6 +293,11 @@ def test_audio_pause(self): """Tests AudioPauseProvider """ + # switch one alert system off + alert_system_off = 'Asy.3208' + with self.sdc_device.mdib.transaction_manager() as mgr: + state = mgr.get_state(alert_system_off) + state.ActivationState = pm_types.AlertActivation.OFF alert_system_descriptors = self.sdc_device.mdib.descriptions.NODETYPE.get(pm.AlertSystemDescriptor) self.assertTrue(alert_system_descriptors is not None) self.assertGreater(len(alert_system_descriptors), 0) @@ -314,7 +319,8 @@ def test_audio_pause(self): for alert_system_descriptor in alert_system_descriptors: state = self.sdc_client.mdib.states.descriptor_handle.get_one(alert_system_descriptor.Handle) # we know that the state has only one SystemSignalActivation entity, which is audible and should be paused now - self.assertEqual(state.SystemSignalActivation[0].State, pm_types.AlertActivation.PAUSED) + if alert_system_descriptor.Handle != alert_system_off: + self.assertEqual(state.SystemSignalActivation[0].State, pm_types.AlertActivation.PAUSED) coding = pm_types.Coding(NomenclatureCodes.MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE) operation = self.sdc_device.mdib.descriptions.coding.get_one(coding) From 399d69692b306d7537cef1171a68998996a9c8cb Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 4 Oct 2023 12:11:49 +0200 Subject: [PATCH 13/18] fix consumer.py #268 --- tutorial/consumer/consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorial/consumer/consumer.py b/tutorial/consumer/consumer.py index 0ad18852..3aa89ca7 100644 --- a/tutorial/consumer/consumer.py +++ b/tutorial/consumer/consumer.py @@ -78,7 +78,7 @@ def set_ensemble_context(mdib: ConsumerMdib, sdc_consumer: SdcConsumer) -> None: print("Got a match: {}".format(one_service)) # now create a new SDCClient (=Consumer) that can be used # for all interactions with the communication partner - my_client = SdcConsumer.from_wsd_service(one_service, ssl_context=None) + my_client = SdcConsumer.from_wsd_service(one_service, ssl_context_container=None) # start all services on the client to make sure we get updates my_client.start_all() # all data interactions happen through the MDIB (MedicalDeviceInformationBase) From 6782b16f0904fbc0d88fe522df5f5c043d6d39bc Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 4 Oct 2023 13:11:55 +0200 Subject: [PATCH 14/18] test and fix EpisodicOperationalStateReports --- src/sdc11073/consumer/consumerimpl.py | 2 +- src/sdc11073/mdib/consumermdibxtra.py | 3 ++- src/sdc11073/mdib/statecontainers.py | 2 +- tests/test_operations.py | 16 +++++++++++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/sdc11073/consumer/consumerimpl.py b/src/sdc11073/consumer/consumerimpl.py index 473b3f46..6b17c119 100644 --- a/src/sdc11073/consumer/consumerimpl.py +++ b/src/sdc11073/consumer/consumerimpl.py @@ -136,7 +136,7 @@ def _mk_lookup(self) -> dict[str, str]: actions.EpisodicMetricReport: 'episodic_metric_report', actions.EpisodicAlertReport: 'episodic_alert_report', actions.EpisodicComponentReport: 'episodic_component_report', - actions.EpisodicOperationalStateReport: 'operational_state_report', + actions.EpisodicOperationalStateReport: 'episodic_operational_state_report', actions.EpisodicContextReport: 'episodic_context_report', actions.PeriodicMetricReport: 'periodic_metric_report', actions.PeriodicAlertReport: 'periodic_alert_report', diff --git a/src/sdc11073/mdib/consumermdibxtra.py b/src/sdc11073/mdib/consumermdibxtra.py index 6e5b403e..46cbec7d 100644 --- a/src/sdc11073/mdib/consumermdibxtra.py +++ b/src/sdc11073/mdib/consumermdibxtra.py @@ -320,7 +320,8 @@ def _on_episodic_alert_report(self, received_message_data: ReceivedMessage): self._mdib.process_incoming_alert_states_report(received_message_data.mdib_version_group, report) def _on_operational_state_report(self, received_message_data: ReceivedMessage): - report = self._msg_reader.process_incoming_states_report(received_message_data) + cls = self._mdib.data_model.msg_types.EpisodicOperationalStateReport + report = cls.from_node(received_message_data.p_msg.msg_node) self._mdib.process_incoming_operational_states_report(received_message_data.mdib_version_group, report) def _on_waveform_report_profiled(self, received_message_data: ReceivedMessage): diff --git a/src/sdc11073/mdib/statecontainers.py b/src/sdc11073/mdib/statecontainers.py index a655d502..dc4aeaf2 100644 --- a/src/sdc11073/mdib/statecontainers.py +++ b/src/sdc11073/mdib/statecontainers.py @@ -221,7 +221,7 @@ class MetricStateProtocol(AbstractStateProtocol): MetricValue: None | pm_types.NumericMetricValue | pm_types.StringMetricValue | pm_types.SampleArrayValue def mk_metric_value(self) -> pm_types.NumericMetricValue | pm_types.StringMetricValue | pm_types.SampleArrayValue: - pass + ... class NumericMetricStateContainer(AbstractMetricStateContainer): diff --git a/tests/test_operations.py b/tests/test_operations.py index 6d84f69a..81c58e27 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -68,7 +68,7 @@ def setUp(self): self.sdc_device.start_all(periodic_reports_interval=1.0) self._loc_validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] self.sdc_device.set_location(utils.random_location(), self._loc_validators) - provide_realtime_data(self.sdc_device) + # provide_realtime_data(self.sdc_device) time.sleep(0.5) # allow init of devices to complete @@ -597,3 +597,17 @@ def test_delayed_processing(self): self.assertEqual(state, msg_types.InvocationState.FINISHED) self.assertIsNone(result.InvocationInfo.InvocationError) self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) + + def test_set_operating_mode(self): + logging.getLogger('sdc.device.subscrMgr').setLevel(logging.DEBUG) + logging.getLogger('ssdc.client.subscr').setLevel(logging.DEBUG) + client_mdib = ConsumerMdib(self.sdc_client) + client_mdib.init_mdib() + + operation_handle = 'SVO.37.3569' + operation = self.sdc_device.get_operation_by_handle(operation_handle) + for op_mode in (pm_types.OperatingMode.NA, pm_types.OperatingMode.ENABLED): + operation.set_operating_mode(op_mode) + time.sleep(1) + operation_state = client_mdib.states.descriptor_handle.get_one(operation_handle) + self.assertEqual(operation_state.OperatingMode, op_mode) \ No newline at end of file From df879ae5737fcd48202442af2561b699fa8fec95 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 4 Oct 2023 13:17:47 +0200 Subject: [PATCH 15/18] removed obsolete method --- src/sdc11073/provider/operations.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/sdc11073/provider/operations.py b/src/sdc11073/provider/operations.py index 7eaee370..e68450fc 100644 --- a/src/sdc11073/provider/operations.py +++ b/src/sdc11073/provider/operations.py @@ -171,17 +171,6 @@ def set_operating_mode(self, mode: OperatingMode): state = mgr.get_state(self.handle) state.OperatingMode = mode - def collect_values(self, number_of_values: int | None = None) \ - -> properties.ValuesCollector | properties.SingleValueCollector: - """Async way to retrieve next value(s). - - Returns a Future-like object that has a result() method. - For details see properties.SingleValueCollector and propertiesValuesCollector documentation. - """ - if number_of_values is None: - return properties.SingleValueCollector(self, 'current_value') - return properties.ValuesCollector(self, 'current_value', number_of_values) - def __str__(self): code = None if self._descriptor_container is None else self._descriptor_container.Type return (f'{self.__class__.__name__} handle={self.handle} code={code} ' From 450c8d876424d67387b564b13926581e359f125d Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 4 Oct 2023 14:25:37 +0200 Subject: [PATCH 16/18] added tests for metricprovider --- src/sdc11073/roles/metricprovider.py | 8 ----- tests/70041_MDIB_Final.xml | 24 +++++++++++++ tests/test_operations.py | 53 +++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/sdc11073/roles/metricprovider.py b/src/sdc11073/roles/metricprovider.py index dc54e5c3..39a19fe4 100644 --- a/src/sdc11073/roles/metricprovider.py +++ b/src/sdc11073/roles/metricprovider.py @@ -148,7 +148,6 @@ def _set_numeric_value(self, params: ExecuteParameters) -> ExecuteResult: def _set_string(self, params: ExecuteParameters) -> ExecuteResult: """Set a string value (ExecuteHandler).""" value = params.operation_request.argument - pm_types = self._mdib.data_model.pm_types self._logger.info('set value %s from %s to %s', params.operation_instance.operation_target_handle, params.operation_instance.current_value, value) @@ -159,12 +158,5 @@ def _set_string(self, params: ExecuteParameters) -> ExecuteResult: if state.MetricValue is None: state.mk_metric_value() state.MetricValue.Value = value - # SF1823: For Metrics with the MetricCategory = Set|Preset that are being modified as a result of a - # SetValue or SetString operation a Metric Provider shall set the MetricQuality / Validity = Vld. - metric_descriptor_container = self._mdib.descriptions.handle.get_one( - params.operation_instance.operation_target_handle) - if metric_descriptor_container.MetricCategory in (pm_types.MetricCategory.SETTING, - pm_types.MetricCategory.PRESETTING): - state.MetricValue.Validity = pm_types.MeasurementValidity.VALID return ExecuteResult(params.operation_instance.operation_target_handle, self._mdib.data_model.msg_types.InvocationState.FINISHED) diff --git a/tests/70041_MDIB_Final.xml b/tests/70041_MDIB_Final.xml index 2116b07a..5ecc0764 100644 --- a/tests/70041_MDIB_Final.xml +++ b/tests/70041_MDIB_Final.xml @@ -144,6 +144,30 @@ An operation to set the time zone of a clock. + + + + + + + + + An operation to set the patient category. + + + + + + + + + + + + An operation to set a numeric value. + + + diff --git a/tests/test_operations.py b/tests/test_operations.py index 81c58e27..eee17309 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -610,4 +610,55 @@ def test_set_operating_mode(self): operation.set_operating_mode(op_mode) time.sleep(1) operation_state = client_mdib.states.descriptor_handle.get_one(operation_handle) - self.assertEqual(operation_state.OperatingMode, op_mode) \ No newline at end of file + self.assertEqual(operation_state.OperatingMode, op_mode) + + def test_set_string_value(self): + """Verify that metricprovider instantiated an operation for SetString call. + + OperationTarget of operation 0815 is an EnumStringMetricState. + """ + set_service = self.sdc_client.client('Set') + client_mdib = ConsumerMdib(self.sdc_client) + client_mdib.init_mdib() + coding = pm_types.Coding('0815') + my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) + + operation_handle = my_operation_descriptor.Handle + for value in ('ADULT', 'PEDIATRIC'): + self._logger.info('string value = %s', value) + future = set_service.set_string(operation_handle=operation_handle, requested_string=value) + result = future.result(timeout=SET_TIMEOUT) + state = result.InvocationInfo.InvocationState + self.assertEqual(state, msg_types.InvocationState.FINISHED) + self.assertIsNone(result.InvocationInfo.InvocationError) + self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) + + # verify that the corresponding state has been updated + state = client_mdib.states.descriptor_handle.get_one(my_operation_descriptor.OperationTarget) + self.assertEqual(state.MetricValue.Value, value) + + def test_set_metric_value(self): + """Verify that metricprovider instantiated an operation for SetNumericValue call. + + OperationTarget of operation 0815-1 is a NumericMetricState. + """ + set_service = self.sdc_client.client('Set') + client_mdib = ConsumerMdib(self.sdc_client) + client_mdib.init_mdib() + coding = pm_types.Coding('0815-1') + my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) + + operation_handle = my_operation_descriptor.Handle + for value in (Decimal(1), Decimal(42)): + self._logger.info('metric value = %s', value) + future = set_service.set_numeric_value(operation_handle=operation_handle, + requested_numeric_value=value) + result = future.result(timeout=SET_TIMEOUT) + state = result.InvocationInfo.InvocationState + self.assertEqual(state, msg_types.InvocationState.FINISHED) + self.assertIsNone(result.InvocationInfo.InvocationError) + self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) + + # verify that the corresponding state has been updated + state = client_mdib.states.descriptor_handle.get_one(my_operation_descriptor.OperationTarget) + self.assertEqual(state.MetricValue.Value, value) From 140450d81113723705dc64a2ce788706c39adad5 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 4 Oct 2023 14:31:27 +0200 Subject: [PATCH 17/18] added tests for metricprovider --- tests/test_operations.py | 100 +++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/test_operations.py b/tests/test_operations.py index eee17309..31022f1e 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -612,53 +612,53 @@ def test_set_operating_mode(self): operation_state = client_mdib.states.descriptor_handle.get_one(operation_handle) self.assertEqual(operation_state.OperatingMode, op_mode) - def test_set_string_value(self): - """Verify that metricprovider instantiated an operation for SetString call. - - OperationTarget of operation 0815 is an EnumStringMetricState. - """ - set_service = self.sdc_client.client('Set') - client_mdib = ConsumerMdib(self.sdc_client) - client_mdib.init_mdib() - coding = pm_types.Coding('0815') - my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) - - operation_handle = my_operation_descriptor.Handle - for value in ('ADULT', 'PEDIATRIC'): - self._logger.info('string value = %s', value) - future = set_service.set_string(operation_handle=operation_handle, requested_string=value) - result = future.result(timeout=SET_TIMEOUT) - state = result.InvocationInfo.InvocationState - self.assertEqual(state, msg_types.InvocationState.FINISHED) - self.assertIsNone(result.InvocationInfo.InvocationError) - self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) - - # verify that the corresponding state has been updated - state = client_mdib.states.descriptor_handle.get_one(my_operation_descriptor.OperationTarget) - self.assertEqual(state.MetricValue.Value, value) - - def test_set_metric_value(self): - """Verify that metricprovider instantiated an operation for SetNumericValue call. - - OperationTarget of operation 0815-1 is a NumericMetricState. - """ - set_service = self.sdc_client.client('Set') - client_mdib = ConsumerMdib(self.sdc_client) - client_mdib.init_mdib() - coding = pm_types.Coding('0815-1') - my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) - - operation_handle = my_operation_descriptor.Handle - for value in (Decimal(1), Decimal(42)): - self._logger.info('metric value = %s', value) - future = set_service.set_numeric_value(operation_handle=operation_handle, - requested_numeric_value=value) - result = future.result(timeout=SET_TIMEOUT) - state = result.InvocationInfo.InvocationState - self.assertEqual(state, msg_types.InvocationState.FINISHED) - self.assertIsNone(result.InvocationInfo.InvocationError) - self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) - - # verify that the corresponding state has been updated - state = client_mdib.states.descriptor_handle.get_one(my_operation_descriptor.OperationTarget) - self.assertEqual(state.MetricValue.Value, value) + def test_set_string_value(self): + """Verify that metricprovider instantiated an operation for SetString call. + + OperationTarget of operation 0815 is an EnumStringMetricState. + """ + set_service = self.sdc_client.client('Set') + client_mdib = ConsumerMdib(self.sdc_client) + client_mdib.init_mdib() + coding = pm_types.Coding('0815') + my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) + + operation_handle = my_operation_descriptor.Handle + for value in ('ADULT', 'PEDIATRIC'): + self._logger.info('string value = %s', value) + future = set_service.set_string(operation_handle=operation_handle, requested_string=value) + result = future.result(timeout=SET_TIMEOUT) + state = result.InvocationInfo.InvocationState + self.assertEqual(state, msg_types.InvocationState.FINISHED) + self.assertIsNone(result.InvocationInfo.InvocationError) + self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) + + # verify that the corresponding state has been updated + state = client_mdib.states.descriptor_handle.get_one(my_operation_descriptor.OperationTarget) + self.assertEqual(state.MetricValue.Value, value) + + def test_set_metric_value(self): + """Verify that metricprovider instantiated an operation for SetNumericValue call. + + OperationTarget of operation 0815-1 is a NumericMetricState. + """ + set_service = self.sdc_client.client('Set') + client_mdib = ConsumerMdib(self.sdc_client) + client_mdib.init_mdib() + coding = pm_types.Coding('0815-1') + my_operation_descriptor = self.sdc_device.mdib.descriptions.coding.get_one(coding, allow_none=True) + + operation_handle = my_operation_descriptor.Handle + for value in (Decimal(1), Decimal(42)): + self._logger.info('metric value = %s', value) + future = set_service.set_numeric_value(operation_handle=operation_handle, + requested_numeric_value=value) + result = future.result(timeout=SET_TIMEOUT) + state = result.InvocationInfo.InvocationState + self.assertEqual(state, msg_types.InvocationState.FINISHED) + self.assertIsNone(result.InvocationInfo.InvocationError) + self.assertEqual(0, len(result.InvocationInfo.InvocationErrorMessage)) + + # verify that the corresponding state has been updated + state = client_mdib.states.descriptor_handle.get_one(my_operation_descriptor.OperationTarget) + self.assertEqual(state.MetricValue.Value, value) From 56360bea990318ac52bd20516324b19ade24c223 Mon Sep 17 00:00:00 2001 From: Deichmann Date: Wed, 4 Oct 2023 14:59:04 +0200 Subject: [PATCH 18/18] removed waveform generation from test_operations --- tests/test_operations.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/test_operations.py b/tests/test_operations.py index 31022f1e..0047a463 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -10,10 +10,8 @@ from sdc11073.xml_types import pm_types, msg_types, pm_qnames as pm from sdc11073.loghelper import basic_logging_setup from sdc11073.mdib import ConsumerMdib -from sdc11073.mdib.providerwaveform import Annotator from sdc11073.roles.nomenclature import NomenclatureCodes from sdc11073.consumer import SdcConsumer -from sdc11073.provider import waveforms from sdc11073.wsdiscovery import WSDiscovery from sdc11073.consumer.components import SdcConsumerComponents from sdc11073.dispatch import RequestDispatcher @@ -33,27 +31,6 @@ NOTIFICATION_TIMEOUT = 5 # also jenkins related value -def provide_realtime_data(sdc_device): - waveform_provider = sdc_device.mdib.xtra.waveform_provider - if waveform_provider is None: - return - paw = waveforms.SawtoothGenerator(min_value=0, max_value=10, waveformperiod=1.1, sampleperiod=0.01) - waveform_provider.register_waveform_generator('0x34F05500', paw) # '0x34F05500 MBUSX_RESP_THERAPY2.00H_Paw' - - flow = waveforms.SinusGenerator(min_value=-8.0, max_value=10.0, waveformperiod=1.2, sampleperiod=0.01) - waveform_provider.register_waveform_generator('0x34F05501', flow) # '0x34F05501 MBUSX_RESP_THERAPY2.01H_Flow' - - co2 = waveforms.TriangleGenerator(min_value=0, max_value=20, waveformperiod=1.0, sampleperiod=0.01) - waveform_provider.register_waveform_generator('0x34F05506', - co2) # '0x34F05506 MBUSX_RESP_THERAPY2.06H_CO2_Signal' - - # make SinusGenerator (0x34F05501) the annotator source - annotator = Annotator(annotation=pm_types.Annotation(pm_types.CodedValue('a', 'b')), - trigger_handle='0x34F05501', - annotated_handles=['0x34F05500', '0x34F05501', '0x34F05506']) - waveform_provider.register_annotation_generator(annotator) - - class Test_BuiltinOperations(unittest.TestCase): """Test role providers (located in sdc11073.roles).""" @@ -68,7 +45,6 @@ def setUp(self): self.sdc_device.start_all(periodic_reports_interval=1.0) self._loc_validators = [pm_types.InstanceIdentifier('Validator', extension_string='System')] self.sdc_device.set_location(utils.random_location(), self._loc_validators) - # provide_realtime_data(self.sdc_device) time.sleep(0.5) # allow init of devices to complete