OpenSMI-Core 0.3.1__tar.gz → 0.4__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.3.1 → opensmi_core-0.4}/PKG-INFO +1 -1
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/OpenSMI_Core.egg-info/PKG-INFO +1 -1
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/OpenSMI_Core.egg-info/scm_version.json +2 -2
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/subscription_manager.py +154 -46
- {opensmi_core-0.3.1 → opensmi_core-0.4}/.github/workflows/api-docs.yml +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/.github/workflows/python-publish.yml +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/.gitignore +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/.gitlab-ci.yml +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/LICENSE +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/LICENSES/MIT.txt +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/README.md +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/pyproject.toml +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/setup.cfg +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/OpenSMI_Core.egg-info/SOURCES.txt +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/OpenSMI_Core.egg-info/dependency_links.txt +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/OpenSMI_Core.egg-info/requires.txt +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/OpenSMI_Core.egg-info/scm_file_list.json +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/OpenSMI_Core.egg-info/top_level.txt +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/__init__.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/async_task_mixin.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/base_server.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/base_ua_object.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/config.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/enums.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/errors.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/frozen.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/hook.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/lifecycle_mixin.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/logging.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/meta.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/mixin_helpers.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/protocols.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/repr_mixin.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/signal.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/ua.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/ua_enums.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/ua_node_util.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/unit_all.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/unit_base.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/src/opensmi/core/unset.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/tests/test_signal.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/tests/test_unit.py +0 -0
- {opensmi_core-0.3.1 → opensmi_core-0.4}/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.4
|
|
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.4
|
|
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
|
|
@@ -10,7 +10,8 @@ import time
|
|
|
10
10
|
from abc import ABC, abstractmethod
|
|
11
11
|
from asyncio import Task
|
|
12
12
|
from collections.abc import Iterable
|
|
13
|
-
from
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Generic, Never, TypeVar
|
|
14
15
|
|
|
15
16
|
import structlog
|
|
16
17
|
from asyncua import ua
|
|
@@ -39,11 +40,21 @@ class UaEventSubscriber(ABC):
|
|
|
39
40
|
"""Handle OPC UA event notification."""
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
_HandlerType = TypeVar("_HandlerType")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class _MonitoredItem(Generic[_HandlerType]):
|
|
48
|
+
handle: int | None
|
|
49
|
+
"""Handle as returned by ``asyncua``. Is ``None`` when renew failed."""
|
|
50
|
+
handlers: list[_HandlerType] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
42
53
|
class SubscriptionManager:
|
|
43
54
|
"""Centrally manages all OPC UA subscription needs.
|
|
44
55
|
|
|
45
56
|
It bundles subscriptions using only one subscription to an OPC UA server
|
|
46
|
-
to
|
|
57
|
+
to stay within max. subscription limits of certain OPC UA servers.
|
|
47
58
|
"""
|
|
48
59
|
|
|
49
60
|
_ua_client: Client
|
|
@@ -65,10 +76,10 @@ class SubscriptionManager:
|
|
|
65
76
|
self._publishing_interval_ms = publishing_interval_ms
|
|
66
77
|
self._ua_client = ua_client
|
|
67
78
|
|
|
68
|
-
self._data_subscription_map: dict[NodeId,
|
|
69
|
-
""" Maps a OPC UA node to a list of handlers for data change notifications. """
|
|
70
|
-
self._event_subscription_map: dict[NodeId,
|
|
71
|
-
""" Maps a OPC UA node to a list of handlers for event notifications. """
|
|
79
|
+
self._data_subscription_map: dict[NodeId, _MonitoredItem[UaDataChangeSubscriber]] = {}
|
|
80
|
+
""" Maps a OPC UA node to a handle and a list of handlers for data change notifications. """
|
|
81
|
+
self._event_subscription_map: dict[NodeId, _MonitoredItem[UaEventSubscriber]] = {}
|
|
82
|
+
""" Maps a OPC UA node to a handle and a list of handlers for event notifications. """
|
|
72
83
|
|
|
73
84
|
self._heartbeat_task: Task[Never] | None = None
|
|
74
85
|
self._last_heartbeat = time.monotonic()
|
|
@@ -83,6 +94,13 @@ class SubscriptionManager:
|
|
|
83
94
|
self.logger.info("Created subscription!", parameters=self._ua_subscription.parameters)
|
|
84
95
|
|
|
85
96
|
async def init(self) -> None:
|
|
97
|
+
"""Create the subscription and start heartbeat monitoring.
|
|
98
|
+
|
|
99
|
+
Must be called once before any ``subscribe_*`` method.
|
|
100
|
+
"""
|
|
101
|
+
if self._heartbeat_task is not None:
|
|
102
|
+
return
|
|
103
|
+
|
|
86
104
|
await self._create_subscription()
|
|
87
105
|
|
|
88
106
|
# subscribe to a node that always changes reliably, for example the current time of the server
|
|
@@ -92,47 +110,80 @@ class SubscriptionManager:
|
|
|
92
110
|
|
|
93
111
|
self._last_heartbeat = time.monotonic()
|
|
94
112
|
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop(), name="heartbeat_task")
|
|
113
|
+
self.logger.info("Subscription manager initialized")
|
|
95
114
|
|
|
96
|
-
async def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
async def shutdown(self) -> None:
|
|
116
|
+
"""Stop heartbeat monitoring and delete the OPC UA subscription."""
|
|
117
|
+
# Stop the heartbeat first, so it can't start renewing subscriptions while shutting down
|
|
118
|
+
if self._heartbeat_task is not None:
|
|
119
|
+
self._heartbeat_task.cancel()
|
|
120
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
121
|
+
await self._heartbeat_task
|
|
122
|
+
self._heartbeat_task = None
|
|
100
123
|
|
|
101
124
|
await self.clear_subscriptions()
|
|
102
|
-
|
|
125
|
+
self.logger.info("Subscription manager shut down")
|
|
126
|
+
|
|
127
|
+
# TODO(CaHa): What about the subscriptions that could not be renewed?
|
|
128
|
+
|
|
129
|
+
async def _renew_data_change_subscriptions(self) -> None:
|
|
130
|
+
for node_id, monitored_item in list(self._data_subscription_map.items()):
|
|
103
131
|
try:
|
|
104
|
-
await self.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
handle = await self._ua_subscription.subscribe_data_change(self._ua_client.get_node(node_id))
|
|
133
|
+
monitored_item.handle = handle
|
|
134
|
+
except UaStatusCodeError as err:
|
|
135
|
+
monitored_item.handle = None
|
|
136
|
+
self.logger.warning("Could not renew data change subscription", node=node_id.to_string(), reason=err)
|
|
137
|
+
|
|
138
|
+
async def _renew_event_subscriptions(self) -> None:
|
|
139
|
+
for node_id, monitored_item in list(self._event_subscription_map.items()):
|
|
140
|
+
try:
|
|
141
|
+
handle = await self._ua_subscription.subscribe_events(node_id)
|
|
142
|
+
monitored_item.handle = handle
|
|
143
|
+
except UaStatusCodeError as err:
|
|
144
|
+
monitored_item.handle = None
|
|
145
|
+
self.logger.warning("Could not renew event subscription", node=node_id.to_string(), reason=err)
|
|
109
146
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
await self.subscribe_event(handler=handler, source_node=node_id)
|
|
147
|
+
async def _renew_subscriptions(self) -> None:
|
|
148
|
+
self.logger.info("Trying to renew subscriptions...", count=self.no_of_monitored_items)
|
|
149
|
+
await self._delete_subscription()
|
|
114
150
|
|
|
151
|
+
while True:
|
|
152
|
+
try:
|
|
153
|
+
await self._create_subscription()
|
|
154
|
+
await self._renew_data_change_subscriptions()
|
|
155
|
+
await self._renew_event_subscriptions()
|
|
156
|
+
self._last_heartbeat = time.monotonic()
|
|
115
157
|
self.logger.info("Successfully renewed subscriptions")
|
|
116
158
|
break
|
|
117
159
|
except Exception as err:
|
|
118
160
|
self.logger.warning("Could not renew subscriptions!", reason=err)
|
|
161
|
+
await self._delete_subscription()
|
|
119
162
|
await asyncio.sleep(1)
|
|
120
163
|
|
|
121
|
-
|
|
122
|
-
self.
|
|
164
|
+
def _invalidate_subscription_handles(self) -> None:
|
|
165
|
+
for item in self._data_subscription_map.values():
|
|
166
|
+
item.handle = None
|
|
167
|
+
for item in self._event_subscription_map.values():
|
|
168
|
+
item.handle = None
|
|
169
|
+
|
|
170
|
+
async def _delete_subscription(self) -> None:
|
|
171
|
+
"""Best-effort deletion of the server-side subscription; the maps are left untouched."""
|
|
172
|
+
self._invalidate_subscription_handles()
|
|
123
173
|
try:
|
|
124
174
|
await self._ua_subscription.delete()
|
|
125
175
|
self.logger.info("Deleted main OPC UA subscription!")
|
|
126
|
-
except ConnectionError:
|
|
127
|
-
pass #
|
|
128
|
-
except AttributeError:
|
|
129
|
-
pass # we can safely ignore it since there is no subscription to delete
|
|
176
|
+
except (ConnectionError, AttributeError):
|
|
177
|
+
pass # connection is gone or there is no subscription to delete
|
|
130
178
|
except Exception as err:
|
|
131
179
|
self.logger.exception("Could not delete main OPC UA subscription!", reason=err)
|
|
132
|
-
|
|
133
180
|
with contextlib.suppress(NameError, AttributeError):
|
|
134
181
|
del self._ua_subscription
|
|
135
182
|
|
|
183
|
+
async def clear_subscriptions(self) -> None:
|
|
184
|
+
"""Delete the OPC UA subscription and forget all registered handlers."""
|
|
185
|
+
self.logger.info("Clearing subscriptions...", count=self.no_of_monitored_items)
|
|
186
|
+
await self._delete_subscription()
|
|
136
187
|
self._data_subscription_map.clear()
|
|
137
188
|
self._event_subscription_map.clear()
|
|
138
189
|
|
|
@@ -153,11 +204,12 @@ class SubscriptionManager:
|
|
|
153
204
|
node = self._ua_client.get_node(node)
|
|
154
205
|
|
|
155
206
|
if node.nodeid not in self._data_subscription_map:
|
|
156
|
-
self._data_subscription_map[node.nodeid] = []
|
|
157
207
|
try:
|
|
158
|
-
await self._ua_subscription.subscribe_data_change(node)
|
|
159
|
-
self._data_subscription_map[node.nodeid]
|
|
160
|
-
self.logger.debug(
|
|
208
|
+
handle = await self._ua_subscription.subscribe_data_change(node)
|
|
209
|
+
self._data_subscription_map[node.nodeid] = _MonitoredItem(handle=handle, handlers=[handler])
|
|
210
|
+
self.logger.debug(
|
|
211
|
+
"Added data change handler", handler=handler, node=node.nodeid.to_string(), handle=handle
|
|
212
|
+
)
|
|
161
213
|
except BadAttributeIdInvalid:
|
|
162
214
|
self.logger.warning("Node does not support subscription!", node=node.nodeid.to_string())
|
|
163
215
|
except UaStatusCodeError as err:
|
|
@@ -169,7 +221,7 @@ class SubscriptionManager:
|
|
|
169
221
|
handler=handler,
|
|
170
222
|
node=node.nodeid.to_string(),
|
|
171
223
|
)
|
|
172
|
-
self._data_subscription_map[node.nodeid].append(handler)
|
|
224
|
+
self._data_subscription_map[node.nodeid].handlers.append(handler)
|
|
173
225
|
|
|
174
226
|
async def subscribe_event(self, handler: UaEventSubscriber, source_node: Node | NodeId) -> None:
|
|
175
227
|
"""Add given handler to receive event notifications for given node."""
|
|
@@ -182,11 +234,57 @@ class SubscriptionManager:
|
|
|
182
234
|
msg = f"Given source_node {source_node} is not supported!"
|
|
183
235
|
raise TypeError(msg)
|
|
184
236
|
|
|
185
|
-
if
|
|
186
|
-
self.
|
|
237
|
+
if node_id not in self._event_subscription_map:
|
|
238
|
+
handle = await self._ua_subscription.subscribe_events(sourcenode=source_node)
|
|
239
|
+
self._event_subscription_map[node_id] = _MonitoredItem(handle=handle, handlers=[handler])
|
|
240
|
+
self.logger.debug("Added event handler", handler=handler, node=node_id.to_string(), handle=handle)
|
|
241
|
+
else:
|
|
242
|
+
# We only need 1 subscription for n handlers.
|
|
243
|
+
self.logger.debug(
|
|
244
|
+
"Added event handler, reusing existing subscription.", handler=handler, node=node_id.to_string()
|
|
245
|
+
)
|
|
246
|
+
self._event_subscription_map[node_id].handlers.append(handler)
|
|
247
|
+
|
|
248
|
+
async def unsubscribe_data_change(
|
|
249
|
+
self, handler: UaDataChangeSubscriber, nodes: Node | NodeId | Iterable[Node | NodeId]
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Remove handler for given nodes; removes server subscription once it has no handlers left."""
|
|
252
|
+
if not isinstance(nodes, Iterable):
|
|
253
|
+
nodes = [nodes]
|
|
254
|
+
|
|
255
|
+
for node in nodes:
|
|
256
|
+
node_id = node.nodeid if isinstance(node, Node) else node
|
|
257
|
+
await self._unsubscribe(handler=handler, node_id=node_id, subscription_map=self._data_subscription_map)
|
|
187
258
|
|
|
188
|
-
|
|
189
|
-
|
|
259
|
+
async def unsubscribe_event(self, handler: UaEventSubscriber, source_node: Node | NodeId) -> None:
|
|
260
|
+
"""Remove handler for events of given node; removes server subscription once it has no handlers left."""
|
|
261
|
+
node_id = source_node.nodeid if isinstance(source_node, Node) else source_node
|
|
262
|
+
await self._unsubscribe(handler=handler, node_id=node_id, subscription_map=self._event_subscription_map)
|
|
263
|
+
|
|
264
|
+
async def _unsubscribe(
|
|
265
|
+
self,
|
|
266
|
+
*,
|
|
267
|
+
handler: _HandlerType,
|
|
268
|
+
node_id: NodeId,
|
|
269
|
+
subscription_map: dict[NodeId, _MonitoredItem[_HandlerType]],
|
|
270
|
+
) -> None:
|
|
271
|
+
item = subscription_map.get(node_id)
|
|
272
|
+
if item is None or handler not in item.handlers:
|
|
273
|
+
return # nothing to do
|
|
274
|
+
|
|
275
|
+
item.handlers.remove(handler)
|
|
276
|
+
|
|
277
|
+
if len(item.handlers) > 0:
|
|
278
|
+
return # other handlers still need the monitored item
|
|
279
|
+
|
|
280
|
+
del subscription_map[node_id]
|
|
281
|
+
if item.handle is None:
|
|
282
|
+
return # nothing to unsubscribe
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
await self._ua_subscription.unsubscribe(item.handle)
|
|
286
|
+
except (UaStatusCodeError, ConnectionError) as err:
|
|
287
|
+
self.logger.warning("Could not delete monitored item", node=node_id.to_string(), reason=err)
|
|
190
288
|
|
|
191
289
|
async def _heartbeat_loop(self) -> Never:
|
|
192
290
|
while True:
|
|
@@ -206,15 +304,20 @@ class SubscriptionManager:
|
|
|
206
304
|
"""Handle OPC UA data change notifications callback."""
|
|
207
305
|
self._last_heartbeat = time.monotonic()
|
|
208
306
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if handler == self:
|
|
212
|
-
continue # current time, not required for anything specific
|
|
213
|
-
await handler.ua_on_data_change(node, val, data)
|
|
214
|
-
except KeyError:
|
|
307
|
+
item = self._data_subscription_map.get(node.nodeid)
|
|
308
|
+
if item is None:
|
|
215
309
|
self.logger.warning(
|
|
216
310
|
"Received DataChange Notification with no handlers", key=node.nodeid.to_string(), value=val
|
|
217
311
|
)
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
for handler in list(item.handlers): # copy, to guard against unsubscription during iteration
|
|
315
|
+
if handler is self:
|
|
316
|
+
continue # current time, not required for anything specific
|
|
317
|
+
try:
|
|
318
|
+
await handler.ua_on_data_change(node, val, data)
|
|
319
|
+
except Exception as err:
|
|
320
|
+
self.logger.warning("Data change handler failed", handler=handler, reason=err)
|
|
218
321
|
|
|
219
322
|
async def status_change_notification(self, status: ua.StatusChangeNotification) -> None:
|
|
220
323
|
"""Handle OPC UA status notifications callback."""
|
|
@@ -226,8 +329,13 @@ class SubscriptionManager:
|
|
|
226
329
|
|
|
227
330
|
# TODO(CaHa): ua_event.emitting_node is always i=2253 (=Server)
|
|
228
331
|
# might be an asyncua problem
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
await handler.ua_on_event(ua_event)
|
|
232
|
-
except KeyError:
|
|
332
|
+
item = self._event_subscription_map.get(ua_event.SourceNode)
|
|
333
|
+
if item is None:
|
|
233
334
|
self.logger.warning("Received Event Notification with no handlers", ua_event=ua_event)
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
for handler in list(item.handlers): # copy, to guard against unsubscription during iteration
|
|
338
|
+
try:
|
|
339
|
+
await handler.ua_on_event(ua_event)
|
|
340
|
+
except Exception as err:
|
|
341
|
+
self.logger.warning("Event handler failed", handler=handler, reason=err)
|
|
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
|
|
File without changes
|