aiohomematic 2025.8.9__tar.gz → 2025.8.10__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.
Files changed (111) hide show
  1. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/LICENSE +1 -1
  2. aiohomematic-2025.8.10/PKG-INFO +124 -0
  3. aiohomematic-2025.8.10/README.md +96 -0
  4. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/__init__.py +15 -1
  5. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/async_support.py +15 -2
  6. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/caches/__init__.py +2 -0
  7. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/caches/dynamic.py +2 -0
  8. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/caches/persistent.py +2 -0
  9. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/caches/visibility.py +2 -0
  10. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/central/__init__.py +43 -18
  11. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/central/decorators.py +60 -15
  12. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/central/xml_rpc_server.py +15 -1
  13. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/client/__init__.py +2 -0
  14. aiohomematic-2025.8.10/aiohomematic/client/_rpc_errors.py +81 -0
  15. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/client/json_rpc.py +68 -19
  16. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/client/xml_rpc.py +15 -8
  17. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/const.py +44 -3
  18. aiohomematic-2025.8.10/aiohomematic/context.py +18 -0
  19. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/converter.py +27 -1
  20. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/decorators.py +88 -19
  21. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/exceptions.py +19 -1
  22. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/hmcli.py +13 -1
  23. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/__init__.py +2 -0
  24. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/calculated/__init__.py +2 -0
  25. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/calculated/climate.py +2 -0
  26. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/calculated/data_point.py +2 -0
  27. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/calculated/operating_voltage_level.py +2 -0
  28. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/calculated/support.py +2 -0
  29. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/__init__.py +2 -0
  30. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/climate.py +3 -1
  31. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/const.py +2 -0
  32. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/cover.py +30 -2
  33. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/data_point.py +2 -0
  34. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/definition.py +2 -0
  35. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/light.py +18 -10
  36. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/lock.py +2 -0
  37. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/siren.py +5 -2
  38. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/support.py +2 -0
  39. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/switch.py +2 -0
  40. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/custom/valve.py +2 -0
  41. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/data_point.py +15 -3
  42. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/decorators.py +29 -8
  43. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/device.py +2 -0
  44. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/event.py +2 -0
  45. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/__init__.py +2 -0
  46. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/action.py +2 -0
  47. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/binary_sensor.py +2 -0
  48. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/button.py +2 -0
  49. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/data_point.py +4 -1
  50. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/number.py +4 -1
  51. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/select.py +4 -1
  52. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/sensor.py +2 -0
  53. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/switch.py +2 -0
  54. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/generic/text.py +2 -0
  55. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/hub/__init__.py +2 -0
  56. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/hub/binary_sensor.py +2 -0
  57. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/hub/button.py +2 -0
  58. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/hub/data_point.py +2 -0
  59. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/hub/number.py +2 -0
  60. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/hub/select.py +2 -0
  61. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/hub/sensor.py +2 -0
  62. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/hub/switch.py +2 -0
  63. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/hub/text.py +2 -0
  64. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/support.py +26 -1
  65. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/model/update.py +2 -0
  66. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/support.py +160 -3
  67. aiohomematic-2025.8.10/aiohomematic/validator.py +112 -0
  68. aiohomematic-2025.8.10/aiohomematic.egg-info/PKG-INFO +124 -0
  69. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic.egg-info/SOURCES.txt +1 -0
  70. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_central_pydevccu.py +7 -7
  71. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_siren.py +4 -2
  72. aiohomematic-2025.8.9/PKG-INFO +0 -69
  73. aiohomematic-2025.8.9/README.md +0 -41
  74. aiohomematic-2025.8.9/aiohomematic/context.py +0 -8
  75. aiohomematic-2025.8.9/aiohomematic/validator.py +0 -65
  76. aiohomematic-2025.8.9/aiohomematic.egg-info/PKG-INFO +0 -69
  77. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/py.typed +0 -0
  78. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/rega_scripts/fetch_all_device_data.fn +0 -0
  79. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/rega_scripts/get_program_descriptions.fn +0 -0
  80. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/rega_scripts/get_serial.fn +0 -0
  81. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/rega_scripts/get_system_variable_descriptions.fn +0 -0
  82. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/rega_scripts/set_program_state.fn +0 -0
  83. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic/rega_scripts/set_system_variable.fn +0 -0
  84. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic.egg-info/dependency_links.txt +0 -0
  85. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic.egg-info/requires.txt +0 -0
  86. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic.egg-info/top_level.txt +0 -0
  87. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic_support/__init__.py +0 -0
  88. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/aiohomematic_support/client_local.py +0 -0
  89. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/pyproject.toml +0 -0
  90. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/setup.cfg +0 -0
  91. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_action.py +0 -0
  92. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_binary_sensor.py +0 -0
  93. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_button.py +0 -0
  94. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_calculated_support.py +0 -0
  95. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_central.py +0 -0
  96. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_climate.py +0 -0
  97. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_cover.py +0 -0
  98. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_decorator.py +0 -0
  99. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_device.py +0 -0
  100. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_entity.py +0 -0
  101. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_event.py +0 -0
  102. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_json_rpc.py +0 -0
  103. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_light.py +0 -0
  104. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_lock.py +0 -0
  105. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_number.py +0 -0
  106. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_select.py +0 -0
  107. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_sensor.py +0 -0
  108. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_support.py +0 -0
  109. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_switch.py +0 -0
  110. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_text.py +0 -0
  111. {aiohomematic-2025.8.9 → aiohomematic-2025.8.10}/tests/test_valve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 Daniel Perna, SukramJ
