aiohomematic 2025.8.9__tar.gz → 2025.9.1__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.8.9 → aiohomematic-2025.9.1}/LICENSE +1 -1
- aiohomematic-2025.9.1/PKG-INFO +125 -0
- aiohomematic-2025.9.1/README.md +97 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/__init__.py +15 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/async_support.py +15 -2
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/caches/__init__.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/caches/dynamic.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/caches/persistent.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/caches/visibility.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/central/__init__.py +43 -18
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/central/decorators.py +60 -15
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/central/xml_rpc_server.py +15 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/client/__init__.py +2 -0
- aiohomematic-2025.9.1/aiohomematic/client/_rpc_errors.py +81 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/client/json_rpc.py +68 -19
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/client/xml_rpc.py +15 -8
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/const.py +44 -3
- aiohomematic-2025.9.1/aiohomematic/context.py +18 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/converter.py +27 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/decorators.py +98 -25
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/exceptions.py +19 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/hmcli.py +13 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/__init__.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/calculated/__init__.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/calculated/climate.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/calculated/data_point.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/calculated/operating_voltage_level.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/calculated/support.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/__init__.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/climate.py +3 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/const.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/cover.py +30 -2
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/data_point.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/definition.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/light.py +18 -10
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/lock.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/siren.py +5 -2
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/support.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/switch.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/custom/valve.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/data_point.py +15 -3
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/decorators.py +29 -8
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/device.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/event.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/__init__.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/action.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/binary_sensor.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/button.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/data_point.py +4 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/number.py +4 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/select.py +4 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/sensor.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/switch.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/generic/text.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/hub/__init__.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/hub/binary_sensor.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/hub/button.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/hub/data_point.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/hub/number.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/hub/select.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/hub/sensor.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/hub/switch.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/hub/text.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/support.py +26 -1
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/model/update.py +2 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/support.py +160 -3
- aiohomematic-2025.9.1/aiohomematic/validator.py +112 -0
- aiohomematic-2025.9.1/aiohomematic.egg-info/PKG-INFO +125 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic.egg-info/SOURCES.txt +1 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_central_pydevccu.py +7 -7
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_siren.py +4 -2
- aiohomematic-2025.8.9/PKG-INFO +0 -69
- aiohomematic-2025.8.9/README.md +0 -41
- aiohomematic-2025.8.9/aiohomematic/context.py +0 -8
- aiohomematic-2025.8.9/aiohomematic/validator.py +0 -65
- aiohomematic-2025.8.9/aiohomematic.egg-info/PKG-INFO +0 -69
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/py.typed +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/rega_scripts/fetch_all_device_data.fn +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/rega_scripts/get_program_descriptions.fn +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/rega_scripts/get_serial.fn +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/rega_scripts/get_system_variable_descriptions.fn +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/rega_scripts/set_program_state.fn +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic/rega_scripts/set_system_variable.fn +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic.egg-info/dependency_links.txt +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic.egg-info/requires.txt +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic.egg-info/top_level.txt +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic_support/__init__.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/aiohomematic_support/client_local.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/pyproject.toml +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/setup.cfg +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_action.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_binary_sensor.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_button.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_calculated_support.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_central.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_climate.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_cover.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_decorator.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_device.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_entity.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_event.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_json_rpc.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_light.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_lock.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_number.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_select.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_sensor.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_support.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_switch.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_text.py +0 -0
- {aiohomematic-2025.8.9 → aiohomematic-2025.9.1}/tests/test_valve.py +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiohomematic
|
|
3
|
+
Version: 2025.9.1
|
|
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
|
+
- Naming: [see](docs/naming.md) for how device, channel and data point names are created.
|
|
113
|
+
- Homematic(IP) Local integration: https://github.com/sukramj/homematicip_local
|
|
114
|
+
- Input select helper: [see](docs/input_select_helper.md) for an overview of how to use the input select helper.
|
|
115
|
+
- Troubleshooting with Home Assistant: [see](docs/homeassistant_troubleshooting.md) for common issues and how to debug them.
|
|
116
|
+
- Unignore mechanism: [see](docs/unignore.md) for how to unignore devices that are ignored by default.
|
|
117
|
+
|
|
118
|
+
## Useful developer links
|
|
119
|
+
|
|
120
|
+
- Architecture overview: [see](docs/architecture.md) for an overview of the architecture of the library.
|
|
121
|
+
- Data flow: [see](docs/data_flow.md) for an overview of how data flows through the library.
|
|
122
|
+
- Extending the model: [see](docs/extension_points.md) for adding custom device profiles and calculated data points.
|
|
123
|
+
- Home Assistant lifecycle (discovery, updates, teardown): [see](docs/homeassistant_lifecycle.md) for details on how the integration works and how to debug issues.
|
|
124
|
+
- RSSI fix: [see](docs/rssi_fix.md) for how RSSI values are fixed for Home Assistant.
|
|
125
|
+
- Sequence diagrams: [see](docs/sequence_diagrams.md) for a sequence diagram of how the library works.
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
- Naming: [see](docs/naming.md) for how device, channel and data point names are created.
|
|
85
|
+
- Homematic(IP) Local integration: https://github.com/sukramj/homematicip_local
|
|
86
|
+
- Input select helper: [see](docs/input_select_helper.md) for an overview of how to use the input select helper.
|
|
87
|
+
- Troubleshooting with Home Assistant: [see](docs/homeassistant_troubleshooting.md) for common issues and how to debug them.
|
|
88
|
+
- Unignore mechanism: [see](docs/unignore.md) for how to unignore devices that are ignored by default.
|
|
89
|
+
|
|
90
|
+
## Useful developer links
|
|
91
|
+
|
|
92
|
+
- Architecture overview: [see](docs/architecture.md) for an overview of the architecture of the library.
|
|
93
|
+
- Data flow: [see](docs/data_flow.md) for an overview of how data flows through the library.
|
|
94
|
+
- Extending the model: [see](docs/extension_points.md) for adding custom device profiles and calculated data points.
|
|
95
|
+
- Home Assistant lifecycle (discovery, updates, teardown): [see](docs/homeassistant_lifecycle.md) for details on how the integration works and how to debug issues.
|
|
96
|
+
- RSSI fix: [see](docs/rssi_fix.md) for how RSSI values are fixed for Home Assistant.
|
|
97
|
+
- 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
|
-
"""
|
|
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
|
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"{__package__}.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.
|
|
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
|
|
383
|
-
"""Return
|
|
384
|
-
return self.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
102
|
-
interface_id: str
|
|
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(
|
|
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
|
|
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
|
-
|
|
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,
|