aiohomematic 2026.1.29__py3-none-any.whl

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 (188) hide show
  1. aiohomematic/__init__.py +110 -0
  2. aiohomematic/_log_context_protocol.py +29 -0
  3. aiohomematic/api.py +410 -0
  4. aiohomematic/async_support.py +250 -0
  5. aiohomematic/backend_detection.py +462 -0
  6. aiohomematic/central/__init__.py +103 -0
  7. aiohomematic/central/async_rpc_server.py +760 -0
  8. aiohomematic/central/central_unit.py +1152 -0
  9. aiohomematic/central/config.py +463 -0
  10. aiohomematic/central/config_builder.py +772 -0
  11. aiohomematic/central/connection_state.py +160 -0
  12. aiohomematic/central/coordinators/__init__.py +38 -0
  13. aiohomematic/central/coordinators/cache.py +414 -0
  14. aiohomematic/central/coordinators/client.py +480 -0
  15. aiohomematic/central/coordinators/connection_recovery.py +1141 -0
  16. aiohomematic/central/coordinators/device.py +1166 -0
  17. aiohomematic/central/coordinators/event.py +514 -0
  18. aiohomematic/central/coordinators/hub.py +532 -0
  19. aiohomematic/central/decorators.py +184 -0
  20. aiohomematic/central/device_registry.py +229 -0
  21. aiohomematic/central/events/__init__.py +104 -0
  22. aiohomematic/central/events/bus.py +1392 -0
  23. aiohomematic/central/events/integration.py +424 -0
  24. aiohomematic/central/events/types.py +194 -0
  25. aiohomematic/central/health.py +762 -0
  26. aiohomematic/central/rpc_server.py +353 -0
  27. aiohomematic/central/scheduler.py +794 -0
  28. aiohomematic/central/state_machine.py +391 -0
  29. aiohomematic/client/__init__.py +203 -0
  30. aiohomematic/client/_rpc_errors.py +187 -0
  31. aiohomematic/client/backends/__init__.py +48 -0
  32. aiohomematic/client/backends/base.py +335 -0
  33. aiohomematic/client/backends/capabilities.py +138 -0
  34. aiohomematic/client/backends/ccu.py +487 -0
  35. aiohomematic/client/backends/factory.py +116 -0
  36. aiohomematic/client/backends/homegear.py +294 -0
  37. aiohomematic/client/backends/json_ccu.py +252 -0
  38. aiohomematic/client/backends/protocol.py +316 -0
  39. aiohomematic/client/ccu.py +1857 -0
  40. aiohomematic/client/circuit_breaker.py +459 -0
  41. aiohomematic/client/config.py +64 -0
  42. aiohomematic/client/handlers/__init__.py +40 -0
  43. aiohomematic/client/handlers/backup.py +157 -0
  44. aiohomematic/client/handlers/base.py +79 -0
  45. aiohomematic/client/handlers/device_ops.py +1085 -0
  46. aiohomematic/client/handlers/firmware.py +144 -0
  47. aiohomematic/client/handlers/link_mgmt.py +199 -0
  48. aiohomematic/client/handlers/metadata.py +436 -0
  49. aiohomematic/client/handlers/programs.py +144 -0
  50. aiohomematic/client/handlers/sysvars.py +100 -0
  51. aiohomematic/client/interface_client.py +1304 -0
  52. aiohomematic/client/json_rpc.py +2068 -0
  53. aiohomematic/client/request_coalescer.py +282 -0
  54. aiohomematic/client/rpc_proxy.py +629 -0
  55. aiohomematic/client/state_machine.py +324 -0
  56. aiohomematic/const.py +2207 -0
  57. aiohomematic/context.py +275 -0
  58. aiohomematic/converter.py +270 -0
  59. aiohomematic/decorators.py +390 -0
  60. aiohomematic/exceptions.py +185 -0
  61. aiohomematic/hmcli.py +997 -0
  62. aiohomematic/i18n.py +193 -0
  63. aiohomematic/interfaces/__init__.py +407 -0
  64. aiohomematic/interfaces/central.py +1067 -0
  65. aiohomematic/interfaces/client.py +1096 -0
  66. aiohomematic/interfaces/coordinators.py +63 -0
  67. aiohomematic/interfaces/model.py +1921 -0
  68. aiohomematic/interfaces/operations.py +217 -0
  69. aiohomematic/logging_context.py +134 -0
  70. aiohomematic/metrics/__init__.py +125 -0
  71. aiohomematic/metrics/_protocols.py +140 -0
  72. aiohomematic/metrics/aggregator.py +534 -0
  73. aiohomematic/metrics/dataclasses.py +489 -0
  74. aiohomematic/metrics/emitter.py +292 -0
  75. aiohomematic/metrics/events.py +183 -0
  76. aiohomematic/metrics/keys.py +300 -0
  77. aiohomematic/metrics/observer.py +563 -0
  78. aiohomematic/metrics/stats.py +172 -0
  79. aiohomematic/model/__init__.py +189 -0
  80. aiohomematic/model/availability.py +65 -0
  81. aiohomematic/model/calculated/__init__.py +89 -0
  82. aiohomematic/model/calculated/climate.py +276 -0
  83. aiohomematic/model/calculated/data_point.py +315 -0
  84. aiohomematic/model/calculated/field.py +147 -0
  85. aiohomematic/model/calculated/operating_voltage_level.py +286 -0
  86. aiohomematic/model/calculated/support.py +232 -0
  87. aiohomematic/model/custom/__init__.py +214 -0
  88. aiohomematic/model/custom/capabilities/__init__.py +67 -0
  89. aiohomematic/model/custom/capabilities/climate.py +41 -0
  90. aiohomematic/model/custom/capabilities/light.py +87 -0
  91. aiohomematic/model/custom/capabilities/lock.py +44 -0
  92. aiohomematic/model/custom/capabilities/siren.py +63 -0
  93. aiohomematic/model/custom/climate.py +1130 -0
  94. aiohomematic/model/custom/cover.py +722 -0
  95. aiohomematic/model/custom/data_point.py +360 -0
  96. aiohomematic/model/custom/definition.py +300 -0
  97. aiohomematic/model/custom/field.py +89 -0
  98. aiohomematic/model/custom/light.py +1174 -0
  99. aiohomematic/model/custom/lock.py +322 -0
  100. aiohomematic/model/custom/mixins.py +445 -0
  101. aiohomematic/model/custom/profile.py +945 -0
  102. aiohomematic/model/custom/registry.py +251 -0
  103. aiohomematic/model/custom/siren.py +462 -0
  104. aiohomematic/model/custom/switch.py +195 -0
  105. aiohomematic/model/custom/text_display.py +289 -0
  106. aiohomematic/model/custom/valve.py +78 -0
  107. aiohomematic/model/data_point.py +1416 -0
  108. aiohomematic/model/device.py +1840 -0
  109. aiohomematic/model/event.py +216 -0
  110. aiohomematic/model/generic/__init__.py +327 -0
  111. aiohomematic/model/generic/action.py +40 -0
  112. aiohomematic/model/generic/action_select.py +62 -0
  113. aiohomematic/model/generic/binary_sensor.py +30 -0
  114. aiohomematic/model/generic/button.py +31 -0
  115. aiohomematic/model/generic/data_point.py +177 -0
  116. aiohomematic/model/generic/dummy.py +150 -0
  117. aiohomematic/model/generic/number.py +76 -0
  118. aiohomematic/model/generic/select.py +56 -0
  119. aiohomematic/model/generic/sensor.py +76 -0
  120. aiohomematic/model/generic/switch.py +54 -0
  121. aiohomematic/model/generic/text.py +33 -0
  122. aiohomematic/model/hub/__init__.py +100 -0
  123. aiohomematic/model/hub/binary_sensor.py +24 -0
  124. aiohomematic/model/hub/button.py +28 -0
  125. aiohomematic/model/hub/connectivity.py +190 -0
  126. aiohomematic/model/hub/data_point.py +342 -0
  127. aiohomematic/model/hub/hub.py +864 -0
  128. aiohomematic/model/hub/inbox.py +135 -0
  129. aiohomematic/model/hub/install_mode.py +393 -0
  130. aiohomematic/model/hub/metrics.py +208 -0
  131. aiohomematic/model/hub/number.py +42 -0
  132. aiohomematic/model/hub/select.py +52 -0
  133. aiohomematic/model/hub/sensor.py +37 -0
  134. aiohomematic/model/hub/switch.py +43 -0
  135. aiohomematic/model/hub/text.py +30 -0
  136. aiohomematic/model/hub/update.py +221 -0
  137. aiohomematic/model/support.py +592 -0
  138. aiohomematic/model/update.py +140 -0
  139. aiohomematic/model/week_profile.py +1827 -0
  140. aiohomematic/property_decorators.py +719 -0
  141. aiohomematic/py.typed +0 -0
  142. aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
  143. aiohomematic/rega_scripts/create_backup_start.fn +28 -0
  144. aiohomematic/rega_scripts/create_backup_status.fn +89 -0
  145. aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
  146. aiohomematic/rega_scripts/get_backend_info.fn +25 -0
  147. aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
  148. aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
  149. aiohomematic/rega_scripts/get_serial.fn +44 -0
  150. aiohomematic/rega_scripts/get_service_messages.fn +83 -0
  151. aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
  152. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
  153. aiohomematic/rega_scripts/set_program_state.fn +17 -0
  154. aiohomematic/rega_scripts/set_system_variable.fn +19 -0
  155. aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
  156. aiohomematic/schemas.py +256 -0
  157. aiohomematic/store/__init__.py +55 -0
  158. aiohomematic/store/dynamic/__init__.py +43 -0
  159. aiohomematic/store/dynamic/command.py +250 -0
  160. aiohomematic/store/dynamic/data.py +175 -0
  161. aiohomematic/store/dynamic/details.py +187 -0
  162. aiohomematic/store/dynamic/ping_pong.py +416 -0
  163. aiohomematic/store/persistent/__init__.py +71 -0
  164. aiohomematic/store/persistent/base.py +285 -0
  165. aiohomematic/store/persistent/device.py +233 -0
  166. aiohomematic/store/persistent/incident.py +380 -0
  167. aiohomematic/store/persistent/paramset.py +241 -0
  168. aiohomematic/store/persistent/session.py +556 -0
  169. aiohomematic/store/serialization.py +150 -0
  170. aiohomematic/store/storage.py +689 -0
  171. aiohomematic/store/types.py +526 -0
  172. aiohomematic/store/visibility/__init__.py +40 -0
  173. aiohomematic/store/visibility/parser.py +141 -0
  174. aiohomematic/store/visibility/registry.py +722 -0
  175. aiohomematic/store/visibility/rules.py +307 -0
  176. aiohomematic/strings.json +237 -0
  177. aiohomematic/support.py +706 -0
  178. aiohomematic/tracing.py +236 -0
  179. aiohomematic/translations/de.json +237 -0
  180. aiohomematic/translations/en.json +237 -0
  181. aiohomematic/type_aliases.py +51 -0
  182. aiohomematic/validator.py +128 -0
  183. aiohomematic-2026.1.29.dist-info/METADATA +296 -0
  184. aiohomematic-2026.1.29.dist-info/RECORD +188 -0
  185. aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
  186. aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
  187. aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
  188. aiohomematic-2026.1.29.dist-info/top_level.txt +1 -0
