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.

Files changed (84) hide show
  1. {aiohomematic-2025.10.18/aiohomematic.egg-info → aiohomematic-2025.10.20}/PKG-INFO +5 -5
  2. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/async_support.py +26 -9
  3. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/central/__init__.py +55 -1
  4. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/client/__init__.py +23 -20
  5. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/client/json_rpc.py +16 -16
  6. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/const.py +3 -2
  7. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/data_point.py +5 -5
  8. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/device.py +1 -1
  9. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/support.py +2 -2
  10. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/support.py +1 -53
  11. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20/aiohomematic.egg-info}/PKG-INFO +5 -5
  12. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic.egg-info/SOURCES.txt +1 -0
  13. aiohomematic-2025.10.20/aiohomematic.egg-info/requires.txt +4 -0
  14. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/pyproject.toml +4 -8
  15. aiohomematic-2025.10.20/requirements.txt +4 -0
  16. aiohomematic-2025.10.18/aiohomematic.egg-info/requires.txt +0 -4
  17. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/LICENSE +0 -0
  18. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/MANIFEST.in +0 -0
  19. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/README.md +0 -0
  20. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/__init__.py +0 -0
  21. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/central/decorators.py +0 -0
  22. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/central/rpc_server.py +0 -0
  23. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/client/_rpc_errors.py +0 -0
  24. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/client/rpc_proxy.py +0 -0
  25. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/context.py +0 -0
  26. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/converter.py +0 -0
  27. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/decorators.py +0 -0
  28. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/exceptions.py +0 -0
  29. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/hmcli.py +0 -0
  30. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/__init__.py +0 -0
  31. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/__init__.py +0 -0
  32. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/climate.py +0 -0
  33. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/data_point.py +0 -0
  34. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/operating_voltage_level.py +0 -0
  35. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/calculated/support.py +0 -0
  36. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/__init__.py +0 -0
  37. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/climate.py +0 -0
  38. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/const.py +0 -0
  39. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/cover.py +0 -0
  40. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/definition.py +0 -0
  41. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/light.py +0 -0
  42. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/lock.py +0 -0
  43. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/siren.py +0 -0
  44. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/support.py +0 -0
  45. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/switch.py +0 -0
  46. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/custom/valve.py +0 -0
  47. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/data_point.py +0 -0
  48. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/event.py +0 -0
  49. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/__init__.py +0 -0
  50. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/action.py +0 -0
  51. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/binary_sensor.py +0 -0
  52. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/button.py +0 -0
  53. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/data_point.py +0 -0
  54. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/number.py +0 -0
  55. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/select.py +0 -0
  56. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/sensor.py +0 -0
  57. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/switch.py +0 -0
  58. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/generic/text.py +0 -0
  59. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/__init__.py +0 -0
  60. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/binary_sensor.py +0 -0
  61. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/button.py +0 -0
  62. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/data_point.py +0 -0
  63. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/number.py +0 -0
  64. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/select.py +0 -0
  65. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/sensor.py +0 -0
  66. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/switch.py +0 -0
  67. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/hub/text.py +0 -0
  68. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/model/update.py +0 -0
  69. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/property_decorators.py +0 -0
  70. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/py.typed +0 -0
  71. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/fetch_all_device_data.fn +0 -0
  72. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/get_program_descriptions.fn +0 -0
  73. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/get_serial.fn +0 -0
  74. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/get_system_variable_descriptions.fn +0 -0
  75. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/set_program_state.fn +0 -0
  76. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/rega_scripts/set_system_variable.fn +0 -0
  77. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/store/__init__.py +0 -0
  78. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/store/dynamic.py +0 -0
  79. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/store/persistent.py +0 -0
  80. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/store/visibility.py +0 -0
  81. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic/validator.py +0 -0
  82. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic.egg-info/dependency_links.txt +0 -0
  83. {aiohomematic-2025.10.18 → aiohomematic-2025.10.20}/aiohomematic.egg-info/top_level.txt +0 -0
  84. {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.18
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.10.0
25
- Requires-Dist: orjson>=3.10.0
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.14.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(self, *, target: Coroutine[Any, Any, Any], name: str) -> None:
104
- """Add task to the executor pool."""
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
- _LOGGER.debug(
109
- "create_task: task cancelled for %s",
110
- name,
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](self, target: Coroutine[Any, Any, R], name: str) -> asyncio.Task[R]: # kwonly: disable
115
- """Create a task from within the event_loop. This method must be run in the event_loop."""
116
- task = self._loop.create_task(target, name=name)
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
- check_config,
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
- CALLBACK_WARN_INTERVAL,
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() < CALLBACK_WARN_INTERVAL
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 (seconds_since_last_event := (datetime.now() - last_events_dt).total_seconds()) > CALLBACK_WARN_INTERVAL:
408
- if self._is_callback_alive:
409
- self.central.fire_interface_event(
410
- interface_id=self.interface_id,
411
- interface_event_type=InterfaceEventType.CALLBACK,
412
- data={
413
- EventKey.AVAILABLE: False,
414
- EventKey.SECONDS_SINCE_LAST_EVENT: int(seconds_since_last_event),
415
- },
416
- )
417
- self._is_callback_alive = False
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(Client):
1514
- """Client implementation for Homegear backend."""
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.18"
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
- CALLBACK_WARN_INTERVAL: Final = CONNECTION_CHECKER_INTERVAL * 40
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(hmed.CDPD.REPEATABLE_FIELDS, {}).items():
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(hmed.CDPD.VISIBLE_REPEATABLE_FIELDS, {}).items():
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=hmed.CDPD.FIELDS,
232
+ field_dict_name=CDPD.FIELDS,
233
233
  )
234
234
  # Add visible device fields
235
235
  self._add_data_points(
236
- field_dict_name=hmed.CDPD.VISIBLE_FIELDS,
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: hmed.CDPD, is_visible: bool | None = None) -> None:
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(), name="refresh_firmware_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 definition as hmed
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[hmed.CDPD.PRIMARY_CHANNEL]
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, Set as AbstractSet
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.18
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.10.0
25
- Requires-Dist: orjson>=3.10.0
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.14.0
27
+ Requires-Dist: voluptuous>=0.15.0
28
28
  Dynamic: license-file
29
29
 
30
30
  [![releasebadge]][release]
@@ -2,6 +2,7 @@ LICENSE
2
2
  MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
+ requirements.txt
5
6
  setup.cfg
6
7
  aiohomematic/__init__.py
7
8
  aiohomematic/async_support.py
@@ -0,0 +1,4 @@
1
+ aiohttp>=3.12.0
2
+ orjson>=3.11.0
3
+ python-slugify>=8.0.0
4
+ voluptuous>=0.15.0
@@ -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",
@@ -0,0 +1,4 @@
1
+ aiohttp>=3.12.0
2
+ orjson>=3.11.0
3
+ python-slugify>=8.0.0
4
+ voluptuous>=0.15.0
@@ -1,4 +0,0 @@
1
- aiohttp>=3.10.0
2
- orjson>=3.10.0
3
- python-slugify>=8.0.0
4
- voluptuous>=0.14.0