aiohomematic 2025.8.8__py3-none-any.whl → 2025.8.10__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.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/__init__.py +15 -1
- aiohomematic/async_support.py +15 -2
- aiohomematic/caches/__init__.py +2 -0
- aiohomematic/caches/dynamic.py +2 -0
- aiohomematic/caches/persistent.py +29 -22
- aiohomematic/caches/visibility.py +277 -252
- aiohomematic/central/__init__.py +69 -49
- aiohomematic/central/decorators.py +60 -15
- aiohomematic/central/xml_rpc_server.py +15 -1
- aiohomematic/client/__init__.py +2 -0
- aiohomematic/client/_rpc_errors.py +81 -0
- aiohomematic/client/json_rpc.py +68 -19
- aiohomematic/client/xml_rpc.py +15 -8
- aiohomematic/const.py +145 -77
- aiohomematic/context.py +11 -1
- aiohomematic/converter.py +27 -1
- aiohomematic/decorators.py +88 -19
- aiohomematic/exceptions.py +19 -1
- aiohomematic/hmcli.py +13 -1
- aiohomematic/model/__init__.py +2 -0
- aiohomematic/model/calculated/__init__.py +2 -0
- aiohomematic/model/calculated/climate.py +2 -0
- aiohomematic/model/calculated/data_point.py +7 -1
- aiohomematic/model/calculated/operating_voltage_level.py +2 -0
- aiohomematic/model/calculated/support.py +2 -0
- aiohomematic/model/custom/__init__.py +2 -0
- aiohomematic/model/custom/climate.py +3 -1
- aiohomematic/model/custom/const.py +2 -0
- aiohomematic/model/custom/cover.py +30 -2
- aiohomematic/model/custom/data_point.py +6 -0
- aiohomematic/model/custom/definition.py +2 -0
- aiohomematic/model/custom/light.py +18 -10
- aiohomematic/model/custom/lock.py +2 -0
- aiohomematic/model/custom/siren.py +5 -2
- aiohomematic/model/custom/support.py +2 -0
- aiohomematic/model/custom/switch.py +2 -0
- aiohomematic/model/custom/valve.py +2 -0
- aiohomematic/model/data_point.py +30 -3
- aiohomematic/model/decorators.py +29 -8
- aiohomematic/model/device.py +9 -5
- aiohomematic/model/event.py +2 -0
- aiohomematic/model/generic/__init__.py +2 -0
- aiohomematic/model/generic/action.py +2 -0
- aiohomematic/model/generic/binary_sensor.py +2 -0
- aiohomematic/model/generic/button.py +2 -0
- aiohomematic/model/generic/data_point.py +4 -1
- aiohomematic/model/generic/number.py +4 -1
- aiohomematic/model/generic/select.py +4 -1
- aiohomematic/model/generic/sensor.py +2 -0
- aiohomematic/model/generic/switch.py +2 -0
- aiohomematic/model/generic/text.py +2 -0
- aiohomematic/model/hub/__init__.py +2 -0
- aiohomematic/model/hub/binary_sensor.py +2 -0
- aiohomematic/model/hub/button.py +2 -0
- aiohomematic/model/hub/data_point.py +6 -0
- aiohomematic/model/hub/number.py +2 -0
- aiohomematic/model/hub/select.py +2 -0
- aiohomematic/model/hub/sensor.py +2 -0
- aiohomematic/model/hub/switch.py +2 -0
- aiohomematic/model/hub/text.py +2 -0
- aiohomematic/model/support.py +26 -1
- aiohomematic/model/update.py +6 -0
- aiohomematic/support.py +175 -5
- aiohomematic/validator.py +49 -2
- aiohomematic-2025.8.10.dist-info/METADATA +124 -0
- aiohomematic-2025.8.10.dist-info/RECORD +78 -0
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.10.dist-info}/licenses/LICENSE +1 -1
- aiohomematic-2025.8.8.dist-info/METADATA +0 -69
- aiohomematic-2025.8.8.dist-info/RECORD +0 -77
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.10.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.10.dist-info}/top_level.txt +0 -0
aiohomematic/__init__.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""
|
|
2
4
|
AioHomematic: a Python 3 library to interact with HomeMatic and HomematicIP backends.
|
|
3
5
|
|
|
6
|
+
Public API at the top-level package is defined by __all__.
|
|
7
|
+
|
|
4
8
|
This package provides a high-level API to discover devices and channels, read and write
|
|
5
9
|
parameters (data points), receive events, and manage programs and system variables.
|
|
6
10
|
|
|
@@ -23,7 +27,7 @@ import sys
|
|
|
23
27
|
import threading
|
|
24
28
|
from typing import Final
|
|
25
29
|
|
|
26
|
-
from aiohomematic import central as hmcu
|
|
30
|
+
from aiohomematic import central as hmcu, validator as _ahm_validator
|
|
27
31
|
from aiohomematic.const import VERSION
|
|
28
32
|
|
|
29
33
|
if sys.stdout.isatty():
|
|
@@ -43,5 +47,15 @@ def signal_handler(sig, frame): # type: ignore[no-untyped-def]
|
|
|
43
47
|
asyncio.run_coroutine_threadsafe(central.stop(), asyncio.get_running_loop())
|
|
44
48
|
|
|
45
49
|
|
|
50
|
+
# Perform lightweight startup validation once on import
|
|
51
|
+
try:
|
|
52
|
+
_ahm_validator.validate_startup()
|
|
53
|
+
except Exception as _exc: # pragma: no cover
|
|
54
|
+
# Fail-fast with a clear message if validation fails during import
|
|
55
|
+
raise RuntimeError(f"AioHomematic startup validation failed: {_exc}") from _exc
|
|
56
|
+
|
|
46
57
|
if threading.current_thread() is threading.main_thread() and sys.stdout.isatty():
|
|
47
58
|
signal.signal(signal.SIGINT, signal_handler)
|
|
59
|
+
|
|
60
|
+
# Define public API for the top-level package
|
|
61
|
+
__all__ = ["__version__"]
|
aiohomematic/async_support.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""Module with support for loop interaction."""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
@@ -26,13 +28,24 @@ class Looper:
|
|
|
26
28
|
self._tasks: Final[set[asyncio.Future[Any]]] = set()
|
|
27
29
|
self._loop = asyncio.get_event_loop()
|
|
28
30
|
|
|
29
|
-
async def block_till_done(self) -> None:
|
|
30
|
-
"""
|
|
31
|
+
async def block_till_done(self, wait_time: float | None = None) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Block until all pending work is done.
|
|
34
|
+
|
|
35
|
+
If wait_time is set, stop waiting after the given number of seconds and log remaining tasks.
|
|
36
|
+
"""
|
|
31
37
|
# To flush out any call_soon_threadsafe
|
|
32
38
|
await asyncio.sleep(0)
|
|
33
39
|
start_time: float | None = None
|
|
40
|
+
deadline: float | None = (monotonic() + wait_time) if wait_time is not None else None
|
|
34
41
|
current_task = asyncio.current_task()
|
|
35
42
|
while tasks := [task for task in self._tasks if task is not current_task and not cancelling(task)]:
|
|
43
|
+
# If we have a deadline and have exceeded it, log remaining tasks and break
|
|
44
|
+
if deadline is not None and monotonic() >= deadline:
|
|
45
|
+
for task in tasks:
|
|
46
|
+
_LOGGER.warning("Shutdown timeout reached; task still pending: %s", task)
|
|
47
|
+
break
|
|
48
|
+
|
|
36
49
|
await self._await_and_log_pending(tasks)
|
|
37
50
|
|
|
38
51
|
if start_time is None:
|
aiohomematic/caches/__init__.py
CHANGED
aiohomematic/caches/dynamic.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""
|
|
2
4
|
Persistent caches used to persist HomeMatic metadata between runs.
|
|
3
5
|
|
|
@@ -208,9 +210,15 @@ class DeviceDescriptionCache(BasePersistentCache):
|
|
|
208
210
|
|
|
209
211
|
def add_device(self, interface_id: str, device_description: DeviceDescription) -> None:
|
|
210
212
|
"""Add a device to the cache."""
|
|
213
|
+
# Fast-path: If the address is not yet known, skip costly removal operations.
|
|
214
|
+
if (address := device_description["ADDRESS"]) not in self._device_descriptions[interface_id]:
|
|
215
|
+
self._raw_device_descriptions[interface_id].append(device_description)
|
|
216
|
+
self._process_device_description(interface_id=interface_id, device_description=device_description)
|
|
217
|
+
return
|
|
218
|
+
# Address exists: remove old entries before adding the new description.
|
|
211
219
|
self._remove_device(
|
|
212
220
|
interface_id=interface_id,
|
|
213
|
-
addresses_to_remove=[
|
|
221
|
+
addresses_to_remove=[address],
|
|
214
222
|
)
|
|
215
223
|
self._raw_device_descriptions[interface_id].append(device_description)
|
|
216
224
|
self._process_device_description(interface_id=interface_id, device_description=device_description)
|
|
@@ -228,23 +236,22 @@ class DeviceDescriptionCache(BasePersistentCache):
|
|
|
228
236
|
|
|
229
237
|
def _remove_device(self, interface_id: str, addresses_to_remove: list[str]) -> None:
|
|
230
238
|
"""Remove a device from the cache."""
|
|
239
|
+
# Use a set for faster membership checks
|
|
240
|
+
addresses_set = set(addresses_to_remove)
|
|
231
241
|
self._raw_device_descriptions[interface_id] = [
|
|
232
|
-
device
|
|
233
|
-
for device in self._raw_device_descriptions[interface_id]
|
|
234
|
-
if device["ADDRESS"] not in addresses_to_remove
|
|
242
|
+
device for device in self._raw_device_descriptions[interface_id] if device["ADDRESS"] not in addresses_set
|
|
235
243
|
]
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return tuple(self._addresses[interface_id].keys())
|
|
244
|
+
addr_map = self._addresses[interface_id]
|
|
245
|
+
desc_map = self._device_descriptions[interface_id]
|
|
246
|
+
for address in addresses_set:
|
|
247
|
+
# Pop with default to avoid KeyError and try/except overhead
|
|
248
|
+
if ADDRESS_SEPARATOR not in address:
|
|
249
|
+
addr_map.pop(address, None)
|
|
250
|
+
desc_map.pop(address, None)
|
|
251
|
+
|
|
252
|
+
def get_addresses(self, interface_id: str) -> frozenset[str]:
|
|
253
|
+
"""Return the addresses by interface as a set."""
|
|
254
|
+
return frozenset(self._addresses[interface_id])
|
|
248
255
|
|
|
249
256
|
def get_device_descriptions(self, interface_id: str) -> Mapping[str, DeviceDescription]:
|
|
250
257
|
"""Return the devices by interface."""
|
|
@@ -288,9 +295,10 @@ class DeviceDescriptionCache(BasePersistentCache):
|
|
|
288
295
|
device_address = get_device_address(address)
|
|
289
296
|
self._device_descriptions[interface_id][address] = device_description
|
|
290
297
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
298
|
+
# Avoid redundant membership checks; set.add is idempotent and cheaper than check+add
|
|
299
|
+
addr_set = self._addresses[interface_id][device_address]
|
|
300
|
+
addr_set.add(device_address)
|
|
301
|
+
addr_set.add(address)
|
|
294
302
|
|
|
295
303
|
async def load(self) -> DataOperationResult:
|
|
296
304
|
"""Load device data from disk into _device_description_cache."""
|
|
@@ -420,13 +428,12 @@ class ParamsetDescriptionCache(BasePersistentCache):
|
|
|
420
428
|
def _add_address_parameter(self, channel_address: str, paramsets: list[dict[str, Any]]) -> None:
|
|
421
429
|
"""Add address parameter to cache."""
|
|
422
430
|
device_address, channel_no = get_split_channel_address(channel_address)
|
|
431
|
+
cache = self._address_parameter_cache
|
|
423
432
|
for paramset in paramsets:
|
|
424
433
|
if not paramset:
|
|
425
434
|
continue
|
|
426
435
|
for parameter in paramset:
|
|
427
|
-
|
|
428
|
-
self._address_parameter_cache[(device_address, parameter)] = set()
|
|
429
|
-
self._address_parameter_cache[(device_address, parameter)].add(channel_no)
|
|
436
|
+
cache.setdefault((device_address, parameter), set()).add(channel_no)
|
|
430
437
|
|
|
431
438
|
async def load(self) -> DataOperationResult:
|
|
432
439
|
"""Load paramset descriptions from disk into paramset cache."""
|