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,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)