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,110 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ AioHomematic: async Python library for Homematic and HomematicIP backends.
5
+
6
+ Overview
7
+ --------
8
+ This package provides a comprehensive async library for interacting with Homematic CCU
9
+ and HomematicIP systems. It enables device discovery, data point manipulation, event
10
+ handling, and management of programs and system variables.
11
+
12
+ The library is designed for integration with Home Assistant but can be used standalone.
13
+ It features automatic data point discovery, flexible customization through device-specific
14
+ implementations, and fast startup through intelligent caching.
15
+
16
+ Architecture
17
+ ------------
18
+ The library is organized into four main layers:
19
+
20
+ - **aiohomematic.central**: Central orchestration managing client lifecycles, device
21
+ creation, event handling, and background tasks.
22
+ - **aiohomematic.client**: Protocol adapters (JSON-RPC/XML-RPC) for backend communication.
23
+ - **aiohomematic.model**: Runtime representation of devices, channels, and data points
24
+ with generic, custom, calculated, and hub data point types.
25
+ - **aiohomematic.store**: Persistence layer for device descriptions, paramsets, and
26
+ runtime caches.
27
+
28
+ Public API
29
+ ----------
30
+ - `__version__`: Library version string.
31
+
32
+ The primary entry point is `CentralConfig` and `CentralUnit` from `aiohomematic.central`.
33
+
34
+ Quick start
35
+ -----------
36
+ Typical usage pattern:
37
+
38
+ from aiohomematic.central import CentralConfig
39
+ from aiohomematic.client import InterfaceConfig, Interface
40
+
41
+ config = CentralConfig(
42
+ name="ccu-main",
43
+ host="192.168.1.100",
44
+ username="admin",
45
+ password="secret",
46
+ central_id="unique-id",
47
+ interface_configs={
48
+ InterfaceConfig(
49
+ central_name="ccu-main",
50
+ interface=Interface.HMIP_RF,
51
+ port=2010,
52
+ ),
53
+ },
54
+ )
55
+
56
+ central = await config.create_central()
57
+ await central.start()
58
+
59
+ # Access devices and data points
60
+ device = central.device_coordinator.get_device_by_address("VCU0000001")
61
+
62
+ await central.stop()
63
+
64
+ Notes
65
+ -----
66
+ Public API at the top-level package is defined by `__all__`.
67
+
68
+ """
69
+
70
+ from __future__ import annotations
71
+
72
+ import asyncio
73
+ import logging
74
+ import signal
75
+ import sys
76
+ import threading
77
+ from typing import Final
78
+
79
+ from aiohomematic import central as hmcu, i18n, validator as _ahm_validator
80
+ from aiohomematic.const import VERSION
81
+
82
+ if sys.stdout.isatty():
83
+ logging.basicConfig(level=logging.INFO)
84
+
85
+ __version__: Final = VERSION
86
+ _LOGGER: Final = logging.getLogger(__name__)
87
+
88
+
89
+ # pylint: disable=unused-argument
90
+ # noinspection PyUnusedLocal
91
+ def signal_handler(sig, frame): # type: ignore[no-untyped-def] # kwonly: disable
92
+ """Handle signal to shut down central."""
93
+ _LOGGER.info(i18n.tr(key="log.core.signal.shutdown", sig=str(sig)))
94
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
95
+ for central in hmcu.CENTRAL_INSTANCES.values():
96
+ asyncio.run_coroutine_threadsafe(central.stop(), asyncio.get_running_loop())
97
+
98
+
99
+ # Perform lightweight startup validation once on import
100
+ try:
101
+ _ahm_validator.validate_startup()
102
+ except Exception as _exc: # pragma: no cover
103
+ # Fail-fast with a clear message if validation fails during import
104
+ raise RuntimeError(i18n.tr(key="exception.startup.validation_failed", reason=_exc)) from _exc
105
+
106
+ if threading.current_thread() is threading.main_thread() and sys.stdout.isatty():
107
+ signal.signal(signal.SIGINT, signal_handler)
108
+
109
+ # Define public API for the top-level package
110
+ __all__ = ["__version__"]
@@ -0,0 +1,29 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Log context protocol interface.
5
+
6
+ This module is intentionally minimal to avoid circular imports.
7
+ It's imported by property_decorators.py which is near the bottom of the import chain.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from abc import abstractmethod
13
+ from collections.abc import Mapping
14
+ from typing import Any, Protocol, runtime_checkable
15
+
16
+
17
+ @runtime_checkable
18
+ class LogContextProtocol(Protocol):
19
+ """
20
+ Protocol for objects providing log context.
21
+
22
+ Implemented by LogContextMixin and used by property_decorators.py
23
+ to avoid circular imports.
24
+ """
25
+
26
+ @property
27
+ @abstractmethod
28
+ def log_context(self) -> Mapping[str, Any]:
29
+ """Return the log context for this object."""
aiohomematic/api.py ADDED
@@ -0,0 +1,410 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Simplified facade API for common Homematic operations.
5
+
6
+ This module provides `HomematicAPI`, a high-level facade that wraps the most
7
+ commonly used operations from `CentralUnit`. It offers a streamlined interface
8
+ for typical use cases without requiring deep knowledge of the internal architecture.
9
+
10
+ Quick start
11
+ -----------
12
+ Using the async context manager (recommended)::
13
+
14
+ from aiohomematic.api import HomematicAPI
15
+
16
+ async with HomematicAPI.connect(
17
+ host="192.168.1.100",
18
+ username="Admin",
19
+ password="secret",
20
+ ) as api:
21
+ # List all devices
22
+ for device in api.list_devices():
23
+ print(f"{device.address}: {device.name}")
24
+
25
+ # Read and write values
26
+ value = await api.read_value(channel_address="VCU0000001:1", parameter="STATE")
27
+ await api.write_value(channel_address="VCU0000001:1", parameter="STATE", value=True)
28
+
29
+ # Connection is automatically closed when exiting the context
30
+
31
+ Manual lifecycle management::
32
+
33
+ from aiohomematic.api import HomematicAPI
34
+ from aiohomematic.central import CentralConfig
35
+
36
+ config = CentralConfig.for_ccu(
37
+ host="192.168.1.100",
38
+ username="Admin",
39
+ password="secret",
40
+ )
41
+ api = HomematicAPI(config=config)
42
+ await api.start()
43
+
44
+ try:
45
+ for device in api.list_devices():
46
+ print(f"{device.address}: {device.name}")
47
+ finally:
48
+ await api.stop()
49
+
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ from collections.abc import Callable, Iterable
55
+ from types import TracebackType
56
+ from typing import Any, Final, Self
57
+
58
+ from aiohomematic.central import CentralConfig, CentralUnit
59
+ from aiohomematic.central.events import DataPointValueReceivedEvent
60
+ from aiohomematic.const import ParamsetKey
61
+ from aiohomematic.interfaces import DeviceProtocol
62
+ from aiohomematic.property_decorators import DelegatedProperty
63
+ from aiohomematic.support import get_device_address
64
+ from aiohomematic.type_aliases import UnsubscribeCallback
65
+
66
+ # Type alias for update callback
67
+ UpdateCallback = Callable[[str, str, Any], None]
68
+
69
+
70
+ class HomematicAPI:
71
+ """
72
+ Simplified facade for common Homematic operations.
73
+
74
+ This class provides a high-level interface for interacting with Homematic
75
+ devices without requiring deep knowledge of the internal architecture.
76
+ It wraps the most commonly used operations from CentralUnit.
77
+
78
+ Attributes:
79
+ central: The underlying CentralUnit instance.
80
+ config: The configuration used to create this API instance.
81
+
82
+ """
83
+
84
+ def __init__(self, *, config: CentralConfig) -> None:
85
+ """
86
+ Initialize the HomematicAPI.
87
+
88
+ Args:
89
+ config: Configuration for the central unit. Use CentralConfig.for_ccu()
90
+ or CentralConfig.for_homegear() for simplified setup.
91
+
92
+ """
93
+ self._config: Final = config
94
+ self._central: CentralUnit | None = None
95
+
96
+ async def __aenter__(self) -> Self:
97
+ """Enter the async context manager and start the API."""
98
+ await self.start()
99
+ return self
100
+
101
+ async def __aexit__( # kwonly: disable
102
+ self,
103
+ exc_type: type[BaseException] | None,
104
+ exc_val: BaseException | None,
105
+ exc_tb: TracebackType | None,
106
+ ) -> None:
107
+ """Exit the async context manager and stop the API."""
108
+ await self.stop()
109
+
110
+ @classmethod
111
+ def connect(
112
+ cls,
113
+ *,
114
+ host: str,
115
+ username: str,
116
+ password: str,
117
+ central_id: str | None = None,
118
+ tls: bool = False,
119
+ verify_tls: bool = True,
120
+ backend: str = "ccu",
121
+ ) -> Self:
122
+ """
123
+ Create a HomematicAPI instance for use as an async context manager.
124
+
125
+ This is the recommended way to use the API, as it ensures proper
126
+ cleanup even when exceptions occur.
127
+
128
+ Args:
129
+ host: The hostname or IP address of the Homematic backend.
130
+ username: The username for authentication.
131
+ password: The password for authentication.
132
+ central_id: Optional unique identifier for this central (defaults to host).
133
+ tls: Whether to use TLS encryption (default: False).
134
+ verify_tls: Whether to verify TLS certificates (default: True).
135
+ backend: The backend type, either "ccu" or "homegear" (default: "ccu").
136
+
137
+ Returns:
138
+ A HomematicAPI instance that can be used as an async context manager.
139
+
140
+ Example:
141
+ async with HomematicAPI.connect(
142
+ host="192.168.1.100",
143
+ username="Admin",
144
+ password="secret",
145
+ ) as api:
146
+ for device in api.list_devices():
147
+ print(device.address)
148
+
149
+ # Connection is automatically closed
150
+
151
+ Raises:
152
+ ValueError: If backend is not "ccu" or "homegear".
153
+
154
+ """
155
+ if backend == "ccu":
156
+ config = CentralConfig.for_ccu(
157
+ name=central_id or host,
158
+ host=host,
159
+ username=username,
160
+ password=password,
161
+ central_id=central_id or host,
162
+ tls=tls,
163
+ verify_tls=verify_tls,
164
+ )
165
+ elif backend == "homegear":
166
+ config = CentralConfig.for_homegear(
167
+ name=central_id or host,
168
+ host=host,
169
+ username=username,
170
+ password=password,
171
+ central_id=central_id or host,
172
+ tls=tls,
173
+ verify_tls=verify_tls,
174
+ )
175
+ else:
176
+ msg = f"Unknown backend: {backend}. Use 'ccu' or 'homegear'."
177
+ raise ValueError(msg)
178
+
179
+ return cls(config=config)
180
+
181
+ @staticmethod
182
+ async def _do_fetch_all_device_data(*, client: Any) -> None:
183
+ """Fetch all device data for a single client."""
184
+ await client.fetch_all_device_data()
185
+
186
+ @staticmethod
187
+ async def _do_get_value(
188
+ *,
189
+ client: Any,
190
+ channel_address: str,
191
+ paramset_key: ParamsetKey,
192
+ parameter: str,
193
+ ) -> Any:
194
+ """Get a value from a client."""
195
+ return await client.get_value(
196
+ channel_address=channel_address,
197
+ paramset_key=paramset_key,
198
+ parameter=parameter,
199
+ )
200
+
201
+ @staticmethod
202
+ async def _do_set_value(
203
+ *,
204
+ client: Any,
205
+ channel_address: str,
206
+ paramset_key: ParamsetKey,
207
+ parameter: str,
208
+ value: Any,
209
+ ) -> None:
210
+ """Set a value on a client."""
211
+ await client.set_value(
212
+ channel_address=channel_address,
213
+ paramset_key=paramset_key,
214
+ parameter=parameter,
215
+ value=value,
216
+ )
217
+
218
+ config: Final = DelegatedProperty[CentralConfig](path="_config")
219
+
220
+ @property
221
+ def central(self) -> CentralUnit:
222
+ """Return the underlying CentralUnit instance."""
223
+ if self._central is None:
224
+ msg = "API not started. Call start() first."
225
+ raise RuntimeError(msg)
226
+ return self._central
227
+
228
+ @property
229
+ def is_connected(self) -> bool:
230
+ """Return True if connected to the backend."""
231
+ return (
232
+ self._central is not None
233
+ and self._central.client_coordinator.has_clients
234
+ and not self._central.connection_state.has_any_issue
235
+ )
236
+
237
+ def get_device(self, *, address: str) -> DeviceProtocol | None:
238
+ """
239
+ Get a device by its address.
240
+
241
+ Args:
242
+ address: The device address (e.g., "VCU0000001").
243
+
244
+ Returns:
245
+ The Device object, or None if not found.
246
+
247
+ Example:
248
+ device = api.get_device(address="VCU0000001")
249
+ if device:
250
+ print(f"Found: {device.name}")
251
+
252
+ """
253
+ return self.central.device_coordinator.get_device(address=address)
254
+
255
+ def list_devices(self) -> Iterable[DeviceProtocol]:
256
+ """
257
+ List all known devices.
258
+
259
+ Returns:
260
+ Iterable of Device objects.
261
+
262
+ Example:
263
+ for device in api.list_devices():
264
+ print(f"{device.address}: {device.name} ({device.model})")
265
+
266
+ """
267
+ return self.central.device_registry.devices
268
+
269
+ async def read_value(
270
+ self,
271
+ *,
272
+ channel_address: str,
273
+ parameter: str,
274
+ paramset_key: ParamsetKey = ParamsetKey.VALUES,
275
+ ) -> Any:
276
+ """
277
+ Read a parameter value from a device channel.
278
+
279
+ This method automatically retries on transient network errors.
280
+
281
+ Args:
282
+ channel_address: The channel address (e.g., "VCU0000001:1").
283
+ parameter: The parameter name (e.g., "STATE", "LEVEL").
284
+ paramset_key: The paramset key (default: VALUES).
285
+
286
+ Returns:
287
+ The current parameter value.
288
+
289
+ Example:
290
+ # Read switch state
291
+ state = await api.read_value(channel_address="VCU0000001:1", parameter="STATE")
292
+
293
+ # Read dimmer level
294
+ level = await api.read_value(channel_address="VCU0000002:1", parameter="LEVEL")
295
+
296
+ """
297
+ device_address = get_device_address(address=channel_address)
298
+ if (device := self.central.device_coordinator.get_device(address=device_address)) is None:
299
+ msg = f"Device not found for address: {device_address}"
300
+ raise ValueError(msg)
301
+ client = self.central.client_coordinator.get_client(interface_id=device.interface_id)
302
+ return await self._do_get_value(
303
+ client=client,
304
+ channel_address=channel_address,
305
+ paramset_key=paramset_key,
306
+ parameter=parameter,
307
+ )
308
+
309
+ async def refresh_data(self) -> None:
310
+ """
311
+ Refresh data from all devices.
312
+
313
+ This fetches the latest values from all connected devices.
314
+ Each client fetch automatically retries on transient network errors.
315
+ """
316
+ for client in self.central.client_coordinator.clients:
317
+ await self._do_fetch_all_device_data(client=client)
318
+
319
+ async def start(self) -> None:
320
+ """
321
+ Start the API and connect to the Homematic backend.
322
+
323
+ This creates the central unit, initializes clients, and starts
324
+ the background scheduler for connection health checks.
325
+ """
326
+ self._central = await self._config.create_central()
327
+ await self._central.start()
328
+
329
+ async def stop(self) -> None:
330
+ """
331
+ Stop the API and disconnect from the backend.
332
+
333
+ This stops all clients, the XML-RPC server, and the background scheduler.
334
+ """
335
+ if self._central is not None:
336
+ await self._central.stop()
337
+ self._central = None
338
+
339
+ def subscribe_to_updates(self, *, callback: UpdateCallback) -> UnsubscribeCallback:
340
+ """
341
+ Subscribe to data point value updates.
342
+
343
+ The callback is invoked whenever a data point value changes.
344
+
345
+ Args:
346
+ callback: Function called with (channel_address, parameter, value)
347
+ when a data point is updated.
348
+
349
+ Returns:
350
+ An unsubscribe function to remove the callback.
351
+
352
+ Example:
353
+ def on_update(address: str, parameter: str, value: Any) -> None:
354
+ print(f"{address}.{parameter} = {value}")
355
+
356
+ unsubscribe = api.subscribe_to_updates(callback=on_update)
357
+
358
+ # Later, to stop receiving updates:
359
+ unsubscribe()
360
+
361
+ """
362
+
363
+ async def event_handler(*, event: DataPointValueReceivedEvent) -> None:
364
+ callback(event.dpk.channel_address, event.dpk.parameter, event.value)
365
+
366
+ return self.central.event_bus.subscribe(
367
+ event_type=DataPointValueReceivedEvent,
368
+ event_key=None,
369
+ handler=event_handler,
370
+ )
371
+
372
+ async def write_value(
373
+ self,
374
+ *,
375
+ channel_address: str,
376
+ parameter: str,
377
+ value: Any,
378
+ paramset_key: ParamsetKey = ParamsetKey.VALUES,
379
+ ) -> None:
380
+ """
381
+ Write a parameter value to a device channel.
382
+
383
+ This method automatically retries on transient network errors.
384
+
385
+ Args:
386
+ channel_address: The channel address (e.g., "VCU0000001:1").
387
+ parameter: The parameter name (e.g., "STATE", "LEVEL").
388
+ value: The value to write.
389
+ paramset_key: The paramset key (default: VALUES).
390
+
391
+ Example:
392
+ # Turn on a switch
393
+ await api.write_value(channel_address="VCU0000001:1", parameter="STATE", value=True)
394
+
395
+ # Set dimmer to 50%
396
+ await api.write_value(channel_address="VCU0000002:1", parameter="LEVEL", value=0.5)
397
+
398
+ """
399
+ device_address = get_device_address(address=channel_address)
400
+ if (device := self.central.device_coordinator.get_device(address=device_address)) is None:
401
+ msg = f"Device not found for address: {device_address}"
402
+ raise ValueError(msg)
403
+ client = self.central.client_coordinator.get_client(interface_id=device.interface_id)
404
+ await self._do_set_value(
405
+ client=client,
406
+ channel_address=channel_address,
407
+ paramset_key=paramset_key,
408
+ parameter=parameter,
409
+ value=value,
410
+ )