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,172 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Mutable statistics classes for metrics tracking.
|
|
5
|
+
|
|
6
|
+
This module provides mutable dataclasses for tracking runtime statistics.
|
|
7
|
+
These are used by components to record metrics which are then aggregated.
|
|
8
|
+
|
|
9
|
+
Public API
|
|
10
|
+
----------
|
|
11
|
+
- CacheStats: Cache hit/miss/size statistics
|
|
12
|
+
- LatencyStats: Request latency statistics (count, min, max, avg)
|
|
13
|
+
- ServiceStats: Service method execution statistics (call count, errors, timing)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
import math
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class SizeOnlyStats:
|
|
24
|
+
"""
|
|
25
|
+
Size-only statistics for registries and trackers.
|
|
26
|
+
|
|
27
|
+
Used for components that are not true caches (no hit/miss semantics):
|
|
28
|
+
- DeviceDescriptionRegistry (authoritative store)
|
|
29
|
+
- ParamsetDescriptionRegistry (authoritative store)
|
|
30
|
+
- ParameterVisibilityRegistry (rule engine with memoization)
|
|
31
|
+
- PingPongTracker (connection health tracker)
|
|
32
|
+
- CommandTracker (sent command tracker)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
size: int = 0
|
|
36
|
+
"""Current number of entries."""
|
|
37
|
+
|
|
38
|
+
evictions: int = 0
|
|
39
|
+
"""Number of entries removed (for memory management, not cache semantics)."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(slots=True)
|
|
43
|
+
class CacheStats:
|
|
44
|
+
"""
|
|
45
|
+
Statistics for cache performance monitoring.
|
|
46
|
+
|
|
47
|
+
Used for true caches with hit/miss semantics:
|
|
48
|
+
- CentralDataCache
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
hits: int = 0
|
|
52
|
+
"""Number of successful cache lookups."""
|
|
53
|
+
|
|
54
|
+
misses: int = 0
|
|
55
|
+
"""Number of cache misses."""
|
|
56
|
+
|
|
57
|
+
size: int = 0
|
|
58
|
+
"""Current number of entries in the cache."""
|
|
59
|
+
|
|
60
|
+
evictions: int = 0
|
|
61
|
+
"""Number of entries evicted from the cache."""
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def hit_rate(self) -> float:
|
|
65
|
+
"""Return hit rate as percentage."""
|
|
66
|
+
if (total := self.hits + self.misses) == 0:
|
|
67
|
+
return 100.0
|
|
68
|
+
return (self.hits / total) * 100
|
|
69
|
+
|
|
70
|
+
def record_hit(self) -> None:
|
|
71
|
+
"""Record a cache hit."""
|
|
72
|
+
self.hits += 1
|
|
73
|
+
|
|
74
|
+
def record_miss(self) -> None:
|
|
75
|
+
"""Record a cache miss."""
|
|
76
|
+
self.misses += 1
|
|
77
|
+
|
|
78
|
+
def reset(self) -> None:
|
|
79
|
+
"""Reset cache statistics."""
|
|
80
|
+
self.hits = 0
|
|
81
|
+
self.misses = 0
|
|
82
|
+
self.size = 0
|
|
83
|
+
self.evictions = 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(slots=True)
|
|
87
|
+
class LatencyStats:
|
|
88
|
+
"""Statistics for request latency tracking."""
|
|
89
|
+
|
|
90
|
+
count: int = 0
|
|
91
|
+
"""Number of latency samples."""
|
|
92
|
+
|
|
93
|
+
total_ms: float = 0.0
|
|
94
|
+
"""Total latency in milliseconds."""
|
|
95
|
+
|
|
96
|
+
min_ms: float = math.inf
|
|
97
|
+
"""Minimum latency in milliseconds."""
|
|
98
|
+
|
|
99
|
+
max_ms: float = 0.0
|
|
100
|
+
"""Maximum latency in milliseconds."""
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def avg_ms(self) -> float:
|
|
104
|
+
"""Return average latency in milliseconds."""
|
|
105
|
+
if self.count == 0:
|
|
106
|
+
return 0.0
|
|
107
|
+
return self.total_ms / self.count
|
|
108
|
+
|
|
109
|
+
def record(self, *, duration_ms: float) -> None:
|
|
110
|
+
"""Record a latency sample."""
|
|
111
|
+
self.count += 1
|
|
112
|
+
self.total_ms += duration_ms
|
|
113
|
+
self.min_ms = min(self.min_ms, duration_ms)
|
|
114
|
+
self.max_ms = max(self.max_ms, duration_ms)
|
|
115
|
+
|
|
116
|
+
def reset(self) -> None:
|
|
117
|
+
"""Reset latency statistics."""
|
|
118
|
+
self.count = 0
|
|
119
|
+
self.total_ms = 0.0
|
|
120
|
+
self.min_ms = math.inf
|
|
121
|
+
self.max_ms = 0.0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(slots=True)
|
|
125
|
+
class ServiceStats:
|
|
126
|
+
"""
|
|
127
|
+
Statistics for service method execution tracking.
|
|
128
|
+
|
|
129
|
+
This class tracks call counts, errors, and timing for service methods
|
|
130
|
+
decorated with @inspector(measure_performance=True).
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
call_count: int = 0
|
|
134
|
+
"""Total number of calls to this method."""
|
|
135
|
+
|
|
136
|
+
error_count: int = 0
|
|
137
|
+
"""Number of calls that raised exceptions."""
|
|
138
|
+
|
|
139
|
+
total_duration_ms: float = 0.0
|
|
140
|
+
"""Total execution time in milliseconds."""
|
|
141
|
+
|
|
142
|
+
max_duration_ms: float = 0.0
|
|
143
|
+
"""Maximum execution time in milliseconds."""
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def avg_duration_ms(self) -> float:
|
|
147
|
+
"""Return average execution time in milliseconds."""
|
|
148
|
+
if self.call_count == 0:
|
|
149
|
+
return 0.0
|
|
150
|
+
return self.total_duration_ms / self.call_count
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def error_rate(self) -> float:
|
|
154
|
+
"""Return error rate as percentage."""
|
|
155
|
+
if self.call_count == 0:
|
|
156
|
+
return 0.0
|
|
157
|
+
return (self.error_count / self.call_count) * 100
|
|
158
|
+
|
|
159
|
+
def record(self, *, duration_ms: float, had_error: bool) -> None:
|
|
160
|
+
"""Record a service call."""
|
|
161
|
+
self.call_count += 1
|
|
162
|
+
self.total_duration_ms += duration_ms
|
|
163
|
+
self.max_duration_ms = max(self.max_duration_ms, duration_ms)
|
|
164
|
+
if had_error:
|
|
165
|
+
self.error_count += 1
|
|
166
|
+
|
|
167
|
+
def reset(self) -> None:
|
|
168
|
+
"""Reset all statistics."""
|
|
169
|
+
self.call_count = 0
|
|
170
|
+
self.error_count = 0
|
|
171
|
+
self.total_duration_ms = 0.0
|
|
172
|
+
self.max_duration_ms = 0.0
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Data point and event model for AioHomematic.
|
|
5
|
+
|
|
6
|
+
Overview
|
|
7
|
+
--------
|
|
8
|
+
This package provides the runtime model layer that transforms device and channel
|
|
9
|
+
parameter descriptions from Homematic backends into typed data point objects and
|
|
10
|
+
events. It orchestrates the creation of data point hierarchies (devices, channels,
|
|
11
|
+
data points) and manages their lifecycle.
|
|
12
|
+
|
|
13
|
+
The model layer is purely domain-focused with no I/O operations. All backend
|
|
14
|
+
communication is delegated to the client layer through protocol interfaces.
|
|
15
|
+
|
|
16
|
+
Subpackages
|
|
17
|
+
-----------
|
|
18
|
+
The model is organized into specialized data point types:
|
|
19
|
+
|
|
20
|
+
- **generic**: Default data point implementations (switch, number, sensor, select,
|
|
21
|
+
binary_sensor, button, action, text) for standard parameter types.
|
|
22
|
+
- **custom**: Device-specific implementations providing higher-level abstractions
|
|
23
|
+
(climate, cover, light, lock, siren, valve) for complex multi-parameter devices.
|
|
24
|
+
- **calculated**: Derived data points computing values from other data points
|
|
25
|
+
(e.g., dew point, apparent temperature, battery level percentage).
|
|
26
|
+
- **hub**: Backend system data points including programs and system variables exposed
|
|
27
|
+
by the CCU/Homegear hub.
|
|
28
|
+
|
|
29
|
+
Public API
|
|
30
|
+
----------
|
|
31
|
+
- `create_data_points_and_events`: Main factory function for populating device
|
|
32
|
+
channels with data points and events based on paramset descriptions.
|
|
33
|
+
|
|
34
|
+
Workflow
|
|
35
|
+
--------
|
|
36
|
+
During device initialization, `create_data_points_and_events` is invoked for each
|
|
37
|
+
device. It performs the following steps:
|
|
38
|
+
|
|
39
|
+
1. Iterates through all device channels and their paramset descriptions.
|
|
40
|
+
2. Applies visibility rules to filter relevant parameters.
|
|
41
|
+
3. Creates event objects for parameters supporting EVENT operations.
|
|
42
|
+
4. Creates appropriate data point instances (generic or custom).
|
|
43
|
+
5. Instantiates calculated data points based on available source data points.
|
|
44
|
+
|
|
45
|
+
The resulting data point objects are registered with their parent channels and
|
|
46
|
+
become accessible through the central unit's query API.
|
|
47
|
+
|
|
48
|
+
Notes
|
|
49
|
+
-----
|
|
50
|
+
The entrypoint function is decorated with `@inspector` for automatic exception
|
|
51
|
+
handling and logging. All data point creation follows the factory pattern with
|
|
52
|
+
type selection based on parameter metadata and device profiles.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
from collections.abc import Mapping
|
|
59
|
+
import logging
|
|
60
|
+
from typing import Final
|
|
61
|
+
|
|
62
|
+
from aiohomematic.const import (
|
|
63
|
+
CLICK_EVENTS,
|
|
64
|
+
DEVICE_ERROR_EVENTS,
|
|
65
|
+
IMPULSE_EVENTS,
|
|
66
|
+
Field,
|
|
67
|
+
Flag,
|
|
68
|
+
Operations,
|
|
69
|
+
Parameter,
|
|
70
|
+
ParameterData,
|
|
71
|
+
ParamsetKey,
|
|
72
|
+
ServiceScope,
|
|
73
|
+
)
|
|
74
|
+
from aiohomematic.decorators import inspector
|
|
75
|
+
from aiohomematic.interfaces.model import ChannelProtocol, DeviceProtocol
|
|
76
|
+
from aiohomematic.model.availability import AvailabilityInfo
|
|
77
|
+
from aiohomematic.model.calculated import create_calculated_data_points
|
|
78
|
+
from aiohomematic.model.event import create_event_and_append_to_channel
|
|
79
|
+
from aiohomematic.model.generic import create_data_point_and_append_to_channel
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
# Data classes
|
|
83
|
+
"AvailabilityInfo",
|
|
84
|
+
# Factory
|
|
85
|
+
"create_data_points_and_events",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# Some parameters are marked as INTERNAL in the paramset and not considered by default,
|
|
89
|
+
# but some are required and should be added here.
|
|
90
|
+
_ALLOWED_INTERNAL_PARAMETERS: Final[Mapping[Field, Parameter]] = {
|
|
91
|
+
Field.DIRECTION: Parameter.DIRECTION,
|
|
92
|
+
Field.ON_TIME_LIST: Parameter.ON_TIME_LIST_1,
|
|
93
|
+
Field.REPETITIONS: Parameter.REPETITIONS,
|
|
94
|
+
}
|
|
95
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@inspector(scope=ServiceScope.INTERNAL)
|
|
99
|
+
def create_data_points_and_events(*, device: DeviceProtocol) -> None:
|
|
100
|
+
"""Create the data points associated to this device."""
|
|
101
|
+
for channel in device.channels.values():
|
|
102
|
+
for paramset_key, paramsset_key_descriptions in channel.paramset_descriptions.items():
|
|
103
|
+
if not device.parameter_visibility_provider.is_relevant_paramset(
|
|
104
|
+
channel=channel,
|
|
105
|
+
paramset_key=paramset_key,
|
|
106
|
+
):
|
|
107
|
+
continue
|
|
108
|
+
for (
|
|
109
|
+
parameter,
|
|
110
|
+
parameter_data,
|
|
111
|
+
) in paramsset_key_descriptions.items():
|
|
112
|
+
parameter_is_un_ignored = channel.device.parameter_visibility_provider.parameter_is_un_ignored(
|
|
113
|
+
channel=channel,
|
|
114
|
+
paramset_key=paramset_key,
|
|
115
|
+
parameter=parameter,
|
|
116
|
+
)
|
|
117
|
+
if channel.device.parameter_visibility_provider.should_skip_parameter(
|
|
118
|
+
channel=channel,
|
|
119
|
+
paramset_key=paramset_key,
|
|
120
|
+
parameter=parameter,
|
|
121
|
+
parameter_is_un_ignored=parameter_is_un_ignored,
|
|
122
|
+
):
|
|
123
|
+
continue
|
|
124
|
+
_process_parameter(
|
|
125
|
+
channel=channel,
|
|
126
|
+
paramset_key=paramset_key,
|
|
127
|
+
parameter=parameter,
|
|
128
|
+
parameter_data=parameter_data,
|
|
129
|
+
parameter_is_un_ignored=parameter_is_un_ignored,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
create_calculated_data_points(channel=channel)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _process_parameter(
|
|
136
|
+
*,
|
|
137
|
+
channel: ChannelProtocol,
|
|
138
|
+
paramset_key: ParamsetKey,
|
|
139
|
+
parameter: str,
|
|
140
|
+
parameter_data: ParameterData,
|
|
141
|
+
parameter_is_un_ignored: bool,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Process individual parameter to create data points and events."""
|
|
144
|
+
if paramset_key == ParamsetKey.MASTER and parameter_data["OPERATIONS"] == 0:
|
|
145
|
+
# required to fix hm master paramset operation values
|
|
146
|
+
parameter_data["OPERATIONS"] = 3
|
|
147
|
+
|
|
148
|
+
if _should_create_event(parameter_data=parameter_data, parameter=parameter):
|
|
149
|
+
create_event_and_append_to_channel(
|
|
150
|
+
channel=channel,
|
|
151
|
+
parameter=parameter,
|
|
152
|
+
parameter_data=parameter_data,
|
|
153
|
+
)
|
|
154
|
+
if _should_skip_data_point(
|
|
155
|
+
parameter_data=parameter_data, parameter=parameter, parameter_is_un_ignored=parameter_is_un_ignored
|
|
156
|
+
):
|
|
157
|
+
_LOGGER.debug(
|
|
158
|
+
"CREATE_DATA_POINTS: Skipping %s (no event or internal)",
|
|
159
|
+
parameter,
|
|
160
|
+
)
|
|
161
|
+
return
|
|
162
|
+
# CLICK_EVENTS are allowed for Buttons
|
|
163
|
+
if parameter not in IMPULSE_EVENTS and (not parameter.startswith(DEVICE_ERROR_EVENTS) or parameter_is_un_ignored):
|
|
164
|
+
create_data_point_and_append_to_channel(
|
|
165
|
+
channel=channel,
|
|
166
|
+
paramset_key=paramset_key,
|
|
167
|
+
parameter=parameter,
|
|
168
|
+
parameter_data=parameter_data,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _should_create_event(*, parameter_data: ParameterData, parameter: str) -> bool:
|
|
173
|
+
"""Determine if an event should be created for the parameter."""
|
|
174
|
+
return bool(
|
|
175
|
+
parameter_data["OPERATIONS"] & Operations.EVENT
|
|
176
|
+
and (parameter in CLICK_EVENTS or parameter.startswith(DEVICE_ERROR_EVENTS) or parameter in IMPULSE_EVENTS)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _should_skip_data_point(*, parameter_data: ParameterData, parameter: str, parameter_is_un_ignored: bool) -> bool:
|
|
181
|
+
"""Determine if a data point should be skipped."""
|
|
182
|
+
return bool(
|
|
183
|
+
(not parameter_data["OPERATIONS"] & Operations.EVENT and not parameter_data["OPERATIONS"] & Operations.WRITE)
|
|
184
|
+
or (
|
|
185
|
+
parameter_data["FLAGS"] & Flag.INTERNAL
|
|
186
|
+
and parameter not in _ALLOWED_INTERNAL_PARAMETERS.values()
|
|
187
|
+
and not parameter_is_un_ignored
|
|
188
|
+
)
|
|
189
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Availability information for Homematic devices.
|
|
5
|
+
|
|
6
|
+
This module provides the AvailabilityInfo dataclass that bundles
|
|
7
|
+
device reachability, battery state, and signal strength information
|
|
8
|
+
into a single unified view for external consumers.
|
|
9
|
+
|
|
10
|
+
Public API of this module is defined by __all__.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Final
|
|
18
|
+
|
|
19
|
+
__all__: Final = ["AvailabilityInfo"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class AvailabilityInfo:
|
|
24
|
+
"""
|
|
25
|
+
Bundled availability information for a Homematic device.
|
|
26
|
+
|
|
27
|
+
Provides a unified view of device connectivity and health status,
|
|
28
|
+
combining reachability, battery state, and signal strength.
|
|
29
|
+
|
|
30
|
+
This dataclass is immutable (frozen) to ensure thread-safety when
|
|
31
|
+
passed between components.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> info = device.availability
|
|
35
|
+
>>> if not info.is_reachable:
|
|
36
|
+
... print(f"Device unreachable since {info.last_updated}")
|
|
37
|
+
>>> if info.low_battery:
|
|
38
|
+
... print(f"Battery low: {info.battery_level}%")
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
is_reachable: bool
|
|
43
|
+
"""Device is reachable (inverse of UNREACH parameter)."""
|
|
44
|
+
|
|
45
|
+
last_updated: datetime | None
|
|
46
|
+
"""Most recent data point modification time across all channels."""
|
|
47
|
+
|
|
48
|
+
battery_level: int | None = None
|
|
49
|
+
"""Battery level percentage (0-100), from OperatingVoltageLevel or BATTERY_STATE."""
|
|
50
|
+
|
|
51
|
+
low_battery: bool | None = None
|
|
52
|
+
"""Low battery indicator from LOW_BAT parameter."""
|
|
53
|
+
|
|
54
|
+
signal_strength: int | None = None
|
|
55
|
+
"""Signal strength in dBm from RSSI_DEVICE (negative values, e.g., -65)."""
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def has_battery(self) -> bool:
|
|
59
|
+
"""Return True if any battery information is available."""
|
|
60
|
+
return self.battery_level is not None or self.low_battery is not None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def has_signal_info(self) -> bool:
|
|
64
|
+
"""Return True if signal strength information is available."""
|
|
65
|
+
return self.signal_strength is not None
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Calculated (derived) data points for AioHomematic.
|
|
5
|
+
|
|
6
|
+
This subpackage provides data points whose values are computed from one or more
|
|
7
|
+
underlying device data points. Typical examples include climate-related metrics
|
|
8
|
+
(such as dew point, apparent temperature, frost point, vapor concentration) and
|
|
9
|
+
battery/voltage related assessments (such as operating voltage level).
|
|
10
|
+
|
|
11
|
+
How it works:
|
|
12
|
+
- Each calculated data point is a lightweight model that subscribes to one or
|
|
13
|
+
more generic data points of a channel and recomputes its value when any of
|
|
14
|
+
the source data points change.
|
|
15
|
+
- Relevance is determined per channel. A calculated data point class exposes an
|
|
16
|
+
"is_relevant_for_model" method that decides if the channel provides the
|
|
17
|
+
necessary inputs.
|
|
18
|
+
- Creation is handled centrally via the factory function below.
|
|
19
|
+
|
|
20
|
+
Factory:
|
|
21
|
+
- create_calculated_data_points(channel): Iterates over the known calculated
|
|
22
|
+
implementations, checks their relevance against the given channel, and, if
|
|
23
|
+
applicable, creates and attaches instances to the channel so they behave like
|
|
24
|
+
normal read-only data points.
|
|
25
|
+
|
|
26
|
+
Modules/classes:
|
|
27
|
+
- ApparentTemperature, DewPoint, DewPointSpread, Enthalphy, FrostPoint, VaporConcentration: Climate-related
|
|
28
|
+
sensors implemented in climate.py using well-known formulas (see
|
|
29
|
+
aiohomematic.model.calculated.support for details and references).
|
|
30
|
+
- OperatingVoltageLevel: Interprets battery/voltage values and exposes a human
|
|
31
|
+
readable operating level classification.
|
|
32
|
+
|
|
33
|
+
These calculated data points complement generic and custom data points by
|
|
34
|
+
exposing useful metrics not directly provided by the device/firmware.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import logging
|
|
40
|
+
from typing import Final
|
|
41
|
+
|
|
42
|
+
from aiohomematic.const import ServiceScope
|
|
43
|
+
from aiohomematic.decorators import inspector
|
|
44
|
+
from aiohomematic.interfaces.model import ChannelProtocol
|
|
45
|
+
from aiohomematic.model.calculated.climate import (
|
|
46
|
+
ApparentTemperature,
|
|
47
|
+
DewPoint,
|
|
48
|
+
DewPointSpread,
|
|
49
|
+
Enthalpy,
|
|
50
|
+
FrostPoint,
|
|
51
|
+
VaporConcentration,
|
|
52
|
+
)
|
|
53
|
+
from aiohomematic.model.calculated.data_point import CalculatedDataPoint
|
|
54
|
+
from aiohomematic.model.calculated.operating_voltage_level import OperatingVoltageLevel
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Base
|
|
58
|
+
"CalculatedDataPoint",
|
|
59
|
+
# Climate
|
|
60
|
+
"ApparentTemperature",
|
|
61
|
+
"DewPoint",
|
|
62
|
+
"DewPointSpread",
|
|
63
|
+
"Enthalpy",
|
|
64
|
+
"FrostPoint",
|
|
65
|
+
"VaporConcentration",
|
|
66
|
+
# Factory
|
|
67
|
+
"create_calculated_data_points",
|
|
68
|
+
# Voltage
|
|
69
|
+
"OperatingVoltageLevel",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
_CALCULATED_DATA_POINTS: Final = (
|
|
73
|
+
ApparentTemperature,
|
|
74
|
+
DewPoint,
|
|
75
|
+
DewPointSpread,
|
|
76
|
+
Enthalpy,
|
|
77
|
+
FrostPoint,
|
|
78
|
+
OperatingVoltageLevel,
|
|
79
|
+
VaporConcentration,
|
|
80
|
+
)
|
|
81
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@inspector(scope=ServiceScope.INTERNAL)
|
|
85
|
+
def create_calculated_data_points(*, channel: ChannelProtocol) -> None:
|
|
86
|
+
"""Decides which data point category should be used, and creates the required data points."""
|
|
87
|
+
for dp in _CALCULATED_DATA_POINTS:
|
|
88
|
+
if dp.is_relevant_for_model(channel=channel):
|
|
89
|
+
channel.add_data_point(data_point=dp(channel=channel))
|