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,489 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Metrics dataclasses for system observability.
|
|
5
|
+
|
|
6
|
+
This module provides frozen dataclasses for metric snapshots.
|
|
7
|
+
All classes are immutable to ensure thread-safe access.
|
|
8
|
+
|
|
9
|
+
Public API
|
|
10
|
+
----------
|
|
11
|
+
- RpcMetrics: RPC communication metrics
|
|
12
|
+
- EventMetrics: EventBus metrics
|
|
13
|
+
- CacheMetrics: Cache statistics
|
|
14
|
+
- HealthMetrics: Connection health metrics
|
|
15
|
+
- RecoveryMetrics: Recovery statistics
|
|
16
|
+
- ModelMetrics: Model statistics
|
|
17
|
+
- ServiceMetrics: Service call statistics
|
|
18
|
+
- MetricsSnapshot: Point-in-time snapshot of all metrics
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Mapping
|
|
24
|
+
from dataclasses import dataclass, field, fields, is_dataclass
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from typing import TYPE_CHECKING, Any
|
|
27
|
+
|
|
28
|
+
from aiohomematic.const import INIT_DATETIME
|
|
29
|
+
from aiohomematic.metrics.stats import CacheStats, ServiceStats, SizeOnlyStats
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _convert_value(*, value: Any) -> Any:
|
|
36
|
+
"""
|
|
37
|
+
Convert a value to a JSON-serializable format.
|
|
38
|
+
|
|
39
|
+
Handles:
|
|
40
|
+
- datetime → ISO format string
|
|
41
|
+
- float → rounded to 2 decimal places
|
|
42
|
+
- Mapping → dict with converted values
|
|
43
|
+
- dataclass → dict with fields and properties
|
|
44
|
+
- list/tuple → list with converted items
|
|
45
|
+
- None, int, str, bool → pass through
|
|
46
|
+
"""
|
|
47
|
+
if value is None or isinstance(value, (bool, int, str)):
|
|
48
|
+
return value
|
|
49
|
+
if isinstance(value, datetime):
|
|
50
|
+
return value.isoformat()
|
|
51
|
+
if isinstance(value, float):
|
|
52
|
+
return round(value, 2)
|
|
53
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
54
|
+
return _dataclass_to_dict(obj=value)
|
|
55
|
+
if isinstance(value, Mapping):
|
|
56
|
+
return {k: _convert_value(value=v) for k, v in value.items()}
|
|
57
|
+
if isinstance(value, (list, tuple)):
|
|
58
|
+
return [_convert_value(value=item) for item in value]
|
|
59
|
+
# Fallback for unknown types
|
|
60
|
+
return str(value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _dataclass_to_dict(*, obj: Any) -> dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
Convert a dataclass instance to a dictionary.
|
|
66
|
+
|
|
67
|
+
Includes both dataclass fields and @property computed values.
|
|
68
|
+
"""
|
|
69
|
+
result: dict[str, Any] = {}
|
|
70
|
+
|
|
71
|
+
# Add dataclass fields
|
|
72
|
+
for f in fields(obj):
|
|
73
|
+
attr_value = getattr(obj, f.name)
|
|
74
|
+
result[f.name] = _convert_value(value=attr_value)
|
|
75
|
+
|
|
76
|
+
# Add @property computed values
|
|
77
|
+
for name in dir(type(obj)):
|
|
78
|
+
if name.startswith("_"):
|
|
79
|
+
continue
|
|
80
|
+
attr = getattr(type(obj), name, None)
|
|
81
|
+
if isinstance(attr, property):
|
|
82
|
+
attr_value = getattr(obj, name)
|
|
83
|
+
result[name] = _convert_value(value=attr_value)
|
|
84
|
+
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, slots=True)
|
|
89
|
+
class RpcMetrics:
|
|
90
|
+
"""
|
|
91
|
+
RPC communication metrics aggregated from all clients.
|
|
92
|
+
|
|
93
|
+
Combines CircuitBreaker and RequestCoalescer metrics.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
total_requests: int = 0
|
|
97
|
+
"""Total number of RPC requests made."""
|
|
98
|
+
|
|
99
|
+
successful_requests: int = 0
|
|
100
|
+
"""Number of successful RPC requests."""
|
|
101
|
+
|
|
102
|
+
failed_requests: int = 0
|
|
103
|
+
"""Number of failed RPC requests."""
|
|
104
|
+
|
|
105
|
+
rejected_requests: int = 0
|
|
106
|
+
"""Number of requests rejected by circuit breakers."""
|
|
107
|
+
|
|
108
|
+
coalesced_requests: int = 0
|
|
109
|
+
"""Number of requests that were coalesced (avoided execution)."""
|
|
110
|
+
|
|
111
|
+
executed_requests: int = 0
|
|
112
|
+
"""Number of requests that actually executed."""
|
|
113
|
+
|
|
114
|
+
pending_requests: int = 0
|
|
115
|
+
"""Currently in-flight requests."""
|
|
116
|
+
|
|
117
|
+
circuit_breakers_open: int = 0
|
|
118
|
+
"""Number of circuit breakers in OPEN state."""
|
|
119
|
+
|
|
120
|
+
circuit_breakers_half_open: int = 0
|
|
121
|
+
"""Number of circuit breakers in HALF_OPEN state."""
|
|
122
|
+
|
|
123
|
+
state_transitions: int = 0
|
|
124
|
+
"""Total circuit breaker state transitions."""
|
|
125
|
+
|
|
126
|
+
avg_latency_ms: float = 0.0
|
|
127
|
+
"""Average request latency in milliseconds."""
|
|
128
|
+
|
|
129
|
+
max_latency_ms: float = 0.0
|
|
130
|
+
"""Maximum request latency in milliseconds."""
|
|
131
|
+
|
|
132
|
+
last_failure_time: datetime | None = None
|
|
133
|
+
"""Timestamp of last failure."""
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def coalesce_rate(self) -> float:
|
|
137
|
+
"""Return coalesce rate as percentage."""
|
|
138
|
+
if self.total_requests == 0:
|
|
139
|
+
return 0.0
|
|
140
|
+
return (self.coalesced_requests / self.total_requests) * 100
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def failure_rate(self) -> float:
|
|
144
|
+
"""Return failure rate as percentage."""
|
|
145
|
+
if self.total_requests == 0:
|
|
146
|
+
return 0.0
|
|
147
|
+
return (self.failed_requests / self.total_requests) * 100
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def rejection_rate(self) -> float:
|
|
151
|
+
"""Return rejection rate as percentage."""
|
|
152
|
+
if self.total_requests == 0:
|
|
153
|
+
return 0.0
|
|
154
|
+
return (self.rejected_requests / self.total_requests) * 100
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def success_rate(self) -> float:
|
|
158
|
+
"""Return success rate as percentage."""
|
|
159
|
+
if self.total_requests == 0:
|
|
160
|
+
return 100.0
|
|
161
|
+
return (self.successful_requests / self.total_requests) * 100
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True, slots=True)
|
|
165
|
+
class RpcServerMetrics:
|
|
166
|
+
"""
|
|
167
|
+
RPC server metrics for incoming requests from CCU.
|
|
168
|
+
|
|
169
|
+
Tracks requests received by the XML-RPC callback server.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
total_requests: int = 0
|
|
173
|
+
"""Total incoming requests received."""
|
|
174
|
+
|
|
175
|
+
total_errors: int = 0
|
|
176
|
+
"""Total request handling errors."""
|
|
177
|
+
|
|
178
|
+
active_tasks: int = 0
|
|
179
|
+
"""Currently active background tasks."""
|
|
180
|
+
|
|
181
|
+
avg_latency_ms: float = 0.0
|
|
182
|
+
"""Average request handling latency in milliseconds."""
|
|
183
|
+
|
|
184
|
+
max_latency_ms: float = 0.0
|
|
185
|
+
"""Maximum request handling latency in milliseconds."""
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def error_rate(self) -> float:
|
|
189
|
+
"""Return error rate as percentage."""
|
|
190
|
+
if self.total_requests == 0:
|
|
191
|
+
return 0.0
|
|
192
|
+
return (self.total_errors / self.total_requests) * 100
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def success_rate(self) -> float:
|
|
196
|
+
"""Return success rate as percentage."""
|
|
197
|
+
if self.total_requests == 0:
|
|
198
|
+
return 100.0
|
|
199
|
+
return ((self.total_requests - self.total_errors) / self.total_requests) * 100
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True, slots=True)
|
|
203
|
+
class EventMetrics:
|
|
204
|
+
"""EventBus metrics."""
|
|
205
|
+
|
|
206
|
+
total_published: int = 0
|
|
207
|
+
"""Total events published."""
|
|
208
|
+
|
|
209
|
+
total_subscriptions: int = 0
|
|
210
|
+
"""Active subscription count."""
|
|
211
|
+
|
|
212
|
+
handlers_executed: int = 0
|
|
213
|
+
"""Total handler executions."""
|
|
214
|
+
|
|
215
|
+
handler_errors: int = 0
|
|
216
|
+
"""Handler exceptions caught."""
|
|
217
|
+
|
|
218
|
+
avg_handler_duration_ms: float = 0.0
|
|
219
|
+
"""Average handler execution time in milliseconds."""
|
|
220
|
+
|
|
221
|
+
max_handler_duration_ms: float = 0.0
|
|
222
|
+
"""Maximum handler execution time in milliseconds."""
|
|
223
|
+
|
|
224
|
+
events_by_type: Mapping[str, int] = field(default_factory=dict)
|
|
225
|
+
"""Event counts per type."""
|
|
226
|
+
|
|
227
|
+
# Operational event counters
|
|
228
|
+
circuit_breaker_trips: int = 0
|
|
229
|
+
"""Number of CircuitBreakerTrippedEvent events."""
|
|
230
|
+
|
|
231
|
+
state_changes: int = 0
|
|
232
|
+
"""Number of ClientStateChangedEvent + CentralStateChangedEvent events."""
|
|
233
|
+
|
|
234
|
+
data_refreshes_triggered: int = 0
|
|
235
|
+
"""Number of DataRefreshTriggeredEvent events."""
|
|
236
|
+
|
|
237
|
+
data_refreshes_completed: int = 0
|
|
238
|
+
"""Number of DataRefreshCompletedEvent events."""
|
|
239
|
+
|
|
240
|
+
programs_executed: int = 0
|
|
241
|
+
"""Number of ProgramExecutedEvent events."""
|
|
242
|
+
|
|
243
|
+
requests_coalesced: int = 0
|
|
244
|
+
"""Number of RequestCoalescedEvent events."""
|
|
245
|
+
|
|
246
|
+
health_records: int = 0
|
|
247
|
+
"""Number of HealthRecordedEvent events."""
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def error_rate(self) -> float:
|
|
251
|
+
"""Return handler error rate as percentage."""
|
|
252
|
+
if self.handlers_executed == 0:
|
|
253
|
+
return 0.0
|
|
254
|
+
return (self.handler_errors / self.handlers_executed) * 100
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass(frozen=True, slots=True)
|
|
258
|
+
class CacheMetrics:
|
|
259
|
+
"""
|
|
260
|
+
Aggregated cache and registry metrics.
|
|
261
|
+
|
|
262
|
+
Distinguishes between true caches (with hit/miss semantics) and
|
|
263
|
+
registries/trackers (size-only).
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
# Registries (authoritative stores, size-only)
|
|
267
|
+
device_descriptions: SizeOnlyStats = field(default_factory=SizeOnlyStats)
|
|
268
|
+
"""Device description registry size."""
|
|
269
|
+
|
|
270
|
+
paramset_descriptions: SizeOnlyStats = field(default_factory=SizeOnlyStats)
|
|
271
|
+
"""Paramset description registry size."""
|
|
272
|
+
|
|
273
|
+
visibility_registry: SizeOnlyStats = field(default_factory=SizeOnlyStats)
|
|
274
|
+
"""Visibility registry memoization size."""
|
|
275
|
+
|
|
276
|
+
# Trackers (size-only)
|
|
277
|
+
ping_pong_tracker: SizeOnlyStats = field(default_factory=SizeOnlyStats)
|
|
278
|
+
"""Ping-pong tracker size."""
|
|
279
|
+
|
|
280
|
+
command_tracker: SizeOnlyStats = field(default_factory=SizeOnlyStats)
|
|
281
|
+
"""Command tracker size (tracks sent commands, no hit/miss semantics)."""
|
|
282
|
+
|
|
283
|
+
# True caches (with hit/miss semantics)
|
|
284
|
+
data_cache: CacheStats = field(default_factory=CacheStats)
|
|
285
|
+
"""Central data cache stats."""
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def overall_hit_rate(self) -> float:
|
|
289
|
+
"""Return overall cache hit rate (data_cache only, command_tracker has no hit/miss semantics)."""
|
|
290
|
+
if (total := self.data_cache.hits + self.data_cache.misses) == 0:
|
|
291
|
+
return 100.0
|
|
292
|
+
return (self.data_cache.hits / total) * 100
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def total_entries(self) -> int:
|
|
296
|
+
"""Return total entries across all caches and registries."""
|
|
297
|
+
return (
|
|
298
|
+
self.device_descriptions.size
|
|
299
|
+
+ self.paramset_descriptions.size
|
|
300
|
+
+ self.visibility_registry.size
|
|
301
|
+
+ self.ping_pong_tracker.size
|
|
302
|
+
+ self.command_tracker.size
|
|
303
|
+
+ self.data_cache.size
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass(frozen=True, slots=True)
|
|
308
|
+
class HealthMetrics:
|
|
309
|
+
"""Connection health metrics."""
|
|
310
|
+
|
|
311
|
+
overall_score: float = 1.0
|
|
312
|
+
"""Weighted health score (0.0 - 1.0)."""
|
|
313
|
+
|
|
314
|
+
clients_total: int = 0
|
|
315
|
+
"""Total registered clients."""
|
|
316
|
+
|
|
317
|
+
clients_healthy: int = 0
|
|
318
|
+
"""Healthy client count."""
|
|
319
|
+
|
|
320
|
+
clients_degraded: int = 0
|
|
321
|
+
"""Degraded client count."""
|
|
322
|
+
|
|
323
|
+
clients_failed: int = 0
|
|
324
|
+
"""Failed client count."""
|
|
325
|
+
|
|
326
|
+
reconnect_attempts: int = 0
|
|
327
|
+
"""Total reconnect attempts."""
|
|
328
|
+
|
|
329
|
+
last_event_time: datetime = field(default=INIT_DATETIME)
|
|
330
|
+
"""Timestamp of last backend event."""
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def availability_rate(self) -> float:
|
|
334
|
+
"""Return client availability as percentage."""
|
|
335
|
+
if self.clients_total == 0:
|
|
336
|
+
return 100.0
|
|
337
|
+
return (self.clients_healthy / self.clients_total) * 100
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def last_event_age_seconds(self) -> float:
|
|
341
|
+
"""Return seconds since last event."""
|
|
342
|
+
if self.last_event_time == INIT_DATETIME:
|
|
343
|
+
return -1.0
|
|
344
|
+
return (datetime.now() - self.last_event_time).total_seconds()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@dataclass(frozen=True, slots=True)
|
|
348
|
+
class RecoveryMetrics:
|
|
349
|
+
"""Recovery statistics."""
|
|
350
|
+
|
|
351
|
+
attempts_total: int = 0
|
|
352
|
+
"""Total recovery attempts."""
|
|
353
|
+
|
|
354
|
+
successes: int = 0
|
|
355
|
+
"""Successful recoveries."""
|
|
356
|
+
|
|
357
|
+
failures: int = 0
|
|
358
|
+
"""Failed recoveries."""
|
|
359
|
+
|
|
360
|
+
max_retries_reached: int = 0
|
|
361
|
+
"""Times max retry limit was hit."""
|
|
362
|
+
|
|
363
|
+
in_progress: bool = False
|
|
364
|
+
"""Recovery currently active."""
|
|
365
|
+
|
|
366
|
+
last_recovery_time: datetime | None = None
|
|
367
|
+
"""Timestamp of last recovery attempt."""
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def success_rate(self) -> float:
|
|
371
|
+
"""Return recovery success rate."""
|
|
372
|
+
if self.attempts_total == 0:
|
|
373
|
+
return 100.0
|
|
374
|
+
return (self.successes / self.attempts_total) * 100
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@dataclass(frozen=True, slots=True)
|
|
378
|
+
class ModelMetrics:
|
|
379
|
+
"""Model statistics."""
|
|
380
|
+
|
|
381
|
+
devices_total: int = 0
|
|
382
|
+
"""Total devices."""
|
|
383
|
+
|
|
384
|
+
devices_available: int = 0
|
|
385
|
+
"""Available devices."""
|
|
386
|
+
|
|
387
|
+
channels_total: int = 0
|
|
388
|
+
"""Total channels."""
|
|
389
|
+
|
|
390
|
+
data_points_generic: int = 0
|
|
391
|
+
"""Generic data points."""
|
|
392
|
+
|
|
393
|
+
data_points_custom: int = 0
|
|
394
|
+
"""Custom data points."""
|
|
395
|
+
|
|
396
|
+
data_points_calculated: int = 0
|
|
397
|
+
"""Calculated data points."""
|
|
398
|
+
|
|
399
|
+
data_points_subscribed: int = 0
|
|
400
|
+
"""Data points with active subscriptions."""
|
|
401
|
+
|
|
402
|
+
data_points_by_category: Mapping[str, int] = field(default_factory=dict)
|
|
403
|
+
"""Data point counts by category (DataPointCategory name -> count)."""
|
|
404
|
+
|
|
405
|
+
programs_total: int = 0
|
|
406
|
+
"""Hub programs."""
|
|
407
|
+
|
|
408
|
+
sysvars_total: int = 0
|
|
409
|
+
"""System variables."""
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@dataclass(frozen=True, slots=True)
|
|
413
|
+
class ServiceMetrics:
|
|
414
|
+
"""
|
|
415
|
+
Aggregated service method metrics (immutable snapshot).
|
|
416
|
+
|
|
417
|
+
Provides statistics for all service methods decorated with
|
|
418
|
+
@inspector(measure_performance=True).
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
total_calls: int = 0
|
|
422
|
+
"""Total calls across all methods."""
|
|
423
|
+
|
|
424
|
+
total_errors: int = 0
|
|
425
|
+
"""Total errors across all methods."""
|
|
426
|
+
|
|
427
|
+
avg_duration_ms: float = 0.0
|
|
428
|
+
"""Average duration across all calls."""
|
|
429
|
+
|
|
430
|
+
max_duration_ms: float = 0.0
|
|
431
|
+
"""Maximum duration across all calls."""
|
|
432
|
+
|
|
433
|
+
by_method: Mapping[str, ServiceStats] = field(default_factory=dict)
|
|
434
|
+
"""Statistics per method name."""
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def error_rate(self) -> float:
|
|
438
|
+
"""Return overall error rate as percentage."""
|
|
439
|
+
if self.total_calls == 0:
|
|
440
|
+
return 0.0
|
|
441
|
+
return (self.total_errors / self.total_calls) * 100
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@dataclass(frozen=True, slots=True)
|
|
445
|
+
class MetricsSnapshot:
|
|
446
|
+
"""Point-in-time snapshot of all system metrics."""
|
|
447
|
+
|
|
448
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
449
|
+
"""Snapshot timestamp."""
|
|
450
|
+
|
|
451
|
+
rpc: RpcMetrics = field(default_factory=RpcMetrics)
|
|
452
|
+
"""RPC client communication metrics (outgoing to CCU)."""
|
|
453
|
+
|
|
454
|
+
rpc_server: RpcServerMetrics = field(default_factory=RpcServerMetrics)
|
|
455
|
+
"""RPC server metrics (incoming from CCU)."""
|
|
456
|
+
|
|
457
|
+
events: EventMetrics = field(default_factory=EventMetrics)
|
|
458
|
+
"""EventBus metrics."""
|
|
459
|
+
|
|
460
|
+
cache: CacheMetrics = field(default_factory=CacheMetrics)
|
|
461
|
+
"""Cache statistics."""
|
|
462
|
+
|
|
463
|
+
health: HealthMetrics = field(default_factory=HealthMetrics)
|
|
464
|
+
"""Connection health metrics."""
|
|
465
|
+
|
|
466
|
+
recovery: RecoveryMetrics = field(default_factory=RecoveryMetrics)
|
|
467
|
+
"""Recovery statistics."""
|
|
468
|
+
|
|
469
|
+
model: ModelMetrics = field(default_factory=ModelMetrics)
|
|
470
|
+
"""Model statistics."""
|
|
471
|
+
|
|
472
|
+
services: ServiceMetrics = field(default_factory=ServiceMetrics)
|
|
473
|
+
"""Service call statistics."""
|
|
474
|
+
|
|
475
|
+
def to_dict(self) -> dict[str, Any]:
|
|
476
|
+
"""
|
|
477
|
+
Convert snapshot to a JSON-serializable dictionary.
|
|
478
|
+
|
|
479
|
+
Automatically converts all fields and computed properties:
|
|
480
|
+
- datetime → ISO format string
|
|
481
|
+
- float → rounded to 2 decimal places
|
|
482
|
+
- Nested dataclasses → recursively converted
|
|
483
|
+
- Mapping → dict with converted values
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Dictionary representation of the snapshot.
|
|
487
|
+
|
|
488
|
+
"""
|
|
489
|
+
return _dataclass_to_dict(obj=self)
|