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,534 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Metrics aggregation from system components.
|
|
5
|
+
|
|
6
|
+
This module provides MetricsAggregator which collects metrics from
|
|
7
|
+
various system components and presents them through a unified interface.
|
|
8
|
+
|
|
9
|
+
Public API
|
|
10
|
+
----------
|
|
11
|
+
- MetricsAggregator: Main class for aggregating metrics
|
|
12
|
+
|
|
13
|
+
Usage
|
|
14
|
+
-----
|
|
15
|
+
from aiohomematic.metrics import MetricsAggregator
|
|
16
|
+
|
|
17
|
+
aggregator = MetricsAggregator(
|
|
18
|
+
central_name="my-central",
|
|
19
|
+
client_provider=central,
|
|
20
|
+
event_bus=central.event_bus,
|
|
21
|
+
health_tracker=central.health_tracker,
|
|
22
|
+
...
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Get individual metric categories
|
|
26
|
+
rpc_metrics = aggregator.rpc
|
|
27
|
+
event_metrics = aggregator.events
|
|
28
|
+
|
|
29
|
+
# Get full snapshot
|
|
30
|
+
snapshot = aggregator.snapshot()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from typing import TYPE_CHECKING, Final
|
|
37
|
+
|
|
38
|
+
from aiohomematic.const import INIT_DATETIME, CircuitState
|
|
39
|
+
from aiohomematic.metrics._protocols import (
|
|
40
|
+
CacheProviderForMetricsProtocol,
|
|
41
|
+
ClientProviderForMetricsProtocol,
|
|
42
|
+
DeviceProviderForMetricsProtocol,
|
|
43
|
+
HubDataPointManagerForMetricsProtocol,
|
|
44
|
+
RecoveryProviderForMetricsProtocol,
|
|
45
|
+
)
|
|
46
|
+
from aiohomematic.metrics.dataclasses import (
|
|
47
|
+
CacheMetrics,
|
|
48
|
+
EventMetrics,
|
|
49
|
+
HealthMetrics,
|
|
50
|
+
MetricsSnapshot,
|
|
51
|
+
ModelMetrics,
|
|
52
|
+
RecoveryMetrics,
|
|
53
|
+
RpcMetrics,
|
|
54
|
+
RpcServerMetrics,
|
|
55
|
+
ServiceMetrics,
|
|
56
|
+
)
|
|
57
|
+
from aiohomematic.metrics.stats import CacheStats, ServiceStats, SizeOnlyStats
|
|
58
|
+
|
|
59
|
+
if TYPE_CHECKING:
|
|
60
|
+
from aiohomematic.central.events import EventBus
|
|
61
|
+
from aiohomematic.central.health import HealthTracker
|
|
62
|
+
from aiohomematic.metrics.observer import MetricsObserver
|
|
63
|
+
from aiohomematic.store.dynamic import CentralDataCache
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# Metrics Aggregator
|
|
68
|
+
# =============================================================================
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MetricsAggregator:
|
|
72
|
+
"""
|
|
73
|
+
Aggregate metrics from various system components.
|
|
74
|
+
|
|
75
|
+
Provides a unified interface for accessing all system metrics.
|
|
76
|
+
This class collects data from:
|
|
77
|
+
- CircuitBreaker (per client)
|
|
78
|
+
- RequestCoalescer (per client)
|
|
79
|
+
- EventBus
|
|
80
|
+
- HealthTracker
|
|
81
|
+
- RecoveryCoordinator
|
|
82
|
+
- Various caches
|
|
83
|
+
- Device registry
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
-------
|
|
87
|
+
```python
|
|
88
|
+
aggregator = MetricsAggregator(
|
|
89
|
+
central_name="my-central",
|
|
90
|
+
client_provider=central,
|
|
91
|
+
event_bus=central.event_bus,
|
|
92
|
+
health_tracker=central.health_tracker,
|
|
93
|
+
...
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Get individual metric categories
|
|
97
|
+
rpc_metrics = aggregator.rpc
|
|
98
|
+
event_metrics = aggregator.events
|
|
99
|
+
|
|
100
|
+
# Get full snapshot
|
|
101
|
+
snapshot = aggregator.snapshot()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
__slots__ = (
|
|
107
|
+
"_cache_provider",
|
|
108
|
+
"_central_name",
|
|
109
|
+
"_client_provider",
|
|
110
|
+
"_data_cache",
|
|
111
|
+
"_device_provider",
|
|
112
|
+
"_event_bus",
|
|
113
|
+
"_health_tracker",
|
|
114
|
+
"_hub_data_point_manager",
|
|
115
|
+
"_observer",
|
|
116
|
+
"_recovery_provider",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
central_name: str,
|
|
123
|
+
client_provider: ClientProviderForMetricsProtocol,
|
|
124
|
+
device_provider: DeviceProviderForMetricsProtocol,
|
|
125
|
+
event_bus: EventBus,
|
|
126
|
+
health_tracker: HealthTracker,
|
|
127
|
+
data_cache: CentralDataCache,
|
|
128
|
+
observer: MetricsObserver | None = None,
|
|
129
|
+
hub_data_point_manager: HubDataPointManagerForMetricsProtocol | None = None,
|
|
130
|
+
cache_provider: CacheProviderForMetricsProtocol | None = None,
|
|
131
|
+
recovery_provider: RecoveryProviderForMetricsProtocol | None = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Initialize the metrics aggregator.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
central_name: Name of the CentralUnit (for service stats isolation)
|
|
138
|
+
client_provider: Provider for client access
|
|
139
|
+
device_provider: Provider for device access
|
|
140
|
+
event_bus: The EventBus instance
|
|
141
|
+
health_tracker: The HealthTracker instance
|
|
142
|
+
data_cache: The CentralDataCache instance
|
|
143
|
+
observer: Optional MetricsObserver for event-driven metrics
|
|
144
|
+
hub_data_point_manager: Optional hub data point manager
|
|
145
|
+
cache_provider: Optional cache provider for cache statistics
|
|
146
|
+
recovery_provider: Optional recovery provider for recovery statistics
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
self._central_name: Final = central_name
|
|
150
|
+
self._client_provider: Final = client_provider
|
|
151
|
+
self._device_provider: Final = device_provider
|
|
152
|
+
self._event_bus: Final = event_bus
|
|
153
|
+
self._health_tracker: Final = health_tracker
|
|
154
|
+
self._observer: Final = observer
|
|
155
|
+
self._data_cache: Final = data_cache
|
|
156
|
+
self._hub_data_point_manager: Final = hub_data_point_manager
|
|
157
|
+
self._cache_provider: Final = cache_provider
|
|
158
|
+
self._recovery_provider: Final = recovery_provider
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def cache(self) -> CacheMetrics:
|
|
162
|
+
"""Return cache statistics."""
|
|
163
|
+
# Get data cache statistics directly from cache
|
|
164
|
+
data_stats = self._data_cache.statistics
|
|
165
|
+
data_cache_size = self._data_cache.size
|
|
166
|
+
|
|
167
|
+
# Get cache sizes from provider if available
|
|
168
|
+
visibility_cache_size = 0
|
|
169
|
+
device_descriptions_size = 0
|
|
170
|
+
paramset_descriptions_size = 0
|
|
171
|
+
if self._cache_provider is not None:
|
|
172
|
+
visibility_cache_size = self._cache_provider.visibility_cache_size
|
|
173
|
+
device_descriptions_size = self._cache_provider.device_descriptions_size
|
|
174
|
+
paramset_descriptions_size = self._cache_provider.paramset_descriptions_size
|
|
175
|
+
|
|
176
|
+
# Aggregate command tracker and ping_pong tracker from all clients
|
|
177
|
+
command_tracker_size = 0
|
|
178
|
+
command_tracker_evictions = 0
|
|
179
|
+
ping_pong_tracker_size = 0
|
|
180
|
+
for client in self._client_provider.clients:
|
|
181
|
+
if (cmd_tracker := getattr(client, "last_value_send_tracker", None)) is not None:
|
|
182
|
+
command_tracker_size += cmd_tracker.size
|
|
183
|
+
command_tracker_evictions += cmd_tracker.statistics.evictions
|
|
184
|
+
if (pp_tracker := getattr(client, "ping_pong_tracker", None)) is not None:
|
|
185
|
+
ping_pong_tracker_size += pp_tracker.size
|
|
186
|
+
|
|
187
|
+
return CacheMetrics(
|
|
188
|
+
# Registries (size-only)
|
|
189
|
+
device_descriptions=SizeOnlyStats(
|
|
190
|
+
size=device_descriptions_size,
|
|
191
|
+
),
|
|
192
|
+
paramset_descriptions=SizeOnlyStats(
|
|
193
|
+
size=paramset_descriptions_size,
|
|
194
|
+
),
|
|
195
|
+
visibility_registry=SizeOnlyStats(
|
|
196
|
+
size=visibility_cache_size,
|
|
197
|
+
),
|
|
198
|
+
# Trackers (size-only)
|
|
199
|
+
ping_pong_tracker=SizeOnlyStats(
|
|
200
|
+
size=ping_pong_tracker_size,
|
|
201
|
+
),
|
|
202
|
+
command_tracker=SizeOnlyStats(
|
|
203
|
+
size=command_tracker_size,
|
|
204
|
+
evictions=command_tracker_evictions,
|
|
205
|
+
),
|
|
206
|
+
# True caches (with hit/miss semantics)
|
|
207
|
+
data_cache=CacheStats(
|
|
208
|
+
size=data_cache_size,
|
|
209
|
+
hits=data_stats.hits,
|
|
210
|
+
misses=data_stats.misses,
|
|
211
|
+
evictions=data_stats.evictions,
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def events(self) -> EventMetrics:
|
|
217
|
+
"""Return EventBus metrics including operational event counts."""
|
|
218
|
+
event_stats = self._event_bus.get_event_stats()
|
|
219
|
+
handler_stats = self._event_bus.get_handler_stats()
|
|
220
|
+
|
|
221
|
+
# Extract operational event counts from event_stats
|
|
222
|
+
circuit_breaker_trips = event_stats.get("CircuitBreakerTrippedEvent", 0)
|
|
223
|
+
client_state_changes = event_stats.get("ClientStateChangedEvent", 0)
|
|
224
|
+
central_state_changes = event_stats.get("CentralStateChangedEvent", 0)
|
|
225
|
+
data_refreshes_triggered = event_stats.get("DataRefreshTriggeredEvent", 0)
|
|
226
|
+
data_refreshes_completed = event_stats.get("DataRefreshCompletedEvent", 0)
|
|
227
|
+
programs_executed = event_stats.get("ProgramExecutedEvent", 0)
|
|
228
|
+
requests_coalesced = event_stats.get("RequestCoalescedEvent", 0)
|
|
229
|
+
health_records = event_stats.get("HealthRecordedEvent", 0)
|
|
230
|
+
|
|
231
|
+
return EventMetrics(
|
|
232
|
+
total_published=sum(event_stats.values()),
|
|
233
|
+
total_subscriptions=self._event_bus.get_total_subscription_count(),
|
|
234
|
+
handlers_executed=handler_stats.total_executions,
|
|
235
|
+
handler_errors=handler_stats.total_errors,
|
|
236
|
+
avg_handler_duration_ms=handler_stats.avg_duration_ms,
|
|
237
|
+
max_handler_duration_ms=handler_stats.max_duration_ms,
|
|
238
|
+
events_by_type=event_stats,
|
|
239
|
+
circuit_breaker_trips=circuit_breaker_trips,
|
|
240
|
+
state_changes=client_state_changes + central_state_changes,
|
|
241
|
+
data_refreshes_triggered=data_refreshes_triggered,
|
|
242
|
+
data_refreshes_completed=data_refreshes_completed,
|
|
243
|
+
programs_executed=programs_executed,
|
|
244
|
+
requests_coalesced=requests_coalesced,
|
|
245
|
+
health_records=health_records,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def health(self) -> HealthMetrics:
|
|
250
|
+
"""Return health metrics."""
|
|
251
|
+
health = self._health_tracker.health
|
|
252
|
+
clients_healthy = len(health.healthy_clients)
|
|
253
|
+
clients_degraded = len(health.degraded_clients)
|
|
254
|
+
clients_failed = len(health.failed_clients)
|
|
255
|
+
|
|
256
|
+
# Aggregate metrics across all clients
|
|
257
|
+
last_event_time = INIT_DATETIME
|
|
258
|
+
reconnect_attempts = 0
|
|
259
|
+
for client_health in health.client_health.values():
|
|
260
|
+
if client_health.last_event_received is not None and client_health.last_event_received > last_event_time:
|
|
261
|
+
last_event_time = client_health.last_event_received
|
|
262
|
+
reconnect_attempts += client_health.reconnect_attempts
|
|
263
|
+
|
|
264
|
+
return HealthMetrics(
|
|
265
|
+
overall_score=health.overall_health_score,
|
|
266
|
+
clients_total=clients_healthy + clients_degraded + clients_failed,
|
|
267
|
+
clients_healthy=clients_healthy,
|
|
268
|
+
clients_degraded=clients_degraded,
|
|
269
|
+
clients_failed=clients_failed,
|
|
270
|
+
reconnect_attempts=reconnect_attempts,
|
|
271
|
+
last_event_time=last_event_time,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def model(self) -> ModelMetrics:
|
|
276
|
+
"""Return model statistics."""
|
|
277
|
+
devices = self._device_provider.devices
|
|
278
|
+
devices_available = sum(1 for d in devices if d.available)
|
|
279
|
+
channels_total = sum(len(d.channels) for d in devices)
|
|
280
|
+
|
|
281
|
+
generic_count = 0
|
|
282
|
+
custom_count = 0
|
|
283
|
+
calculated_count = 0
|
|
284
|
+
by_category: dict[str, int] = {}
|
|
285
|
+
|
|
286
|
+
for device in devices:
|
|
287
|
+
for channel in device.channels.values():
|
|
288
|
+
for dp in channel.generic_data_points:
|
|
289
|
+
generic_count += 1
|
|
290
|
+
cat_name = dp.category.name
|
|
291
|
+
by_category[cat_name] = by_category.get(cat_name, 0) + 1
|
|
292
|
+
|
|
293
|
+
for dp in channel.calculated_data_points:
|
|
294
|
+
calculated_count += 1
|
|
295
|
+
cat_name = dp.category.name
|
|
296
|
+
by_category[cat_name] = by_category.get(cat_name, 0) + 1
|
|
297
|
+
|
|
298
|
+
if (custom_dp := channel.custom_data_point) is not None:
|
|
299
|
+
custom_count += 1
|
|
300
|
+
cat_name = custom_dp.category.name
|
|
301
|
+
by_category[cat_name] = by_category.get(cat_name, 0) + 1
|
|
302
|
+
|
|
303
|
+
# Subscription counting available via EventBus.get_total_subscription_count()
|
|
304
|
+
subscribed_count = self._event_bus.get_total_subscription_count()
|
|
305
|
+
|
|
306
|
+
programs_total = 0
|
|
307
|
+
sysvars_total = 0
|
|
308
|
+
if self._hub_data_point_manager is not None:
|
|
309
|
+
for dp in self._hub_data_point_manager.program_data_points:
|
|
310
|
+
programs_total += 1
|
|
311
|
+
cat_name = dp.category.name
|
|
312
|
+
by_category[cat_name] = by_category.get(cat_name, 0) + 1
|
|
313
|
+
|
|
314
|
+
for dp in self._hub_data_point_manager.sysvar_data_points:
|
|
315
|
+
sysvars_total += 1
|
|
316
|
+
cat_name = dp.category.name
|
|
317
|
+
by_category[cat_name] = by_category.get(cat_name, 0) + 1
|
|
318
|
+
|
|
319
|
+
return ModelMetrics(
|
|
320
|
+
devices_total=len(devices),
|
|
321
|
+
devices_available=devices_available,
|
|
322
|
+
channels_total=channels_total,
|
|
323
|
+
data_points_generic=generic_count,
|
|
324
|
+
data_points_custom=custom_count,
|
|
325
|
+
data_points_calculated=calculated_count,
|
|
326
|
+
data_points_subscribed=subscribed_count,
|
|
327
|
+
data_points_by_category=dict(sorted(by_category.items())),
|
|
328
|
+
programs_total=programs_total,
|
|
329
|
+
sysvars_total=sysvars_total,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def recovery(self) -> RecoveryMetrics:
|
|
334
|
+
"""Return recovery metrics."""
|
|
335
|
+
if self._recovery_provider is None:
|
|
336
|
+
return RecoveryMetrics()
|
|
337
|
+
|
|
338
|
+
if not (recovery_states := self._recovery_provider.recovery_states):
|
|
339
|
+
return RecoveryMetrics(
|
|
340
|
+
in_progress=self._recovery_provider.in_recovery,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Aggregate metrics across all interface recovery states
|
|
344
|
+
attempts_total = 0
|
|
345
|
+
successes = 0
|
|
346
|
+
failures = 0
|
|
347
|
+
max_retries_reached = 0
|
|
348
|
+
last_recovery_time: datetime | None = None
|
|
349
|
+
|
|
350
|
+
for state in recovery_states.values():
|
|
351
|
+
attempts_total += state.attempt_count
|
|
352
|
+
failures += state.consecutive_failures
|
|
353
|
+
# Count successes as attempts minus current consecutive failures
|
|
354
|
+
if state.attempt_count > state.consecutive_failures:
|
|
355
|
+
successes += state.attempt_count - state.consecutive_failures
|
|
356
|
+
# Check if max retries reached (can_retry is False when at limit)
|
|
357
|
+
if not state.can_retry:
|
|
358
|
+
max_retries_reached += 1
|
|
359
|
+
# Track most recent recovery attempt
|
|
360
|
+
if state.last_attempt is not None and (
|
|
361
|
+
last_recovery_time is None or state.last_attempt > last_recovery_time
|
|
362
|
+
):
|
|
363
|
+
last_recovery_time = state.last_attempt
|
|
364
|
+
|
|
365
|
+
return RecoveryMetrics(
|
|
366
|
+
attempts_total=attempts_total,
|
|
367
|
+
successes=successes,
|
|
368
|
+
failures=failures,
|
|
369
|
+
max_retries_reached=max_retries_reached,
|
|
370
|
+
in_progress=self._recovery_provider.in_recovery,
|
|
371
|
+
last_recovery_time=last_recovery_time,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def rpc(self) -> RpcMetrics:
|
|
376
|
+
"""Return aggregated RPC metrics from all clients."""
|
|
377
|
+
# Get significant event counters from observer (failures, rejections, transitions, coalesced)
|
|
378
|
+
failed_requests = 0
|
|
379
|
+
rejected_requests = 0
|
|
380
|
+
coalesced_requests = 0
|
|
381
|
+
state_transitions = 0
|
|
382
|
+
total_latency_ms = 0.0
|
|
383
|
+
max_latency_ms = 0.0
|
|
384
|
+
latency_count = 0
|
|
385
|
+
|
|
386
|
+
if self._observer is not None:
|
|
387
|
+
# Circuit breaker metrics from observer (only significant events)
|
|
388
|
+
failed_requests = self._observer.get_aggregated_counter(pattern="circuit.failure.")
|
|
389
|
+
rejected_requests = self._observer.get_aggregated_counter(pattern="circuit.rejection.")
|
|
390
|
+
state_transitions = self._observer.get_aggregated_counter(pattern="circuit.state_transition.")
|
|
391
|
+
|
|
392
|
+
# Coalescer metrics from observer (only significant events)
|
|
393
|
+
coalesced_requests = self._observer.get_aggregated_counter(pattern="coalescer.coalesced.")
|
|
394
|
+
|
|
395
|
+
# Latency metrics from observer
|
|
396
|
+
latency_tracker = self._observer.get_aggregated_latency(pattern="ping_pong.rtt")
|
|
397
|
+
if latency_tracker.count > 0:
|
|
398
|
+
total_latency_ms = latency_tracker.total_ms
|
|
399
|
+
latency_count = latency_tracker.count
|
|
400
|
+
max_latency_ms = latency_tracker.max_ms
|
|
401
|
+
|
|
402
|
+
# Read local counters directly from circuit breakers and coalescers
|
|
403
|
+
# These are high-frequency metrics that don't emit events
|
|
404
|
+
total_requests = 0
|
|
405
|
+
executed_requests = 0
|
|
406
|
+
pending_requests = 0
|
|
407
|
+
circuit_breakers_open = 0
|
|
408
|
+
circuit_breakers_half_open = 0
|
|
409
|
+
last_failure_time: datetime | None = None
|
|
410
|
+
|
|
411
|
+
for client in self._client_provider.clients:
|
|
412
|
+
# Circuit breaker state and local counters
|
|
413
|
+
if (cb := getattr(client, "circuit_breaker", None)) is not None:
|
|
414
|
+
# Total requests from local counter (no event emission)
|
|
415
|
+
total_requests += cb.total_requests
|
|
416
|
+
|
|
417
|
+
if cb.state == CircuitState.OPEN:
|
|
418
|
+
circuit_breakers_open += 1
|
|
419
|
+
elif cb.state == CircuitState.HALF_OPEN:
|
|
420
|
+
circuit_breakers_half_open += 1
|
|
421
|
+
|
|
422
|
+
if cb.last_failure_time is not None and (
|
|
423
|
+
last_failure_time is None or cb.last_failure_time > last_failure_time
|
|
424
|
+
):
|
|
425
|
+
last_failure_time = cb.last_failure_time
|
|
426
|
+
|
|
427
|
+
# Coalescer local counters
|
|
428
|
+
if (coalescer := getattr(client, "request_coalescer", None)) is not None:
|
|
429
|
+
pending_requests += coalescer.pending_count
|
|
430
|
+
executed_requests += coalescer.executed_requests
|
|
431
|
+
|
|
432
|
+
# Calculate successful requests from total minus failures and rejections
|
|
433
|
+
successful_requests = total_requests - failed_requests - rejected_requests
|
|
434
|
+
avg_latency_ms = total_latency_ms / latency_count if latency_count > 0 else 0.0
|
|
435
|
+
|
|
436
|
+
return RpcMetrics(
|
|
437
|
+
total_requests=total_requests,
|
|
438
|
+
successful_requests=successful_requests,
|
|
439
|
+
failed_requests=failed_requests,
|
|
440
|
+
rejected_requests=rejected_requests,
|
|
441
|
+
coalesced_requests=coalesced_requests,
|
|
442
|
+
executed_requests=executed_requests,
|
|
443
|
+
pending_requests=pending_requests,
|
|
444
|
+
circuit_breakers_open=circuit_breakers_open,
|
|
445
|
+
circuit_breakers_half_open=circuit_breakers_half_open,
|
|
446
|
+
state_transitions=state_transitions,
|
|
447
|
+
avg_latency_ms=avg_latency_ms,
|
|
448
|
+
max_latency_ms=max_latency_ms,
|
|
449
|
+
last_failure_time=last_failure_time,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def rpc_server(self) -> RpcServerMetrics:
|
|
454
|
+
"""Return RPC server metrics (incoming requests from CCU)."""
|
|
455
|
+
if self._observer is None:
|
|
456
|
+
return RpcServerMetrics()
|
|
457
|
+
|
|
458
|
+
total_requests = self._observer.get_counter(key="rpc_server.request")
|
|
459
|
+
total_errors = self._observer.get_counter(key="rpc_server.error")
|
|
460
|
+
active_tasks = int(self._observer.get_gauge(key="rpc_server.active_tasks"))
|
|
461
|
+
|
|
462
|
+
# Get latency metrics
|
|
463
|
+
latency = self._observer.get_latency(key="rpc_server.latency")
|
|
464
|
+
avg_latency_ms = 0.0
|
|
465
|
+
max_latency_ms = 0.0
|
|
466
|
+
if latency is not None and latency.count > 0:
|
|
467
|
+
avg_latency_ms = latency.total_ms / latency.count
|
|
468
|
+
max_latency_ms = latency.max_ms
|
|
469
|
+
|
|
470
|
+
return RpcServerMetrics(
|
|
471
|
+
total_requests=total_requests,
|
|
472
|
+
total_errors=total_errors,
|
|
473
|
+
active_tasks=active_tasks,
|
|
474
|
+
avg_latency_ms=avg_latency_ms,
|
|
475
|
+
max_latency_ms=max_latency_ms,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
@property
|
|
479
|
+
def services(self) -> ServiceMetrics:
|
|
480
|
+
"""Return service call metrics from MetricsObserver."""
|
|
481
|
+
if self._observer is None:
|
|
482
|
+
return ServiceMetrics()
|
|
483
|
+
|
|
484
|
+
# Build stats by method from observer data
|
|
485
|
+
stats_by_method: dict[str, ServiceStats] = {}
|
|
486
|
+
|
|
487
|
+
# Get all latency keys for service calls (pattern: service.call.{method})
|
|
488
|
+
for key in self._observer.get_keys_by_prefix(prefix="service.call."):
|
|
489
|
+
# Extract method name from key (service.call.method_name -> method_name)
|
|
490
|
+
parts = key.split(".")
|
|
491
|
+
if len(parts) >= 3:
|
|
492
|
+
method_name = parts[2]
|
|
493
|
+
if (latency := self._observer.get_latency(key=key)) is None:
|
|
494
|
+
continue
|
|
495
|
+
error_count = self._observer.get_counter(key=f"service.error.{method_name}")
|
|
496
|
+
|
|
497
|
+
stats_by_method[method_name] = ServiceStats(
|
|
498
|
+
call_count=latency.count,
|
|
499
|
+
error_count=error_count,
|
|
500
|
+
total_duration_ms=latency.total_ms,
|
|
501
|
+
max_duration_ms=latency.max_ms,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if not stats_by_method:
|
|
505
|
+
return ServiceMetrics()
|
|
506
|
+
|
|
507
|
+
total_calls = sum(s.call_count for s in stats_by_method.values())
|
|
508
|
+
total_errors = sum(s.error_count for s in stats_by_method.values())
|
|
509
|
+
total_duration = sum(s.total_duration_ms for s in stats_by_method.values())
|
|
510
|
+
max_duration = max((s.max_duration_ms for s in stats_by_method.values()), default=0.0)
|
|
511
|
+
|
|
512
|
+
avg_duration = total_duration / total_calls if total_calls > 0 else 0.0
|
|
513
|
+
|
|
514
|
+
return ServiceMetrics(
|
|
515
|
+
total_calls=total_calls,
|
|
516
|
+
total_errors=total_errors,
|
|
517
|
+
avg_duration_ms=avg_duration,
|
|
518
|
+
max_duration_ms=max_duration,
|
|
519
|
+
by_method=stats_by_method,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def snapshot(self) -> MetricsSnapshot:
|
|
523
|
+
"""Return point-in-time snapshot of all metrics."""
|
|
524
|
+
return MetricsSnapshot(
|
|
525
|
+
timestamp=datetime.now(),
|
|
526
|
+
rpc=self.rpc,
|
|
527
|
+
rpc_server=self.rpc_server,
|
|
528
|
+
events=self.events,
|
|
529
|
+
cache=self.cache,
|
|
530
|
+
health=self.health,
|
|
531
|
+
recovery=self.recovery,
|
|
532
|
+
model=self.model,
|
|
533
|
+
services=self.services,
|
|
534
|
+
)
|