OpenSMI-Core 0.2.2__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/PKG-INFO +1 -1
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/PKG-INFO +1 -1
- opensmi_core-0.3.1/src/OpenSMI_Core.egg-info/scm_version.json +8 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/lifecycle_mixin.py +5 -1
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/subscription_manager.py +72 -29
- opensmi_core-0.2.2/src/OpenSMI_Core.egg-info/scm_version.json +0 -8
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/.github/workflows/api-docs.yml +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/.github/workflows/python-publish.yml +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/.gitignore +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/.gitlab-ci.yml +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/LICENSE +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/LICENSES/MIT.txt +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/README.md +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/pyproject.toml +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/setup.cfg +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/SOURCES.txt +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/dependency_links.txt +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/requires.txt +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/scm_file_list.json +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/top_level.txt +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/__init__.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/async_task_mixin.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/base_server.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/base_ua_object.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/config.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/enums.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/errors.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/frozen.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/hook.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/logging.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/meta.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/mixin_helpers.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/protocols.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/repr_mixin.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/signal.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/ua.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/ua_enums.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/ua_node_util.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/unit_all.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/unit_base.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/unset.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/tests/test_signal.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/tests/test_unit.py +0 -0
- {opensmi_core-0.2.2 → opensmi_core-0.3.1}/tools/auto_generate_units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OpenSMI-Core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Smart Machine Interface core functionality for server and client applications.
|
|
5
5
|
Author-email: Carsten Harms <Carsten.Harms@dfki.de>, Simon Jungbluth <Simon.Jungbluth@smartfactory.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OpenSMI-Core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Smart Machine Interface core functionality for server and client applications.
|
|
5
5
|
Author-email: Carsten Harms <Carsten.Harms@dfki.de>, Simon Jungbluth <Simon.Jungbluth@smartfactory.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,6 +21,7 @@ reverse order. Order is resolved once, in `LifecycleMixin.__init_subclass__`, fr
|
|
|
21
21
|
import graphlib
|
|
22
22
|
import inspect
|
|
23
23
|
import time
|
|
24
|
+
from asyncio import CancelledError
|
|
24
25
|
from collections.abc import AsyncGenerator, Callable
|
|
25
26
|
from typing import Final, Self, TypeVar, overload
|
|
26
27
|
|
|
@@ -143,7 +144,10 @@ class LifecycleMixin:
|
|
|
143
144
|
started.append(func)
|
|
144
145
|
get_logger(self, _LOGGER).debug("started successfully", func=func.__qualname__)
|
|
145
146
|
except BaseException as init_exception:
|
|
146
|
-
|
|
147
|
+
if isinstance(init_exception, CancelledError):
|
|
148
|
+
get_logger(self, _LOGGER).error("Initialization cancelled")
|
|
149
|
+
else:
|
|
150
|
+
get_logger(self, _LOGGER).exception("Failed to initialize!")
|
|
147
151
|
self.__state = LifecycleState.INITIALIZATION_FAILED
|
|
148
152
|
rollback_exceptions: list[Exception] = await self.__shutdown(started)
|
|
149
153
|
if rollback_exceptions:
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import contextlib
|
|
9
|
+
import time
|
|
8
10
|
from abc import ABC, abstractmethod
|
|
11
|
+
from asyncio import Task
|
|
9
12
|
from collections.abc import Iterable
|
|
10
|
-
from typing import Any
|
|
13
|
+
from typing import Any, Never
|
|
11
14
|
|
|
12
15
|
import structlog
|
|
13
16
|
from asyncua import ua
|
|
@@ -46,49 +49,74 @@ class SubscriptionManager:
|
|
|
46
49
|
_ua_client: Client
|
|
47
50
|
_ua_subscription: Subscription
|
|
48
51
|
|
|
49
|
-
def __init__(
|
|
50
|
-
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
ua_client: Client,
|
|
56
|
+
publishing_interval_ms: float = 100,
|
|
57
|
+
heartbeat_stale_after: float = 10.0,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Create a new instance.
|
|
60
|
+
|
|
61
|
+
:param ua_client: OPC UA client to use.
|
|
62
|
+
:param publishing_interval_ms: Publishing interval in milliseconds for OPC UA subscription
|
|
63
|
+
:param heartbeat_stale_after: Time in seconds until any subscription should be received.
|
|
64
|
+
"""
|
|
65
|
+
self._publishing_interval_ms = publishing_interval_ms
|
|
66
|
+
self._ua_client = ua_client
|
|
67
|
+
|
|
51
68
|
self._data_subscription_map: dict[NodeId, list[UaDataChangeSubscriber]] = {}
|
|
52
69
|
""" Maps a OPC UA node to a list of handlers for data change notifications. """
|
|
53
70
|
self._event_subscription_map: dict[NodeId, list[UaEventSubscriber]] = {}
|
|
54
71
|
""" Maps a OPC UA node to a list of handlers for event notifications. """
|
|
55
72
|
|
|
56
|
-
self.
|
|
57
|
-
|
|
73
|
+
self._heartbeat_task: Task[Never] | None = None
|
|
74
|
+
self._last_heartbeat = time.monotonic()
|
|
75
|
+
self._heartbeat_stale_after = heartbeat_stale_after
|
|
58
76
|
|
|
59
77
|
self.logger = structlog.getLogger("opensmi.SubscriptionManager")
|
|
60
78
|
|
|
61
|
-
async def _create_subscription(self
|
|
62
|
-
self._ua_subscription = await
|
|
79
|
+
async def _create_subscription(self) -> None:
|
|
80
|
+
self._ua_subscription = await self._ua_client.create_subscription(
|
|
81
|
+
period=self._publishing_interval_ms, handler=self
|
|
82
|
+
)
|
|
63
83
|
self.logger.info("Created subscription!", parameters=self._ua_subscription.parameters)
|
|
64
84
|
|
|
65
|
-
async def init(self
|
|
66
|
-
self.
|
|
67
|
-
self._ua_client = ua_client
|
|
68
|
-
await self._create_subscription(ua_client)
|
|
85
|
+
async def init(self) -> None:
|
|
86
|
+
await self._create_subscription()
|
|
69
87
|
|
|
70
88
|
# subscribe to a node that always changes reliably, for example the current time of the server
|
|
71
89
|
# this is done to prevent the subscription from timing out
|
|
72
|
-
current_time_node =
|
|
90
|
+
current_time_node = self._ua_client.get_node(ua.object_ids.ObjectIds.Server_ServerStatus_CurrentTime)
|
|
73
91
|
await self.subscribe_data_change(self, current_time_node) # pyright: ignore[reportArgumentType]
|
|
74
92
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
self._last_heartbeat = time.monotonic()
|
|
94
|
+
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop(), name="heartbeat_task")
|
|
95
|
+
|
|
96
|
+
async def _renew_subscriptions(self) -> None:
|
|
97
|
+
self.logger.info(f"Trying to renew {self.no_of_monitored_items} subscriptions...")
|
|
98
|
+
old_data_subscription_map = self._data_subscription_map.copy()
|
|
99
|
+
old_event_subscription_map = self._event_subscription_map.copy()
|
|
100
|
+
|
|
101
|
+
await self.clear_subscriptions()
|
|
102
|
+
while True:
|
|
103
|
+
try:
|
|
104
|
+
await self._create_subscription()
|
|
105
|
+
# add all old data change subscriptions back
|
|
106
|
+
for node_id, handlers in old_data_subscription_map.items():
|
|
107
|
+
for handler in handlers:
|
|
108
|
+
await self.subscribe_data_change(handler=handler, nodes=[self._ua_client.get_node(node_id)])
|
|
109
|
+
|
|
110
|
+
# add all old event subscriptions back
|
|
111
|
+
for node_id, handlers in old_event_subscription_map.items():
|
|
112
|
+
for handler in handlers:
|
|
113
|
+
await self.subscribe_event(handler=handler, source_node=node_id)
|
|
114
|
+
|
|
115
|
+
self.logger.info("Successfully renewed subscriptions")
|
|
116
|
+
break
|
|
117
|
+
except Exception as err:
|
|
118
|
+
self.logger.warning("Could not renew subscriptions!", reason=err)
|
|
119
|
+
await asyncio.sleep(1)
|
|
92
120
|
|
|
93
121
|
async def clear_subscriptions(self) -> None:
|
|
94
122
|
self.logger.info(f"Clearing {self.no_of_monitored_items} subscriptions...")
|
|
@@ -160,11 +188,24 @@ class SubscriptionManager:
|
|
|
160
188
|
await self._ua_subscription.subscribe_events(sourcenode=source_node)
|
|
161
189
|
self._event_subscription_map[node_id].append(handler)
|
|
162
190
|
|
|
191
|
+
async def _heartbeat_loop(self) -> Never:
|
|
192
|
+
while True:
|
|
193
|
+
await asyncio.sleep(self._heartbeat_stale_after / 4)
|
|
194
|
+
try:
|
|
195
|
+
stale_for = time.monotonic() - self._last_heartbeat
|
|
196
|
+
if stale_for > self._heartbeat_stale_after:
|
|
197
|
+
self.logger.warning("Heartbeat stale, subscriptions presumed dead", stale_for=stale_for)
|
|
198
|
+
await self._renew_subscriptions()
|
|
199
|
+
except Exception as err:
|
|
200
|
+
self.logger.warning("Heartbeat check failed", reason=err)
|
|
201
|
+
|
|
163
202
|
###########################################
|
|
164
203
|
# asyncua callbacks
|
|
165
204
|
|
|
166
205
|
async def datachange_notification(self, node: Node, val: Any, data: DataChangeNotif) -> None:
|
|
167
206
|
"""Handle OPC UA data change notifications callback."""
|
|
207
|
+
self._last_heartbeat = time.monotonic()
|
|
208
|
+
|
|
168
209
|
try:
|
|
169
210
|
for handler in self._data_subscription_map[node.nodeid]: # list of handlers
|
|
170
211
|
if handler == self:
|
|
@@ -181,6 +222,8 @@ class SubscriptionManager:
|
|
|
181
222
|
|
|
182
223
|
async def event_notification(self, ua_event: UaEvent) -> None:
|
|
183
224
|
"""Handle OPC UA event notifications callback."""
|
|
225
|
+
self._last_heartbeat = time.monotonic()
|
|
226
|
+
|
|
184
227
|
# TODO(CaHa): ua_event.emitting_node is always i=2253 (=Server)
|
|
185
228
|
# might be an asyncua problem
|
|
186
229
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|