aiohomematic 2025.10.8__py3-none-any.whl → 2025.10.9__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 +58 -30
  4. aiohomematic/central/decorators.py +1 -1
  5. aiohomematic/central/rpc_server.py +1 -1
  6. aiohomematic/client/__init__.py +18 -12
  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 +23 -6
  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 +3 -2
  35. aiohomematic/model/device.py +10 -10
  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 +1 -1
  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 +933 -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.9.dist-info}/METADATA +1 -1
  66. aiohomematic-2025.10.9.dist-info/RECORD +78 -0
  67. aiohomematic_support/client_local.py +2 -2
  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.9.dist-info}/WHEEL +0 -0
  72. {aiohomematic-2025.10.8.dist-info → aiohomematic-2025.10.9.dist-info}/licenses/LICENSE +0 -0
  73. {aiohomematic-2025.10.8.dist-info → aiohomematic-2025.10.9.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,
@@ -102,7 +98,7 @@ from aiohomematic.const import (
102
98
  DEFAULT_MAX_READ_WORKERS,
103
99
  DEFAULT_PERIODIC_REFRESH_INTERVAL,
104
100
  DEFAULT_PROGRAM_MARKERS,
105
- DEFAULT_STORAGE_FOLDER,
101
+ DEFAULT_STORAGE_DIRECTORY,
106
102
  DEFAULT_SYS_SCAN_INTERVAL,
107
103
  DEFAULT_SYSVAR_MARKERS,
108
104
  DEFAULT_TLS,
@@ -163,6 +159,14 @@ from aiohomematic.model.hub import (
163
159
  ProgramDpType,
164
160
  )
165
161
  from aiohomematic.property_decorators import info_property
162
+ from aiohomematic.store import (
163
+ CentralDataCache,
164
+ DeviceDescriptionCache,
165
+ DeviceDetailsCache,
166
+ ParameterVisibilityCache,
167
+ ParamsetDescriptionCache,
168
+ SessionRecorder,
169
+ )
166
170
  from aiohomematic.support import (
167
171
  LogContextMixin,
168
172
  PayloadMixin,
@@ -181,7 +185,7 @@ _LOGGER_EVENT: Final = logging.getLogger(f"{__package__}.event")
181
185
 
182
186
  # {central_name, central}
183
187
  CENTRAL_INSTANCES: Final[dict[str, CentralUnit]] = {}
184
- ConnectionProblemIssuer = AioJsonRpcAioHttpClient | AioXmlRpcProxy
188
+ ConnectionProblemIssuer = AioJsonRpcAioHttpClient | BaseRpcProxy
185
189
 
186
190
  INTERFACE_EVENT_SCHEMA = vol.Schema(
187
191
  {
@@ -218,7 +222,9 @@ class CentralUnit(LogContextMixin, PayloadMixin):
218
222
  self._device_descriptions: Final = DeviceDescriptionCache(central=self)
219
223
  self._paramset_descriptions: Final = ParamsetDescriptionCache(central=self)
220
224
  self._parameter_visibility: Final = ParameterVisibilityCache(central=self)
221
-
225
+ self._recorder: Final = SessionRecorder(
226
+ central=self, default_ttl_seconds=600, active=central_config.start_recorder
227
+ )
222
228
  self._primary_client: hmcl.Client | None = None
223
229
  # {interface_id, client}
224
230
  self._clients: Final[dict[str, hmcl.Client]] = {}
@@ -344,6 +350,11 @@ class CentralUnit(LogContextMixin, PayloadMixin):
344
350
  """Return parameter_visibility cache."""
345
351
  return self._parameter_visibility
346
352
 
353
+ @property
354
+ def recorder(self) -> SessionRecorder:
355
+ """Return the session recorder."""
356
+ return self._recorder
357
+
347
358
  @property
348
359
  def poll_clients(self) -> tuple[hmcl.Client, ...]:
349
360
  """Return clients that need to poll data."""
@@ -458,10 +469,13 @@ class CentralUnit(LogContextMixin, PayloadMixin):
458
469
  return channel
459
470
  return None
460
471
 
461
- async def save_caches(
462
- self, *, save_device_descriptions: bool = False, save_paramset_descriptions: bool = False
472
+ async def save_files(
473
+ self,
474
+ *,
475
+ save_device_descriptions: bool = False,
476
+ save_paramset_descriptions: bool = False,
463
477
  ) -> None:
464
- """Save persistent caches."""
478
+ """Save persistent files to disk."""
465
479
  if save_device_descriptions:
466
480
  await self._device_descriptions.save()
467
481
  if save_paramset_descriptions:
@@ -479,6 +493,15 @@ class CentralUnit(LogContextMixin, PayloadMixin):
479
493
  _LOGGER.debug("START: Central %s already started", self.name)
480
494
  return
481
495
 
496
+ if self._config.start_recorder:
497
+ await self._recorder.deactivate(
498
+ delay=self._config.start_recorder_for_minutes * 60,
499
+ auto_save=True,
500
+ randomize_output=True,
501
+ use_ts_in_filename=False,
502
+ )
503
+ _LOGGER.debug("START: Starting Recorder for %s minutes", self._config.start_recorder_for_minutes)
504
+
482
505
  self._state = CentralUnitState.INITIALIZING
483
506
  _LOGGER.debug("START: Initializing Central %s", self.name)
484
507
  if self._config.enabled_interface_configs and (
@@ -536,7 +559,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
536
559
  self._state = CentralUnitState.STOPPING
537
560
  _LOGGER.debug("STOP: Stopping Central %s", self.name)
538
561
 
539
- await self.save_caches(save_device_descriptions=True, save_paramset_descriptions=True)
562
+ await self.save_files(save_device_descriptions=True, save_paramset_descriptions=True)
540
563
  self._stop_scheduler()
541
564
  await self._stop_clients()
542
565
  if self._json_rpc_client and self._json_rpc_client.is_activated:
@@ -952,13 +975,13 @@ class CentralUnit(LogContextMixin, PayloadMixin):
952
975
  return len(self._clients) > 0
953
976
 
954
977
  async def _load_caches(self) -> bool:
955
- """Load files to caches."""
978
+ """Load files to store."""
956
979
  if DataOperationResult.LOAD_FAIL in (
957
980
  await self._device_descriptions.load(),
958
981
  await self._paramset_descriptions.load(),
959
982
  ):
960
- _LOGGER.warning("LOAD_CACHES failed: Unable to load caches for %s. Clearing files", self.name)
961
- await self.clear_caches()
983
+ _LOGGER.warning("LOAD_CACHES failed: Unable to load store for %s. Clearing files", self.name)
984
+ await self.clear_files()
962
985
  return False
963
986
  await self._device_details.load()
964
987
  await self._data_cache.load()
@@ -1047,7 +1070,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1047
1070
  for address in addresses:
1048
1071
  if device := self._devices.get(address):
1049
1072
  self.remove_device(device=device)
1050
- await self.save_caches(save_device_descriptions=True, save_paramset_descriptions=True)
1073
+ await self.save_files(save_device_descriptions=True, save_paramset_descriptions=True)
1051
1074
 
1052
1075
  @callback_backend_system(system_event=BackendSystemEvent.NEW_DEVICES)
1053
1076
  async def add_new_devices(self, *, interface_id: str, device_descriptions: tuple[DeviceDescription, ...]) -> None:
@@ -1149,7 +1172,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1149
1172
  extract_exc_args(exc=exc),
1150
1173
  )
1151
1174
 
1152
- await self.save_caches(
1175
+ await self.save_files(
1153
1176
  save_device_descriptions=save_descriptions,
1154
1177
  save_paramset_descriptions=save_descriptions,
1155
1178
  )
@@ -1607,10 +1630,11 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1607
1630
  )
1608
1631
  return candidates
1609
1632
 
1610
- async def clear_caches(self) -> None:
1611
- """Clear all stored data."""
1633
+ async def clear_files(self) -> None:
1634
+ """Remove all stored files and caches."""
1612
1635
  await self._device_descriptions.clear()
1613
1636
  await self._paramset_descriptions.clear()
1637
+ await self._recorder.clear()
1614
1638
  self._device_details.clear()
1615
1639
  self._data_cache.clear()
1616
1640
 
@@ -1988,7 +2012,8 @@ class CentralConfig:
1988
2012
  periodic_refresh_interval: int = DEFAULT_PERIODIC_REFRESH_INTERVAL,
1989
2013
  program_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_PROGRAM_MARKERS,
1990
2014
  start_direct: bool = False,
1991
- storage_folder: str = DEFAULT_STORAGE_FOLDER,
2015
+ start_recorder_for_minutes: int = 0,
2016
+ storage_directory: str = DEFAULT_STORAGE_DIRECTORY,
1992
2017
  sys_scan_interval: int = DEFAULT_SYS_SCAN_INTERVAL,
1993
2018
  sysvar_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_SYSVAR_MARKERS,
1994
2019
  tls: bool = DEFAULT_TLS,
@@ -2023,7 +2048,9 @@ class CentralConfig:
2023
2048
  self.periodic_refresh_interval = periodic_refresh_interval
2024
2049
  self.program_markers: Final = program_markers
2025
2050
  self.start_direct: Final = start_direct
2026
- self.storage_folder: Final = storage_folder
2051
+ self.start_recorder_for_minutes: Final = start_recorder_for_minutes
2052
+ self.start_recorder = start_recorder_for_minutes > 0
2053
+ self.storage_directory: Final = storage_directory
2027
2054
  self.sys_scan_interval: Final = sys_scan_interval
2028
2055
  self.sysvar_markers: Final = sysvar_markers
2029
2056
  self.tls: Final = tls
@@ -2058,7 +2085,7 @@ class CentralConfig:
2058
2085
 
2059
2086
  @property
2060
2087
  def use_caches(self) -> bool:
2061
- """Return if caches should be used."""
2088
+ """Return if store should be used."""
2062
2089
  return self.start_direct is False
2063
2090
 
2064
2091
  def check_config(self) -> None:
@@ -2068,7 +2095,7 @@ class CentralConfig:
2068
2095
  host=self.host,
2069
2096
  username=self.username,
2070
2097
  password=self.password,
2071
- storage_folder=self.storage_folder,
2098
+ storage_directory=self.storage_directory,
2072
2099
  callback_host=self.callback_host,
2073
2100
  callback_port_xml_rpc=self.callback_port_xml_rpc,
2074
2101
  json_port=self.json_port,
@@ -2105,6 +2132,7 @@ class CentralConfig:
2105
2132
  client_session=self.client_session,
2106
2133
  tls=self.tls,
2107
2134
  verify_tls=self.verify_tls,
2135
+ session_recorder=central.recorder,
2108
2136
  )
2109
2137
 
2110
2138
 
@@ -2122,7 +2150,7 @@ class CentralConnectionState:
2122
2150
  self._json_issues.append(iid)
2123
2151
  _LOGGER.debug("add_issue: add issue [%s] for JsonRpcAioHttpClient", iid)
2124
2152
  return True
2125
- if isinstance(issuer, AioXmlRpcProxy) and iid not in self._rpc_proxy_issues:
2153
+ if isinstance(issuer, BaseRpcProxy) and iid not in self._rpc_proxy_issues:
2126
2154
  self._rpc_proxy_issues.append(iid)
2127
2155
  _LOGGER.debug("add_issue: add issue [%s] for %s", iid, issuer.interface_id)
2128
2156
  return True
@@ -2134,7 +2162,7 @@ class CentralConnectionState:
2134
2162
  self._json_issues.remove(iid)
2135
2163
  _LOGGER.debug("remove_issue: removing issue [%s] for JsonRpcAioHttpClient", iid)
2136
2164
  return True
2137
- if isinstance(issuer, AioXmlRpcProxy) and issuer.interface_id in self._rpc_proxy_issues:
2165
+ if isinstance(issuer, BaseRpcProxy) and issuer.interface_id in self._rpc_proxy_issues:
2138
2166
  self._rpc_proxy_issues.remove(iid)
2139
2167
  _LOGGER.debug("remove_issue: removing issue [%s] for %s", iid, issuer.interface_id)
2140
2168
  return True
@@ -2144,7 +2172,7 @@ class CentralConnectionState:
2144
2172
  """Add issue to collection."""
2145
2173
  if isinstance(issuer, AioJsonRpcAioHttpClient):
2146
2174
  return iid in self._json_issues
2147
- if isinstance(issuer, (AioXmlRpcProxy)):
2175
+ if isinstance(issuer, (BaseRpcProxy)):
2148
2176
  return iid in self._rpc_proxy_issues
2149
2177
 
2150
2178
  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,7 +55,7 @@ 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,
@@ -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:
@@ -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)}"
aiohomematic/const.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
  Constants used by aiohomematic.
5
5
 
@@ -19,7 +19,7 @@ import sys
19
19
  from types import MappingProxyType
20
20
  from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
21
21
 
22
- VERSION: Final = "2025.10.8"
22
+ VERSION: Final = "2025.10.9"
23
23
 
24
24
  # Detect test speedup mode via environment
25
25
  _TEST_SPEEDUP: Final = (
@@ -27,7 +27,7 @@ _TEST_SPEEDUP: Final = (
27
27
  )
28
28
 
29
29
  # default
30
- DEFAULT_STORAGE_FOLDER: Final = "aiohomematic_storage"
30
+ DEFAULT_STORAGE_DIRECTORY: Final = "aiohomematic_storage"
31
31
  DEFAULT_CUSTOM_ID: Final = "custom_id"
32
32
  DEFAULT_DELAY_NEW_DEVICE_CREATION: Final = False
33
33
  DEFAULT_ENABLE_DEVICE_FIRMWARE_CHECK: Final = False
@@ -49,6 +49,12 @@ DEFAULT_UN_IGNORES: Final[frozenset[str]] = frozenset()
49
49
  DEFAULT_USE_GROUP_CHANNEL_FOR_COVER_STATE: Final = True
50
50
  DEFAULT_VERIFY_TLS: Final = False
51
51
 
52
+ MANU_TEMP_CUSTOM_ID: Final = "manu_temp"
53
+ INTERNAL_CUSTOM_IDS: Final[tuple[str, ...]] = (
54
+ DEFAULT_CUSTOM_ID,
55
+ MANU_TEMP_CUSTOM_ID,
56
+ )
57
+
52
58
  # Default encoding for json service calls, persistent cache
53
59
  UTF_8: Final = "utf-8"
54
60
  # Default encoding for xmlrpc service calls and script files
@@ -82,7 +88,7 @@ SYSVAR_ENABLE_DEFAULT: Final[frozenset[str]] = ALWAYS_ENABLE_SYSVARS_BY_ID
82
88
 
83
89
  ADDRESS_SEPARATOR: Final = ":"
84
90
  BLOCK_LOG_TIMEOUT: Final = 60
85
- CACHE_PATH: Final = "cache"
91
+ CONTENT_PATH: Final = "cache"
86
92
  CONF_PASSWORD: Final = "password"
87
93
  CONF_USERNAME: Final = "username"
88
94
 
@@ -94,8 +100,12 @@ DEVICE_FIRMWARE_CHECK_INTERVAL: Final = 21600 # 6h
94
100
  DEVICE_FIRMWARE_DELIVERING_CHECK_INTERVAL: Final = 3600 # 1h
95
101
  DEVICE_FIRMWARE_UPDATING_CHECK_INTERVAL: Final = 300 # 5m
96
102
  DUMMY_SERIAL: Final = "SN0815"
97
- FILE_DEVICES: Final = "homematic_devices.json"
98
- FILE_PARAMSETS: Final = "homematic_paramsets.json"
103
+ FILE_DEVICES: Final = "homematic_devices"
104
+ FILE_PARAMSETS: Final = "homematic_paramsets"
105
+ FILE_SESSION_RECORDER: Final = "homematic_session_recorder"
106
+ FILE_NAME_TS_PATTERN: Final = "%Y%m%d_%H%M%S"
107
+ SUB_DIRECTORY_CACHE: Final = "cache"
108
+ SUB_DIRECTORY_SESSION: Final = "session"
99
109
  HUB_PATH: Final = "hub"
100
110
  IDENTIFIER_SEPARATOR: Final = "@"
101
111
  INIT_DATETIME: Final = datetime.strptime("01.01.1970 00:00:00", DATETIME_FORMAT)
@@ -499,6 +509,13 @@ class RegaScript(StrEnum):
499
509
  SET_SYSTEM_VARIABLE: Final = "set_system_variable.fn"
500
510
 
501
511
 
512
+ class RPCType(StrEnum):
513
+ """Enum with Homematic rpc types."""
514
+
515
+ XML_RPC = "xmlrpc"
516
+ JSON_RPC = "jsonrpc"
517
+
518
+
502
519
  class Interface(StrEnum):
503
520
  """Enum with Homematic interfaces."""
504
521
 
aiohomematic/context.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
  Collection of context variables.
5
5
 
aiohomematic/converter.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
  Converters used by aiohomematic.
5
5