Skip to content

Commit

Permalink
handling of SystemErrorReport implemented (Draegerwerk#283)
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the title above -->
<!--- Link the corresponding issues after you created the pull request
-->

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)

## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->
- [x] I have updated the [changelog](../CHANGELOG.md) accordingly.
- [ ] I have added tests to cover my changes.
  • Loading branch information
deichmab-draeger authored Nov 10, 2023
1 parent 20e9845 commit 35af6fb
Show file tree
Hide file tree
Showing 15 changed files with 111 additions and 45 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 20 additions & 22 deletions src/sdc11073/consumer/consumerimpl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}


Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/sdc11073/consumer/serviceclients/stateeventservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
10 changes: 10 additions & 0 deletions src/sdc11073/provider/porttypes/stateeventserviceimpl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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."""
Expand Down
16 changes: 15 additions & 1 deletion src/sdc11073/xml_types/actions.py
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
14 changes: 14 additions & 0 deletions src/sdc11073/xml_types/msg_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion tests/test_alertsignaldelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -52,7 +53,7 @@ def setUp(self):
ssl_context_container=None,
validate=CLIENT_VALIDATE,
log_prefix='<client> ')
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')
Expand Down
45 changes: 35 additions & 10 deletions tests/test_client_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -1343,7 +1366,8 @@ def setUp(self):
epr="client1",
validate=CLIENT_VALIDATE,
log_prefix='<cl1> ')
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],
Expand All @@ -1352,7 +1376,8 @@ def setUp(self):
epr="client2",
validate=CLIENT_VALIDATE,
log_prefix='<cl2> ')
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))
Expand Down Expand Up @@ -1417,7 +1442,7 @@ def setUp(self):
validate=CLIENT_VALIDATE,
log_prefix='<Final> ',
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))
Expand Down Expand Up @@ -1473,7 +1498,7 @@ def setUp(self):
log_prefix='<Final> ',
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))
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions tests/test_compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/test_device_periodic_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 35af6fb

Please sign in to comment.