aiohomematic 2025.10.8__py3-none-any.whl → 2025.10.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiohomematic might be problematic. Click here for more details.

Files changed (73) hide show
  1. aiohomematic/__init__.py +3 -3
  2. aiohomematic/async_support.py +1 -1
  3. aiohomematic/central/__init__.py +69 -30
  4. aiohomematic/central/decorators.py +1 -1
  5. aiohomematic/central/rpc_server.py +1 -1
  6. aiohomematic/client/__init__.py +22 -14
  7. aiohomematic/client/_rpc_errors.py +1 -1
  8. aiohomematic/client/json_rpc.py +29 -3
  9. aiohomematic/client/rpc_proxy.py +20 -2
  10. aiohomematic/const.py +33 -7
  11. aiohomematic/context.py +1 -1
  12. aiohomematic/converter.py +1 -1
  13. aiohomematic/decorators.py +1 -1
  14. aiohomematic/exceptions.py +1 -1
  15. aiohomematic/hmcli.py +1 -1
  16. aiohomematic/model/__init__.py +1 -1
  17. aiohomematic/model/calculated/__init__.py +1 -1
  18. aiohomematic/model/calculated/climate.py +1 -1
  19. aiohomematic/model/calculated/data_point.py +1 -1
  20. aiohomematic/model/calculated/operating_voltage_level.py +1 -1
  21. aiohomematic/model/calculated/support.py +1 -1
  22. aiohomematic/model/custom/__init__.py +1 -1
  23. aiohomematic/model/custom/climate.py +7 -4
  24. aiohomematic/model/custom/const.py +1 -1
  25. aiohomematic/model/custom/cover.py +1 -1
  26. aiohomematic/model/custom/data_point.py +1 -1
  27. aiohomematic/model/custom/definition.py +1 -1
  28. aiohomematic/model/custom/light.py +1 -1
  29. aiohomematic/model/custom/lock.py +1 -1
  30. aiohomematic/model/custom/siren.py +1 -1
  31. aiohomematic/model/custom/support.py +1 -1
  32. aiohomematic/model/custom/switch.py +1 -1
  33. aiohomematic/model/custom/valve.py +1 -1
  34. aiohomematic/model/data_point.py +4 -4
  35. aiohomematic/model/device.py +13 -13
  36. aiohomematic/model/event.py +1 -1
  37. aiohomematic/model/generic/__init__.py +1 -1
  38. aiohomematic/model/generic/action.py +1 -1
  39. aiohomematic/model/generic/binary_sensor.py +1 -1
  40. aiohomematic/model/generic/button.py +1 -1
  41. aiohomematic/model/generic/data_point.py +1 -1
  42. aiohomematic/model/generic/number.py +1 -1
  43. aiohomematic/model/generic/select.py +1 -1
  44. aiohomematic/model/generic/sensor.py +1 -1
  45. aiohomematic/model/generic/switch.py +1 -1
  46. aiohomematic/model/generic/text.py +1 -1
  47. aiohomematic/model/hub/__init__.py +1 -1
  48. aiohomematic/model/hub/binary_sensor.py +1 -1
  49. aiohomematic/model/hub/button.py +1 -1
  50. aiohomematic/model/hub/data_point.py +1 -1
  51. aiohomematic/model/hub/number.py +1 -1
  52. aiohomematic/model/hub/select.py +1 -1
  53. aiohomematic/model/hub/sensor.py +1 -1
  54. aiohomematic/model/hub/switch.py +1 -1
  55. aiohomematic/model/hub/text.py +1 -1
  56. aiohomematic/model/support.py +1 -1
  57. aiohomematic/model/update.py +3 -3
  58. aiohomematic/property_decorators.py +2 -2
  59. aiohomematic/store/__init__.py +34 -0
  60. aiohomematic/{caches → store}/dynamic.py +4 -4
  61. aiohomematic/store/persistent.py +970 -0
  62. aiohomematic/{caches → store}/visibility.py +4 -4
  63. aiohomematic/support.py +16 -12
  64. aiohomematic/validator.py +1 -1
  65. {aiohomematic-2025.10.8.dist-info → aiohomematic-2025.10.10.dist-info}/METADATA +1 -1
  66. aiohomematic-2025.10.10.dist-info/RECORD +78 -0
  67. aiohomematic_support/client_local.py +8 -8
  68. aiohomematic/caches/__init__.py +0 -12
  69. aiohomematic/caches/persistent.py +0 -478
  70. aiohomematic-2025.10.8.dist-info/RECORD +0 -78
  71. {aiohomematic-2025.10.8.dist-info → aiohomematic-2025.10.10.dist-info}/WHEEL +0 -0
  72. {aiohomematic-2025.10.8.dist-info → aiohomematic-2025.10.10.dist-info}/licenses/LICENSE +0 -0
  73. {aiohomematic-2025.10.8.dist-info → aiohomematic-2025.10.10.dist-info}/top_level.txt +0 -0