@@ -0,0 +1,514 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Event coordinator for managing event subscriptions and handling.
5
+
6
+ This module provides centralized event subscription management and coordinates
7
+ event handling between data points, system variables, and the EventBus.
8
+
9
+ The EventCoordinator provides:
10
+ - Data point event subscription management
11
+ - System variable event subscription management
12
+ - Event routing and coordination
13
+ - Integration with EventBus for modern event handling
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Callable, Mapping
19
+ from datetime import datetime
20
+ from functools import partial
21
+ import logging
22
+ from typing import TYPE_CHECKING, Any, Final, TypedDict, Unpack
23
+
24
+ from aiohomematic.interfaces import TaskSchedulerProtocol
25
+ from aiohomematic.property_decorators import DelegatedProperty
26
+
27
+ if TYPE_CHECKING:
28
+ from aiohomematic.model.data_point import BaseDataPoint # noqa: F401
29
+
30
+ from aiohomematic.async_support import loop_check
31
+ from aiohomematic.central.decorators import callback_event
32
+ from aiohomematic.central.events import (
33
+ DataPointsCreatedEvent,
34
+ DataPointStatusReceivedEvent,
35
+ DataPointValueReceivedEvent,
36
+ DeviceLifecycleEvent,
37
+ DeviceLifecycleEventType,
38
+ DeviceTriggerEvent,
39
+ EventBus,
40
+ RpcParameterReceivedEvent,
41
+ )
42
+ from aiohomematic.const import (
43
+ DataPointCategory,
44
+ DataPointKey,
45
+ DeviceTriggerEventType,
46
+ EventData,
47
+ Parameter,
48
+ ParamsetKey,
49
+ SystemEventType,
50
+ )
51
+ from aiohomematic.interfaces import (
52
+ BaseParameterDataPointProtocolAny,
53
+ ClientProviderProtocol,
54
+ EventBusProviderProtocol,
55
+ EventPublisherProtocol,
56
+ GenericDataPointProtocol,
57
+ GenericEventProtocol,
58
+ HealthTrackerProtocol,
59
+ LastEventTrackerProtocol,
60
+ )
61
+
62
+ _LOGGER: Final = logging.getLogger(__name__)
63
+ _LOGGER_EVENT: Final = logging.getLogger(f"{__package__}.event")
64
+
65
+
66
+ class SystemEventArgs(TypedDict, total=False):
67
+ """Arguments for all system events (DEVICES_CREATED, DELETE_DEVICES, HUB_REFRESHED)."""
68
+
69
+ # DEVICES_CREATED / HUB_REFRESHED - accepts various mapping types with different value types
70
+ new_data_points: Any
71
+
72
+ # DELETE_DEVICES / DEVICES_DELAYED
73
+ addresses: tuple[str, ...]
74
+ new_addresses: tuple[str, ...]
75
+
76
+ # Additional fields used by various event callers
77
+ source: Any
78
+ interface_id: str
79
+
80
+
81
+ # Type aliases for specific event argument types (for internal documentation)
82
+ DevicesCreatedEventArgs = SystemEventArgs
83
+ DeviceRemovedEventArgs = SystemEventArgs
84
+ HubRefreshedEventArgs = SystemEventArgs
85
+
86
+
87
+ class EventCoordinator(EventBusProviderProtocol, EventPublisherProtocol, LastEventTrackerProtocol):
88
+ """Coordinator for event subscription and handling."""
89
+
90
+ __slots__ = (
91
+ "_client_provider",
92
+ "_data_point_unsubscribes",
93
+ "_event_bus",
94
+ "_health_tracker",
95
+ "_last_event_seen_for_interface",
96
+ "_status_unsubscribes",
97
+ "_task_scheduler",
98
+ )
99
+
100
+ def __init__(
101
+ self,
102
+ *,
103
+ client_provider: ClientProviderProtocol,
104
+ event_bus: EventBus,
105
+ health_tracker: HealthTrackerProtocol,
106
+ task_scheduler: TaskSchedulerProtocol,
107
+ ) -> None:
108
+ """
109
+ Initialize the event coordinator.
110
+
111
+ Args:
112
+ ----
113
+ client_provider: Provider for client access
114
+ event_bus: EventBus for event subscription and publishing
115
+ health_tracker: Health tracker for recording events
116
+ task_scheduler: Provider for task scheduling
117
+
118
+ """
119
+ self._client_provider: Final = client_provider
120
+ self._event_bus: Final = event_bus
121
+ self._health_tracker: Final = health_tracker
122
+ self._task_scheduler: Final = task_scheduler
123
+
124
+ # Store last event seen datetime by interface_id
125
+ self._last_event_seen_for_interface: Final[dict[str, datetime]] = {}
126
+
127
+ # Store data point subscription unsubscribe callbacks for cleanup
128
+ self._data_point_unsubscribes: Final[list[Callable[[], None]]] = []
129
+
130
+ # Store status subscription unsubscribe callbacks for cleanup
131
+ self._status_unsubscribes: Final[list[Callable[[], None]]] = []
132
+
133
+ event_bus: Final = DelegatedProperty[EventBus](path="_event_bus")
134
+
135
+ def add_data_point_subscription(self, *, data_point: BaseParameterDataPointProtocolAny) -> None:
136
+ """
137
+ Add data point to event subscription.
138
+
139
+ This method subscribes the data point's event handler to the EventBus.
140
+
141
+ Args:
142
+ ----
143
+ data_point: Data point to subscribe to events for
144
+
145
+ """
146
+ if isinstance(data_point, GenericDataPointProtocol | GenericEventProtocol) and (
147
+ data_point.is_readable or data_point.has_events
148
+ ):
149
+ # Subscribe data point's event method to EventBus with filtering
150
+
151
+ async def event_handler(*, event: DataPointValueReceivedEvent) -> None:
152
+ """Filter and handle data point events."""
153
+ if event.dpk == data_point.dpk:
154
+ await data_point.event(value=event.value, received_at=event.received_at)
155
+
156
+ self._data_point_unsubscribes.append(
157
+ self._event_bus.subscribe(
158
+ event_type=DataPointValueReceivedEvent, event_key=data_point.dpk, handler=event_handler
159
+ )
160
+ )
161
+
162
+ # Also subscribe for status events if applicable
163
+ self._add_status_subscription(data_point=data_point)
164
+
165
+ def clear(self) -> None:
166
+ """Clear all event subscriptions created by this coordinator."""
167
+ # Clear data point value event subscriptions
168
+ for unsubscribe in self._data_point_unsubscribes:
169
+ unsubscribe()
170
+ self._data_point_unsubscribes.clear()
171
+
172
+ # Clear status event subscriptions
173
+ for unsubscribe in self._status_unsubscribes:
174
+ unsubscribe()
175
+ self._status_unsubscribes.clear()
176
+
177
+ @callback_event
178
+ async def data_point_event(self, *, interface_id: str, channel_address: str, parameter: str, value: Any) -> None:
179
+ """
180
+ Handle data point event from backend.
181
+
182
+ Args:
183
+ ----
184
+ interface_id: Interface identifier
185
+ channel_address: Channel address
186
+ parameter: Parameter name
187
+ value: New value
188
+
189
+ """
190
+ _LOGGER_EVENT.debug(
191
+ "EVENT: interface_id = %s, channel_address = %s, parameter = %s, value = %s",
192
+ interface_id,
193
+ channel_address,
194
+ parameter,
195
+ str(value),
196
+ )
197
+
198
+ if not self._client_provider.has_client(interface_id=interface_id):
199
+ return
200
+
201
+ self.set_last_event_seen_for_interface(interface_id=interface_id)
202
+
203
+ # Handle PONG response
204
+ if parameter == Parameter.PONG:
205
+ if "#" in value:
206
+ v_interface_id, token = value.split("#")
207
+ if (
208
+ v_interface_id == interface_id
209
+ and (client := self._client_provider.get_client(interface_id=interface_id))
210
+ and client.capabilities.ping_pong
211
+ ):
212
+ client.ping_pong_tracker.handle_received_pong(pong_token=token)
213
+ return
214
+
215
+ received_at = datetime.now()
216
+
217
+ # Check if this is a STATUS parameter (e.g., LEVEL_STATUS)
218
+ # If so, also publish a status event to the main parameter
219
+ if parameter.endswith("_STATUS"):
220
+ main_param = parameter[:-7] # Remove "_STATUS" suffix
221
+ main_dpk = DataPointKey(
222
+ interface_id=interface_id,
223
+ channel_address=channel_address,
224
+ paramset_key=ParamsetKey.VALUES,
225
+ parameter=main_param,
226
+ )
227
+ # Publish status update event to main parameter (if subscribed)
228
+ await self._event_bus.publish(
229
+ event=DataPointStatusReceivedEvent(
230
+ timestamp=datetime.now(),
231
+ dpk=main_dpk,
232
+ status_value=value,
233
+ received_at=received_at,
234
+ )
235
+ )
236
+
237
+ # Always publish normal parameter event (for the parameter itself)
238
+ dpk = DataPointKey(
239
+ interface_id=interface_id,
240
+ channel_address=channel_address,
241
+ paramset_key=ParamsetKey.VALUES,
242
+ parameter=parameter,
243
+ )
244
+
245
+ # Publish to EventBus (await directly for synchronous event processing)
246
+ await self._event_bus.publish(
247
+ event=DataPointValueReceivedEvent(
248
+ timestamp=datetime.now(),
249
+ dpk=dpk,
250
+ value=value,
251
+ received_at=received_at,
252
+ )
253
+ )
254
+
255
+ def get_last_event_seen_for_interface(self, *, interface_id: str) -> datetime | None:
256
+ """
257
+ Return the last event seen for an interface.
258
+
259
+ Args:
260
+ ----
261
+ interface_id: Interface identifier
262
+
263
+ Returns:
264
+ -------
265
+ Datetime of last event or None if no event seen yet
266
+
267
+ """
268
+ return self._last_event_seen_for_interface.get(interface_id)
269
+
270
+ def publish_backend_parameter_event(
271
+ self, *, interface_id: str, channel_address: str, parameter: str, value: Any
272
+ ) -> None:
273
+ """
274
+ Publish backend parameter callback.
275
+
276
+ Re-published events from the backend for parameter updates.
277
+
278
+ Args:
279
+ ----
280
+ interface_id: Interface identifier
281
+ channel_address: Channel address
282
+ parameter: Parameter name
283
+ value: New value
284
+
285
+ """
286
+
287
+ async def _publish_backend_parameter_event() -> None:
288
+ """Publish a backend parameter event to the event bus."""
289
+ await self._event_bus.publish(
290
+ event=RpcParameterReceivedEvent(
291
+ timestamp=datetime.now(),
292
+ interface_id=interface_id,
293
+ channel_address=channel_address,
294
+ parameter=parameter,
295
+ value=value,
296
+ )
297
+ )
298
+
299
+ # Publish to EventBus asynchronously using partial to defer coroutine creation
300
+ # and avoid lambda closure capturing variables
301
+ self._task_scheduler.create_task(
302
+ target=partial(_publish_backend_parameter_event),
303
+ name=f"event-bus-backend-param-{channel_address}-{parameter}",
304
+ )
305
+
306
+ @loop_check
307
+ def publish_device_trigger_event(self, *, trigger_type: DeviceTriggerEventType, event_data: EventData) -> None:
308
+ """
309
+ Publish device trigger event for Homematic callbacks.
310
+
311
+ Events like KEYPRESS, IMPULSE, etc. are converted to DeviceTriggerEvent.
312
+
313
+ Args:
314
+ ----
315
+ trigger_type: Type of Homematic event
316
+ event_data: Typed event data containing interface_id, address, parameter, value
317
+
318
+ """
319
+ timestamp = datetime.now()
320
+
321
+ if not (event_data.interface_id and event_data.device_address and event_data.parameter):
322
+ return
323
+
324
+ async def _publish_device_trigger_event() -> None:
325
+ """Publish a device trigger event to the event bus."""
326
+ await self._event_bus.publish(
327
+ event=DeviceTriggerEvent(
328
+ timestamp=timestamp,
329
+ trigger_type=trigger_type,
330
+ model=event_data.model,
331
+ interface_id=event_data.interface_id,
332
+ device_address=event_data.device_address,
333
+ channel_no=event_data.channel_no,
334
+ parameter=event_data.parameter,
335
+ value=event_data.value,
336
+ )
337
+ )
338
+
339
+ # Publish to EventBus using partial to defer coroutine creation
340
+ # and avoid lambda closure capturing variables
341
+ self._task_scheduler.create_task(
342
+ target=partial(_publish_device_trigger_event),
343
+ name=f"event-bus-device-trigger-{event_data.device_address}-{event_data.channel_no}-{event_data.parameter}",
344
+ )
345
+
346
+ @loop_check
347
+ def publish_system_event(self, *, system_event: SystemEventType, **kwargs: Unpack[SystemEventArgs]) -> None:
348
+ """
349
+ Publish system event handlers.
350
+
351
+ System-level events like DEVICES_CREATED, HUB_REFRESHED, etc.
352
+ Converts legacy system events to focused integration events.
353
+
354
+ Args:
355
+ ----
356
+ system_event: Type of system event
357
+ **kwargs: Additional event data
358
+
359
+ """
360
+ timestamp = datetime.now()
361
+
362
+ # Handle device lifecycle events
363
+ if system_event == SystemEventType.DEVICES_CREATED:
364
+ self._emit_devices_created_events(timestamp=timestamp, **kwargs)
365
+ elif system_event == SystemEventType.DEVICES_DELAYED:
366
+ self._emit_devices_delayed_event(timestamp=timestamp, **kwargs)
367
+ elif system_event == SystemEventType.DELETE_DEVICES:
368
+ self._emit_device_removed_event(timestamp=timestamp, **kwargs)
369
+ elif system_event == SystemEventType.HUB_REFRESHED:
370
+ self._emit_hub_refreshed_event(timestamp=timestamp, **kwargs)
371
+
372
+ def set_last_event_seen_for_interface(self, *, interface_id: str) -> None:
373
+ """
374
+ Set the last event seen timestamp for an interface.
375
+
376
+ Args:
377
+ ----
378
+ interface_id: Interface identifier
379
+
380
+ """
381
+ self._last_event_seen_for_interface[interface_id] = datetime.now()
382
+
383
+ # Update health tracker with event received
384
+ self._health_tracker.record_event_received(interface_id=interface_id)
385
+
386
+ def _add_status_subscription(self, *, data_point: BaseParameterDataPointProtocolAny) -> None:
387
+ """
388
+ Add status parameter event subscription for a data point.
389
+
390
+ This method subscribes the data point to receive STATUS parameter events
391
+ if the data point has a paired STATUS parameter.
392
+
393
+ Args:
394
+ ----
395
+ data_point: Data point to subscribe for status events
396
+
397
+ """
398
+ if not hasattr(data_point, "status_dpk") or data_point.status_dpk is None:
399
+ return
400
+
401
+ async def status_event_handler(*, event: DataPointStatusReceivedEvent) -> None:
402
+ """Filter and handle status events."""
403
+ if event.dpk == data_point.dpk:
404
+ data_point.update_status(status_value=event.status_value)
405
+
406
+ self._status_unsubscribes.append(
407
+ self._event_bus.subscribe(
408
+ event_type=DataPointStatusReceivedEvent,
409
+ event_key=data_point.dpk,
410
+ handler=status_event_handler,
411
+ )
412
+ )
413
+
414
+ def _emit_device_removed_event(self, *, timestamp: datetime, **kwargs: Unpack[DeviceRemovedEventArgs]) -> None:
415
+ """Emit DeviceLifecycleEvent for DELETE_DEVICES."""
416
+ if not (device_addresses := kwargs.get("addresses", ())):
417
+ return
418
+
419
+ async def _publish_event() -> None:
420
+ """Publish device removed event."""
421
+ await self._event_bus.publish(
422
+ event=DeviceLifecycleEvent(
423
+ timestamp=timestamp,
424
+ event_type=DeviceLifecycleEventType.REMOVED,
425
+ device_addresses=device_addresses,
426
+ )
427
+ )
428
+
429
+ self._task_scheduler.create_task(
430
+ target=partial(_publish_event),
431
+ name="event-bus-devices-removed",
432
+ )
433
+
434
+ def _emit_devices_created_events(self, *, timestamp: datetime, **kwargs: Unpack[DevicesCreatedEventArgs]) -> None:
435
+ """Emit DeviceLifecycleEvent and DataPointsCreatedEvent for DEVICES_CREATED."""
436
+ new_data_points: Mapping[DataPointCategory, Any] = kwargs.get("new_data_points", {})
437
+
438
+ # Extract device addresses from data points
439
+ device_addresses: set[str] = set()
440
+
441
+ for category, data_points in new_data_points.items():
442
+ if category == DataPointCategory.EVENT:
443
+ continue
444
+ for dp in data_points:
445
+ device_addresses.add(dp.device.address)
446
+
447
+ async def _publish_events() -> None:
448
+ """Publish device lifecycle and data points created events."""
449
+ # Emit DeviceLifecycleEvent for device creation
450
+ if device_addresses:
451
+ await self._event_bus.publish(
452
+ event=DeviceLifecycleEvent(
453
+ timestamp=timestamp,
454
+ event_type=DeviceLifecycleEventType.CREATED,
455
+ device_addresses=tuple(sorted(device_addresses)),
456
+ )
457
+ )
458
+
459
+ # Emit DataPointsCreatedEvent for data point discovery
460
+ if new_data_points:
461
+ await self._event_bus.publish(
462
+ event=DataPointsCreatedEvent(
463
+ timestamp=timestamp,
464
+ new_data_points=new_data_points,
465
+ )
466
+ )
467
+
468
+ self._task_scheduler.create_task(
469
+ target=partial(_publish_events),
470
+ name="event-bus-devices-created",
471
+ )
472
+
473
+ def _emit_devices_delayed_event(self, *, timestamp: datetime, **kwargs: Unpack[SystemEventArgs]) -> None:
474
+ """Emit DeviceLifecycleEvent for DEVICES_DELAYED."""
475
+ if not (new_addresses := kwargs.get("new_addresses", ())):
476
+ return
477
+
478
+ interface_id = kwargs.get("interface_id")
479
+
480
+ async def _publish_event() -> None:
481
+ """Publish devices delayed event."""
482
+ await self._event_bus.publish(
483
+ event=DeviceLifecycleEvent(
484
+ timestamp=timestamp,
485
+ event_type=DeviceLifecycleEventType.DELAYED,
486
+ device_addresses=new_addresses,
487
+ interface_id=interface_id,
488
+ )
489
+ )
490
+
491
+ self._task_scheduler.create_task(
492
+ target=partial(_publish_event),
493
+ name="event-bus-devices-delayed",
494
+ )
495
+
496
+ def _emit_hub_refreshed_event(self, *, timestamp: datetime, **kwargs: Unpack[HubRefreshedEventArgs]) -> None:
497
+ """Emit DataPointsCreatedEvent for HUB_REFRESHED."""
498
+ new_data_points: Any
499
+ if not (new_data_points := kwargs.get("new_data_points", {})):
500
+ return
501
+
502
+ async def _publish_event() -> None:
503
+ """Publish data points created event."""
504
+ await self._event_bus.publish(
505
+ event=DataPointsCreatedEvent(
506
+ timestamp=timestamp,
507
+ new_data_points=new_data_points,
508
+ )
509
+ )
510
+
511
+ self._task_scheduler.create_task(
512
+ target=partial(_publish_event),
513
+ name="event-bus-hub-refreshed",
514
+ )