3
+ Copyright (c) 2021-2025 Daniel Perna, SukramJ
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiohomematic
3
+ Version: 2025.8.10
4
+ Summary: Homematic interface for Home Assistant running on Python 3.
5
+ Home-page: https://github.com/sukramj/aiohomematic
6
+ Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
7
+ License: MIT License
8
+ Project-URL: Source Code, https://github.com/sukramj/aiohomematic
9
+ Project-URL: Bug Reports, https://github.com/sukramj/aiohomematic/issues
10
+ Project-URL: Docs: Dev, https://github.com/sukramj/aiohomematic
11
+ Project-URL: Forum, https://github.com/sukramj/aiohomematic/discussions
12
+ Keywords: home,automation,homematic
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Home Automation
20
+ Requires-Python: >=3.13.0
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: aiohttp>=3.10.0
24
+ Requires-Dist: orjson>=3.10.0
25
+ Requires-Dist: python-slugify>=8.0.0
26
+ Requires-Dist: voluptuous>=0.14.0
27
+ Dynamic: license-file
28
+
29
+ # AIO Homematic (hahomematic)
30
+
31
+ A lightweight Python 3 library that powers Home Assistant integrations for controlling and monitoring [HomeMatic](https://www.eq-3.com/products/homematic.html) and [HomematicIP](https://www.homematic-ip.com/en/start.html) devices. Some third‑party devices/gateways (e.g., Bosch, Intertechno) may be supported as well.
32
+
33
+ This project is the modern successor to [pyhomematic](https://github.com/danielperna84/pyhomematic), focusing on automatic entity creation, fewer manual device definitions, and faster startups.
34
+
35
+ ## How it works
36
+
37
+ Unlike pyhomematic, which required manual device mappings, aiohomematic automatically creates entities for each relevant parameter on every device channel (unless blacklisted). To achieve this it:
38
+
39
+ - Fetches and caches device paramsets (VALUES) for fast successive startups.
40
+ - Provides hooks for custom entity classes where complex behavior is needed (e.g., thermostats, lights, covers, climate, locks, sirens).
41
+ - Includes helpers for robust operation, such as automatic reconnection after CCU restarts.
42
+
43
+ ## Key features
44
+
45
+ - Automatic entity discovery from device/channel parameters.
46
+ - Extensible via custom entity classes for complex devices.
47
+ - Caching of paramsets to speed up restarts.
48
+ - Designed to integrate with Home Assistant.
49
+
50
+ ## Quickstart for Home Assistant
51
+
52
+ Use the Home Assistant custom integration "Homematic(IP) Local", which is powered by aiohomematic.
53
+
54
+ 1. Prerequisites
55
+ - Home Assistant 2024.6 or newer recommended.
56
+ - A CCU3, RaspberryMatic, or Homegear instance reachable from Home Assistant.
57
+ - For HomematicIP devices, ensure CCU firmware meets the minimum versions listed below.
58
+ 2. Install the integration
59
+ - Add the custom repository and install: https://github.com/sukramj/homematicip_local
60
+ - Follow the installation guide: https://github.com/sukramj/homematicip_local/wiki/Installation
61
+ 3. Configure via Home Assistant UI
62
+ - In Home Assistant: Settings → Devices & Services → Add Integration → search for "Homematic(IP) Local".
63
+ - Enter the CCU/Homegear host (IP or hostname). If you use HTTPS on the CCU, enable SSL and accept the certificate if self‑signed.
64
+ - Provide credentials if your CCU requires them.
65
+ - Choose which interfaces to enable (HM, HmIP, Virtual). Default ports are typically 2001 (HM), 2010 (HmIP), 9292 (Virtual).
66
+ 4. Network callbacks
67
+ - The integration needs to receive XML‑RPC callbacks from the CCU. Make sure Home Assistant is reachable from the CCU (no NAT/firewall blocking). The default callback port is 43439; you can adjust it in advanced options.
68
+ 5. Verify
69
+ - After setup, devices should appear under Devices & Services → Homematic(IP) Local. Discovery may take a few seconds after the first connection while paramsets are fetched and cached for faster restarts.
70
+
71
+ If you need to use aiohomematic directly in Python, see the Public API and example below.
72
+
73
+ ## Requirements
74
+
75
+ Due to a bug in earlier CCU2/CCU3 firmware, aiohomematic requires at least the following versions when used with HomematicIP devices:
76
+
77
+ - CCU2: 2.53.27
78
+ - CCU3: 3.53.26
79
+
80
+ See details here: https://github.com/jens-maus/RaspberryMatic/issues/843. Other CCU‑like platforms using the buggy HmIPServer version are not supported.
81
+
82
+ ## Public API and imports
83
+
84
+ - The public API of aiohomematic is explicitly defined via **all** in each module and subpackage.
85
+ - Backwards‑compatible imports should target these modules:
86
+ - aiohomematic.central: CentralUnit, CentralConfig and related schemas
87
+ - aiohomematic.client: Client, InterfaceConfig, create_client, get_client
88
+ - aiohomematic.model: device/data point abstractions (see subpackages for details)
89
+ - aiohomematic.exceptions: library exception types intended for consumers
90
+ - aiohomematic.const: constants and enums (stable subset; see module **all**)
91
+ - The top‑level package only exposes **version** to avoid import cycles and keep startup lean. Prefer importing from the specific submodules listed above.
92
+
93
+ Example:
94
+
95
+ from aiohomematic.central import CentralConfig
96
+ from aiohomematic import client as hmcl
97
+
98
+ cfg = CentralConfig(
99
+ central_id="ccu-main",
100
+ host="ccu.local",
101
+ username="admin",
102
+ password="secret",
103
+ default_callback_port=43439,
104
+ interface_configs={hmcl.InterfaceConfig(interface=hmcl.Interface.HMIP, port=2010, enabled=True)},
105
+ )
106
+ central = cfg.create_central()
107
+
108
+ ## Useful links
109
+
110
+ - Changelog: [see](changelog.md) for release history and latest changes.
111
+ - Definition of calculated data points: [see](docs/calculated_data_points.md)
112
+ - Homematic(IP) Local integration: https://github.com/sukramj/homematicip_local
113
+ - Input select helper: [see](docs/input_select_helper.md) for an overview of how to use the input select helper.
114
+ - Troubleshooting with Home Assistant: [see](docs/homeassistant_troubleshooting.md) for common issues and how to debug them.
115
+ - Unignore mechanism: [see](docs/unignore.md) for how to unignore devices that are ignored by default.
116
+
117
+ ## Useful developer links
118
+
119
+ - Architecture overview: [see](docs/architecture.md) for an overview of the architecture of the library.
120
+ - Data flow: [see](docs/data_flow.md) for an overview of how data flows through the library.
121
+ - Extending the model: [see](docs/extension_points.md) for adding custom device profiles and calculated data points.
122
+ - Home Assistant lifecycle (discovery, updates, teardown): [see](docs/homeassistant_lifecycle.md) for details on how the integration works and how to debug issues.
123
+ - RSSI fix: [see](docs/rssi_fix.md) for how RSSI values are fixed for Home Assistant.
124
+ - Sequence diagrams: [see](docs/sequence_diagrams.md) for a sequence diagram of how the library works.
@@ -0,0 +1,96 @@
1
+ # AIO Homematic (hahomematic)
2
+
3
+ A lightweight Python 3 library that powers Home Assistant integrations for controlling and monitoring [HomeMatic](https://www.eq-3.com/products/homematic.html) and [HomematicIP](https://www.homematic-ip.com/en/start.html) devices. Some third‑party devices/gateways (e.g., Bosch, Intertechno) may be supported as well.
4
+
5
+ This project is the modern successor to [pyhomematic](https://github.com/danielperna84/pyhomematic), focusing on automatic entity creation, fewer manual device definitions, and faster startups.
6
+
7
+ ## How it works
8
+
9
+ Unlike pyhomematic, which required manual device mappings, aiohomematic automatically creates entities for each relevant parameter on every device channel (unless blacklisted). To achieve this it:
10
+
11
+ - Fetches and caches device paramsets (VALUES) for fast successive startups.
12
+ - Provides hooks for custom entity classes where complex behavior is needed (e.g., thermostats, lights, covers, climate, locks, sirens).
13
+ - Includes helpers for robust operation, such as automatic reconnection after CCU restarts.
14
+
15
+ ## Key features
16
+
17
+ - Automatic entity discovery from device/channel parameters.
18
+ - Extensible via custom entity classes for complex devices.
19
+ - Caching of paramsets to speed up restarts.
20
+ - Designed to integrate with Home Assistant.
21
+
22
+ ## Quickstart for Home Assistant
23
+
24
+ Use the Home Assistant custom integration "Homematic(IP) Local", which is powered by aiohomematic.
25
+
26
+ 1. Prerequisites
27
+ - Home Assistant 2024.6 or newer recommended.
28
+ - A CCU3, RaspberryMatic, or Homegear instance reachable from Home Assistant.
29
+ - For HomematicIP devices, ensure CCU firmware meets the minimum versions listed below.
30
+ 2. Install the integration
31
+ - Add the custom repository and install: https://github.com/sukramj/homematicip_local
32
+ - Follow the installation guide: https://github.com/sukramj/homematicip_local/wiki/Installation
33
+ 3. Configure via Home Assistant UI
34
+ - In Home Assistant: Settings → Devices & Services → Add Integration → search for "Homematic(IP) Local".
35
+ - Enter the CCU/Homegear host (IP or hostname). If you use HTTPS on the CCU, enable SSL and accept the certificate if self‑signed.
36
+ - Provide credentials if your CCU requires them.
37
+ - Choose which interfaces to enable (HM, HmIP, Virtual). Default ports are typically 2001 (HM), 2010 (HmIP), 9292 (Virtual).
38
+ 4. Network callbacks
39
+ - The integration needs to receive XML‑RPC callbacks from the CCU. Make sure Home Assistant is reachable from the CCU (no NAT/firewall blocking). The default callback port is 43439; you can adjust it in advanced options.
40
+ 5. Verify
41
+ - After setup, devices should appear under Devices & Services → Homematic(IP) Local. Discovery may take a few seconds after the first connection while paramsets are fetched and cached for faster restarts.
42
+
43
+ If you need to use aiohomematic directly in Python, see the Public API and example below.
44
+
45
+ ## Requirements
46
+
47
+ Due to a bug in earlier CCU2/CCU3 firmware, aiohomematic requires at least the following versions when used with HomematicIP devices:
48
+
49
+ - CCU2: 2.53.27
50
+ - CCU3: 3.53.26
51
+
52
+ See details here: https://github.com/jens-maus/RaspberryMatic/issues/843. Other CCU‑like platforms using the buggy HmIPServer version are not supported.
53
+
54
+ ## Public API and imports
55
+
56
+ - The public API of aiohomematic is explicitly defined via **all** in each module and subpackage.
57
+ - Backwards‑compatible imports should target these modules:
58
+ - aiohomematic.central: CentralUnit, CentralConfig and related schemas
59
+ - aiohomematic.client: Client, InterfaceConfig, create_client, get_client
60
+ - aiohomematic.model: device/data point abstractions (see subpackages for details)
61
+ - aiohomematic.exceptions: library exception types intended for consumers
62
+ - aiohomematic.const: constants and enums (stable subset; see module **all**)
63
+ - The top‑level package only exposes **version** to avoid import cycles and keep startup lean. Prefer importing from the specific submodules listed above.
64
+
65
+ Example:
66
+
67
+ from aiohomematic.central import CentralConfig
68
+ from aiohomematic import client as hmcl
69
+
70
+ cfg = CentralConfig(
71
+ central_id="ccu-main",
72
+ host="ccu.local",
73
+ username="admin",
74
+ password="secret",
75
+ default_callback_port=43439,
76
+ interface_configs={hmcl.InterfaceConfig(interface=hmcl.Interface.HMIP, port=2010, enabled=True)},
77
+ )
78
+ central = cfg.create_central()
79
+
80
+ ## Useful links
81
+
82
+ - Changelog: [see](changelog.md) for release history and latest changes.
83
+ - Definition of calculated data points: [see](docs/calculated_data_points.md)
84
+ - Homematic(IP) Local integration: https://github.com/sukramj/homematicip_local
85
+ - Input select helper: [see](docs/input_select_helper.md) for an overview of how to use the input select helper.
86
+ - Troubleshooting with Home Assistant: [see](docs/homeassistant_troubleshooting.md) for common issues and how to debug them.
87
+ - Unignore mechanism: [see](docs/unignore.md) for how to unignore devices that are ignored by default.
88
+
89
+ ## Useful developer links
90
+
91
+ - Architecture overview: [see](docs/architecture.md) for an overview of the architecture of the library.
92
+ - Data flow: [see](docs/data_flow.md) for an overview of how data flows through the library.
93
+ - Extending the model: [see](docs/extension_points.md) for adding custom device profiles and calculated data points.
94
+ - Home Assistant lifecycle (discovery, updates, teardown): [see](docs/homeassistant_lifecycle.md) for details on how the integration works and how to debug issues.
95
+ - RSSI fix: [see](docs/rssi_fix.md) for how RSSI values are fixed for Home Assistant.
96
+ - Sequence diagrams: [see](docs/sequence_diagrams.md) for a sequence diagram of how the library works.
@@ -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__"]
@@ -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
- """Block until all pending work is done."""
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:
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025 Daniel Perna, SukramJ
1
3
  """
2
4
  Cache packages for AioHomematic.
3
5
 
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025 Daniel Perna, SukramJ
1
3
  """
2
4
  Dynamic caches used at runtime by the central unit and clients.
3
5
 
@@ -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
 
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025 Daniel Perna, SukramJ
1
3
  """
2
4
  Parameter visibility rules and cache for HomeMatic data points.
3
5
 
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025 Daniel Perna, SukramJ
1
3
  """
2
4
  Central unit and core orchestration for HomeMatic CCU and compatible backends.
3
5
 
@@ -119,6 +121,7 @@ from aiohomematic.const import (
119
121
  TIMEOUT,
120
122
  UN_IGNORE_WILDCARD,
121
123
  BackendSystemEvent,
124
+ CentralUnitState,
122
125
  DataOperationResult,
123
126
  DataPointCategory,
124
127
  DataPointKey,
@@ -163,6 +166,7 @@ from aiohomematic.support import check_config, extract_exc_args, get_channel_no,
163
166
  __all__ = ["CentralConfig", "CentralUnit", "INTERFACE_EVENT_SCHEMA"]
164
167
 
165
168
  _LOGGER: Final = logging.getLogger(__name__)
169
+ _LOGGER_EVENT: Final = logging.getLogger(f"{__name__}_event")
166
170
 
167
171
  # {central_name, central}
168
172
  CENTRAL_INSTANCES: Final[dict[str, CentralUnit]] = {}
@@ -184,7 +188,7 @@ class CentralUnit(PayloadMixin):
184
188
 
185
189
  def __init__(self, central_config: CentralConfig) -> None:
186
190
  """Init the central unit."""
187
- self._started: bool = False
191
+ self._state: CentralUnitState = CentralUnitState.NEW
188
192
  self._clients_started: bool = False
189
193
  self._device_add_semaphore: Final = asyncio.Semaphore()
190
194
  self._connection_state: Final = CentralConnectionState()
@@ -379,9 +383,9 @@ class CentralUnit(PayloadMixin):
379
383
  )
380
384
 
381
385
  @property
382
- def started(self) -> bool:
383
- """Return if the central is started."""
384
- return self._started
386
+ def state(self) -> CentralUnitState:
387
+ """Return the central state."""
388
+ return self._state
385
389
 
386
390
  @property
387
391
  def supports_ping_pong(self) -> bool:
@@ -455,9 +459,17 @@ class CentralUnit(PayloadMixin):
455
459
  async def start(self) -> None:
456
460
  """Start processing of the central unit."""
457
461
 
458
- if self._started:
462
+ _LOGGER.debug("START: Central %s is %s", self.name, self._state)
463
+ if self._state == CentralUnitState.INITIALIZING:
464
+ _LOGGER.debug("START: Central %s already starting", self.name)
465
+ return
466
+
467
+ if self._state == CentralUnitState.RUNNING:
459
468
  _LOGGER.debug("START: Central %s already started", self.name)
460
469
  return
470
+
471
+ self._state = CentralUnitState.INITIALIZING
472
+ _LOGGER.debug("START: Initializing Central %s", self.name)
461
473
  if self._config.enabled_interface_configs and (
462
474
  ip_addr := await self._identify_ip_addr(port=self._config.connection_check_port)
463
475
  ):
@@ -479,6 +491,7 @@ class CentralUnit(PayloadMixin):
479
491
  self._listen_port = xml_rpc_server.listen_port
480
492
  self._xml_rpc_server.add_central(self)
481
493
  except OSError as oserr:
494
+ self._state = CentralUnitState.STOPPED_BY_ERROR
482
495
  raise AioHomematicException(
483
496
  f"START: Failed to start central unit {self.name}: {extract_exc_args(exc=oserr)}"
484
497
  ) from oserr
@@ -492,13 +505,24 @@ class CentralUnit(PayloadMixin):
492
505
  if self._config.enable_server:
493
506
  self._start_scheduler()
494
507
 
495
- self._started = True
508
+ self._state = CentralUnitState.RUNNING
509
+ _LOGGER.debug("START: Central %s is %s", self.name, self._state)
496
510
 
497
511
  async def stop(self) -> None:
498
512
  """Stop processing of the central unit."""
499
- if not self._started:
513
+ _LOGGER.debug("STOP: Central %s is %s", self.name, self._state)
514
+ if self._state == CentralUnitState.STOPPING:
515
+ _LOGGER.debug("STOP: Central %s is already stopping", self.name)
516
+ return
517
+ if self._state == CentralUnitState.STOPPED:
518
+ _LOGGER.debug("STOP: Central %s is already stopped", self.name)
519
+ return
520
+ if self._state != CentralUnitState.RUNNING:
500
521
  _LOGGER.debug("STOP: Central %s not started", self.name)
501
522
  return
523
+ self._state = CentralUnitState.STOPPING
524
+ _LOGGER.debug("STOP: Stopping Central %s", self.name)
525
+
502
526
  await self.save_caches(save_device_descriptions=True, save_paramset_descriptions=True)
503
527
  self._stop_scheduler()
504
528
  await self._stop_clients()
@@ -522,8 +546,8 @@ class CentralUnit(PayloadMixin):
522
546
 
523
547
  # cancel outstanding tasks to speed up teardown
524
548
  self.looper.cancel_tasks()
525
- # wait until tasks are finished
526
- await self.looper.block_till_done()
549
+ # wait until tasks are finished (with wait_time safeguard)
550
+ await self.looper.block_till_done(wait_time=5.0)
527
551
 
528
552
  # Wait briefly for any auxiliary threads to finish without blocking forever
529
553
  max_wait_seconds = 5.0
@@ -532,7 +556,8 @@ class CentralUnit(PayloadMixin):
532
556
  while self._has_active_threads and waited < max_wait_seconds:
533
557
  await asyncio.sleep(interval)
534
558
  waited += interval
535
- self._started = False
559
+ self._state = CentralUnitState.STOPPED
560
+ _LOGGER.debug("STOP: Central %s is %s", self.name, self._state)
536
561
 
537
562
  async def restart_clients(self) -> None:
538
563
  """Restart clients."""
@@ -1074,7 +1099,7 @@ class CentralUnit(PayloadMixin):
1074
1099
  @callback_event
1075
1100
  async def data_point_event(self, interface_id: str, channel_address: str, parameter: str, value: Any) -> None:
1076
1101
  """If a device emits some sort event, we will handle it here."""
1077
- _LOGGER.debug(
1102
+ _LOGGER_EVENT.debug(
1078
1103
  "EVENT: interface_id = %s, channel_address = %s, parameter = %s, value = %s",
1079
1104
  interface_id,
1080
1105
  channel_address,
@@ -1112,7 +1137,7 @@ class CentralUnit(PayloadMixin):
1112
1137
  if callable(callback_handler):
1113
1138
  await callback_handler(value)
1114
1139
  except RuntimeError as rterr: # pragma: no cover
1115
- _LOGGER.debug(
1140
+ _LOGGER_EVENT.debug(
1116
1141
  "EVENT: RuntimeError [%s]. Failed to call callback for: %s, %s, %s",
1117
1142
  extract_exc_args(exc=rterr),
1118
1143
  interface_id,
@@ -1120,7 +1145,7 @@ class CentralUnit(PayloadMixin):
1120
1145
  parameter,
1121
1146
  )
1122
1147
  except Exception as exc: # pragma: no cover
1123
- _LOGGER.warning(
1148
+ _LOGGER_EVENT.warning(
1124
1149
  "EVENT failed: Unable to call callback for: %s, %s, %s, %s",
1125
1150
  interface_id,
1126
1151
  channel_address,
@@ -1130,7 +1155,7 @@ class CentralUnit(PayloadMixin):
1130
1155
 
1131
1156
  def data_point_path_event(self, state_path: str, value: str) -> None:
1132
1157
  """If a device emits some sort event, we will handle it here."""
1133
- _LOGGER.debug(
1158
+ _LOGGER_EVENT.debug(
1134
1159
  "DATA_POINT_PATH_EVENT: topic = %s, payload = %s",
1135
1160
  state_path,
1136
1161
  value,
@@ -1149,7 +1174,7 @@ class CentralUnit(PayloadMixin):
1149
1174
 
1150
1175
  def sysvar_data_point_path_event(self, state_path: str, value: str) -> None:
1151
1176
  """If a device emits some sort event, we will handle it here."""
1152
- _LOGGER.debug(
1177
+ _LOGGER_EVENT.debug(
1153
1178
  "SYSVAR_DATA_POINT_PATH_EVENT: topic = %s, payload = %s",
1154
1179
  state_path,
1155
1180
  value,
@@ -1161,13 +1186,13 @@ class CentralUnit(PayloadMixin):
1161
1186
  if callable(callback_handler):
1162
1187
  self._looper.create_task(callback_handler(value), name=f"sysvar-data-point-event-{state_path}")
1163
1188
  except RuntimeError as rterr: # pragma: no cover
1164
- _LOGGER.debug(
1189
+ _LOGGER_EVENT.debug(
1165
1190
  "EVENT: RuntimeError [%s]. Failed to call callback for: %s",
1166
1191
  extract_exc_args(exc=rterr),
1167
1192
  state_path,
1168
1193
  )
1169
1194
  except Exception as exc: # pragma: no cover
1170
- _LOGGER.warning(
1195
+ _LOGGER_EVENT.warning(
1171
1196
  "EVENT failed: Unable to call callback for: %s, %s",
1172
1197
  state_path,
1173
1198
  extract_exc_args(exc=exc),
@@ -1621,7 +1646,7 @@ class _Scheduler(threading.Thread):
1621
1646
  async def _run_scheduler_tasks(self) -> None:
1622
1647
  """Run all tasks."""
1623
1648
  while self._active:
1624
- if not self._central.started:
1649
+ if self._central.state != CentralUnitState.RUNNING:
1625
1650
  _LOGGER.debug("SCHEDULER: Waiting till central %s is started", self._central.name)
1626
1651
  await asyncio.sleep(SCHEDULER_NOT_STARTED_SLEEP)
1627
1652
  continue
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025 Daniel Perna, SukramJ
1
3
  """Decorators for central used within aiohomematic."""
2
4
 
3
5
  from __future__ import annotations
@@ -17,6 +19,9 @@ from aiohomematic.support import extract_exc_args
17
19
 
18
20
  _LOGGER: Final = logging.getLogger(__name__)
19
21
  _INTERFACE_ID: Final = "interface_id"
22
+ _CHANNEL_ADDRESS: Final = "channel_address"
23
+ _PARAMETER: Final = "parameter"
24
+ _VALUE: Final = "value"
20
25
 
21
26
 
22
27
  def callback_backend_system(system_event: BackendSystemEvent) -> Callable:
@@ -83,28 +88,68 @@ def callback_backend_system(system_event: BackendSystemEvent) -> Callable:
83
88
  return decorator_backend_system_callback
84
89
 
85
90
 
86
- def callback_event[**P, R](
87
- func: Callable[P, R],
88
- ) -> Callable:
91
+ def callback_event[**P, R](func: Callable[P, R]) -> Callable:
89
92
  """Check if event_callback is set and call it AFTER original function."""
90
93
 
91
- @wraps(func)
92
- async def async_wrapper_event_callback(*args: P.args, **kwargs: P.kwargs) -> R:
93
- """Wrap callback events."""
94
- return_value = cast(R, await func(*args, **kwargs)) # type: ignore[misc]
95
- _exec_event_callback(*args, **kwargs)
96
- return return_value
97
-
98
94
  def _exec_event_callback(*args: Any, **kwargs: Any) -> None:
99
95
  """Execute the callback for a data_point event."""
100
96
  try:
101
- args = args[1:]
102
- interface_id: str = args[0] if len(args) > 1 else str(kwargs[_INTERFACE_ID])
97
+ # Expected signature: (self, interface_id, channel_address, parameter, value)
98
+ interface_id: str
99
+ if len(args) > 1:
100
+ interface_id = cast(str, args[1])
101
+ channel_address = cast(str, args[2])
102
+ parameter = cast(str, args[3])
103
+ value = args[4] if len(args) > 4 else kwargs.get(_VALUE)
104
+ else:
105
+ interface_id = cast(str, kwargs[_INTERFACE_ID])
106
+ channel_address = cast(str, kwargs[_CHANNEL_ADDRESS])
107
+ parameter = cast(str, kwargs[_PARAMETER])
108
+ value = kwargs[_VALUE]
109
+
103
110
  if client := hmcl.get_client(interface_id=interface_id):
104
111
  client.modified_at = datetime.now()
105
- client.central.fire_backend_parameter_callback(*args, **kwargs)
112
+ client.central.fire_backend_parameter_callback(
113
+ interface_id=interface_id, channel_address=channel_address, parameter=parameter, value=value
114
+ )
106
115
  except Exception as exc: # pragma: no cover
107
- _LOGGER.warning("EXEC_DATA_POINT_EVENT_CALLBACK failed: Unable to reduce kwargs for event_callback")
116
+ _LOGGER.warning("EXEC_DATA_POINT_EVENT_CALLBACK failed: Unable to process args/kwargs for event_callback")
108
117
  raise AioHomematicException(f"args-exception event_callback [{extract_exc_args(exc=exc)}]") from exc
109
118
 
110
- return async_wrapper_event_callback
119
+ def _schedule_or_exec(*args: Any, **kwargs: Any) -> None:
120
+ """Schedule event callback on central looper when possible, else execute inline."""
121
+ try:
122
+ # Prefer scheduling on the CentralUnit looper when available to avoid blocking hot path
123
+ unit = args[0]
124
+ if isinstance(unit, hmcu.CentralUnit):
125
+ unit.looper.create_task(
126
+ _async_wrap_sync(_exec_event_callback, *args, **kwargs),
127
+ name="wrapper_event_callback",
128
+ )
129
+ return
130
+ except Exception:
131
+ # Fall through to inline execution on any error
132
+ pass
133
+ _exec_event_callback(*args, **kwargs)
134
+
135
+ @wraps(func)
136
+ async def async_wrapper_event_callback(*args: P.args, **kwargs: P.kwargs) -> R:
137
+ """Wrap async callback events."""
138
+ return_value = cast(R, await func(*args, **kwargs)) # type: ignore[misc]
139
+ _schedule_or_exec(*args, **kwargs)
140
+ return return_value
141
+
142
+ @wraps(func)
143
+ def wrapper_event_callback(*args: P.args, **kwargs: P.kwargs) -> R:
144
+ """Wrap sync callback events."""
145
+ return_value = func(*args, **kwargs)
146
+ _schedule_or_exec(*args, **kwargs)
147
+ return return_value
148
+
149
+ # Helper to create a trivial coroutine from a sync callable
150
+ async def _async_wrap_sync(cb: Callable[..., None], *a: Any, **kw: Any) -> None:
151
+ cb(*a, **kw)
152
+
153
+ if inspect.iscoroutinefunction(func):
154
+ return async_wrapper_event_callback
155
+ return wrapper_event_callback
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025 Daniel Perna, SukramJ
1
3
  """
2
4
  XML-RPC server module.
3
5
 
@@ -16,7 +18,7 @@ from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
16
18
  from aiohomematic import central as hmcu
17
19
  from aiohomematic.central.decorators import callback_backend_system
18
20
  from aiohomematic.const import IP_ANY_V4, PORT_ANY, BackendSystemEvent
19
- from aiohomematic.support import find_free_port
21
+ from aiohomematic.support import find_free_port, log_boundary_error
20
22
 
21
23
  _LOGGER: Final = logging.getLogger(__name__)
22
24
 
@@ -45,6 +47,18 @@ class RPCFunctions:
45
47
  @callback_backend_system(system_event=BackendSystemEvent.ERROR)
46
48
  def error(self, interface_id: str, error_code: str, msg: str) -> None:
47
49
  """When some error occurs the CCU / Homegear will send its error message here."""
50
+ # Structured boundary log (warning level). XML-RPC server received error notification.
51
+ try:
52
+ raise RuntimeError(str(msg))
53
+ except RuntimeError as err:
54
+ log_boundary_error(
55
+ logger=_LOGGER,
56
+ boundary="xml-rpc-server",
57
+ action="error",
58
+ err=err,
59
+ level=logging.WARNING,
60
+ context={"interface_id": interface_id, "error_code": int(error_code)},
61
+ )
48
62
  _LOGGER.warning(
49
63
  "ERROR failed: interface_id = %s, error_code = %i, message = %s",
50
64
  interface_id,
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025 Daniel Perna, SukramJ
1
3
  """
2
4
  Client adapters for communicating with HomeMatic CCU and compatible backends.
3
5