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.
Files changed (44) hide show
  1. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/PKG-INFO +1 -1
  2. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/PKG-INFO +1 -1
  3. opensmi_core-0.3.1/src/OpenSMI_Core.egg-info/scm_version.json +8 -0
  4. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/lifecycle_mixin.py +5 -1
  5. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/subscription_manager.py +72 -29
  6. opensmi_core-0.2.2/src/OpenSMI_Core.egg-info/scm_version.json +0 -8
  7. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/.github/workflows/api-docs.yml +0 -0
  8. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/.github/workflows/python-publish.yml +0 -0
  9. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/.gitignore +0 -0
  10. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/.gitlab-ci.yml +0 -0
  11. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/LICENSE +0 -0
  12. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/LICENSES/MIT.txt +0 -0
  13. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/README.md +0 -0
  14. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/pyproject.toml +0 -0
  15. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/setup.cfg +0 -0
  16. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/SOURCES.txt +0 -0
  17. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/dependency_links.txt +0 -0
  18. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/requires.txt +0 -0
  19. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/scm_file_list.json +0 -0
  20. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/OpenSMI_Core.egg-info/top_level.txt +0 -0
  21. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/__init__.py +0 -0
  22. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/async_task_mixin.py +0 -0
  23. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/base_server.py +0 -0
  24. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/base_ua_object.py +0 -0
  25. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/config.py +0 -0
  26. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/enums.py +0 -0
  27. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/errors.py +0 -0
  28. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/frozen.py +0 -0
  29. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/hook.py +0 -0
  30. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/logging.py +0 -0
  31. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/meta.py +0 -0
  32. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/mixin_helpers.py +0 -0
  33. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/protocols.py +0 -0
  34. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/repr_mixin.py +0 -0
  35. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/signal.py +0 -0
  36. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/ua.py +0 -0
  37. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/ua_enums.py +0 -0
  38. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/ua_node_util.py +0 -0
  39. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/unit_all.py +0 -0
  40. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/unit_base.py +0 -0
  41. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/src/opensmi/core/unset.py +0 -0
  42. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/tests/test_signal.py +0 -0
  43. {opensmi_core-0.2.2 → opensmi_core-0.3.1}/tests/test_unit.py +0 -0
  44. {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.2.2
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.2.2
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
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.3.1",
3
+ "distance": 0,
4
+ "node": "g672f1787bfda4d814e6bfae7730db36dc7cd0999",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-12"
8
+ }
@@ -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
- get_logger(self, _LOGGER).exception("Failed to initialize!")
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__(self) -> None:
50
- """Create a new instance."""
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.period: float = 100
57
- """Publishing interval in milliseconds for OPC UA subscription."""
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, ua_client: Client):
62
- self._ua_subscription = await ua_client.create_subscription(period=self.period, handler=self)
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, ua_client: Client, period: float = 100) -> None:
66
- self.period = period
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 = ua_client.get_node(ua.object_ids.ObjectIds.Server_ServerStatus_CurrentTime)
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
- # async def renew_subscriptions(self, ua_client: Client) -> None:
76
- # self.logger.info(f"Renewing {self.no_of_monitored_items} subscriptions...")
77
- # old_data_subscription_map = self._data_subscription_map.copy()
78
- # old_event_subscription_map = self._event_subscription_map.copy()
79
- #
80
- # await self.clear_subscriptions()
81
- # await self._create_subscription(ua_client)
82
- #
83
- # # add all old data change subscriptions back
84
- # for node_id, handlers in old_data_subscription_map.items():
85
- # for handler in handlers:
86
- # await self.subscribe_data_change(handler=handler, nodes=[ua_client.get_node(node_id)])
87
- #
88
- # # add all old event subscriptions back
89
- # for node_id, handlers in old_event_subscription_map.items():
90
- # for handler in handlers:
91
- # await self.subscribe_event(handler=handler, source_node=node_id)
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:
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.2.2",
3
- "distance": 0,
4
- "node": "g8f5549a283bfc653baf8cb55a097dd3f5f43eab5",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-06"
8
- }
File without changes
File without changes
File without changes
File without changes