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.
- aiohomematic/__init__.py +110 -0
- aiohomematic/_log_context_protocol.py +29 -0
- aiohomematic/api.py +410 -0
- aiohomematic/async_support.py +250 -0
- aiohomematic/backend_detection.py +462 -0
- aiohomematic/central/__init__.py +103 -0
- aiohomematic/central/async_rpc_server.py +760 -0
- aiohomematic/central/central_unit.py +1152 -0
- aiohomematic/central/config.py +463 -0
- aiohomematic/central/config_builder.py +772 -0
- aiohomematic/central/connection_state.py +160 -0
- aiohomematic/central/coordinators/__init__.py +38 -0
- aiohomematic/central/coordinators/cache.py +414 -0
- aiohomematic/central/coordinators/client.py +480 -0
- aiohomematic/central/coordinators/connection_recovery.py +1141 -0
- aiohomematic/central/coordinators/device.py +1166 -0
- aiohomematic/central/coordinators/event.py +514 -0
- aiohomematic/central/coordinators/hub.py +532 -0
- aiohomematic/central/decorators.py +184 -0
- aiohomematic/central/device_registry.py +229 -0
- aiohomematic/central/events/__init__.py +104 -0
- aiohomematic/central/events/bus.py +1392 -0
- aiohomematic/central/events/integration.py +424 -0
- aiohomematic/central/events/types.py +194 -0
- aiohomematic/central/health.py +762 -0
- aiohomematic/central/rpc_server.py +353 -0
- aiohomematic/central/scheduler.py +794 -0
- aiohomematic/central/state_machine.py +391 -0
- aiohomematic/client/__init__.py +203 -0
- aiohomematic/client/_rpc_errors.py +187 -0
- aiohomematic/client/backends/__init__.py +48 -0
- aiohomematic/client/backends/base.py +335 -0
- aiohomematic/client/backends/capabilities.py +138 -0
- aiohomematic/client/backends/ccu.py +487 -0
- aiohomematic/client/backends/factory.py +116 -0
- aiohomematic/client/backends/homegear.py +294 -0
- aiohomematic/client/backends/json_ccu.py +252 -0
- aiohomematic/client/backends/protocol.py +316 -0
- aiohomematic/client/ccu.py +1857 -0
- aiohomematic/client/circuit_breaker.py +459 -0
- aiohomematic/client/config.py +64 -0
- aiohomematic/client/handlers/__init__.py +40 -0
- aiohomematic/client/handlers/backup.py +157 -0
- aiohomematic/client/handlers/base.py +79 -0
- aiohomematic/client/handlers/device_ops.py +1085 -0
- aiohomematic/client/handlers/firmware.py +144 -0
- aiohomematic/client/handlers/link_mgmt.py +199 -0
- aiohomematic/client/handlers/metadata.py +436 -0
- aiohomematic/client/handlers/programs.py +144 -0
- aiohomematic/client/handlers/sysvars.py +100 -0
- aiohomematic/client/interface_client.py +1304 -0
- aiohomematic/client/json_rpc.py +2068 -0
- aiohomematic/client/request_coalescer.py +282 -0
- aiohomematic/client/rpc_proxy.py +629 -0
- aiohomematic/client/state_machine.py +324 -0
- aiohomematic/const.py +2207 -0
- aiohomematic/context.py +275 -0
- aiohomematic/converter.py +270 -0
- aiohomematic/decorators.py +390 -0
- aiohomematic/exceptions.py +185 -0
- aiohomematic/hmcli.py +997 -0
- aiohomematic/i18n.py +193 -0
- aiohomematic/interfaces/__init__.py +407 -0
- aiohomematic/interfaces/central.py +1067 -0
- aiohomematic/interfaces/client.py +1096 -0
- aiohomematic/interfaces/coordinators.py +63 -0
- aiohomematic/interfaces/model.py +1921 -0
- aiohomematic/interfaces/operations.py +217 -0
- aiohomematic/logging_context.py +134 -0
- aiohomematic/metrics/__init__.py +125 -0
- aiohomematic/metrics/_protocols.py +140 -0
- aiohomematic/metrics/aggregator.py +534 -0
- aiohomematic/metrics/dataclasses.py +489 -0
- aiohomematic/metrics/emitter.py +292 -0
- aiohomematic/metrics/events.py +183 -0
- aiohomematic/metrics/keys.py +300 -0
- aiohomematic/metrics/observer.py +563 -0
- aiohomematic/metrics/stats.py +172 -0
- aiohomematic/model/__init__.py +189 -0
- aiohomematic/model/availability.py +65 -0
- aiohomematic/model/calculated/__init__.py +89 -0
- aiohomematic/model/calculated/climate.py +276 -0
- aiohomematic/model/calculated/data_point.py +315 -0
- aiohomematic/model/calculated/field.py +147 -0
- aiohomematic/model/calculated/operating_voltage_level.py +286 -0
- aiohomematic/model/calculated/support.py +232 -0
- aiohomematic/model/custom/__init__.py +214 -0
- aiohomematic/model/custom/capabilities/__init__.py +67 -0
- aiohomematic/model/custom/capabilities/climate.py +41 -0
- aiohomematic/model/custom/capabilities/light.py +87 -0
- aiohomematic/model/custom/capabilities/lock.py +44 -0
- aiohomematic/model/custom/capabilities/siren.py +63 -0
- aiohomematic/model/custom/climate.py +1130 -0
- aiohomematic/model/custom/cover.py +722 -0
- aiohomematic/model/custom/data_point.py +360 -0
- aiohomematic/model/custom/definition.py +300 -0
- aiohomematic/model/custom/field.py +89 -0
- aiohomematic/model/custom/light.py +1174 -0
- aiohomematic/model/custom/lock.py +322 -0
- aiohomematic/model/custom/mixins.py +445 -0
- aiohomematic/model/custom/profile.py +945 -0
- aiohomematic/model/custom/registry.py +251 -0
- aiohomematic/model/custom/siren.py +462 -0
- aiohomematic/model/custom/switch.py +195 -0
- aiohomematic/model/custom/text_display.py +289 -0
- aiohomematic/model/custom/valve.py +78 -0
- aiohomematic/model/data_point.py +1416 -0
- aiohomematic/model/device.py +1840 -0
- aiohomematic/model/event.py +216 -0
- aiohomematic/model/generic/__init__.py +327 -0
- aiohomematic/model/generic/action.py +40 -0
- aiohomematic/model/generic/action_select.py +62 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +31 -0
- aiohomematic/model/generic/data_point.py +177 -0
- aiohomematic/model/generic/dummy.py +150 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +56 -0
- aiohomematic/model/generic/sensor.py +76 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +33 -0
- aiohomematic/model/hub/__init__.py +100 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/connectivity.py +190 -0
- aiohomematic/model/hub/data_point.py +342 -0
- aiohomematic/model/hub/hub.py +864 -0
- aiohomematic/model/hub/inbox.py +135 -0
- aiohomematic/model/hub/install_mode.py +393 -0
- aiohomematic/model/hub/metrics.py +208 -0
- aiohomematic/model/hub/number.py +42 -0
- aiohomematic/model/hub/select.py +52 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +43 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/hub/update.py +221 -0
- aiohomematic/model/support.py +592 -0
- aiohomematic/model/update.py +140 -0
- aiohomematic/model/week_profile.py +1827 -0
- aiohomematic/property_decorators.py +719 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
- aiohomematic/rega_scripts/create_backup_start.fn +28 -0
- aiohomematic/rega_scripts/create_backup_status.fn +89 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
- aiohomematic/rega_scripts/get_backend_info.fn +25 -0
- aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_service_messages.fn +83 -0
- aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
- aiohomematic/rega_scripts/set_program_state.fn +17 -0
- aiohomematic/rega_scripts/set_system_variable.fn +19 -0
- aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
- aiohomematic/schemas.py +256 -0
- aiohomematic/store/__init__.py +55 -0
- aiohomematic/store/dynamic/__init__.py +43 -0
- aiohomematic/store/dynamic/command.py +250 -0
- aiohomematic/store/dynamic/data.py +175 -0
- aiohomematic/store/dynamic/details.py +187 -0
- aiohomematic/store/dynamic/ping_pong.py +416 -0
- aiohomematic/store/persistent/__init__.py +71 -0
- aiohomematic/store/persistent/base.py +285 -0
- aiohomematic/store/persistent/device.py +233 -0
- aiohomematic/store/persistent/incident.py +380 -0
- aiohomematic/store/persistent/paramset.py +241 -0
- aiohomematic/store/persistent/session.py +556 -0
- aiohomematic/store/serialization.py +150 -0
- aiohomematic/store/storage.py +689 -0
- aiohomematic/store/types.py +526 -0
- aiohomematic/store/visibility/__init__.py +40 -0
- aiohomematic/store/visibility/parser.py +141 -0
- aiohomematic/store/visibility/registry.py +722 -0
- aiohomematic/store/visibility/rules.py +307 -0
- aiohomematic/strings.json +237 -0
- aiohomematic/support.py +706 -0
- aiohomematic/tracing.py +236 -0
- aiohomematic/translations/de.json +237 -0
- aiohomematic/translations/en.json +237 -0
- aiohomematic/type_aliases.py +51 -0
- aiohomematic/validator.py +128 -0
- aiohomematic-2026.1.29.dist-info/METADATA +296 -0
- aiohomematic-2026.1.29.dist-info/RECORD +188 -0
- aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
- aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
- aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|