diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc5aac1..63ddcd34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## Changed +### 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 - change python classes of `addressing_types.py` to match ws-addressing standard of 2006 instead of 2004 +- The final OperationInvokedReport has OperationTargetRef parameter set. + This required refactoring of Operations handling. ## [2.0.0a6] - 2023-09-11 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/consumer/operations.py b/src/sdc11073/consumer/operations.py index ea87d504..15d93d2c 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,14 +101,35 @@ 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 part + 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.""" @@ -85,25 +142,31 @@ def on_operation_invoked_report(self, message_data: ReceivedMessage): 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) + if transaction_id in self._transactions: + self._transactions[transaction_id].report_parts.append(report_part) + if invocation_state in self.nonFinalOperationStates: + pass + else: + 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._logger.info('transaction Id {} ok', transaction_id) # noqa: PLE1205 - future_obj.set_result(report_part) + 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/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, 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/descriptorcontainers.py b/src/sdc11073/mdib/descriptorcontainers.py index 8fb8fb36..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.""" diff --git a/src/sdc11073/mdib/statecontainers.py b/src/sdc11073/mdib/statecontainers.py index f0a06b28..dc4aeaf2 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: + ... + + class NumericMetricStateContainer(AbstractMetricStateContainer): """Represents NumericMetricState in BICEPS.""" diff --git a/src/sdc11073/provider/dpwshostedservice.py b/src/sdc11073/provider/dpwshostedservice.py index 74225a07..b1e9a536 100644 --- a/src/sdc11073/provider/dpwshostedservice.py +++ b/src/sdc11073/provider/dpwshostedservice.py @@ -5,15 +5,15 @@ from lxml import etree as etree_ -from ..dispatch import RequestDispatcher, DispatchKey -from ..namespaces import EventingActions -from ..namespaces import default_ns_helper as ns_hlp -from ..xml_types import mex_types -from ..xml_types.addressing_types import EndpointReferenceType -from ..xml_types.dpws_types import HostedServiceType - +from sdc11073.dispatch import DispatchKey, RequestDispatcher +from sdc11073.namespaces import EventingActions +from sdc11073.namespaces import default_ns_helper as ns_hlp +from sdc11073.xml_types import mex_types +from sdc11073.xml_types.addressing_types import EndpointReferenceType +from sdc11073.xml_types.dpws_types import HostedServiceType if typing.TYPE_CHECKING: + import pathlib from sdc11073 import xml_utils _wsdl_ns = ns_hlp.WSDL.namespace @@ -27,14 +27,14 @@ WSDL_S12 = ns_hlp.WSDL12.namespace # old soap 12 namespace, used in wsdl 1.1. used only for wsdl -def etree_from_file(path) -> xml_utils.LxmlElement: +def etree_from_file(path: str | pathlib.Path) -> xml_utils.LxmlElement: parser = etree_.ETCompatXMLParser(resolve_entities=False) - doc = etree_.parse(path, parser=parser) + doc = etree_.parse(str(path), parser=parser) return doc.getroot() class _EventService(RequestDispatcher): - """ A service that offers subscriptions""" + """A service that offers subscriptions.""" def __init__(self, sdc_device, subscriptions_manager, offered_subscriptions): super().__init__() @@ -72,12 +72,10 @@ def _on_renew_status(self, request_data): class DPWSHostedService(_EventService): - """ Container for DPWSPortTypeBase instances""" + """Container for DPWSPortTypeBase instances.""" def __init__(self, sdc_device, subscriptions_manager, path_element, port_type_impls): - """ - - :param sdc_device: + """:param sdc_device: :param path_element: :param port_type_impls: list of DPWSPortTypeBase """ @@ -111,7 +109,7 @@ def mk_dpws_hosted_instance(self) -> HostedServiceType: return dpws_hosted def _on_get_wsdl(self) -> bytes: - """ return wsdl""" + """Return wsdl.""" self._logger.debug('_onGetWsdl returns {}', self._wsdl_string) return self._wsdl_string diff --git a/src/sdc11073/provider/operations.py b/src/sdc11073/provider/operations.py index 8ff07bd7..e68450fc 100644 --- a/src/sdc11073/provider/operations.py +++ b/src/sdc11073/provider/operations.py @@ -1,117 +1,229 @@ +from __future__ import annotations + import inspect import sys +import time +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, 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 + +if TYPE_CHECKING: + from lxml.etree import QName + 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, InvocationState + from sdc11073.xml_types.pm_types import CodedValue, OperatingMode + + +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.""" -from .sco import OperationDefinition -from ..xml_types import msg_qnames as msg, pm_qnames as pm + operation_instance: OperationDefinitionProtocol + operation_request: AbstractSet + soap_message: ReceivedSoapMessage -class SetStringOperation(OperationDefinition): +@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: + """Base class of all provided operations. + + An operation is a point for remote control over the network. + """ + + 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) + OP_DESCR_QNAME: QName | None = None # to be defined in derived classes + OP_STATE_QNAME: QName | None = None # to be defined in derived classes + + 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): + """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: 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 descriptor_container(self) -> AbstractDescriptorProtocol: # noqa: D102 + return self._descriptor_container + + def execute_operation(self, + soap_request: ReceivedSoapMessage, + operation_request: AbstractSet) -> ExecuteResult: + """Execute the operation itself. + + This method calls the provided operation_handler. + """ + 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 execute_result + + 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 + 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): + """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 "%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._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 "%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 + 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 __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} ' + f'operation-target={self.operation_target_handle}') + + +class SetStringOperation(OperationDefinitionBase): + """Implementation of SetString operation.""" + 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.""" - @classmethod - def from_operation_container(cls, operation_container): - return cls(handle=operation_container.handle, - operation_target_handle=operation_container.OperationTarget) +class ActivateOperation(OperationDefinitionBase): + """Parameters of an ActivateOperation.""" -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): + """Parameters of an SetAlertStateOperation.""" -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): + """Parameters of an SetComponentStateOperation.""" -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): + """Parameters of an SetMetricStateOperation.""" -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 @@ -123,8 +235,6 @@ def __init__(self, handle, operation_target_handle, coded_value=None, log_prefix _operation_lookup_by_type = {c.OP_DESCR_QNAME: c for c in _classes_with_qname} -def get_operation_class(q_name): - """ - :param q_name: a QName instance - """ +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/contextserviceimpl.py b/src/sdc11073/provider/porttypes/contextserviceimpl.py index 6c511b22..37291dc1 100644 --- a/src/sdc11073/provider/porttypes/contextserviceimpl.py +++ b/src/sdc11073/provider/porttypes/contextserviceimpl.py @@ -1,17 +1,25 @@ from __future__ import annotations from collections import OrderedDict -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING -from .porttypebase import ServiceWithOperations, WSDLMessageDescription, WSDLOperationBinding, msg_prefix -from .porttypebase import mk_wsdl_two_way_operation, mk_wsdl_one_way_operation +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, +) from .stateeventserviceimpl import fill_episodic_report_body, fill_periodic_report_body -from ...dispatch import DispatchKey -from ...namespaces import PrefixesEnum if TYPE_CHECKING: - from ...mdib.statecontainers import AbstractStateContainer - from ..periodicreports import PeriodicStates + from sdc11073.mdib.mdibbase import MdibVersionGroup + from sdc11073.mdib.statecontainers import AbstractStateContainer + from sdc11073.provider.periodicreports import PeriodicStates class ContextService(ServiceWithOperations): @@ -81,7 +89,7 @@ def _on_get_context_states(self, request_data): # MDS descriptor, then all context states that are part of this MDS SHALL be included in the result list. descr = self._mdib.descriptions.handle.get_one(handle, allow_none=True) if descr: - if descr.NODETYPE == pm_names.MdsDescriptor: + if pm_names.MdsDescriptor == descr.NODETYPE: tmp = list(self._mdib.context_states.objects) if tmp: for state in tmp: @@ -101,8 +109,8 @@ def add_wsdl_port_type(self, parent_node): mk_wsdl_one_way_operation(port_type, operation_name='EpisodicContextReport') mk_wsdl_one_way_operation(port_type, operation_name='PeriodicContextReport') - def send_episodic_context_report(self, states: List[AbstractStateContainer], - mdib_version_group): + def send_episodic_context_report(self, states: list[AbstractStateContainer], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model nsh = data_model.ns_helper subscription_mgr = self.hosting_service.subscriptions_manager @@ -113,11 +121,10 @@ 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.value, mdib_version_group, - 'send_episodic_context_report') + subscription_mgr.send_to_subscribers(body_node, report.action.value, mdib_version_group) - def send_periodic_context_report(self, periodic_states_list: List[PeriodicStates], - mdib_version_group): + def send_periodic_context_report(self, periodic_states_list: list[PeriodicStates], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.PeriodicContextReport() @@ -127,5 +134,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.value, mdib_version_group, - 'send_periodic_context_report') + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) diff --git a/src/sdc11073/provider/porttypes/descriptioneventserviceimpl.py b/src/sdc11073/provider/porttypes/descriptioneventserviceimpl.py index fb6cd9c8..0779951d 100644 --- a/src/sdc11073/provider/porttypes/descriptioneventserviceimpl.py +++ b/src/sdc11073/provider/porttypes/descriptioneventserviceimpl.py @@ -1,15 +1,23 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING -from ...namespaces import PrefixesEnum -from .porttypebase import DPWSPortTypeBase, WSDLMessageDescription, WSDLOperationBinding, mk_wsdl_one_way_operation -from .porttypebase import msg_prefix +from sdc11073.namespaces import PrefixesEnum + +from .porttypebase import ( + DPWSPortTypeBase, + WSDLMessageDescription, + WSDLOperationBinding, + mk_wsdl_one_way_operation, + msg_prefix, +) if TYPE_CHECKING: - from ...mdib.descriptorcontainers import AbstractDescriptorContainer - from ...mdib.statecontainers import AbstractStateContainer from sdc11073 import xml_utils + from sdc11073.mdib.descriptorcontainers import AbstractDescriptorContainer + from sdc11073.mdib.mdibbase import MdibVersionGroup + from sdc11073.mdib.statecontainers import AbstractStateContainer + class DescriptionEventService(DPWSPortTypeBase): port_type_name = PrefixesEnum.SDC.tag('DescriptionEventService') @@ -24,22 +32,24 @@ def add_wsdl_port_type(self, parent_node): port_type = self._mk_port_type_node(parent_node, True) mk_wsdl_one_way_operation(port_type, operation_name='DescriptionModificationReport') - def send_descriptor_updates(self, updated: List[AbstractDescriptorContainer], - created: List[AbstractDescriptorContainer], - deleted: List[AbstractDescriptorContainer], - updated_states: List[AbstractStateContainer], - mdib_version_group): + def send_descriptor_updates(self, updated: list[AbstractDescriptorContainer], + created: list[AbstractDescriptorContainer], + deleted: list[AbstractDescriptorContainer], + updated_states: list[AbstractStateContainer], + mdib_version_group: MdibVersionGroup): subscription_mgr = self.hosting_service.subscriptions_manager action = self._sdc_definitions.Actions.DescriptionModificationReport - # body_node = self._msg_factory.mk_description_modification_report_body( - # mdib_version_group, updated, created, deleted, updated_states) 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.value, mdib_version_group, 'send_descriptor_updates') + subscription_mgr.send_to_subscribers(body_node, action.value, mdib_version_group) - def mk_description_modification_report_body(self, mdib_version_group, updated, created, deleted, - updated_states) -> xml_utils.LxmlElement: + def mk_description_modification_report_body(self, + mdib_version_group: MdibVersionGroup, + updated: list[AbstractDescriptorContainer], + created: list[AbstractDescriptorContainer], + deleted: list[AbstractDescriptorContainer], + updated_states: list[AbstractStateContainer]) -> xml_utils.LxmlElement: # This method creates one ReportPart for every descriptor. # An optimization is possible by grouping all descriptors with the same parent handle into one ReportPart. # This is not implemented, and I think it is not needed. diff --git a/src/sdc11073/provider/porttypes/porttypebase.py b/src/sdc11073/provider/porttypes/porttypebase.py index 5b504915..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') @@ -89,8 +95,7 @@ def __repr__(self): 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 53d25e84..21338b18 100644 --- a/src/sdc11073/provider/porttypes/setserviceimpl.py +++ b/src/sdc11073/provider/porttypes/setserviceimpl.py @@ -1,26 +1,36 @@ 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 + from sdc11073.mdib.mdibbase import MdibVersionGroup + @runtime_checkable class SetServiceProtocol(Protocol): def notify_operation(self, operation: OperationDefinition, transaction_id: int, - invocation_state, - mdib_version_group, - error: Optional[Enum] = None, - error_message: Optional[str] = None): + invocation_state: Enum, + mdib_version_group: MdibVersionGroup, + operation_target: str | None = None, + error: Enum | None = None, + error_message: str | None = None): ... @@ -79,9 +89,10 @@ def register_hosting_service(self, hosting_service): hosting_service.register_post_handler(DispatchKey(actions.SetComponentState, msg_names.SetComponentState), self._on_set_component_state) - def _on_activate(self, request_data): # pylint:disable=unused-argument + def _on_activate(self, request_data): """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) @@ -89,9 +100,10 @@ def _on_activate(self, request_data): # pylint:disable=unused-argument response = data_model.msg_types.ActivateResponse() return self._handle_operation_request(request_data, activate, response) - def _on_set_value(self, request_data): # pylint:disable=unused-argument + def _on_set_value(self, request_data): """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 @@ -99,9 +111,10 @@ def _on_set_value(self, request_data): # pylint:disable=unused-argument response = data_model.msg_types.SetValueResponse() return self._handle_operation_request(request_data, set_value, response) - def _on_set_string(self, request_data): # pylint:disable=unused-argument + def _on_set_string(self, request_data): """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 @@ -109,9 +122,10 @@ def _on_set_string(self, request_data): # pylint:disable=unused-argument response = data_model.msg_types.SetStringResponse() return self._handle_operation_request(request_data, set_string, response) - def _on_set_metric_state(self, request_data): # pylint:disable=unused-argument + def _on_set_metric_state(self, request_data): """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 @@ -119,9 +133,10 @@ def _on_set_metric_state(self, request_data): # pylint:disable=unused-argument response = data_model.msg_types.SetMetricStateResponse() return self._handle_operation_request(request_data, set_metric_state, response) - def _on_set_alert_state(self, request_data): # pylint:disable=unused-argument + def _on_set_alert_state(self, request_data): """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 @@ -129,9 +144,10 @@ def _on_set_alert_state(self, request_data): # pylint:disable=unused-argument response = data_model.msg_types.SetAlertStateResponse() return self._handle_operation_request(request_data, set_alert_state, response) - def _on_set_component_state(self, request_data): # pylint:disable=unused-argument + def _on_set_component_state(self, request_data): """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 +158,11 @@ def _on_set_component_state(self, request_data): # pylint:disable=unused-argume def notify_operation(self, operation: OperationDefinition, transaction_id: int, - invocation_state, - mdib_version_group, - error: Optional[Enum] = None, - error_message: Optional[str] = None): + invocation_state: Enum, + mdib_version_group: MdibVersionGroup, + 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,20 +177,19 @@ 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( '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.value, mdib_version_group, 'notify_operation') + subscription_mgr.send_to_subscribers(body_node, report.action.value, 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 ff661da4..915b5858 100644 --- a/src/sdc11073/provider/porttypes/stateeventserviceimpl.py +++ b/src/sdc11073/provider/porttypes/stateeventserviceimpl.py @@ -1,15 +1,22 @@ from __future__ import annotations from collections import defaultdict -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING -from .porttypebase import DPWSPortTypeBase, WSDLMessageDescription, WSDLOperationBinding, mk_wsdl_one_way_operation -from .porttypebase import msg_prefix -from ...namespaces import PrefixesEnum +from sdc11073.namespaces import PrefixesEnum + +from .porttypebase import ( + DPWSPortTypeBase, + WSDLMessageDescription, + WSDLOperationBinding, + mk_wsdl_one_way_operation, + msg_prefix, +) if TYPE_CHECKING: - from ...mdib.statecontainers import AbstractStateContainer - from ..periodicreports import PeriodicStates + from sdc11073.mdib.mdibbase import MdibVersionGroup + from sdc11073.mdib.statecontainers import AbstractStateContainer + from sdc11073.provider.periodicreports import PeriodicStates class StateEventService(DPWSPortTypeBase): @@ -58,19 +65,18 @@ def add_wsdl_port_type(self, parent_node): mk_wsdl_one_way_operation(port_type, operation_name='PeriodicMetricReport') mk_wsdl_one_way_operation(port_type, operation_name='EpisodicMetricReport') - def send_episodic_metric_report(self, states: List[AbstractStateContainer], - mdib_version_group): + def send_episodic_metric_report(self, states: list[AbstractStateContainer], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.EpisodicMetricReport() 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.value, mdib_version_group, - 'send_episodic_metric_report') + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) - def send_periodic_metric_report(self, periodic_states_list: List[PeriodicStates], - mdib_version_group): + def send_periodic_metric_report(self, periodic_states_list: list[PeriodicStates], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.PeriodicMetricReport() @@ -78,22 +84,20 @@ 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.value, mdib_version_group, - 'send_periodic_metric_report') + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) - def send_episodic_alert_report(self, states: List[AbstractStateContainer], - mdib_version_group): + def send_episodic_alert_report(self, states: list[AbstractStateContainer], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.EpisodicAlertReport() 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.value, mdib_version_group, - 'send_episodic_alert_report') + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) - def send_periodic_alert_report(self, periodic_states_list: List[PeriodicStates], - mdib_version_group): + def send_periodic_alert_report(self, periodic_states_list: list[PeriodicStates], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.PeriodicAlertReport() @@ -101,22 +105,20 @@ 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.value, mdib_version_group, - 'send_periodic_alert_report') + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) - def send_episodic_operational_state_report(self, states: List[AbstractStateContainer], - mdib_version_group): + def send_episodic_operational_state_report(self, states: list[AbstractStateContainer], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.EpisodicOperationalStateReport() 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.value, mdib_version_group, - 'send_episodic_operational_state_report') + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) - def send_periodic_operational_state_report(self, periodic_states_list: List[PeriodicStates], - mdib_version_group): + def send_periodic_operational_state_report(self, periodic_states_list: list[PeriodicStates], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.PeriodicOperationalStateReport() @@ -124,22 +126,20 @@ 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.value, mdib_version_group, - 'send_periodic_operational_state_report') + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) - def send_episodic_component_state_report(self, states: List[AbstractStateContainer], - mdib_version_group): + def send_episodic_component_state_report(self, states: list[AbstractStateContainer], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.EpisodicComponentReport() 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.value, mdib_version_group, - 'send_episodic_component_state_report') + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) - def send_periodic_component_state_report(self, periodic_states_list: List[PeriodicStates], - mdib_version_group): + def send_periodic_component_state_report(self, periodic_states_list: list[PeriodicStates], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.PeriodicComponentReport() @@ -147,12 +147,11 @@ 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.value, mdib_version_group, - 'send_periodic_component_state_report') + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) def fill_episodic_report_body(report, states): - """Helper that splits states list into separate lists per source mds and adds them to report accordingly. """ + """Helper that splits states list into separate lists per source mds and adds them to report accordingly.""" lookup = _separate_states_by_source_mds(states) for source_mds_handle, states in lookup.items(): report_part = report.add_report_part() diff --git a/src/sdc11073/provider/porttypes/waveformserviceimpl.py b/src/sdc11073/provider/porttypes/waveformserviceimpl.py index ca3b4495..18b94e35 100644 --- a/src/sdc11073/provider/porttypes/waveformserviceimpl.py +++ b/src/sdc11073/provider/porttypes/waveformserviceimpl.py @@ -1,13 +1,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING -from .porttypebase import DPWSPortTypeBase, WSDLMessageDescription, WSDLOperationBinding, mk_wsdl_one_way_operation -from .porttypebase import msg_prefix -from ...namespaces import PrefixesEnum +from sdc11073.namespaces import PrefixesEnum + +from .porttypebase import ( + DPWSPortTypeBase, + WSDLMessageDescription, + WSDLOperationBinding, + mk_wsdl_one_way_operation, + msg_prefix, +) if TYPE_CHECKING: - from ...mdib.statecontainers import AbstractStateContainer + from sdc11073.mdib.mdibbase import MdibVersionGroup + from sdc11073.mdib.statecontainers import AbstractStateContainer class WaveformService(DPWSPortTypeBase): @@ -24,12 +31,12 @@ def _mk_offered_subscriptions(self): # unclear if this is needed, it seems wsdl uses Waveform name, action uses WaveformStream return [self._sdc_device.mdib.sdc_definitions.Actions.Waveform] - def send_realtime_samples_report(self, realtime_sample_states: List[AbstractStateContainer], - mdib_version_group): + def send_realtime_samples_report(self, realtime_sample_states: list[AbstractStateContainer], + mdib_version_group: MdibVersionGroup): data_model = self._sdc_definitions.data_model subscription_mgr = self.hosting_service.subscriptions_manager report = data_model.msg_types.WaveformStream() 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.value, mdib_version_group, None) + subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) diff --git a/src/sdc11073/provider/providerimpl.py b/src/sdc11073/provider/providerimpl.py index 83d2f5e7..922608a9 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 58bd66d8..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,149 +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 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): - """ 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 - """ + """Thread that enqueues and processes all operations. + + Progress notifications are sent via subscription manager. + """ + + 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 @@ -165,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: @@ -191,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( @@ -200,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): @@ -221,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 @@ -237,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. @@ -291,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/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 5c6fe822..50a255ee 100644 --- a/src/sdc11073/provider/subscriptionmgr_base.py +++ b/src/sdc11073/provider/subscriptionmgr_base.py @@ -16,7 +16,7 @@ from sdc11073.etc import apply_map from sdc11073.pysoap.soapclient import HTTPReturnCodeError from sdc11073.pysoap.soapenvelope import Fault, faultcodeEnum -from sdc11073.xml_types import eventing_types as evt_types, actions +from sdc11073.xml_types import eventing_types as evt_types from sdc11073.xml_types import isoduration from sdc11073.xml_types.addressing_types import HeaderInformationBlock from sdc11073.xml_types.basetypes import MessageType @@ -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/src/sdc11073/roles/alarmprovider.py b/src/sdc11073/roles/alarmprovider.py index 5a20519a..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,27 +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 OperationDefinition 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.SetAlertStateOperationDescriptor - and 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: @@ -56,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_argument_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 @@ -81,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, []): @@ -103,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()): @@ -127,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. @@ -152,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: @@ -167,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: @@ -198,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] @@ -215,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, []) @@ -240,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) @@ -256,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 @@ -274,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): @@ -284,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, []) @@ -294,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, value): - """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. @@ -303,14 +340,14 @@ def _delegate_alert_signal(self, operation_instance, value): :param value: AlertSignalStateContainer instance :return: """ + 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) @@ -319,13 +356,16 @@ 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 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) @@ -347,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: @@ -356,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: @@ -370,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: @@ -379,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 c4baefc0..6eded5cc 100644 --- a/src/sdc11073/roles/audiopauseprovider.py +++ b/src/sdc11073/roles/audiopauseprovider.py @@ -1,66 +1,88 @@ -from . import providerbase -from sdc11073.roles import nomenclature +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(nomenclature.NomenclatureCodes.MDC_OP_SET_ALL_ALARMS_AUDIO_PAUSE) -MDC_OP_SET_CANCEL_ALARMS_AUDIO_PAUSE = Coding(nomenclature.NomenclatureCodes.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(ProviderRole): + """Example for handling of global 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" + 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, request): # pylint: disable=unused-argument - """ 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, request): # pylint: disab 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,7 +105,7 @@ def _set_global_audio_pause(self, operation_instance, request): # pylint: disab 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 @@ -96,33 +118,37 @@ 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 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, request): # 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, []) @@ -141,19 +167,31 @@ 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 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 @@ -163,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 = {nomenclature.NomenclatureCodes.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(nomenclature.NomenclatureCodes.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 = {nomenclature.NomenclatureCodes.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(nomenclature.NomenclatureCodes.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 940adef3..e0454555 100644 --- a/src/sdc11073/roles/clockprovider.py +++ b/src/sdc11073/roles/clockprovider.py @@ -1,35 +1,48 @@ -from . import providerbase -from sdc11073.roles import nomenclature +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(nomenclature.NomenclatureCodes.MDC_OP_SET_TIME_SYNC_REF_SRC) - self.MDC_ACT_SET_TIME_ZONE = pm_types.CodedValue(nomenclature.NomenclatureCodes.MDC_ACT_SET_TIME_ZONE) - - self.OP_SET_NTP = pm_types.CodedValue(nomenclature.NomenclatureCodes.OP_SET_NTP) - self.OP_SET_TZ = pm_types.CodedValue(nomenclature.NomenclatureCodes.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,36 +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_argument_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_argument_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, value): - """This is the handler for the set ntp server operation. - It sets the ReferenceSource value of clock state""" + 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 @@ -78,19 +96,22 @@ def _set_ntp_string(self, operation_instance, value): 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 ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) - def _set_tz_string(self, operation_instance, value): - """This is the handler for the set time zone operation. - It sets the TimeZone value of clock state.""" + 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 @@ -99,16 +120,18 @@ def _set_tz_string(self, operation_instance, value): 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 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. @@ -121,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 @@ -131,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 = {nomenclature.NomenclatureCodes.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_argument_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 = {nomenclature.NomenclatureCodes.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_argument_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 6da25c59..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,19 +43,20 @@ 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) + operation_handler=self._set_context_state) return None - def _set_context_state(self, operation_instance, proposed_context_states): - """ This is the code that executes the operation itself. - """ + 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: raise ValueError(f'handle {proposed_st.Handle} not found') @@ -44,26 +65,32 @@ def _set_context_state(self, operation_instance, proposed_context_states): # 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.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', @@ -71,17 +98,29 @@ def _set_context_state(self, operation_instance, proposed_context_states): '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 9d40f549..39a19fe4 100644 --- a/src/sdc11073/roles/metricprovider.py +++ b/src/sdc11073/roles/metricprovider.py @@ -1,86 +1,102 @@ -from .providerbase import ProviderRole +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from sdc11073.mdib.statecontainers import AbstractMetricStateContainer, MetricStateProtocol +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 ExecuteParameters, OperationDefinitionBase + + 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 - and op_target_descriptor_container.NODETYPE == pm_names.NumericMetricDescriptor): - 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_argument_handler=self._set_numeric_value) - if (operation_descriptor_container.NODETYPE == pm_names.SetStringOperationDescriptor - and 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_argument_handler=self._set_string) - if operation_descriptor_container.NODETYPE == pm_names.SetMetricStateOperationDescriptor: + 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 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: # noqa: SIM300 + if op_target_descriptor_container.NODETYPE in (pm_names.StringMetricDescriptor, + pm_names.EnumStringMetricDescriptor): + op_cls = operation_cls_getter(pm_names.SetStringOperationDescriptor) + 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: # 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_argument_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, value): - ''' - - :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 - operation_instance.current_value = value + proposed_states = params.operation_request.argument + params.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) + 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) + 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: @@ -88,10 +104,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 @@ -100,6 +116,47 @@ 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 + + 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 + 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 + return ExecuteResult(params.operation_instance.operation_target_handle, + self._mdib.data_model.msg_types.InvocationState.FINISHED) 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 0564cafa..10fe4e89 100644 --- a/src/sdc11073/roles/patientcontextprovider.py +++ b/src/sdc11073/roles/patientcontextprovider.py @@ -1,46 +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_argument_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_argument_handler=self._set_context_state, - timeout_handler=None) + 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 3f614890..94a3fd09 100644 --- a/src/sdc11073/roles/product.py +++ b/src/sdc11073/roles/product.py @@ -1,104 +1,50 @@ -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_argument_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) - return None +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.provider.sco import AbstractScoOperationsRegistry - def _set_component_state(self, operation_instance, value): - """ - - :param operation_instance: the operation - :param value: a list of proposed metric states - :return: - """ - # 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) + from .providerbase import 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 """ + """Register all actively provided operations.""" sco_handle = self._sco.sco_descriptor_container.Handle self._logger.info('init_operations for sco {}.', sco_handle) @@ -137,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 @@ -184,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), ]) @@ -211,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 8f2e06f6..941a5b8f 100644 --- a/src/sdc11073/roles/providerbase.py +++ b/src/sdc11073/roles/providerbase.py @@ -1,137 +1,94 @@ -from functools import partial +from __future__ import annotations -from .. import loghelper -from .. import observableproperties as properties +from typing import TYPE_CHECKING, Callable + +from lxml.etree import QName + +from sdc11073 import loghelper +from sdc11073.provider.operations import 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 + +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. - def init_operations(self, sco): - pass + Implement method in derived class if needed. + """ - 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""" - return + def init_operations(self, sco: AbstractScoOperationsRegistry): + """Initialize and start whatever the provider needs. + + Implement method in derived class if needed. + """ - def make_missing_operations(self, sco): # pylint: disable=unused-argument, no-self-use + 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. + + 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. """ + return None + + 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_post_commit(self, mdib, transaction): - pass - - def _set_numeric_value(self, operation_instance, value): - """ sets a numerical metric value""" - 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 - with self._mdib.transaction_manager() as mgr: - # state = mgr.getMetricState(operation_target_handle) - state = mgr.get_state(operation_target_handle) - 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) - if metric_descriptor_container.MetricCategory in (pm_types.MetricCategory.SETTING, - pm_types.MetricCategory.PRESETTING): - state.MetricValue.Validity = pm_types.MeasurementValidity.VALID - - def _set_string(self, operation_instance, value): - """ sets a string value""" - 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 - with self._mdib.transaction_manager() as mgr: - # state = mgr.getMetricState(operation_target_handle) - state = mgr.get_state(operation_target_handle) - 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) - 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=None, timeout_handler=None): + def on_pre_commit(self, mdib: ProviderMdib, transaction: TransactionManagerProtocol): + """Manipulate the transaction if needed. + + Derived classes can overwrite this method. """ - :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 + def on_post_commit(self, mdib: ProviderMdib, transaction: TransactionManagerProtocol): + """Run stuff after transaction. + + Derived classes can overwrite this method. """ - operation = cls(handle=handle, - operation_target_handle=operation_target_handle, - 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 + + 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..5ecc0764 100644 --- a/tests/70041_MDIB_Final.xml +++ b/tests/70041_MDIB_Final.xml @@ -1,1398 +1,1446 @@ - - - - - - - - - - - - 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 + + + + + + + + + + + + Normal + + + + + + + + + + + + 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. + + + + + + + + + + + An operation to set the patient category. + + + + + + + + + + + + An operation to set a numeric value. + + + + + + + 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 + + + + + + + + + + + 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_client_device.py b/tests/test_client_device.py index cf0a2183..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 @@ -396,8 +398,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') @@ -748,9 +750,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 +966,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 +1389,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 +1602,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) diff --git a/tests/test_operations.py b/tests/test_operations.py index c7c790f2..0047a463 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -8,13 +8,10 @@ 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 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 @@ -34,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).""" @@ -69,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 @@ -143,6 +118,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 +129,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 +155,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 +176,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 @@ -290,6 +269,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) @@ -311,7 +295,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) @@ -395,10 +380,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 +408,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? @@ -550,3 +527,114 @@ 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)) + + 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) + + 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) diff --git a/tests/test_tutorial.py b/tests/test_tutorial.py index c7b39e1d..335fd988 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 SdcV1Definitions -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,31 +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_argument_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_argument_handler=self._handle_operation_2) + self._handle_operation_2) return operation return None - def _handle_operation_1(self, operation_instance, argument): + def _handle_operation_1(self, params: ExecuteParameters) -> ExecuteResult: """This operation does not manipulate the mdib at all, it only registers the call.""" + argument = params.operation_request.argument self.operation1_called += 1 self.operation1_args = argument self._logger.info('_handle_operation_1 called arg={}', argument) + return ExecuteResult(params.operation_instance.operation_target_handle, InvocationState.FINISHED) - def _handle_operation_2(self, operation_instance, argument): + def _handle_operation_2(self, params: ExecuteParameters) -> ExecuteResult: """This operation manipulate it operation target, and only registers the call.""" + 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 ExecuteResult(params.operation_instance.operation_target_handle, InvocationState.FINISHED) class MyProvider2(ProviderRole): @@ -128,25 +143,33 @@ 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): - if operation_descriptor_container.coding != MY_CODE_3.coding: + 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, + self._handle_operation_3) + return operation + else: return None - self._logger.info('instantiating operation 3 from existing descriptor ' - 'handle={}'.format(operation_descriptor_container.Handle)) - return self._mk_operation_from_operation_descriptor(operation_descriptor_container, - operation_cls_getter, - current_argument_handler=self._handle_operation_3) - def _handle_operation_3(self, operation_instance, argument): + def _handle_operation_3(self, params: ExecuteParameters) -> ExecuteResult: """This operation manipulate it operation target, and only registers the call.""" self.operation3_called += 1 + 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 ExecuteResult(params.operation_instance.operation_target_handle, InvocationState.FINISHED) class MyProductImpl(BaseProduct): @@ -227,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), 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)