aiohomematic/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # SPDX-License-Identifier: MIT
2
- # Copyright (c) 2021-2025 Daniel Perna, SukramJ
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, caches, device creation and events.
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.caches: Persistent and runtime caches for descriptions, values, and metadata.
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 Daniel Perna, SukramJ
2
+ # Copyright (c) 2021-2025
3
3
  """Module with support for loop interaction."""
4
4
 
5
5
  from __future__ import annotations
@@ -1,5 +1,5 @@
1
1
  # SPDX-License-Identifier: MIT
2
- # Copyright (c) 2021-2025 Daniel Perna, SukramJ
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: caches, client adapters
13
- (JSON-RPC/XML-RPC), device and data point models, and visibility/description caches.
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 caches
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.json_rpc import AioJsonRpcAioHttpClient
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
- DEFAULT_STORAGE_FOLDER,
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 | AioXmlRpcProxy
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 save_caches(
462
- self, *, save_device_descriptions: bool = False, save_paramset_descriptions: bool = False
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 caches."""
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.save_caches(save_device_descriptions=True, save_paramset_descriptions=True)
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 caches."""
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 caches for %s. Clearing files", self.name)
961
- await self.clear_caches()
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.save_caches(save_device_descriptions=True, save_paramset_descriptions=True)
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.save_caches(
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 clear_caches(self) -> None:
1611
- """Clear all stored data."""
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
- storage_folder: str = DEFAULT_STORAGE_FOLDER,
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.storage_folder: Final = storage_folder
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 caches should be used."""
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
- storage_folder=self.storage_folder,
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, AioXmlRpcProxy) and iid not in self._rpc_proxy_issues:
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, AioXmlRpcProxy) and issuer.interface_id in self._rpc_proxy_issues:
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, (AioXmlRpcProxy)):
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 Daniel Perna, SukramJ
2
+ # Copyright (c) 2021-2025
3
3
  """Decorators for central used within aiohomematic."""
4
4
 
5
5
  from __future__ import annotations
@@ -1,5 +1,5 @@
1
1
  # SPDX-License-Identifier: MIT
2
- # Copyright (c) 2021-2025 Daniel Perna, SukramJ
2
+ # Copyright (c) 2021-2025
3
3
  """
4
4
  XML-RPC server module.
5
5
 
@@ -1,5 +1,5 @@
1
1
  # SPDX-License-Identifier: MIT
2
- # Copyright (c) 2021-2025 Daniel Perna, SukramJ
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.caches.dynamic import CommandCache, PingPongCache
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__ = ["Client", "InterfaceConfig", "create_client", "get_client"]
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: _ClientConfig) -> None:
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.save_caches(save_paramset_descriptions=True)
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: _ClientConfig) -> None:
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 _ClientConfig:
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 _ClientConfig(central=central, interface_config=interface_config).create_client()
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(cb=_async_event_changed, custom_id=DEFAULT_CUSTOM_ID)
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 Daniel Perna, SukramJ
2
+ # Copyright (c) 2021-2025
3
3
  """
4
4
  Error mapping helpers for RPC transports.
5
5
 
@@ -1,5 +1,5 @@
1
1
  # SPDX-License-Identifier: MIT
2
- # Copyright (c) 2021-2025 Daniel Perna, SukramJ
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
- caches supported methods at runtime.
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 Daniel Perna, SukramJ
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)}"