aiohomematic 2025.10.18__tar.gz → 2025.10.20__tar.gz
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-2025.10.18/aiohomematic.egg-info → aiohomematic-2025.10.20}/PKG-INFO +5 -5
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/async_support.py +26 -9
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/central/__init__.py +55 -1
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/client/__init__.py +23 -20
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/client/json_rpc.py +16 -16
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/const.py +3 -2
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/data_point.py +5 -5
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/device.py +1 -1
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/support.py +2 -2
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/support.py +1 -53
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20/aiohomematic.egg-info}/PKG-INFO +5 -5
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic.egg-info/SOURCES.txt +1 -0
- aiohomematic-2025.10.20/aiohomematic.egg-info/requires.txt +4 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/pyproject.toml +4 -8
- aiohomematic-2025.10.20/requirements.txt +4 -0
- aiohomematic-2025.10.18/aiohomematic.egg-info/requires.txt +0 -4
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/LICENSE +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/MANIFEST.in +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/README.md +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/__init__.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/central/decorators.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/central/rpc_server.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/client/_rpc_errors.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/client/rpc_proxy.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/context.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/converter.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/decorators.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/exceptions.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/hmcli.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/__init__.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/__init__.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/climate.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/data_point.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/operating_voltage_level.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/support.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/__init__.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/climate.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/const.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/cover.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/definition.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/light.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/lock.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/siren.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/support.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/switch.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/valve.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/data_point.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/event.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/__init__.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/action.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/binary_sensor.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/button.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/data_point.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/number.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/select.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/sensor.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/switch.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/text.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/__init__.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/binary_sensor.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/button.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/data_point.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/number.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/select.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/sensor.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/switch.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/text.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/update.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/property_decorators.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/py.typed +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/fetch_all_device_data.fn +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/get_program_descriptions.fn +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/get_serial.fn +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/get_system_variable_descriptions.fn +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/set_program_state.fn +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/set_system_variable.fn +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/store/__init__.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/store/dynamic.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/store/persistent.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/store/visibility.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/validator.py +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic.egg-info/dependency_links.txt +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic.egg-info/top_level.txt +0 -0
- {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.20
|
|
4
4
|
Summary: Homematic interface for Home Assistant running on Python 3.
|
|
5
5
|
Home-page: https://github.com/sukramj/aiohomematic
|
|
6
6
|
Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
|
|
@@ -9,7 +9,7 @@ Project-URL: Source Code, https://github.com/sukramj/aiohomematic
|
|
|
9
9
|
Project-URL: Bug Reports, https://github.com/sukramj/aiohomematic/issues
|
|
10
10
|
Project-URL: Docs: Dev, https://github.com/sukramj/aiohomematic
|
|
11
11
|
Project-URL: Forum, https://github.com/sukramj/aiohomematic/discussions
|
|
12
|
-
Keywords: home,automation,homematic
|
|
12
|
+
Keywords: home,automation,homematic,openccu,homegear
|
|
13
13
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: End Users/Desktop
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -21,10 +21,10 @@ Classifier: Topic :: Home Automation
|
|
|
21
21
|
Requires-Python: >=3.13.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: aiohttp>=3.
|
|
25
|
-
Requires-Dist: orjson>=3.
|
|
24
|
+
Requires-Dist: aiohttp>=3.12.0
|
|
25
|
+
Requires-Dist: orjson>=3.11.0
|
|
26
26
|
Requires-Dist: python-slugify>=8.0.0
|
|
27
|
-
Requires-Dist: voluptuous>=0.
|
|
27
|
+
Requires-Dist: voluptuous>=0.15.0
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
30
|
[![releasebadge]][release]
|
|
@@ -8,9 +8,11 @@ import asyncio
|
|
|
8
8
|
from collections.abc import Callable, Collection, Coroutine
|
|
9
9
|
from concurrent.futures import ThreadPoolExecutor
|
|
10
10
|
from concurrent.futures._base import CancelledError
|
|
11
|
+
import contextlib
|
|
11
12
|
from functools import wraps
|
|
12
13
|
import logging
|
|
13
14
|
from time import monotonic
|
|
15
|
+
from types import CoroutineType
|
|
14
16
|
from typing import Any, Final, cast
|
|
15
17
|
|
|
16
18
|
from aiohomematic.const import BLOCK_LOG_TIMEOUT
|
|
@@ -100,20 +102,35 @@ class Looper:
|
|
|
100
102
|
return pending_set
|
|
101
103
|
return set()
|
|
102
104
|
|
|
103
|
-
def create_task(
|
|
104
|
-
|
|
105
|
+
def create_task(
|
|
106
|
+
self, *, target: Coroutine[Any, Any, Any] | Callable[[], Coroutine[Any, Any, Any]], name: str
|
|
107
|
+
) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Schedule a coroutine to run in the loop.
|
|
110
|
+
|
|
111
|
+
Accepts either an already-created coroutine object or a zero-argument
|
|
112
|
+
callable that returns a coroutine. The callable form defers coroutine
|
|
113
|
+
creation until inside the event loop, which avoids "was never awaited"
|
|
114
|
+
warnings if callers only inspect the parameters (e.g. in tests).
|
|
115
|
+
"""
|
|
105
116
|
try:
|
|
106
117
|
self._loop.call_soon_threadsafe(self._async_create_task, target, name)
|
|
107
118
|
except CancelledError:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
# Scheduling failed; if a coroutine object was provided, close it to
|
|
120
|
+
# avoid 'was never awaited' warnings.
|
|
121
|
+
if asyncio.iscoroutine(target):
|
|
122
|
+
with contextlib.suppress(Exception):
|
|
123
|
+
cast(CoroutineType, target).close()
|
|
124
|
+
_LOGGER.debug("create_task: task cancelled for %s", name)
|
|
112
125
|
return
|
|
113
126
|
|
|
114
|
-
def _async_create_task[R](
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
def _async_create_task[R]( # kwonly: disable
|
|
128
|
+
self, target: Coroutine[Any, Any, R] | Callable[[], Coroutine[Any, Any, R]], name: str
|
|
129
|
+
) -> asyncio.Task[R]:
|
|
130
|
+
"""Create a task from within the event loop. Must be run in the event loop."""
|
|
131
|
+
# If target is a callable, call it here to create the coroutine inside the loop
|
|
132
|
+
coro: Coroutine[Any, Any, R] = target if asyncio.iscoroutine(target) else target()
|
|
133
|
+
task = self._loop.create_task(coro, name=name)
|
|
117
134
|
self._tasks.add(task)
|
|
118
135
|
task.add_done_callback(self._tasks.remove)
|
|
119
136
|
return task
|
|
@@ -110,6 +110,7 @@ from aiohomematic.const import (
|
|
|
110
110
|
DEVICE_FIRMWARE_CHECK_INTERVAL,
|
|
111
111
|
DEVICE_FIRMWARE_DELIVERING_CHECK_INTERVAL,
|
|
112
112
|
DEVICE_FIRMWARE_UPDATING_CHECK_INTERVAL,
|
|
113
|
+
IDENTIFIER_SEPARATOR,
|
|
113
114
|
IGNORE_FOR_UN_IGNORE_PARAMETERS,
|
|
114
115
|
IP_ANY_V4,
|
|
115
116
|
LOCAL_HOST,
|
|
@@ -173,12 +174,16 @@ from aiohomematic.store import (
|
|
|
173
174
|
from aiohomematic.support import (
|
|
174
175
|
LogContextMixin,
|
|
175
176
|
PayloadMixin,
|
|
176
|
-
|
|
177
|
+
check_or_create_directory,
|
|
178
|
+
check_password,
|
|
177
179
|
extract_device_addresses_from_device_descriptions,
|
|
178
180
|
extract_exc_args,
|
|
179
181
|
get_channel_no,
|
|
180
182
|
get_device_address,
|
|
181
183
|
get_ip_addr,
|
|
184
|
+
is_hostname,
|
|
185
|
+
is_ipv4_address,
|
|
186
|
+
is_port,
|
|
182
187
|
)
|
|
183
188
|
|
|
184
189
|
__all__ = ["CentralConfig", "CentralUnit", "INTERFACE_EVENT_SCHEMA"]
|
|
@@ -2219,6 +2224,55 @@ class CentralConnectionState:
|
|
|
2219
2224
|
)
|
|
2220
2225
|
|
|
2221
2226
|
|
|
2227
|
+
def check_config(
|
|
2228
|
+
*,
|
|
2229
|
+
central_name: str,
|
|
2230
|
+
host: str,
|
|
2231
|
+
username: str,
|
|
2232
|
+
password: str,
|
|
2233
|
+
storage_directory: str,
|
|
2234
|
+
callback_host: str | None,
|
|
2235
|
+
callback_port_xml_rpc: int | None,
|
|
2236
|
+
json_port: int | None,
|
|
2237
|
+
interface_configs: AbstractSet[hmcl.InterfaceConfig] | None = None,
|
|
2238
|
+
) -> list[str]:
|
|
2239
|
+
"""Check config. Throws BaseHomematicException on failure."""
|
|
2240
|
+
config_failures: list[str] = []
|
|
2241
|
+
if central_name and IDENTIFIER_SEPARATOR in central_name:
|
|
2242
|
+
config_failures.append(f"Instance name must not contain {IDENTIFIER_SEPARATOR}")
|
|
2243
|
+
|
|
2244
|
+
if not (is_hostname(hostname=host) or is_ipv4_address(address=host)):
|
|
2245
|
+
config_failures.append("Invalid hostname or ipv4 address")
|
|
2246
|
+
if not username:
|
|
2247
|
+
config_failures.append("Username must not be empty")
|
|
2248
|
+
if not password:
|
|
2249
|
+
config_failures.append("Password is required")
|
|
2250
|
+
if not check_password(password=password):
|
|
2251
|
+
config_failures.append("Password is not valid")
|
|
2252
|
+
try:
|
|
2253
|
+
check_or_create_directory(directory=storage_directory)
|
|
2254
|
+
except BaseHomematicException as bhexc:
|
|
2255
|
+
config_failures.append(extract_exc_args(exc=bhexc)[0])
|
|
2256
|
+
if callback_host and not (is_hostname(hostname=callback_host) or is_ipv4_address(address=callback_host)):
|
|
2257
|
+
config_failures.append("Invalid callback hostname or ipv4 address")
|
|
2258
|
+
if callback_port_xml_rpc and not is_port(port=callback_port_xml_rpc):
|
|
2259
|
+
config_failures.append("Invalid xml rpc callback port")
|
|
2260
|
+
if json_port and not is_port(port=json_port):
|
|
2261
|
+
config_failures.append("Invalid json port")
|
|
2262
|
+
if interface_configs and not _has_primary_client(interface_configs=interface_configs):
|
|
2263
|
+
config_failures.append(f"No primary interface ({', '.join(PRIMARY_CLIENT_CANDIDATE_INTERFACES)}) defined")
|
|
2264
|
+
|
|
2265
|
+
return config_failures
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
def _has_primary_client(*, interface_configs: AbstractSet[hmcl.InterfaceConfig]) -> bool:
|
|
2269
|
+
"""Check if all configured clients exists in central."""
|
|
2270
|
+
for interface_config in interface_configs:
|
|
2271
|
+
if interface_config.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES:
|
|
2272
|
+
return True
|
|
2273
|
+
return False
|
|
2274
|
+
|
|
2275
|
+
|
|
2222
2276
|
def _get_new_data_points(
|
|
2223
2277
|
*,
|
|
2224
2278
|
new_devices: set[Device],
|
|
@@ -58,7 +58,8 @@ from aiohomematic import central as hmcu
|
|
|
58
58
|
from aiohomematic.client.json_rpc import AioJsonRpcAioHttpClient
|
|
59
59
|
from aiohomematic.client.rpc_proxy import AioXmlRpcProxy, BaseRpcProxy
|
|
60
60
|
from aiohomematic.const import (
|
|
61
|
-
|
|
61
|
+
CALLBACK_WARN_ARM_INTERVAL,
|
|
62
|
+
CALLBACK_WARN_DISARM_INTERVAL,
|
|
62
63
|
DATETIME_FORMAT_MILLIS,
|
|
63
64
|
DEFAULT_MAX_WORKERS,
|
|
64
65
|
DP_KEY_VALUE,
|
|
@@ -145,7 +146,6 @@ class Client(ABC, LogContextMixin):
|
|
|
145
146
|
self._last_value_send_cache = CommandCache(interface_id=client_config.interface_id)
|
|
146
147
|
self._available: bool = True
|
|
147
148
|
self._connection_error_count: int = 0
|
|
148
|
-
self._is_callback_alive: bool = True
|
|
149
149
|
self._is_initialized: bool = False
|
|
150
150
|
self._ping_pong_cache: Final = PingPongCache(
|
|
151
151
|
central=client_config.central, interface_id=client_config.interface_id
|
|
@@ -395,7 +395,7 @@ class Client(ABC, LogContextMixin):
|
|
|
395
395
|
return False
|
|
396
396
|
if not self.supports_push_updates:
|
|
397
397
|
return True
|
|
398
|
-
return (datetime.now() - self.modified_at).total_seconds() <
|
|
398
|
+
return (datetime.now() - self.modified_at).total_seconds() < CALLBACK_WARN_ARM_INTERVAL
|
|
399
399
|
|
|
400
400
|
def is_callback_alive(self) -> bool:
|
|
401
401
|
"""Return if XmlRPC-Server is alive based on received events for this client."""
|
|
@@ -404,31 +404,29 @@ class Client(ABC, LogContextMixin):
|
|
|
404
404
|
if (
|
|
405
405
|
last_events_dt := self.central.get_last_event_seen_for_interface(interface_id=self.interface_id)
|
|
406
406
|
) is not None:
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
407
|
+
if (
|
|
408
|
+
seconds_since_last_event := (datetime.now() - last_events_dt).total_seconds()
|
|
409
|
+
) > CALLBACK_WARN_ARM_INTERVAL:
|
|
410
|
+
self.central.fire_interface_event(
|
|
411
|
+
interface_id=self.interface_id,
|
|
412
|
+
interface_event_type=InterfaceEventType.CALLBACK,
|
|
413
|
+
data={
|
|
414
|
+
EventKey.AVAILABLE: False,
|
|
415
|
+
EventKey.SECONDS_SINCE_LAST_EVENT: int(seconds_since_last_event),
|
|
416
|
+
},
|
|
417
|
+
)
|
|
418
418
|
_LOGGER.warning(
|
|
419
419
|
"IS_CALLBACK_ALIVE: Callback for %s has not received events for %is",
|
|
420
420
|
self.interface_id,
|
|
421
421
|
seconds_since_last_event,
|
|
422
422
|
)
|
|
423
423
|
return False
|
|
424
|
-
|
|
425
|
-
if not self._is_callback_alive:
|
|
424
|
+
if ((datetime.now() - last_events_dt).total_seconds()) < CALLBACK_WARN_DISARM_INTERVAL:
|
|
426
425
|
self.central.fire_interface_event(
|
|
427
426
|
interface_id=self.interface_id,
|
|
428
427
|
interface_event_type=InterfaceEventType.CALLBACK,
|
|
429
428
|
data={EventKey.AVAILABLE: True},
|
|
430
429
|
)
|
|
431
|
-
self._is_callback_alive = True
|
|
432
430
|
return True
|
|
433
431
|
|
|
434
432
|
@abstractmethod
|
|
@@ -1510,8 +1508,13 @@ class ClientJsonCCU(ClientCCU):
|
|
|
1510
1508
|
return True
|
|
1511
1509
|
|
|
1512
1510
|
|
|
1513
|
-
class ClientHomegear(
|
|
1514
|
-
"""
|
|
1511
|
+
class ClientHomegear(ClientCCU):
|
|
1512
|
+
"""
|
|
1513
|
+
Client implementation for Homegear backend.
|
|
1514
|
+
|
|
1515
|
+
Inherit from ClientCCU to share common behavior used by tests and code paths
|
|
1516
|
+
that expect a CCU-like client interface for Homegear selections.
|
|
1517
|
+
"""
|
|
1515
1518
|
|
|
1516
1519
|
@property
|
|
1517
1520
|
def model(self) -> str:
|
|
@@ -1525,7 +1528,7 @@ class ClientHomegear(Client):
|
|
|
1525
1528
|
"""Return the supports_ping_pong info of the backend."""
|
|
1526
1529
|
return False
|
|
1527
1530
|
|
|
1528
|
-
@inspector(re_raise=False)
|
|
1531
|
+
@inspector(re_raise=False, measure_performance=True)
|
|
1529
1532
|
async def fetch_all_device_data(self) -> None:
|
|
1530
1533
|
"""Fetch all device data from the backend."""
|
|
1531
1534
|
return
|
|
@@ -483,18 +483,6 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
|
|
|
483
483
|
)
|
|
484
484
|
raise
|
|
485
485
|
|
|
486
|
-
except ClientConnectorError as cceerr:
|
|
487
|
-
self.clear_session()
|
|
488
|
-
message = f"ClientConnectorError[{cceerr}]"
|
|
489
|
-
log_boundary_error(
|
|
490
|
-
logger=_LOGGER,
|
|
491
|
-
boundary="json-rpc",
|
|
492
|
-
action=str(method),
|
|
493
|
-
err=cceerr,
|
|
494
|
-
level=logging.ERROR,
|
|
495
|
-
log_context=self.log_context,
|
|
496
|
-
)
|
|
497
|
-
raise ClientException(message) from cceerr
|
|
498
486
|
except ClientConnectorCertificateError as cccerr:
|
|
499
487
|
self.clear_session()
|
|
500
488
|
message = f"ClientConnectorCertificateError[{cccerr}]"
|
|
@@ -512,6 +500,18 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
|
|
|
512
500
|
log_context=self.log_context,
|
|
513
501
|
)
|
|
514
502
|
raise ClientException(message) from cccerr
|
|
503
|
+
except ClientConnectorError as cceerr:
|
|
504
|
+
self.clear_session()
|
|
505
|
+
message = f"ClientConnectorError[{cceerr}]"
|
|
506
|
+
log_boundary_error(
|
|
507
|
+
logger=_LOGGER,
|
|
508
|
+
boundary="json-rpc",
|
|
509
|
+
action=str(method),
|
|
510
|
+
err=cceerr,
|
|
511
|
+
level=logging.ERROR,
|
|
512
|
+
log_context=self.log_context,
|
|
513
|
+
)
|
|
514
|
+
raise ClientException(message) from cceerr
|
|
515
515
|
except (ClientError, OSError) as err:
|
|
516
516
|
self.clear_session()
|
|
517
517
|
log_boundary_error(
|
|
@@ -1182,11 +1182,11 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
|
|
|
1182
1182
|
"""Get the supported methods of the backend."""
|
|
1183
1183
|
supported_methods: tuple[str, ...] = ()
|
|
1184
1184
|
|
|
1185
|
-
await self._login_or_renew()
|
|
1186
|
-
if not (session_id := self._session_id):
|
|
1187
|
-
raise ClientException("Error while logging in")
|
|
1188
|
-
|
|
1189
1185
|
try:
|
|
1186
|
+
await self._login_or_renew()
|
|
1187
|
+
if not (session_id := self._session_id):
|
|
1188
|
+
raise ClientException("Error while logging in")
|
|
1189
|
+
|
|
1190
1190
|
response = await self._do_post(
|
|
1191
1191
|
session_id=session_id,
|
|
1192
1192
|
method=_JsonRpcMethod.SYSTEM_LIST_METHODS,
|
|
@@ -19,7 +19,7 @@ import sys
|
|
|
19
19
|
from types import MappingProxyType
|
|
20
20
|
from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
|
|
21
21
|
|
|
22
|
-
VERSION: Final = "2025.10.
|
|
22
|
+
VERSION: Final = "2025.10.20"
|
|
23
23
|
|
|
24
24
|
# Detect test speedup mode via environment
|
|
25
25
|
_TEST_SPEEDUP: Final = (
|
|
@@ -133,7 +133,8 @@ WAIT_FOR_CALLBACK: Final[int | None] = None
|
|
|
133
133
|
SCHEDULER_NOT_STARTED_SLEEP: Final = 0.2 if _TEST_SPEEDUP else 10
|
|
134
134
|
SCHEDULER_LOOP_SLEEP: Final = 0.2 if _TEST_SPEEDUP else 5
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
CALLBACK_WARN_ARM_INTERVAL: Final = CONNECTION_CHECKER_INTERVAL * 40
|
|
137
|
+
CALLBACK_WARN_DISARM_INTERVAL: Final = CONNECTION_CHECKER_INTERVAL * 20
|
|
137
138
|
|
|
138
139
|
# Path
|
|
139
140
|
PROGRAM_SET_PATH_ROOT: Final = "program/set"
|
|
@@ -204,12 +204,12 @@ class CustomDataPoint(BaseDataPoint):
|
|
|
204
204
|
def _init_data_points(self) -> None:
|
|
205
205
|
"""Init data point collection."""
|
|
206
206
|
# Add repeating fields
|
|
207
|
-
for field_name, parameter in self._device_def.get(
|
|
207
|
+
for field_name, parameter in self._device_def.get(CDPD.REPEATABLE_FIELDS, {}).items():
|
|
208
208
|
if dp := self._device.get_generic_data_point(channel_address=self._channel.address, parameter=parameter):
|
|
209
209
|
self._add_data_point(field=field_name, data_point=dp, is_visible=False)
|
|
210
210
|
|
|
211
211
|
# Add visible repeating fields
|
|
212
|
-
for field_name, parameter in self._device_def.get(
|
|
212
|
+
for field_name, parameter in self._device_def.get(CDPD.VISIBLE_REPEATABLE_FIELDS, {}).items():
|
|
213
213
|
if dp := self._device.get_generic_data_point(channel_address=self._channel.address, parameter=parameter):
|
|
214
214
|
self._add_data_point(field=field_name, data_point=dp, is_visible=True)
|
|
215
215
|
|
|
@@ -229,11 +229,11 @@ class CustomDataPoint(BaseDataPoint):
|
|
|
229
229
|
|
|
230
230
|
# Add device fields
|
|
231
231
|
self._add_data_points(
|
|
232
|
-
field_dict_name=
|
|
232
|
+
field_dict_name=CDPD.FIELDS,
|
|
233
233
|
)
|
|
234
234
|
# Add visible device fields
|
|
235
235
|
self._add_data_points(
|
|
236
|
-
field_dict_name=
|
|
236
|
+
field_dict_name=CDPD.VISIBLE_FIELDS,
|
|
237
237
|
is_visible=True,
|
|
238
238
|
)
|
|
239
239
|
|
|
@@ -243,7 +243,7 @@ class CustomDataPoint(BaseDataPoint):
|
|
|
243
243
|
if hmed.get_include_default_data_points(device_profile=self._device_profile):
|
|
244
244
|
self._mark_data_points(custom_data_point_def=hmed.get_default_data_points())
|
|
245
245
|
|
|
246
|
-
def _add_data_points(self, *, field_dict_name:
|
|
246
|
+
def _add_data_points(self, *, field_dict_name: CDPD, is_visible: bool | None = None) -> None:
|
|
247
247
|
"""Add data points to custom data point."""
|
|
248
248
|
fields = self._device_def.get(field_dict_name, {})
|
|
249
249
|
for channel_no, channel in fields.items():
|
|
@@ -640,7 +640,7 @@ class Device(LogContextMixin, PayloadMixin):
|
|
|
640
640
|
await self._central.refresh_firmware_data(device_address=self._address)
|
|
641
641
|
|
|
642
642
|
if refresh_after_update_intervals:
|
|
643
|
-
self._central.looper.create_task(target=refresh_data
|
|
643
|
+
self._central.looper.create_task(target=refresh_data, name="refresh_firmware_data")
|
|
644
644
|
|
|
645
645
|
return update_result
|
|
646
646
|
|
|
@@ -32,7 +32,7 @@ from aiohomematic.const import (
|
|
|
32
32
|
ParameterType,
|
|
33
33
|
)
|
|
34
34
|
from aiohomematic.model import device as hmd
|
|
35
|
-
from aiohomematic.model.custom import
|
|
35
|
+
from aiohomematic.model.custom.const import CDPD
|
|
36
36
|
from aiohomematic.support import to_bool
|
|
37
37
|
|
|
38
38
|
__all__ = [
|
|
@@ -565,7 +565,7 @@ def check_channel_is_the_only_primary_channel(
|
|
|
565
565
|
device_has_multiple_channels: bool,
|
|
566
566
|
) -> bool:
|
|
567
567
|
"""Check if this channel is the only primary channel."""
|
|
568
|
-
primary_channel: int = device_def[
|
|
568
|
+
primary_channel: int = device_def[CDPD.PRIMARY_CHANNEL]
|
|
569
569
|
return bool(primary_channel == current_channel_no and device_has_multiple_channels is False)
|
|
570
570
|
|
|
571
571
|
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import base64
|
|
12
12
|
from collections import defaultdict
|
|
13
|
-
from collections.abc import Callable, Collection, Mapping
|
|
13
|
+
from collections.abc import Callable, Collection, Mapping
|
|
14
14
|
import contextlib
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from datetime import datetime
|
|
@@ -30,7 +30,6 @@ from typing import Any, Final, cast
|
|
|
30
30
|
|
|
31
31
|
import orjson
|
|
32
32
|
|
|
33
|
-
from aiohomematic import client as hmcl
|
|
34
33
|
from aiohomematic.const import (
|
|
35
34
|
ADDRESS_SEPARATOR,
|
|
36
35
|
ALLOWED_HOSTNAME_PATTERN,
|
|
@@ -38,12 +37,10 @@ from aiohomematic.const import (
|
|
|
38
37
|
CHANNEL_ADDRESS_PATTERN,
|
|
39
38
|
DEVICE_ADDRESS_PATTERN,
|
|
40
39
|
HTMLTAG_PATTERN,
|
|
41
|
-
IDENTIFIER_SEPARATOR,
|
|
42
40
|
INIT_DATETIME,
|
|
43
41
|
ISO_8859_1,
|
|
44
42
|
MAX_CACHE_AGE,
|
|
45
43
|
NO_CACHE_ENTRY,
|
|
46
|
-
PRIMARY_CLIENT_CANDIDATE_INTERFACES,
|
|
47
44
|
TIMEOUT,
|
|
48
45
|
UTF_8,
|
|
49
46
|
CommandRxMode,
|
|
@@ -95,55 +92,6 @@ def build_xml_rpc_headers(
|
|
|
95
92
|
return [("Authorization", f"Basic {base64_message}")]
|
|
96
93
|
|
|
97
94
|
|
|
98
|
-
def check_config(
|
|
99
|
-
*,
|
|
100
|
-
central_name: str,
|
|
101
|
-
host: str,
|
|
102
|
-
username: str,
|
|
103
|
-
password: str,
|
|
104
|
-
storage_directory: str,
|
|
105
|
-
callback_host: str | None,
|
|
106
|
-
callback_port_xml_rpc: int | None,
|
|
107
|
-
json_port: int | None,
|
|
108
|
-
interface_configs: AbstractSet[hmcl.InterfaceConfig] | None = None,
|
|
109
|
-
) -> list[str]:
|
|
110
|
-
"""Check config. Throws BaseHomematicException on failure."""
|
|
111
|
-
config_failures: list[str] = []
|
|
112
|
-
if central_name and IDENTIFIER_SEPARATOR in central_name:
|
|
113
|
-
config_failures.append(f"Instance name must not contain {IDENTIFIER_SEPARATOR}")
|
|
114
|
-
|
|
115
|
-
if not (is_hostname(hostname=host) or is_ipv4_address(address=host)):
|
|
116
|
-
config_failures.append("Invalid hostname or ipv4 address")
|
|
117
|
-
if not username:
|
|
118
|
-
config_failures.append("Username must not be empty")
|
|
119
|
-
if not password:
|
|
120
|
-
config_failures.append("Password is required")
|
|
121
|
-
if not check_password(password=password):
|
|
122
|
-
config_failures.append("Password is not valid")
|
|
123
|
-
try:
|
|
124
|
-
check_or_create_directory(directory=storage_directory)
|
|
125
|
-
except BaseHomematicException as bhexc:
|
|
126
|
-
config_failures.append(extract_exc_args(exc=bhexc)[0])
|
|
127
|
-
if callback_host and not (is_hostname(hostname=callback_host) or is_ipv4_address(address=callback_host)):
|
|
128
|
-
config_failures.append("Invalid callback hostname or ipv4 address")
|
|
129
|
-
if callback_port_xml_rpc and not is_port(port=callback_port_xml_rpc):
|
|
130
|
-
config_failures.append("Invalid xml rpc callback port")
|
|
131
|
-
if json_port and not is_port(port=json_port):
|
|
132
|
-
config_failures.append("Invalid json port")
|
|
133
|
-
if interface_configs and not has_primary_client(interface_configs=interface_configs):
|
|
134
|
-
config_failures.append(f"No primary interface ({', '.join(PRIMARY_CLIENT_CANDIDATE_INTERFACES)}) defined")
|
|
135
|
-
|
|
136
|
-
return config_failures
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def has_primary_client(*, interface_configs: AbstractSet[hmcl.InterfaceConfig]) -> bool:
|
|
140
|
-
"""Check if all configured clients exists in central."""
|
|
141
|
-
for interface_config in interface_configs:
|
|
142
|
-
if interface_config.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES:
|
|
143
|
-
return True
|
|
144
|
-
return False
|
|
145
|
-
|
|
146
|
-
|
|
147
95
|
def delete_file(directory: str, file_name: str) -> None: # kwonly: disable
|
|
148
96
|
"""Delete the file. File can contain a wildcard."""
|
|
149
97
|
if os.path.exists(directory):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.20
|
|
4
4
|
Summary: Homematic interface for Home Assistant running on Python 3.
|
|
5
5
|
Home-page: https://github.com/sukramj/aiohomematic
|
|
6
6
|
Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
|
|
@@ -9,7 +9,7 @@ Project-URL: Source Code, https://github.com/sukramj/aiohomematic
|
|
|
9
9
|
Project-URL: Bug Reports, https://github.com/sukramj/aiohomematic/issues
|
|
10
10
|
Project-URL: Docs: Dev, https://github.com/sukramj/aiohomematic
|
|
11
11
|
Project-URL: Forum, https://github.com/sukramj/aiohomematic/discussions
|
|
12
|
-
Keywords: home,automation,homematic
|
|
12
|
+
Keywords: home,automation,homematic,openccu,homegear
|
|
13
13
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: End Users/Desktop
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
@@ -21,10 +21,10 @@ Classifier: Topic :: Home Automation
|
|
|
21
21
|
Requires-Python: >=3.13.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: aiohttp>=3.
|
|
25
|
-
Requires-Dist: orjson>=3.
|
|
24
|
+
Requires-Dist: aiohttp>=3.12.0
|
|
25
|
+
Requires-Dist: orjson>=3.11.0
|
|
26
26
|
Requires-Dist: python-slugify>=8.0.0
|
|
27
|
-
Requires-Dist: voluptuous>=0.
|
|
27
|
+
Requires-Dist: voluptuous>=0.15.0
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
30
|
[![releasebadge]][release]
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "aiohomematic"
|
|
7
|
-
dynamic = ["version"]
|
|
7
|
+
dynamic = ["version", "dependencies"]
|
|
8
8
|
license = {text = "MIT License"}
|
|
9
9
|
description = "Homematic interface for Home Assistant running on Python 3."
|
|
10
10
|
readme = "README.md"
|
|
@@ -12,7 +12,7 @@ authors = [
|
|
|
12
12
|
{name = "SukramJ", email = "sukramj@icloud.com"},
|
|
13
13
|
{name = "Daniel Perna", email = "danielperna84@gmail.com"},
|
|
14
14
|
]
|
|
15
|
-
keywords = ["home", "automation", "homematic"]
|
|
15
|
+
keywords = ["home", "automation", "homematic", "openccu", "homegear"]
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 5 - Production/Stable",
|
|
18
18
|
"Intended Audience :: End Users/Desktop",
|
|
@@ -24,12 +24,6 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Home Automation",
|
|
25
25
|
]
|
|
26
26
|
requires-python = ">=3.13.0"
|
|
27
|
-
dependencies = [
|
|
28
|
-
"aiohttp>=3.10.0",
|
|
29
|
-
"orjson>=3.10.0",
|
|
30
|
-
"python-slugify>=8.0.0",
|
|
31
|
-
"voluptuous>=0.14.0",
|
|
32
|
-
]
|
|
33
27
|
|
|
34
28
|
[project.urls]
|
|
35
29
|
"Source Code" = "https://github.com/sukramj/aiohomematic"
|
|
@@ -45,6 +39,7 @@ include-package-data = true
|
|
|
45
39
|
|
|
46
40
|
[tool.setuptools.dynamic]
|
|
47
41
|
version = {attr = "aiohomematic.const.VERSION"}
|
|
42
|
+
dependencies = { file = ["requirements.txt"] }
|
|
48
43
|
|
|
49
44
|
[tool.setuptools.packages.find]
|
|
50
45
|
include = ["aiohomematic", "aiohomematic.*"]
|
|
@@ -131,6 +126,7 @@ disable = [
|
|
|
131
126
|
"cyclic-import",
|
|
132
127
|
"duplicate-code",
|
|
133
128
|
"inconsistent-return-statements",
|
|
129
|
+
"import-outside-toplevel", # C0415
|
|
134
130
|
"locally-disabled",
|
|
135
131
|
"not-context-manager",
|
|
136
132
|
"too-few-public-methods",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/__init__.py
RENAMED
|
File without changes
|
{aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/climate.py
RENAMED
|
File without changes
|
{aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/data_point.py
RENAMED
|
File without changes
|
|
File without changes
|
{aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/support.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/binary_sensor.py
RENAMED
|
File without changes
|
|
File without changes
|
{aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/data_point.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/set_program_state.fn
RENAMED
|
File without changes
|
{aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/set_system_variable.fn
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|