aiohomematic 2025.10.8__tar.gz → 2025.10.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.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/PKG-INFO +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/__init__.py +3 -3
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/async_support.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/central/__init__.py +69 -30
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/central/decorators.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/central/rpc_server.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/client/__init__.py +22 -14
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/client/_rpc_errors.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/client/json_rpc.py +29 -3
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/client/rpc_proxy.py +20 -2
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/const.py +33 -7
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/context.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/converter.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/decorators.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/exceptions.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/hmcli.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/__init__.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/calculated/__init__.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/calculated/climate.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/calculated/data_point.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/calculated/operating_voltage_level.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/calculated/support.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/__init__.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/climate.py +7 -4
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/const.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/cover.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/data_point.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/definition.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/light.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/lock.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/siren.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/support.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/switch.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/custom/valve.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/data_point.py +4 -4
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/device.py +13 -13
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/event.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/__init__.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/action.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/binary_sensor.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/button.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/data_point.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/number.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/select.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/sensor.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/switch.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/generic/text.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/hub/__init__.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/hub/binary_sensor.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/hub/button.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/hub/data_point.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/hub/number.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/hub/select.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/hub/sensor.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/hub/switch.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/hub/text.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/support.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/model/update.py +3 -3
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/property_decorators.py +2 -2
- aiohomematic-2025.10.10/aiohomematic/store/__init__.py +34 -0
- {aiohomematic-2025.10.8/aiohomematic/caches → aiohomematic-2025.10.10/aiohomematic/store}/dynamic.py +4 -4
- aiohomematic-2025.10.10/aiohomematic/store/persistent.py +970 -0
- {aiohomematic-2025.10.8/aiohomematic/caches → aiohomematic-2025.10.10/aiohomematic/store}/visibility.py +4 -4
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/support.py +16 -12
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/validator.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic.egg-info/PKG-INFO +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic.egg-info/SOURCES.txt +5 -4
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic_support/client_local.py +8 -8
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_central.py +3 -2
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_central_pydevccu.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_device.py +3 -3
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_dynamic_caches.py +4 -4
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_entity.py +1 -1
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_logging_support.py +4 -4
- aiohomematic-2025.10.10/tests/test_session_recorder.py +38 -0
- aiohomematic-2025.10.8/aiohomematic/caches/__init__.py +0 -12
- aiohomematic-2025.10.8/aiohomematic/caches/persistent.py +0 -478
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/LICENSE +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/README.md +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/py.typed +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/rega_scripts/fetch_all_device_data.fn +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/rega_scripts/get_program_descriptions.fn +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/rega_scripts/get_serial.fn +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/rega_scripts/get_system_variable_descriptions.fn +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/rega_scripts/set_program_state.fn +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic/rega_scripts/set_system_variable.fn +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic.egg-info/dependency_links.txt +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic.egg-info/requires.txt +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic.egg-info/top_level.txt +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/aiohomematic_support/__init__.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/pyproject.toml +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/setup.cfg +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_action.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_async_support.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_binary_sensor.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_button.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_calculated_support.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_climate.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_cover.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_decorator.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_event.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_json_rpc.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_json_rpc_client_integration.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_kwonly_lint.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_light.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_lock.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_number.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_select.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_sensor.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_siren.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_support.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_support_extra.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_switch.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_text.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_valve.py +0 -0
- {aiohomematic-2025.10.8 → aiohomematic-2025.10.10}/tests/test_xml_rpc_proxy_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.10
|
|
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>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
|
-
# Copyright (c) 2021-2025
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
3
|
"""
|
|
4
4
|
AioHomematic: a Python 3 library to interact with Homematic and HomematicIP backends.
|
|
5
5
|
|
|
@@ -9,10 +9,10 @@ This package provides a high-level API to discover devices and channels, read an
|
|
|
9
9
|
parameters (data points), receive events, and manage programs and system variables.
|
|
10
10
|
|
|
11
11
|
Key layers and responsibilities:
|
|
12
|
-
- aiohomematic.central: Orchestrates clients,
|
|
12
|
+
- aiohomematic.central: Orchestrates clients, store, device creation and events.
|
|
13
13
|
- aiohomematic.client: Interface-specific clients (JSON-RPC/XML-RPC, Homegear) handling IO.
|
|
14
14
|
- aiohomematic.model: Data point abstraction for generic, hub, and calculated entities.
|
|
15
|
-
- aiohomematic.
|
|
15
|
+
- aiohomematic.store: Persistent and runtime store for descriptions, values, and metadata.
|
|
16
16
|
|
|
17
17
|
Typical usage is to construct a CentralConfig, create a CentralUnit and start it, then
|
|
18
18
|
consume data points and events or issue write commands via the exposed API.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
|
-
# Copyright (c) 2021-2025
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
3
|
"""
|
|
4
4
|
Central unit and core orchestration for Homematic CCU and compatible backends.
|
|
5
5
|
|
|
@@ -9,8 +9,8 @@ This package provides the central coordination layer for aiohomematic. It models
|
|
|
9
9
|
Homematic CCU (or compatible backend such as Homegear) and orchestrates
|
|
10
10
|
interfaces, devices, channels, data points, events, and background jobs.
|
|
11
11
|
|
|
12
|
-
The central unit ties together the various submodules:
|
|
13
|
-
(JSON-RPC/XML-RPC), device and data point models, and visibility/description
|
|
12
|
+
The central unit ties together the various submodules: store, client adapters
|
|
13
|
+
(JSON-RPC/XML-RPC), device and data point models, and visibility/description store.
|
|
14
14
|
It exposes high-level APIs to query and manipulate the backend state while
|
|
15
15
|
encapsulating transport and scheduling details.
|
|
16
16
|
|
|
@@ -52,7 +52,7 @@ Example (simplified):
|
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
central = cfg.create_central()
|
|
55
|
-
central.start() # start XML-RPC server, create/init clients, load
|
|
55
|
+
central.start() # start XML-RPC server, create/init clients, load store
|
|
56
56
|
# ... interact with devices / data points via central ...
|
|
57
57
|
central.stop()
|
|
58
58
|
|
|
@@ -79,13 +79,9 @@ import voluptuous as vol
|
|
|
79
79
|
|
|
80
80
|
from aiohomematic import client as hmcl
|
|
81
81
|
from aiohomematic.async_support import Looper, loop_check
|
|
82
|
-
from aiohomematic.caches.dynamic import CentralDataCache, DeviceDetailsCache
|
|
83
|
-
from aiohomematic.caches.persistent import DeviceDescriptionCache, ParamsetDescriptionCache
|
|
84
|
-
from aiohomematic.caches.visibility import ParameterVisibilityCache
|
|
85
82
|
from aiohomematic.central import rpc_server as rpc
|
|
86
83
|
from aiohomematic.central.decorators import callback_backend_system, callback_event
|
|
87
|
-
from aiohomematic.client
|
|
88
|
-
from aiohomematic.client.rpc_proxy import AioXmlRpcProxy
|
|
84
|
+
from aiohomematic.client import AioJsonRpcAioHttpClient, BaseRpcProxy
|
|
89
85
|
from aiohomematic.const import (
|
|
90
86
|
CALLBACK_TYPE,
|
|
91
87
|
CATEGORIES,
|
|
@@ -100,9 +96,11 @@ from aiohomematic.const import (
|
|
|
100
96
|
DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS,
|
|
101
97
|
DEFAULT_INTERFACES_REQUIRING_PERIODIC_REFRESH,
|
|
102
98
|
DEFAULT_MAX_READ_WORKERS,
|
|
99
|
+
DEFAULT_OPTIONAL_SETTINGS,
|
|
103
100
|
DEFAULT_PERIODIC_REFRESH_INTERVAL,
|
|
104
101
|
DEFAULT_PROGRAM_MARKERS,
|
|
105
|
-
|
|
102
|
+
DEFAULT_SESSION_RECORDER_START_FOR_SECONDS,
|
|
103
|
+
DEFAULT_STORAGE_DIRECTORY,
|
|
106
104
|
DEFAULT_SYS_SCAN_INTERVAL,
|
|
107
105
|
DEFAULT_SYSVAR_MARKERS,
|
|
108
106
|
DEFAULT_TLS,
|
|
@@ -134,6 +132,7 @@ from aiohomematic.const import (
|
|
|
134
132
|
Interface,
|
|
135
133
|
InterfaceEventType,
|
|
136
134
|
Operations,
|
|
135
|
+
OptionalSettings,
|
|
137
136
|
Parameter,
|
|
138
137
|
ParamsetKey,
|
|
139
138
|
ProxyInitState,
|
|
@@ -163,6 +162,14 @@ from aiohomematic.model.hub import (
|
|
|
163
162
|
ProgramDpType,
|
|
164
163
|
)
|
|
165
164
|
from aiohomematic.property_decorators import info_property
|
|
165
|
+
from aiohomematic.store import (
|
|
166
|
+
CentralDataCache,
|
|
167
|
+
DeviceDescriptionCache,
|
|
168
|
+
DeviceDetailsCache,
|
|
169
|
+
ParameterVisibilityCache,
|
|
170
|
+
ParamsetDescriptionCache,
|
|
171
|
+
SessionRecorder,
|
|
172
|
+
)
|
|
166
173
|
from aiohomematic.support import (
|
|
167
174
|
LogContextMixin,
|
|
168
175
|
PayloadMixin,
|
|
@@ -181,7 +188,7 @@ _LOGGER_EVENT: Final = logging.getLogger(f"{__package__}.event")
|
|
|
181
188
|
|
|
182
189
|
# {central_name, central}
|
|
183
190
|
CENTRAL_INSTANCES: Final[dict[str, CentralUnit]] = {}
|
|
184
|
-
ConnectionProblemIssuer = AioJsonRpcAioHttpClient |
|
|
191
|
+
ConnectionProblemIssuer = AioJsonRpcAioHttpClient | BaseRpcProxy
|
|
185
192
|
|
|
186
193
|
INTERFACE_EVENT_SCHEMA = vol.Schema(
|
|
187
194
|
{
|
|
@@ -218,7 +225,9 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
218
225
|
self._device_descriptions: Final = DeviceDescriptionCache(central=self)
|
|
219
226
|
self._paramset_descriptions: Final = ParamsetDescriptionCache(central=self)
|
|
220
227
|
self._parameter_visibility: Final = ParameterVisibilityCache(central=self)
|
|
221
|
-
|
|
228
|
+
self._recorder: Final = SessionRecorder(
|
|
229
|
+
central=self, ttl_seconds=600, active=central_config.session_recorder_start
|
|
230
|
+
)
|
|
222
231
|
self._primary_client: hmcl.Client | None = None
|
|
223
232
|
# {interface_id, client}
|
|
224
233
|
self._clients: Final[dict[str, hmcl.Client]] = {}
|
|
@@ -344,6 +353,11 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
344
353
|
"""Return parameter_visibility cache."""
|
|
345
354
|
return self._parameter_visibility
|
|
346
355
|
|
|
356
|
+
@property
|
|
357
|
+
def recorder(self) -> SessionRecorder:
|
|
358
|
+
"""Return the session recorder."""
|
|
359
|
+
return self._recorder
|
|
360
|
+
|
|
347
361
|
@property
|
|
348
362
|
def poll_clients(self) -> tuple[hmcl.Client, ...]:
|
|
349
363
|
"""Return clients that need to poll data."""
|
|
@@ -458,10 +472,13 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
458
472
|
return channel
|
|
459
473
|
return None
|
|
460
474
|
|
|
461
|
-
async def
|
|
462
|
-
self,
|
|
475
|
+
async def save_files(
|
|
476
|
+
self,
|
|
477
|
+
*,
|
|
478
|
+
save_device_descriptions: bool = False,
|
|
479
|
+
save_paramset_descriptions: bool = False,
|
|
463
480
|
) -> None:
|
|
464
|
-
"""Save persistent
|
|
481
|
+
"""Save persistent files to disk."""
|
|
465
482
|
if save_device_descriptions:
|
|
466
483
|
await self._device_descriptions.save()
|
|
467
484
|
if save_paramset_descriptions:
|
|
@@ -479,6 +496,15 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
479
496
|
_LOGGER.debug("START: Central %s already started", self.name)
|
|
480
497
|
return
|
|
481
498
|
|
|
499
|
+
if self._config.session_recorder_start:
|
|
500
|
+
await self._recorder.deactivate(
|
|
501
|
+
delay=self._config.session_recorder_start_for_seconds,
|
|
502
|
+
auto_save=True,
|
|
503
|
+
randomize_output=self._config.session_recorder_randomize_output,
|
|
504
|
+
use_ts_in_file_name=False,
|
|
505
|
+
)
|
|
506
|
+
_LOGGER.debug("START: Starting Recorder for %s seconds", self._config.session_recorder_start_for_seconds)
|
|
507
|
+
|
|
482
508
|
self._state = CentralUnitState.INITIALIZING
|
|
483
509
|
_LOGGER.debug("START: Initializing Central %s", self.name)
|
|
484
510
|
if self._config.enabled_interface_configs and (
|
|
@@ -536,7 +562,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
536
562
|
self._state = CentralUnitState.STOPPING
|
|
537
563
|
_LOGGER.debug("STOP: Stopping Central %s", self.name)
|
|
538
564
|
|
|
539
|
-
await self.
|
|
565
|
+
await self.save_files(save_device_descriptions=True, save_paramset_descriptions=True)
|
|
540
566
|
self._stop_scheduler()
|
|
541
567
|
await self._stop_clients()
|
|
542
568
|
if self._json_rpc_client and self._json_rpc_client.is_activated:
|
|
@@ -952,13 +978,13 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
952
978
|
return len(self._clients) > 0
|
|
953
979
|
|
|
954
980
|
async def _load_caches(self) -> bool:
|
|
955
|
-
"""Load files to
|
|
981
|
+
"""Load files to store."""
|
|
956
982
|
if DataOperationResult.LOAD_FAIL in (
|
|
957
983
|
await self._device_descriptions.load(),
|
|
958
984
|
await self._paramset_descriptions.load(),
|
|
959
985
|
):
|
|
960
|
-
_LOGGER.warning("LOAD_CACHES failed: Unable to load
|
|
961
|
-
await self.
|
|
986
|
+
_LOGGER.warning("LOAD_CACHES failed: Unable to load store for %s. Clearing files", self.name)
|
|
987
|
+
await self.clear_files()
|
|
962
988
|
return False
|
|
963
989
|
await self._device_details.load()
|
|
964
990
|
await self._data_cache.load()
|
|
@@ -1047,7 +1073,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
1047
1073
|
for address in addresses:
|
|
1048
1074
|
if device := self._devices.get(address):
|
|
1049
1075
|
self.remove_device(device=device)
|
|
1050
|
-
await self.
|
|
1076
|
+
await self.save_files(save_device_descriptions=True, save_paramset_descriptions=True)
|
|
1051
1077
|
|
|
1052
1078
|
@callback_backend_system(system_event=BackendSystemEvent.NEW_DEVICES)
|
|
1053
1079
|
async def add_new_devices(self, *, interface_id: str, device_descriptions: tuple[DeviceDescription, ...]) -> None:
|
|
@@ -1149,7 +1175,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
1149
1175
|
extract_exc_args(exc=exc),
|
|
1150
1176
|
)
|
|
1151
1177
|
|
|
1152
|
-
await self.
|
|
1178
|
+
await self.save_files(
|
|
1153
1179
|
save_device_descriptions=save_descriptions,
|
|
1154
1180
|
save_paramset_descriptions=save_descriptions,
|
|
1155
1181
|
)
|
|
@@ -1607,10 +1633,11 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
1607
1633
|
)
|
|
1608
1634
|
return candidates
|
|
1609
1635
|
|
|
1610
|
-
async def
|
|
1611
|
-
"""
|
|
1636
|
+
async def clear_files(self) -> None:
|
|
1637
|
+
"""Remove all stored files and caches."""
|
|
1612
1638
|
await self._device_descriptions.clear()
|
|
1613
1639
|
await self._paramset_descriptions.clear()
|
|
1640
|
+
await self._recorder.clear()
|
|
1614
1641
|
self._device_details.clear()
|
|
1615
1642
|
self._data_cache.clear()
|
|
1616
1643
|
|
|
@@ -1985,10 +2012,11 @@ class CentralConfig:
|
|
|
1985
2012
|
listen_ip_addr: str | None = None,
|
|
1986
2013
|
listen_port_xml_rpc: int | None = None,
|
|
1987
2014
|
max_read_workers: int = DEFAULT_MAX_READ_WORKERS,
|
|
2015
|
+
optional_settings: tuple[OptionalSettings | str, ...] = DEFAULT_OPTIONAL_SETTINGS,
|
|
1988
2016
|
periodic_refresh_interval: int = DEFAULT_PERIODIC_REFRESH_INTERVAL,
|
|
1989
2017
|
program_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_PROGRAM_MARKERS,
|
|
1990
2018
|
start_direct: bool = False,
|
|
1991
|
-
|
|
2019
|
+
storage_directory: str = DEFAULT_STORAGE_DIRECTORY,
|
|
1992
2020
|
sys_scan_interval: int = DEFAULT_SYS_SCAN_INTERVAL,
|
|
1993
2021
|
sysvar_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_SYSVAR_MARKERS,
|
|
1994
2022
|
tls: bool = DEFAULT_TLS,
|
|
@@ -1998,6 +2026,7 @@ class CentralConfig:
|
|
|
1998
2026
|
) -> None:
|
|
1999
2027
|
"""Init the client config."""
|
|
2000
2028
|
self._interface_configs: Final = interface_configs
|
|
2029
|
+
self._optional_settings: Final = frozenset(optional_settings or ())
|
|
2001
2030
|
self.requires_xml_rpc_server: Final = any(
|
|
2002
2031
|
ic for ic in interface_configs if ic.rpc_server == RpcServerType.XML_RPC
|
|
2003
2032
|
)
|
|
@@ -2023,7 +2052,16 @@ class CentralConfig:
|
|
|
2023
2052
|
self.periodic_refresh_interval = periodic_refresh_interval
|
|
2024
2053
|
self.program_markers: Final = program_markers
|
|
2025
2054
|
self.start_direct: Final = start_direct
|
|
2026
|
-
self.
|
|
2055
|
+
self.session_recorder_randomize_output = (
|
|
2056
|
+
OptionalSettings.SR_DISABLE_RANDOMIZE_OUTPUT not in self._optional_settings
|
|
2057
|
+
)
|
|
2058
|
+
self.session_recorder_start_for_seconds: Final = (
|
|
2059
|
+
DEFAULT_SESSION_RECORDER_START_FOR_SECONDS
|
|
2060
|
+
if OptionalSettings.SR_RECORD_SYSTEM_INIT in self._optional_settings
|
|
2061
|
+
else 0
|
|
2062
|
+
)
|
|
2063
|
+
self.session_recorder_start = self.session_recorder_start_for_seconds > 0
|
|
2064
|
+
self.storage_directory: Final = storage_directory
|
|
2027
2065
|
self.sys_scan_interval: Final = sys_scan_interval
|
|
2028
2066
|
self.sysvar_markers: Final = sysvar_markers
|
|
2029
2067
|
self.tls: Final = tls
|
|
@@ -2058,7 +2096,7 @@ class CentralConfig:
|
|
|
2058
2096
|
|
|
2059
2097
|
@property
|
|
2060
2098
|
def use_caches(self) -> bool:
|
|
2061
|
-
"""Return if
|
|
2099
|
+
"""Return if store should be used."""
|
|
2062
2100
|
return self.start_direct is False
|
|
2063
2101
|
|
|
2064
2102
|
def check_config(self) -> None:
|
|
@@ -2068,7 +2106,7 @@ class CentralConfig:
|
|
|
2068
2106
|
host=self.host,
|
|
2069
2107
|
username=self.username,
|
|
2070
2108
|
password=self.password,
|
|
2071
|
-
|
|
2109
|
+
storage_directory=self.storage_directory,
|
|
2072
2110
|
callback_host=self.callback_host,
|
|
2073
2111
|
callback_port_xml_rpc=self.callback_port_xml_rpc,
|
|
2074
2112
|
json_port=self.json_port,
|
|
@@ -2105,6 +2143,7 @@ class CentralConfig:
|
|
|
2105
2143
|
client_session=self.client_session,
|
|
2106
2144
|
tls=self.tls,
|
|
2107
2145
|
verify_tls=self.verify_tls,
|
|
2146
|
+
session_recorder=central.recorder,
|
|
2108
2147
|
)
|
|
2109
2148
|
|
|
2110
2149
|
|
|
@@ -2122,7 +2161,7 @@ class CentralConnectionState:
|
|
|
2122
2161
|
self._json_issues.append(iid)
|
|
2123
2162
|
_LOGGER.debug("add_issue: add issue [%s] for JsonRpcAioHttpClient", iid)
|
|
2124
2163
|
return True
|
|
2125
|
-
if isinstance(issuer,
|
|
2164
|
+
if isinstance(issuer, BaseRpcProxy) and iid not in self._rpc_proxy_issues:
|
|
2126
2165
|
self._rpc_proxy_issues.append(iid)
|
|
2127
2166
|
_LOGGER.debug("add_issue: add issue [%s] for %s", iid, issuer.interface_id)
|
|
2128
2167
|
return True
|
|
@@ -2134,7 +2173,7 @@ class CentralConnectionState:
|
|
|
2134
2173
|
self._json_issues.remove(iid)
|
|
2135
2174
|
_LOGGER.debug("remove_issue: removing issue [%s] for JsonRpcAioHttpClient", iid)
|
|
2136
2175
|
return True
|
|
2137
|
-
if isinstance(issuer,
|
|
2176
|
+
if isinstance(issuer, BaseRpcProxy) and issuer.interface_id in self._rpc_proxy_issues:
|
|
2138
2177
|
self._rpc_proxy_issues.remove(iid)
|
|
2139
2178
|
_LOGGER.debug("remove_issue: removing issue [%s] for %s", iid, issuer.interface_id)
|
|
2140
2179
|
return True
|
|
@@ -2144,7 +2183,7 @@ class CentralConnectionState:
|
|
|
2144
2183
|
"""Add issue to collection."""
|
|
2145
2184
|
if isinstance(issuer, AioJsonRpcAioHttpClient):
|
|
2146
2185
|
return iid in self._json_issues
|
|
2147
|
-
if isinstance(issuer, (
|
|
2186
|
+
if isinstance(issuer, (BaseRpcProxy)):
|
|
2148
2187
|
return iid in self._rpc_proxy_issues
|
|
2149
2188
|
|
|
2150
2189
|
def handle_exception_log(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
|
-
# Copyright (c) 2021-2025
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
3
|
"""
|
|
4
4
|
Client adapters for communicating with Homematic CCU and compatible backends.
|
|
5
5
|
|
|
@@ -55,12 +55,11 @@ import logging
|
|
|
55
55
|
from typing import Any, Final, cast
|
|
56
56
|
|
|
57
57
|
from aiohomematic import central as hmcu
|
|
58
|
-
from aiohomematic.
|
|
58
|
+
from aiohomematic.client.json_rpc import AioJsonRpcAioHttpClient
|
|
59
59
|
from aiohomematic.client.rpc_proxy import AioXmlRpcProxy, BaseRpcProxy
|
|
60
60
|
from aiohomematic.const import (
|
|
61
61
|
CALLBACK_WARN_INTERVAL,
|
|
62
62
|
DATETIME_FORMAT_MILLIS,
|
|
63
|
-
DEFAULT_CUSTOM_ID,
|
|
64
63
|
DEFAULT_MAX_WORKERS,
|
|
65
64
|
DP_KEY_VALUE,
|
|
66
65
|
DUMMY_SERIAL,
|
|
@@ -81,6 +80,7 @@ from aiohomematic.const import (
|
|
|
81
80
|
ForcedDeviceAvailability,
|
|
82
81
|
Interface,
|
|
83
82
|
InterfaceEventType,
|
|
83
|
+
InternalCustomID,
|
|
84
84
|
Operations,
|
|
85
85
|
ParameterData,
|
|
86
86
|
ParameterType,
|
|
@@ -97,6 +97,7 @@ from aiohomematic.exceptions import BaseHomematicException, ClientException, NoC
|
|
|
97
97
|
from aiohomematic.model.device import Device
|
|
98
98
|
from aiohomematic.model.support import convert_value
|
|
99
99
|
from aiohomematic.property_decorators import hm_property
|
|
100
|
+
from aiohomematic.store import CommandCache, PingPongCache
|
|
100
101
|
from aiohomematic.support import (
|
|
101
102
|
LogContextMixin,
|
|
102
103
|
build_xml_rpc_headers,
|
|
@@ -108,7 +109,13 @@ from aiohomematic.support import (
|
|
|
108
109
|
supports_rx_mode,
|
|
109
110
|
)
|
|
110
111
|
|
|
111
|
-
__all__ = [
|
|
112
|
+
__all__ = [
|
|
113
|
+
"AioJsonRpcAioHttpClient",
|
|
114
|
+
"BaseRpcProxy",
|
|
115
|
+
"Client",
|
|
116
|
+
"InterfaceConfig",
|
|
117
|
+
"ClientConfig",
|
|
118
|
+
]
|
|
112
119
|
|
|
113
120
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
114
121
|
|
|
@@ -132,7 +139,7 @@ _CCU_JSON_VALUE_TYPE: Final = {
|
|
|
132
139
|
class Client(ABC, LogContextMixin):
|
|
133
140
|
"""Client object to access the backends via XML-RPC or JSON-RPC."""
|
|
134
141
|
|
|
135
|
-
def __init__(self, *, client_config:
|
|
142
|
+
def __init__(self, *, client_config: ClientConfig) -> None:
|
|
136
143
|
"""Initialize the Client."""
|
|
137
144
|
self._config: Final = client_config
|
|
138
145
|
self._last_value_send_cache = CommandCache(interface_id=client_config.interface_id)
|
|
@@ -1086,7 +1093,7 @@ class Client(ABC, LogContextMixin):
|
|
|
1086
1093
|
device_address,
|
|
1087
1094
|
)
|
|
1088
1095
|
return
|
|
1089
|
-
await self.central.
|
|
1096
|
+
await self.central.save_files(save_paramset_descriptions=True)
|
|
1090
1097
|
|
|
1091
1098
|
def __str__(self) -> str:
|
|
1092
1099
|
"""Provide some useful information."""
|
|
@@ -1096,7 +1103,7 @@ class Client(ABC, LogContextMixin):
|
|
|
1096
1103
|
class ClientCCU(Client):
|
|
1097
1104
|
"""Client implementation for CCU backend."""
|
|
1098
1105
|
|
|
1099
|
-
def __init__(self, *, client_config:
|
|
1106
|
+
def __init__(self, *, client_config: ClientConfig) -> None:
|
|
1100
1107
|
"""Initialize the Client."""
|
|
1101
1108
|
self._json_rpc_client: Final = client_config.central.json_rpc_client
|
|
1102
1109
|
super().__init__(client_config=client_config)
|
|
@@ -1591,7 +1598,7 @@ class ClientHomegear(Client):
|
|
|
1591
1598
|
return SystemInformation(available_interfaces=(Interface.BIDCOS_RF,), serial=f"{self.interface}_{DUMMY_SERIAL}")
|
|
1592
1599
|
|
|
1593
1600
|
|
|
1594
|
-
class
|
|
1601
|
+
class ClientConfig:
|
|
1595
1602
|
"""Config for a Client."""
|
|
1596
1603
|
|
|
1597
1604
|
def __init__(
|
|
@@ -1600,6 +1607,7 @@ class _ClientConfig:
|
|
|
1600
1607
|
central: hmcu.CentralUnit,
|
|
1601
1608
|
interface_config: InterfaceConfig,
|
|
1602
1609
|
) -> None:
|
|
1610
|
+
"""Initialize the config."""
|
|
1603
1611
|
self.central: Final = central
|
|
1604
1612
|
self.version: str = "0"
|
|
1605
1613
|
self.system_information = SystemInformation()
|
|
@@ -1667,6 +1675,7 @@ class _ClientConfig:
|
|
|
1667
1675
|
async def create_rpc_proxy(
|
|
1668
1676
|
self, *, interface: Interface, auth_enabled: bool | None = None, max_workers: int = DEFAULT_MAX_WORKERS
|
|
1669
1677
|
) -> BaseRpcProxy:
|
|
1678
|
+
"""Return a RPC proxy for the backend communication."""
|
|
1670
1679
|
return await self._create_xml_rpc_proxy(auth_enabled=auth_enabled, max_workers=max_workers)
|
|
1671
1680
|
|
|
1672
1681
|
async def _create_xml_rpc_proxy(
|
|
@@ -1690,16 +1699,13 @@ class _ClientConfig:
|
|
|
1690
1699
|
headers=xml_rpc_headers,
|
|
1691
1700
|
tls=config.tls,
|
|
1692
1701
|
verify_tls=config.verify_tls,
|
|
1702
|
+
session_recorder=self.central.recorder,
|
|
1693
1703
|
)
|
|
1694
1704
|
await xml_proxy.do_init()
|
|
1695
1705
|
return xml_proxy
|
|
1696
1706
|
|
|
1697
1707
|
async def _create_simple_rpc_proxy(self, *, interface: Interface) -> BaseRpcProxy:
|
|
1698
1708
|
"""Return a RPC proxy for the backend communication."""
|
|
1699
|
-
return await self._create_xml_rpc_proxy()
|
|
1700
|
-
|
|
1701
|
-
async def _create_simple_xml_rpc_proxy(self) -> AioXmlRpcProxy:
|
|
1702
|
-
"""Return a XmlRPC proxy for the backend communication."""
|
|
1703
1709
|
return await self._create_xml_rpc_proxy(auth_enabled=True, max_workers=0)
|
|
1704
1710
|
|
|
1705
1711
|
|
|
@@ -1744,7 +1750,7 @@ async def create_client(
|
|
|
1744
1750
|
interface_config: InterfaceConfig,
|
|
1745
1751
|
) -> Client:
|
|
1746
1752
|
"""Return a new client for with a given interface_config."""
|
|
1747
|
-
return await
|
|
1753
|
+
return await ClientConfig(central=central, interface_config=interface_config).create_client()
|
|
1748
1754
|
|
|
1749
1755
|
|
|
1750
1756
|
def get_client(interface_id: str) -> Client | None:
|
|
@@ -1809,7 +1815,9 @@ async def _track_single_data_point_state_change_or_timeout(
|
|
|
1809
1815
|
)
|
|
1810
1816
|
return
|
|
1811
1817
|
if (
|
|
1812
|
-
unsub := dp.register_data_point_updated_callback(
|
|
1818
|
+
unsub := dp.register_data_point_updated_callback(
|
|
1819
|
+
cb=_async_event_changed, custom_id=InternalCustomID.DEFAULT
|
|
1820
|
+
)
|
|
1813
1821
|
) is None:
|
|
1814
1822
|
return
|
|
1815
1823
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
|
-
# Copyright (c) 2021-2025
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
3
|
"""
|
|
4
4
|
Asynchronous JSON-RPC client for Homematic CCU-compatible backends.
|
|
5
5
|
|
|
@@ -24,7 +24,7 @@ used directly for advanced tasks. Typical flow:
|
|
|
24
24
|
Notes
|
|
25
25
|
-----
|
|
26
26
|
- Some JSON-RPC methods are backend/firmware dependent. The client detects and
|
|
27
|
-
|
|
27
|
+
store supported methods at runtime.
|
|
28
28
|
- Binary/text encodings are handled carefully (UTF-8 / ISO-8859-1) for script IO.
|
|
29
29
|
|
|
30
30
|
"""
|
|
@@ -92,6 +92,7 @@ from aiohomematic.exceptions import (
|
|
|
92
92
|
)
|
|
93
93
|
from aiohomematic.model.support import convert_value
|
|
94
94
|
from aiohomematic.property_decorators import hm_property
|
|
95
|
+
from aiohomematic.store import SessionRecorder
|
|
95
96
|
from aiohomematic.support import (
|
|
96
97
|
LogContextMixin,
|
|
97
98
|
cleanup_text_from_html_tags,
|
|
@@ -192,6 +193,7 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
|
|
|
192
193
|
client_session: ClientSession | None,
|
|
193
194
|
tls: bool = False,
|
|
194
195
|
verify_tls: bool = False,
|
|
196
|
+
session_recorder: SessionRecorder | None = None,
|
|
195
197
|
) -> None:
|
|
196
198
|
"""Session setup."""
|
|
197
199
|
self._client_session: Final = (
|
|
@@ -210,6 +212,7 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
|
|
|
210
212
|
self._script_cache: Final[dict[str, str]] = {}
|
|
211
213
|
self._last_session_id_refresh: datetime | None = None
|
|
212
214
|
self._session_id: str | None = None
|
|
215
|
+
self._session_recorder: Final = session_recorder
|
|
213
216
|
self._supported_methods: tuple[str, ...] | None = None
|
|
214
217
|
self._sema: Final = Semaphore(value=MAX_CONCURRENT_HTTP_SESSIONS)
|
|
215
218
|
|
|
@@ -431,7 +434,7 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
|
|
|
431
434
|
|
|
432
435
|
if response.status == 200:
|
|
433
436
|
json_response = await asyncio.shield(self._get_json_reponse(response=response))
|
|
434
|
-
|
|
437
|
+
self._record_session(method=method, params=params, response=json_response)
|
|
435
438
|
if error := json_response[_JsonKey.ERROR]:
|
|
436
439
|
# Map JSON-RPC error to actionable exception with context
|
|
437
440
|
ctx = RpcContext(protocol="json-rpc", method=str(method), host=self._url)
|
|
@@ -466,6 +469,7 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
|
|
|
466
469
|
raise exc
|
|
467
470
|
raise ClientException(message)
|
|
468
471
|
except BaseHomematicException as bhe:
|
|
472
|
+
self._record_session(method=method, params=params, exc=bhe)
|
|
469
473
|
if method in (_JsonRpcMethod.SESSION_LOGIN, _JsonRpcMethod.SESSION_LOGOUT, _JsonRpcMethod.SESSION_RENEW):
|
|
470
474
|
self.clear_session()
|
|
471
475
|
# Domain error at boundary -> warning
|
|
@@ -531,6 +535,28 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
|
|
|
531
535
|
)
|
|
532
536
|
raise ClientException(exc) from exc
|
|
533
537
|
|
|
538
|
+
def _record_session(
|
|
539
|
+
self,
|
|
540
|
+
*,
|
|
541
|
+
method: str,
|
|
542
|
+
params: Mapping[str, Any],
|
|
543
|
+
response: dict[str, Any] | None = None,
|
|
544
|
+
exc: Exception | None = None,
|
|
545
|
+
) -> bool:
|
|
546
|
+
"""Record the session."""
|
|
547
|
+
if method == _JsonRpcMethod.SESSION_LOGIN and isinstance(params, dict):
|
|
548
|
+
if params.get(_JsonKey.USERNAME):
|
|
549
|
+
params[_JsonKey.USERNAME] = "********"
|
|
550
|
+
if params.get(_JsonKey.PASSWORD):
|
|
551
|
+
params[_JsonKey.PASSWORD] = "********"
|
|
552
|
+
|
|
553
|
+
if self._session_recorder and self._session_recorder.active:
|
|
554
|
+
self._session_recorder.add_json_rpc_session(
|
|
555
|
+
method=method, params=dict(params), response=response, session_exc=exc
|
|
556
|
+
)
|
|
557
|
+
return True
|
|
558
|
+
return False
|
|
559
|
+
|
|
534
560
|
async def _get_json_reponse(self, *, response: ClientResponse) -> dict[str, Any] | Any:
|
|
535
561
|
"""Return the json object from response."""
|
|
536
562
|
try:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
|
-
# Copyright (c) 2021-2025
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
3
|
"""
|
|
4
4
|
XML-RPC transport proxy with concurrency control and connection awareness.
|
|
5
5
|
|
|
@@ -43,6 +43,7 @@ from aiohomematic.exceptions import (
|
|
|
43
43
|
NoConnectionException,
|
|
44
44
|
UnsupportedException,
|
|
45
45
|
)
|
|
46
|
+
from aiohomematic.store import SessionRecorder
|
|
46
47
|
from aiohomematic.support import extract_exc_args, get_tls_context, log_boundary_error
|
|
47
48
|
|
|
48
49
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
@@ -96,10 +97,12 @@ class BaseRpcProxy(ABC):
|
|
|
96
97
|
magic_method: Callable,
|
|
97
98
|
tls: bool = False,
|
|
98
99
|
verify_tls: bool = False,
|
|
100
|
+
session_recorder: SessionRecorder | None = None,
|
|
99
101
|
) -> None:
|
|
100
102
|
"""Initialize new proxy for server and get local ip."""
|
|
101
103
|
self._interface_id: Final = interface_id
|
|
102
104
|
self._connection_state: Final = connection_state
|
|
105
|
+
self._session_recorder: Final = session_recorder
|
|
103
106
|
self._magic_method: Final = magic_method
|
|
104
107
|
self._looper: Final = Looper()
|
|
105
108
|
self._proxy_executor: Final = (
|
|
@@ -136,6 +139,17 @@ class BaseRpcProxy(ABC):
|
|
|
136
139
|
"""Magic method dispatcher."""
|
|
137
140
|
return self._magic_method(self._async_request, *args, **kwargs)
|
|
138
141
|
|
|
142
|
+
def _record_session(
|
|
143
|
+
self, *, method: str, params: tuple[Any, ...], response: Any | None = None, exc: Exception | None = None
|
|
144
|
+
) -> bool:
|
|
145
|
+
"""Record the session."""
|
|
146
|
+
if method in (_RpcMethod.PING,):
|
|
147
|
+
return False
|
|
148
|
+
if self._session_recorder and self._session_recorder.active:
|
|
149
|
+
self._session_recorder.add_xml_rpc_session(method=method, params=params, response=response, session_exc=exc)
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
139
153
|
|
|
140
154
|
# noinspection PyProtectedMember,PyUnresolvedReferences
|
|
141
155
|
class AioXmlRpcProxy(BaseRpcProxy, xmlrpc.client.ServerProxy):
|
|
@@ -151,6 +165,7 @@ class AioXmlRpcProxy(BaseRpcProxy, xmlrpc.client.ServerProxy):
|
|
|
151
165
|
headers: list[tuple[str, str]],
|
|
152
166
|
tls: bool = False,
|
|
153
167
|
verify_tls: bool = False,
|
|
168
|
+
session_recorder: SessionRecorder | None = None,
|
|
154
169
|
) -> None:
|
|
155
170
|
"""Initialize new proxy for server and get local ip."""
|
|
156
171
|
super().__init__(
|
|
@@ -160,6 +175,7 @@ class AioXmlRpcProxy(BaseRpcProxy, xmlrpc.client.ServerProxy):
|
|
|
160
175
|
magic_method=xmlrpc.client._Method,
|
|
161
176
|
tls=tls,
|
|
162
177
|
verify_tls=verify_tls,
|
|
178
|
+
session_recorder=session_recorder,
|
|
163
179
|
)
|
|
164
180
|
|
|
165
181
|
xmlrpc.client.ServerProxy.__init__(
|
|
@@ -193,10 +209,12 @@ class AioXmlRpcProxy(BaseRpcProxy, xmlrpc.client.ServerProxy):
|
|
|
193
209
|
executor=self._proxy_executor,
|
|
194
210
|
)
|
|
195
211
|
)
|
|
212
|
+
self._record_session(method=method, params=args[1], response=result)
|
|
196
213
|
self._connection_state.remove_issue(issuer=self, iid=self._interface_id)
|
|
197
214
|
return result
|
|
198
215
|
raise NoConnectionException(f"No connection to {self._interface_id}")
|
|
199
|
-
except BaseHomematicException:
|
|
216
|
+
except BaseHomematicException as bhe:
|
|
217
|
+
self._record_session(method=args[0], params=args[1:], exc=bhe)
|
|
200
218
|
raise
|
|
201
219
|
except SSLError as sslerr:
|
|
202
220
|
message = f"SSLError on {self._interface_id}: {extract_exc_args(exc=sslerr)}"
|