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,324 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Client state machine for managing connection lifecycle.
|
|
5
|
+
|
|
6
|
+
This module provides a state machine for tracking client connection states
|
|
7
|
+
with validated transitions and event emission.
|
|
8
|
+
|
|
9
|
+
The state machine ensures:
|
|
10
|
+
- Only valid state transitions occur
|
|
11
|
+
- State changes are logged for debugging
|
|
12
|
+
- Invalid transitions raise exceptions for early error detection
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
import logging
|
|
19
|
+
from typing import TYPE_CHECKING, Final
|
|
20
|
+
|
|
21
|
+
from aiohomematic.central.events.types import ClientStateChangedEvent
|
|
22
|
+
from aiohomematic.const import ClientState, FailureReason
|
|
23
|
+
from aiohomematic.property_decorators import DelegatedProperty
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from aiohomematic.central.events import EventBus
|
|
27
|
+
|
|
28
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Valid state transitions define which state changes are allowed.
|
|
31
|
+
# This forms a directed graph where each state maps to its valid successors.
|
|
32
|
+
#
|
|
33
|
+
# State Diagram:
|
|
34
|
+
#
|
|
35
|
+
# CREATED ──► INITIALIZING ──► INITIALIZED ──► CONNECTING ──► CONNECTED
|
|
36
|
+
# │ ▲ │
|
|
37
|
+
# ▼ │ ▼
|
|
38
|
+
# FAILED ◄─────────────────────────┬┴─────── DISCONNECTED
|
|
39
|
+
# │ │ ▲
|
|
40
|
+
# ├─────► INITIALIZING │ │
|
|
41
|
+
# ├─────► CONNECTING ◄──────────┴──────── RECONNECTING
|
|
42
|
+
# ├─────► DISCONNECTED (for graceful shutdown) ▲
|
|
43
|
+
# └─────► RECONNECTING ────────────────────────┘
|
|
44
|
+
#
|
|
45
|
+
# STOPPED ◄── STOPPING ◄─────────────────────────(from CONNECTED/DISCONNECTED/RECONNECTING)
|
|
46
|
+
#
|
|
47
|
+
_VALID_TRANSITIONS: Final[dict[ClientState, frozenset[ClientState]]] = {
|
|
48
|
+
# Initial state after client creation - can only begin initialization
|
|
49
|
+
ClientState.CREATED: frozenset({ClientState.INITIALIZING}),
|
|
50
|
+
# During initialization (loading metadata, etc.) - succeeds or fails
|
|
51
|
+
ClientState.INITIALIZING: frozenset({ClientState.INITIALIZED, ClientState.FAILED}),
|
|
52
|
+
# Initialization complete - ready to establish connection
|
|
53
|
+
ClientState.INITIALIZED: frozenset({ClientState.CONNECTING}),
|
|
54
|
+
# Attempting to connect to backend - succeeds or fails
|
|
55
|
+
ClientState.CONNECTING: frozenset({ClientState.CONNECTED, ClientState.FAILED}),
|
|
56
|
+
# Fully connected and operational
|
|
57
|
+
ClientState.CONNECTED: frozenset(
|
|
58
|
+
{
|
|
59
|
+
ClientState.DISCONNECTED, # Connection lost unexpectedly
|
|
60
|
+
ClientState.RECONNECTING, # Attempting automatic reconnection
|
|
61
|
+
ClientState.STOPPING, # Graceful shutdown requested
|
|
62
|
+
}
|
|
63
|
+
),
|
|
64
|
+
# Connection was lost or intentionally closed
|
|
65
|
+
ClientState.DISCONNECTED: frozenset(
|
|
66
|
+
{
|
|
67
|
+
ClientState.CONNECTING, # Manual reconnection attempt
|
|
68
|
+
ClientState.DISCONNECTED, # Idempotent - allows repeated deinitialize calls
|
|
69
|
+
ClientState.RECONNECTING, # Automatic reconnection attempt
|
|
70
|
+
ClientState.STOPPING, # Graceful shutdown requested
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
# Automatic reconnection in progress
|
|
74
|
+
ClientState.RECONNECTING: frozenset(
|
|
75
|
+
{
|
|
76
|
+
ClientState.CONNECTED, # Reconnection succeeded
|
|
77
|
+
ClientState.DISCONNECTED, # Reconnection abandoned
|
|
78
|
+
ClientState.FAILED, # Reconnection failed permanently
|
|
79
|
+
ClientState.CONNECTING, # Retry connection establishment
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
# Graceful shutdown in progress - one-way to STOPPED
|
|
83
|
+
ClientState.STOPPING: frozenset({ClientState.STOPPED}),
|
|
84
|
+
# Terminal state - client is fully stopped, no transitions allowed
|
|
85
|
+
ClientState.STOPPED: frozenset(),
|
|
86
|
+
# Error state - allows retry via re-initialization, reconnection, or graceful shutdown
|
|
87
|
+
ClientState.FAILED: frozenset(
|
|
88
|
+
{
|
|
89
|
+
ClientState.INITIALIZING, # Retry initialization
|
|
90
|
+
ClientState.CONNECTING, # Retry connection
|
|
91
|
+
ClientState.RECONNECTING, # Automatic reconnection attempt
|
|
92
|
+
ClientState.DISCONNECTED, # Graceful shutdown via deinitialize_proxy
|
|
93
|
+
}
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class InvalidStateTransitionError(Exception):
|
|
99
|
+
"""Raised when an invalid state transition is attempted."""
|
|
100
|
+
|
|
101
|
+
def __init__(self, *, current: ClientState, target: ClientState, interface_id: str) -> None:
|
|
102
|
+
"""Initialize the error."""
|
|
103
|
+
self.current = current
|
|
104
|
+
self.target = target
|
|
105
|
+
self.interface_id = interface_id
|
|
106
|
+
super().__init__(
|
|
107
|
+
f"Invalid state transition from {current.value} to {target.value} for interface {interface_id}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ClientStateMachine:
|
|
112
|
+
"""
|
|
113
|
+
State machine for client connection lifecycle.
|
|
114
|
+
|
|
115
|
+
This class manages the connection state of a client with validated
|
|
116
|
+
transitions and event emission via EventBus.
|
|
117
|
+
|
|
118
|
+
Thread Safety
|
|
119
|
+
-------------
|
|
120
|
+
This class is NOT thread-safe. All calls should happen from the same
|
|
121
|
+
event loop/thread.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
-------
|
|
125
|
+
from aiohomematic.central.events import ClientStateChangedEvent, EventBus
|
|
126
|
+
|
|
127
|
+
def on_state_changed(*, event: ClientStateChangedEvent) -> None:
|
|
128
|
+
print(f"State changed: {event.old_state} -> {event.new_state}")
|
|
129
|
+
|
|
130
|
+
event_bus = EventBus()
|
|
131
|
+
sm = ClientStateMachine(interface_id="BidCos-RF", event_bus=event_bus)
|
|
132
|
+
|
|
133
|
+
# Subscribe to state changes via EventBus
|
|
134
|
+
event_bus.subscribe(
|
|
135
|
+
event_type=ClientStateChangedEvent,
|
|
136
|
+
event_key="BidCos-RF",
|
|
137
|
+
handler=on_state_changed,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
sm.transition_to(target=ClientState.INITIALIZING)
|
|
141
|
+
sm.transition_to(target=ClientState.INITIALIZED)
|
|
142
|
+
sm.transition_to(target=ClientState.CONNECTING)
|
|
143
|
+
sm.transition_to(target=ClientState.CONNECTED)
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
__slots__ = (
|
|
148
|
+
"_event_bus",
|
|
149
|
+
"_failure_message",
|
|
150
|
+
"_failure_reason",
|
|
151
|
+
"_interface_id",
|
|
152
|
+
"_state",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
*,
|
|
158
|
+
interface_id: str,
|
|
159
|
+
event_bus: EventBus | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Initialize the state machine.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
----
|
|
166
|
+
interface_id: Interface identifier for logging
|
|
167
|
+
event_bus: Optional EventBus for state change events
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
self._interface_id: Final = interface_id
|
|
171
|
+
self._event_bus = event_bus
|
|
172
|
+
self._state: ClientState = ClientState.CREATED
|
|
173
|
+
self._failure_reason: FailureReason = FailureReason.NONE
|
|
174
|
+
self._failure_message: str = ""
|
|
175
|
+
|
|
176
|
+
failure_message: Final = DelegatedProperty[str](path="_failure_message")
|
|
177
|
+
failure_reason: Final = DelegatedProperty[FailureReason](path="_failure_reason")
|
|
178
|
+
state: Final = DelegatedProperty[ClientState](path="_state")
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def can_reconnect(self) -> bool:
|
|
182
|
+
"""Return True if reconnection is allowed from current state."""
|
|
183
|
+
return ClientState.RECONNECTING in _VALID_TRANSITIONS.get(self._state, frozenset())
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def is_available(self) -> bool:
|
|
187
|
+
"""Return True if client is available (connected or reconnecting)."""
|
|
188
|
+
return self._state in (ClientState.CONNECTED, ClientState.RECONNECTING)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def is_connected(self) -> bool:
|
|
192
|
+
"""Return True if client is in connected state."""
|
|
193
|
+
return self._state == ClientState.CONNECTED
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def is_failed(self) -> bool:
|
|
197
|
+
"""Return True if client is in failed state."""
|
|
198
|
+
return self._state == ClientState.FAILED
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def is_stopped(self) -> bool:
|
|
202
|
+
"""Return True if client is stopped."""
|
|
203
|
+
return self._state == ClientState.STOPPED
|
|
204
|
+
|
|
205
|
+
def can_transition_to(self, *, target: ClientState) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Check if transition to target state is valid.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
----
|
|
211
|
+
target: Target state to check
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
-------
|
|
215
|
+
True if transition is valid, False otherwise
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
return target in _VALID_TRANSITIONS.get(self._state, frozenset())
|
|
219
|
+
|
|
220
|
+
def reset(self) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Reset state machine to CREATED state.
|
|
223
|
+
|
|
224
|
+
This should only be used during testing or exceptional recovery.
|
|
225
|
+
"""
|
|
226
|
+
old_state = self._state
|
|
227
|
+
self._state = ClientState.CREATED
|
|
228
|
+
self._failure_reason = FailureReason.NONE
|
|
229
|
+
self._failure_message = ""
|
|
230
|
+
_LOGGER.warning( # i18n-log: ignore
|
|
231
|
+
"STATE_MACHINE: %s: Reset from %s to CREATED",
|
|
232
|
+
self._interface_id,
|
|
233
|
+
old_state.value,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def transition_to(
|
|
237
|
+
self,
|
|
238
|
+
*,
|
|
239
|
+
target: ClientState,
|
|
240
|
+
reason: str = "",
|
|
241
|
+
force: bool = False,
|
|
242
|
+
failure_reason: FailureReason = FailureReason.NONE,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Transition to a new state.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
----
|
|
249
|
+
target: Target state to transition to
|
|
250
|
+
reason: Human-readable reason for the transition
|
|
251
|
+
force: If True, skip validation (use with caution)
|
|
252
|
+
failure_reason: Categorized failure reason (only used when target is FAILED)
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
------
|
|
256
|
+
InvalidStateTransitionError: If transition is not valid and force=False
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
if not force and not self.can_transition_to(target=target):
|
|
260
|
+
raise InvalidStateTransitionError(
|
|
261
|
+
current=self._state,
|
|
262
|
+
target=target,
|
|
263
|
+
interface_id=self._interface_id,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
old_state = self._state
|
|
267
|
+
self._state = target
|
|
268
|
+
|
|
269
|
+
# Track failure reason when entering FAILED state
|
|
270
|
+
if target == ClientState.FAILED:
|
|
271
|
+
self._failure_reason = failure_reason
|
|
272
|
+
self._failure_message = reason
|
|
273
|
+
elif target in (ClientState.CONNECTED, ClientState.INITIALIZED):
|
|
274
|
+
# Clear failure info on successful states
|
|
275
|
+
self._failure_reason = FailureReason.NONE
|
|
276
|
+
self._failure_message = ""
|
|
277
|
+
|
|
278
|
+
# Log at INFO level for important transitions, DEBUG for others
|
|
279
|
+
if target in (ClientState.CONNECTED, ClientState.DISCONNECTED, ClientState.FAILED):
|
|
280
|
+
failure_info = f" [reason={failure_reason.value}]" if target == ClientState.FAILED else ""
|
|
281
|
+
_LOGGER.info( # i18n-log: ignore
|
|
282
|
+
"CLIENT_STATE: %s: %s -> %s%s%s",
|
|
283
|
+
self._interface_id,
|
|
284
|
+
old_state.value,
|
|
285
|
+
target.value,
|
|
286
|
+
f" ({reason})" if reason else "",
|
|
287
|
+
failure_info,
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
_LOGGER.debug(
|
|
291
|
+
"CLIENT_STATE: %s: %s -> %s%s",
|
|
292
|
+
self._interface_id,
|
|
293
|
+
old_state.value,
|
|
294
|
+
target.value,
|
|
295
|
+
f" ({reason})" if reason else "",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Emit state change event
|
|
299
|
+
self._emit_state_change_event(
|
|
300
|
+
old_state=old_state,
|
|
301
|
+
new_state=target,
|
|
302
|
+
trigger=reason or None,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def _emit_state_change_event(
|
|
306
|
+
self,
|
|
307
|
+
*,
|
|
308
|
+
old_state: ClientState,
|
|
309
|
+
new_state: ClientState,
|
|
310
|
+
trigger: str | None,
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Emit a client state change event."""
|
|
313
|
+
if self._event_bus is None:
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
self._event_bus.publish_sync(
|
|
317
|
+
event=ClientStateChangedEvent(
|
|
318
|
+
timestamp=datetime.now(),
|
|
319
|
+
interface_id=self._interface_id,
|
|
320
|
+
old_state=old_state,
|
|
321
|
+
new_state=new_state,
|
|
322
|
+
trigger=trigger,
|
|
323
|
+
)
|
|
324
|
+
)
|