diff --git a/CHANGELOG.md b/CHANGELOG.md index 7928d7ac..416da9d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - added a way to process operations directly (directly send 'Fin' instead of Wait, Started,...) +- added handling of SystemErrorReports. ### Fixed - basic_logging_setup only handles sdc logger, no more side effect due to calling logging.basicConfig. diff --git a/src/sdc11073/consumer/consumerimpl.py b/src/sdc11073/consumer/consumerimpl.py index 6b17c119..c90fdd9f 100644 --- a/src/sdc11073/consumer/consumerimpl.py +++ b/src/sdc11073/consumer/consumerimpl.py @@ -145,6 +145,7 @@ def _mk_lookup(self) -> dict[str, str]: actions.PeriodicContextReport: 'periodic_context_report', actions.DescriptionModificationReport: 'description_modification_report', actions.OperationInvokedReport: 'operation_invoked_report', + actions.SystemErrorReport: 'system_error_report' } @@ -164,20 +165,21 @@ class SdcConsumer: # the following observables can be used to observe the incoming notifications by message type. # They contain only the body node of the notification, not the envelope - waveform_report = properties.ObservableProperty() - episodic_metric_report = properties.ObservableProperty() - episodic_alert_report = properties.ObservableProperty() - episodic_component_report = properties.ObservableProperty() - episodic_operational_state_report = properties.ObservableProperty() - episodic_context_report = properties.ObservableProperty() - periodic_metric_report = properties.ObservableProperty() - periodic_alert_report = properties.ObservableProperty() - periodic_component_report = properties.ObservableProperty() - periodic_operational_state_report = properties.ObservableProperty() - periodic_context_report = properties.ObservableProperty() - description_modification_report = properties.ObservableProperty() - operation_invoked_report = properties.ObservableProperty() - subscription_end_data = properties.ObservableProperty() # SubscriptionEndData + waveform_report: ReceivedMessage = properties.ObservableProperty() + episodic_metric_report: ReceivedMessage = properties.ObservableProperty() + episodic_alert_report: ReceivedMessage = properties.ObservableProperty() + episodic_component_report: ReceivedMessage = properties.ObservableProperty() + episodic_operational_state_report: ReceivedMessage = properties.ObservableProperty() + episodic_context_report: ReceivedMessage = properties.ObservableProperty() + periodic_metric_report: ReceivedMessage = properties.ObservableProperty() + periodic_alert_report: ReceivedMessage = properties.ObservableProperty() + periodic_component_report: ReceivedMessage = properties.ObservableProperty() + periodic_operational_state_report: ReceivedMessage = properties.ObservableProperty() + periodic_context_report: ReceivedMessage = properties.ObservableProperty() + description_modification_report: ReceivedMessage = properties.ObservableProperty() + operation_invoked_report: ReceivedMessage = properties.ObservableProperty() + subscription_end_data: ReceivedMessage = properties.ObservableProperty() + system_error_report: ReceivedMessage = properties.ObservableProperty() SSL_CIPHERS = None # None : use SSL default @@ -421,9 +423,8 @@ def subscription_mgr(self) -> ConsumerSubscriptionManagerProtocol: """Return the subscription manager.""" return self._subscription_mgr - def start_all(self, not_subscribed_actions: list[str] | None = None, # noqa: PLR0913 + def start_all(self, not_subscribed_actions: Iterable[str] | None = None, fixed_renew_interval: float | None = None, - subscribe_periodic_reports: bool = False, shared_http_server: Any | None = None, check_get_service: bool = True) -> None: """Start background threads, read metadata from device, instantiate detected port type clients and subscribe. @@ -432,7 +433,6 @@ def start_all(self, not_subscribed_actions: list[str] | None = None, # noqa: PL :param fixed_renew_interval: an interval in seconds or None if None, renew is sent when remaining time <= 50% of granted time if set, subscription renew is sent in this interval. - :param subscribe_periodic_reports: :param shared_http_server: if provided, use this http server, else client creates its own. :param check_get_service: if True (default) it checks that a GetService is detected, which is the minimal requirement for a sdc provider. @@ -477,12 +477,12 @@ def start_all(self, not_subscribed_actions: list[str] | None = None, # noqa: PL # flag 'self.all_subscribed' tells mdib that mdib state versions shall not have any gaps # => log warnings for missing versions self.all_subscribed = True - not_subscribed_actions_set = set() + not_subscribed_actions_set = set() if not_subscribed_actions is None else set(not_subscribed_actions) if not_subscribed_actions: - not_subscribed_episodic_actions = [a for a in not_subscribed_actions if "Periodic" not in a] + not_subscribed_episodic_actions = [a for a in not_subscribed_actions + if ("Episodic" in a or "DescriptionModificationReport" in a)] if not_subscribed_episodic_actions: self.all_subscribed = False - not_subscribed_actions_set = set(not_subscribed_actions) # start operationInvoked subscription and tell all operations_manager_class = self._components.operations_manager_class @@ -502,8 +502,6 @@ def start_all(self, not_subscribed_actions: list[str] | None = None, # noqa: PL available_actions.extend(client.get_available_subscriptions()) if len(available_actions) > 0: subscribe_actions = {a for a in available_actions if a.action not in not_subscribed_actions_set} - if not subscribe_periodic_reports: - subscribe_actions = {a for a in subscribe_actions if a.action not in periodic_actions} if len(subscribe_actions) > 0: filter_type = eventing_types.FilterType() filter_type.text = ' '.join(x.action for x in subscribe_actions) diff --git a/src/sdc11073/consumer/serviceclients/stateeventservice.py b/src/sdc11073/consumer/serviceclients/stateeventservice.py index 1b7f5f1d..785edc02 100644 --- a/src/sdc11073/consumer/serviceclients/stateeventservice.py +++ b/src/sdc11073/consumer/serviceclients/stateeventservice.py @@ -18,4 +18,5 @@ class StateEventClient(HostedServiceClient): DispatchKey(Actions.PeriodicAlertReport, msg_qnames.PeriodicAlertReport), DispatchKey(Actions.PeriodicComponentReport, msg_qnames.PeriodicComponentReport), DispatchKey(Actions.PeriodicOperationalStateReport, msg_qnames.PeriodicOperationalStateReport), + DispatchKey(Actions.SystemErrorReport, msg_qnames.SystemErrorReport), ) diff --git a/src/sdc11073/provider/porttypes/stateeventserviceimpl.py b/src/sdc11073/provider/porttypes/stateeventserviceimpl.py index 915b5858..12a80eff 100644 --- a/src/sdc11073/provider/porttypes/stateeventserviceimpl.py +++ b/src/sdc11073/provider/porttypes/stateeventserviceimpl.py @@ -17,6 +17,7 @@ from sdc11073.mdib.mdibbase import MdibVersionGroup from sdc11073.mdib.statecontainers import AbstractStateContainer from sdc11073.provider.periodicreports import PeriodicStates + from sdc11073.xml_types.msg_types import SystemErrorReportPart class StateEventService(DPWSPortTypeBase): @@ -149,6 +150,15 @@ def send_periodic_component_state_report(self, periodic_states_list: list[Period len(periodic_states_list)) subscription_mgr.send_to_subscribers(report, report.action.value, mdib_version_group) + def send_system_error_report(self, report_parts: list[SystemErrorReportPart], + mdib_version_group: MdibVersionGroup): + data_model = self._sdc_definitions.data_model + subscription_mgr = self.hosting_service.subscriptions_manager + report = data_model.msg_types.SystemErrorReport() + report.ReportPart.extend(report_parts) + report.set_mdib_version_group(mdib_version_group) + self._logger.debug('sending SystemErrorReport') + 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.""" diff --git a/src/sdc11073/xml_types/actions.py b/src/sdc11073/xml_types/actions.py index 776af0bc..e74909a0 100644 --- a/src/sdc11073/xml_types/actions.py +++ b/src/sdc11073/xml_types/actions.py @@ -1,11 +1,13 @@ from enum import Enum + from sdc11073.namespaces import default_ns_helper as ns_hlp _ActionsNamespace = ns_hlp.SDC.namespace class Actions(str, Enum): - """ Central definition of all action strings used in BICEPS""" + """Central definition of all action strings used in BICEPS.""" + OperationInvokedReport = _ActionsNamespace + '/SetService/OperationInvokedReport' EpisodicContextReport = _ActionsNamespace + '/ContextService/EpisodicContextReport' EpisodicMetricReport = _ActionsNamespace + '/StateEventService/EpisodicMetricReport' @@ -54,3 +56,15 @@ class Actions(str, Enum): GetDescriptorResponse = _ActionsNamespace + '/ContainmentTreeService/GetDescriptorResponse' GetContainmentTree = _ActionsNamespace + '/ContainmentTreeService/GetContainmentTree' GetContainmentTreeResponse = _ActionsNamespace + '/ContainmentTreeService/GetContainmentTreeResponse' + + +# some sets of actions, useful when user wants to exclude some actions from subscriptions. +# these are the typical sets: +periodic_actions = {Actions.PeriodicContextReport, + Actions.PeriodicMetricReport, + Actions.PeriodicOperationalStateReport, + Actions.PeriodicAlertReport, + Actions.PeriodicComponentReport, + } + +periodic_actions_and_system_error_report = set(periodic_actions).add(Actions.SystemErrorReport) diff --git a/src/sdc11073/xml_types/msg_types.py b/src/sdc11073/xml_types/msg_types.py index c6df3e0e..0f677919 100644 --- a/src/sdc11073/xml_types/msg_types.py +++ b/src/sdc11073/xml_types/msg_types.py @@ -5,6 +5,7 @@ from . import ext_qnames as ext from . import msg_qnames as msg from . import pm_qnames as pm +from . import pm_types from . import xml_structure as cp from .actions import Actions from .basetypes import MessageType @@ -376,6 +377,19 @@ def from_node(cls, node): return instance +class SystemErrorReportPart(AbstractReportPart): + ErrorCode = cp.SubElementProperty(msg.ErrorCode, value_class=pm_types.CodedValue) + ErrorInfo = cp.SubElementListProperty(msg.ErrorInfo, value_class=pm_types.LocalizedText) + _props = ('ErrorCode', 'ErrorInfo') + + +class SystemErrorReport(AbstractReport): + NODETYPE = msg.SystemErrorReport + action = Actions.SystemErrorReport + ReportPart = cp.SubElementListProperty(msg.ReportPart, value_class=SystemErrorReportPart) + _props = ('ReportPart',) + + class AbstractGet(MessageType): pass diff --git a/tests/test_alertsignaldelegate.py b/tests/test_alertsignaldelegate.py index dc0e74b5..d1fc0f01 100644 --- a/tests/test_alertsignaldelegate.py +++ b/tests/test_alertsignaldelegate.py @@ -9,6 +9,7 @@ from sdc11073.mdib.consumermdib import ConsumerMdib from sdc11073.wsdiscovery import WSDiscovery from sdc11073.xml_types import msg_types, pm_types +from sdc11073.xml_types.actions import periodic_actions from tests import utils from tests.mockstuff import SomeDevice @@ -52,7 +53,7 @@ def setUp(self): ssl_context_container=None, validate=CLIENT_VALIDATE, log_prefix=' ') - self.sdc_client.start_all() + self.sdc_client.start_all(not_subscribed_actions=periodic_actions) time.sleep(1) sys.stderr.write(f'\n############### setUp done {self._testMethodName} ##############\n') diff --git a/tests/test_client_device.py b/tests/test_client_device.py index 8b38689e..14332200 100644 --- a/tests/test_client_device.py +++ b/tests/test_client_device.py @@ -30,7 +30,8 @@ from sdc11073.pysoap.soapclient import HTTPReturnCodeError from sdc11073.pysoap.soapclient_async import SoapClientAsync from sdc11073.pysoap.soapenvelope import Soap12Envelope, faultcodeEnum -from sdc11073.xml_types import pm_types, msg_qnames as msg, pm_qnames as pm +from sdc11073.xml_types import pm_types, msg_types, msg_qnames as msg, pm_qnames as pm +from sdc11073.xml_types.actions import periodic_actions from sdc11073.xml_types.addressing_types import HeaderInformationBlock from sdc11073.consumer import SdcConsumer from sdc11073.consumer.components import SdcConsumerComponents @@ -419,7 +420,7 @@ def _run_client_with_device(ssl_context_container): ssl_context_container=ssl_context_container, validate=CLIENT_VALIDATE, specific_components=specific_components) - sdc_client.start_all(subscribe_periodic_reports=True) + sdc_client.start_all(not_subscribed_actions=periodic_actions) time.sleep(1.5) log_watcher.setPaused(True) @@ -498,7 +499,7 @@ def setUp(self): ssl_context_container=None, validate=CLIENT_VALIDATE, specific_components=specific_components) - self.sdc_client.start_all(subscribe_periodic_reports=True) + self.sdc_client.start_all() # with periodic reports and system error report time.sleep(1) sys.stderr.write('\n############### setUp done {} ##############\n'.format(self._testMethodName)) self.logger.info('############### setUp done {} ##############'.format(self._testMethodName)) @@ -640,7 +641,8 @@ def test_no_renew(self): # make renew period much longer than max subscription duration # => all subscription expired, all soap clients closed self.logger.info('starting client again with fixed_renew_interval=1000') - self.sdc_client.start_all(fixed_renew_interval=1000) + self.sdc_client.start_all(not_subscribed_actions=periodic_actions, + fixed_renew_interval=1000) time.sleep(1) self.assertGreater(len(self.sdc_device._soap_client_pool._soap_clients), 0) sleep_time = int(self.sdc_device._max_subscription_duration + 3) @@ -738,7 +740,7 @@ def test_instance_id(self): validate=CLIENT_VALIDATE, specific_components=specific_components, log_prefix='consumer2 ') - sdc_client.start_all(subscribe_periodic_reports=True) + sdc_client.start_all(not_subscribed_actions=periodic_actions) cl_mdib = ConsumerMdib(sdc_client) cl_mdib.init_mdib() @@ -1301,6 +1303,27 @@ def are_equivalent(node1, node2): dev_descriptor = self.sdc_device.mdib.descriptions.handle.get_one(cl_descriptor.Handle) self.assertEqual(dev_descriptor.Extension, cl_descriptor.Extension) + def test_system_error_report(self): + """Verify that a SystemErrorReport is successfully sent to consumer.""" + # Initially the observable shall be None + self.assertIsNone(self.sdc_client.system_error_report) + report_part1 = msg_types.SystemErrorReportPart() + report_part1.ErrorCode = pm_types.CodedValue('xyz') + report_part1.ErrorInfo.append(pm_types.LocalizedText('Oscar was it!')) + report_part2 = msg_types.SystemErrorReportPart() + report_part2.ErrorCode = pm_types.CodedValue('0815') + report_part2.ErrorInfo.append(pm_types.LocalizedText('Now it was Felix!')) + self.sdc_device.hosted_services.state_event_service.send_system_error_report( + [report_part1, report_part2], self.sdc_device.mdib.mdib_version_group) + + # Now the observable shall contain the received message with a SystemErrorReport in payload. + message = self.sdc_client.system_error_report + self.assertIsNotNone(message) + self.assertEqual(message.p_msg.msg_node.tag, msg.SystemErrorReport) + system_error_report = msg_types.SystemErrorReport.from_node(message.p_msg.msg_node) + self.assertEqual(system_error_report.ReportPart[0], report_part1) + self.assertEqual(system_error_report.ReportPart[1], report_part2) + class Test_DeviceCommonHttpServer(unittest.TestCase): @@ -1343,7 +1366,8 @@ def setUp(self): epr="client1", validate=CLIENT_VALIDATE, log_prefix=' ') - self.sdc_client_1.start_all(shared_http_server=self.httpserver) + self.sdc_client_1.start_all(shared_http_server=self.httpserver, + not_subscribed_actions=periodic_actions) x_addr = self.sdc_device_2.get_xaddrs() self.sdc_client_2 = SdcConsumer(x_addr[0], @@ -1352,7 +1376,8 @@ def setUp(self): epr="client2", validate=CLIENT_VALIDATE, log_prefix=' ') - self.sdc_client_2.start_all(shared_http_server=self.httpserver) + self.sdc_client_2.start_all(shared_http_server=self.httpserver, + not_subscribed_actions=periodic_actions) self._all_cl_dev = ((self.sdc_client_1, self.sdc_device_1), (self.sdc_client_2, self.sdc_device_2)) @@ -1417,7 +1442,7 @@ def setUp(self): validate=CLIENT_VALIDATE, log_prefix=' ', request_chunk_size=512) - self.sdc_client.start_all() + self.sdc_client.start_all(not_subscribed_actions=periodic_actions) time.sleep(1) sys.stderr.write('\n############### setUp done {} ##############\n'.format(self._testMethodName)) @@ -1473,7 +1498,7 @@ def setUp(self): log_prefix=' ', specific_components=specific_components, request_chunk_size=512) - self.sdc_client.start_all() + self.sdc_client.start_all(not_subscribed_actions=periodic_actions) time.sleep(1) sys.stderr.write('\n############### setUp done {} ##############\n'.format(self._testMethodName)) @@ -1558,7 +1583,7 @@ def setUp(self): validate=CLIENT_VALIDATE, log_prefix='', request_chunk_size=512) - self.sdc_client.start_all(subscribe_periodic_reports=True) + self.sdc_client.start_all() # subscribe all time.sleep(1) sys.stderr.write('\n############### setUp done {} ##############\n'.format(self._testMethodName)) diff --git a/tests/test_compression.py b/tests/test_compression.py index 3fbb22fa..95b96441 100644 --- a/tests/test_compression.py +++ b/tests/test_compression.py @@ -6,7 +6,7 @@ from sdc11073.consumer import SdcConsumer from sdc11073.httpserver import compression from sdc11073.wsdiscovery import WSDiscovery -from sdc11073.location import SdcLocation +from sdc11073.xml_types.actions import periodic_actions from sdc11073.xml_types.pm_types import InstanceIdentifier from tests import utils from tests.mockstuff import SomeDevice @@ -67,7 +67,7 @@ def _start_with_compression(self, compression_flag): self.sdc_client.set_used_compression() else: self.sdc_client.set_used_compression(compression_flag) - self.sdc_client.start_all() + self.sdc_client.start_all(not_subscribed_actions=periodic_actions) time.sleep(0.5) # Get http connection to execute the call diff --git a/tests/test_device.py b/tests/test_device.py index 985d8103..23effa99 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -4,7 +4,6 @@ from sdc11073 import wsdiscovery from sdc11073.xml_types import pm_types -from sdc11073.location import SdcLocation from tests import utils from tests.mockstuff import SomeDevice diff --git a/tests/test_device_periodic_reports.py b/tests/test_device_periodic_reports.py index cd880402..92ffe7c8 100644 --- a/tests/test_device_periodic_reports.py +++ b/tests/test_device_periodic_reports.py @@ -61,7 +61,7 @@ def tearDown(self): def test_periodic_reports(self): """Test waits 10 seconds and counts reports that have been received in that time.""" - self.sdc_client.start_all(subscribe_periodic_reports=True) + self.sdc_client.start_all() metric_coll = ValuesCollector(self.sdc_client, 'periodic_metric_report', 5) alert_coll = ValuesCollector(self.sdc_client, 'periodic_alert_report', 2) diff --git a/tests/test_operations.py b/tests/test_operations.py index 07793ee7..ad7ad7a7 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -58,7 +58,7 @@ def setUp(self): ssl_context_container=None, validate=CLIENT_VALIDATE, specific_components=specific_components) - self.sdc_client.start_all(subscribe_periodic_reports=True) + self.sdc_client.start_all() time.sleep(1) self._logger.info('############### setUp done %s ##############', self._testMethodName) time.sleep(0.5) @@ -334,7 +334,7 @@ def test_audio_pause_two_clients(self): validate=CLIENT_VALIDATE, specific_components=specific_components, log_prefix='client2') - sdc_client2.start_all(subscribe_periodic_reports=True) + sdc_client2.start_all() try: client_mdib2 = ConsumerMdib(sdc_client2) client_mdib2.init_mdib() diff --git a/tests/test_string_enum_descriptors.py b/tests/test_string_enum_descriptors.py index fb247632..31f4d94b 100644 --- a/tests/test_string_enum_descriptors.py +++ b/tests/test_string_enum_descriptors.py @@ -9,6 +9,7 @@ from sdc11073.mdib.consumermdib import ConsumerMdib from sdc11073.wsdiscovery import WSDiscovery from sdc11073.xml_types import pm_types +from sdc11073.xml_types.actions import periodic_actions from tests import utils from tests.mockstuff import SomeDevice @@ -51,7 +52,7 @@ def setUp(self): ssl_context_container=None, validate=CLIENT_VALIDATE) - self.sdc_client.start_all() + self.sdc_client.start_all(not_subscribed_actions=periodic_actions) time.sleep(1) sys.stderr.write('\n############### setUp done {} ##############\n'.format(self._testMethodName)) diff --git a/tests/test_tutorial.py b/tests/test_tutorial.py index d2654511..be140c3b 100644 --- a/tests/test_tutorial.py +++ b/tests/test_tutorial.py @@ -25,6 +25,7 @@ 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 sdc11073.xml_types.actions import periodic_actions_and_system_error_report from tests import utils if TYPE_CHECKING: @@ -290,7 +291,7 @@ def test_createClient(self): my_client = SdcConsumer.from_wsd_service(services[0], ssl_context_container=None) self.my_clients.append(my_client) - my_client.start_all() + my_client.start_all(not_subscribed_actions=periodic_actions_and_system_error_report) ############# Mdib usage ############################## # In data oriented tests a mdib instance is very handy: # The mdib collects all data and makes it easily available for the test @@ -332,7 +333,7 @@ def test_call_operation(self): my_client = SdcConsumer.from_wsd_service(services[0], ssl_context_container=None) self.my_clients.append(my_client) - my_client.start_all() + my_client.start_all(not_subscribed_actions=periodic_actions_and_system_error_report) my_mdib = ConsumerMdib(my_client) my_mdib.init_mdib() @@ -389,7 +390,7 @@ def test_operation_handler(self): self.service = SdcConsumer.from_wsd_service(services[0], ssl_context_container=None) my_client = self.service self.my_clients.append(my_client) - my_client.start_all() + my_client.start_all(not_subscribed_actions=periodic_actions_and_system_error_report) my_mdib = ConsumerMdib(my_client) my_mdib.init_mdib() diff --git a/tutorial/consumer/consumer.py b/tutorial/consumer/consumer.py index 3aa89ca7..aabf62d4 100644 --- a/tutorial/consumer/consumer.py +++ b/tutorial/consumer/consumer.py @@ -3,6 +3,7 @@ import uuid from sdc11073.xml_types import pm_types, msg_types from sdc11073.xml_types import pm_qnames as pm +from sdc11073.xml_types.actions import periodic_actions from sdc11073.wsdiscovery import WSDiscovery from sdc11073.definitions_sdc import SdcV1Definitions from sdc11073.consumer import SdcConsumer @@ -80,7 +81,7 @@ def set_ensemble_context(mdib: ConsumerMdib, sdc_consumer: SdcConsumer) -> None: # for all interactions with the communication partner 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() + my_client.start_all(not_subscribed_actions=periodic_actions) # all data interactions happen through the MDIB (MedicalDeviceInformationBase) # that contains data as described in the BICEPS standard # this variable will contain the data from the provider