aiohomematic-test-support 2025.10.14__py3-none-any.whl → 2025.10.16__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-test-support might be problematic. Click here for more details.

@@ -1,2 +1,2 @@
1
- __version__ = "2025.10.14"
1
+ __version__ = "2025.10.16"
2
2
  """Module to support aiohomematic testing with a local client."""
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from aiohomematic.client.json_rpc import _JsonKey
5
6
  from aiohomematic.const import LOCAL_HOST, Interface, ProgramData, SystemVariableData, SysvarType
6
7
 
7
8
  CENTRAL_NAME = "CentralTest"
@@ -32,7 +33,7 @@ SYSVAR_DATA: list[SystemVariableData] = [
32
33
  SystemVariableData(
33
34
  vid="2",
34
35
  legacy_name="alarm_ext",
35
- description="",
36
+ description="HAHM",
36
37
  data_type=SysvarType.ALARM,
37
38
  unit=None,
38
39
  value=False,
@@ -56,7 +57,7 @@ SYSVAR_DATA: list[SystemVariableData] = [
56
57
  SystemVariableData(
57
58
  vid="4",
58
59
  legacy_name="logic_ext",
59
- description="",
60
+ description="HAHM",
60
61
  data_type=SysvarType.LOGIC,
61
62
  unit=None,
62
63
  value=False,
@@ -80,7 +81,7 @@ SYSVAR_DATA: list[SystemVariableData] = [
80
81
  SystemVariableData(
81
82
  vid="6",
82
83
  legacy_name="list_ext",
83
- description="",
84
+ description="HAHM",
84
85
  data_type=SysvarType.LIST,
85
86
  unit=None,
86
87
  value=0,
@@ -104,7 +105,7 @@ SYSVAR_DATA: list[SystemVariableData] = [
104
105
  SystemVariableData(
105
106
  vid="8",
106
107
  legacy_name="string_ext",
107
- description="",
108
+ description="HAHM",
108
109
  data_type=SysvarType.STRING,
109
110
  unit=None,
110
111
  value="test1",
@@ -128,7 +129,7 @@ SYSVAR_DATA: list[SystemVariableData] = [
128
129
  SystemVariableData(
129
130
  vid="10",
130
131
  legacy_name="float_ext",
131
- description="",
132
+ description="HAHM",
132
133
  data_type=SysvarType.FLOAT,
133
134
  unit="°C",
134
135
  value=23.2,
@@ -152,7 +153,7 @@ SYSVAR_DATA: list[SystemVariableData] = [
152
153
  SystemVariableData(
153
154
  vid="12",
154
155
  legacy_name="integer_ext",
155
- description="",
156
+ description="HAHM",
156
157
  data_type=SysvarType.INTEGER,
157
158
  unit=None,
158
159
  value=17,
@@ -163,6 +164,31 @@ SYSVAR_DATA: list[SystemVariableData] = [
163
164
  ),
164
165
  ]
165
166
 
167
+
168
+ SYSVAR_DATA_JSON = [
169
+ {
170
+ _JsonKey.ID: sv.vid,
171
+ _JsonKey.IS_INTERNAL: False,
172
+ _JsonKey.MAX_VALUE: sv.max_value,
173
+ _JsonKey.MIN_VALUE: sv.min_value,
174
+ _JsonKey.NAME: sv.legacy_name,
175
+ _JsonKey.TYPE: sv.data_type,
176
+ _JsonKey.UNIT: sv.unit,
177
+ _JsonKey.VALUE: sv.value,
178
+ _JsonKey.VALUE_LIST: ";".join(sv.values) if sv.values else None,
179
+ }
180
+ for sv in SYSVAR_DATA
181
+ ]
182
+ SYSVAR_DATA_JSON_DESCRIPTION = [
183
+ {
184
+ _JsonKey.ID: sv.vid,
185
+ _JsonKey.DESCRIPTION: sv.description,
186
+ }
187
+ for sv in SYSVAR_DATA
188
+ ]
189
+
190
+ SYSVAR_DATA_XML = {sv.legacy_name: sv.value for sv in SYSVAR_DATA}
191
+
166
192
  PROGRAM_DATA: list[ProgramData] = [
167
193
  ProgramData(
168
194
  legacy_name="p1",
@@ -181,7 +207,23 @@ PROGRAM_DATA: list[ProgramData] = [
181
207
  last_execute_time="",
182
208
  ),
183
209
  ]
184
-
210
+ PROGRAM_DATA_JSON = [
211
+ {
212
+ _JsonKey.ID: p.pid,
213
+ _JsonKey.IS_ACTIVE: p.is_active,
214
+ _JsonKey.IS_INTERNAL: p.is_internal,
215
+ _JsonKey.LAST_EXECUTE_TIME: p.last_execute_time,
216
+ _JsonKey.NAME: p.legacy_name,
217
+ }
218
+ for p in PROGRAM_DATA
219
+ ]
220
+ PROGRAM_DATA_JSON_DESCRIPTION = [
221
+ {
222
+ _JsonKey.ID: p.pid,
223
+ _JsonKey.DESCRIPTION: p.description,
224
+ }
225
+ for p in PROGRAM_DATA
226
+ ]
185
227
 
186
228
  ADDRESS_DEVICE_TRANSLATION = {
187
229
  "VCU3432945": "HmIP-STV.json",
@@ -10,7 +10,7 @@ import importlib.resources
10
10
  import json
11
11
  import logging
12
12
  import os
13
- from typing import Any, Final, cast
13
+ from typing import Any, Self, cast
14
14
  from unittest.mock import MagicMock, Mock, patch
15
15
  import zipfile
16
16
 
@@ -30,173 +30,92 @@ from aiohomematic.const import (
30
30
  Parameter,
31
31
  ParamsetKey,
32
32
  RPCType,
33
- SourceOfDeviceCreation,
34
33
  )
35
34
  from aiohomematic.model.custom import CustomDataPoint
36
35
  from aiohomematic.store.persistent import _freeze_params, _unfreeze_params
37
36
  from aiohomematic_test_support import const
38
- from aiohomematic_test_support.client_local import ClientLocal, LocalRessources
39
37
 
40
38
  _LOGGER = logging.getLogger(__name__)
41
39
 
42
- EXCLUDE_METHODS_FROM_MOCKS: Final[list[str]] = []
43
- INCLUDE_PROPERTIES_IN_MOCKS: Final[list[str]] = []
44
-
45
40
 
46
41
  # pylint: disable=protected-access
47
- class FactoryWithLocalClient:
42
+ class FactoryWithClient:
48
43
  """Factory for a central with one local client."""
49
44
 
50
- def __init__(self, *, client_session: ClientSession | None = None):
51
- """Init the central factory."""
52
- self._client_session = client_session
53
- self.system_event_mock = MagicMock()
54
- self.ha_event_mock = MagicMock()
55
-
56
- async def get_raw_central(
57
- self,
58
- *,
59
- interface_config: InterfaceConfig | None,
60
- un_ignore_list: list[str] | None = None,
61
- ignore_custom_device_definition_models: list[str] | None = None,
62
- ) -> CentralUnit:
63
- """Return a central based on give address_device_translation."""
64
- interface_configs = {interface_config} if interface_config else set()
65
- central = CentralConfig(
66
- name=const.CENTRAL_NAME,
67
- host=const.CCU_HOST,
68
- username=const.CCU_USERNAME,
69
- password=const.CCU_PASSWORD,
70
- central_id="test1234",
71
- interface_configs=interface_configs,
72
- client_session=self._client_session,
73
- un_ignore_list=frozenset(un_ignore_list or []),
74
- ignore_custom_device_definition_models=frozenset(ignore_custom_device_definition_models or []),
75
- start_direct=True,
76
- ).create_central()
77
-
78
- central.register_backend_system_callback(cb=self.system_event_mock)
79
- central.register_homematic_callback(cb=self.ha_event_mock)
80
-
81
- return central
82
-
83
- async def get_unpatched_default_central(
45
+ def __init__(
84
46
  self,
85
47
  *,
86
- port: int,
87
- address_device_translation: dict[str, str],
48
+ player: SessionPlayer,
49
+ address_device_translation: set[str] | None = None,
88
50
  do_mock_client: bool = True,
89
- ignore_devices_on_create: list[str] | None = None,
90
- un_ignore_list: list[str] | None = None,
51
+ exclude_methods_from_mocks: set[str] | None = None,
91
52
  ignore_custom_device_definition_models: list[str] | None = None,
92
- ) -> tuple[CentralUnit, Client | Mock]:
93
- """Return a central based on give address_device_translation."""
94
- interface_config = InterfaceConfig(
95
- central_name=const.CENTRAL_NAME,
96
- interface=Interface.BIDCOS_RF,
97
- port=port,
98
- )
99
-
100
- central = await self.get_raw_central(
101
- interface_config=interface_config,
102
- un_ignore_list=un_ignore_list,
103
- ignore_custom_device_definition_models=ignore_custom_device_definition_models,
104
- )
105
-
106
- _client = ClientLocal(
107
- client_config=ClientConfig(
108
- central=central,
109
- interface_config=interface_config,
110
- ),
111
- local_resources=LocalRessources(
112
- address_device_translation=address_device_translation,
113
- ignore_devices_on_create=ignore_devices_on_create if ignore_devices_on_create else [],
114
- ),
115
- )
116
- await _client.init_client()
117
- client = get_mock(_client) if do_mock_client else _client
118
-
119
- assert central
120
- assert client
121
- return central, client
122
-
123
- async def get_default_central(
124
- self,
125
- *,
126
- port: int = const.CCU_MINI_PORT,
127
- address_device_translation: dict[str, str],
128
- do_mock_client: bool = True,
129
- add_sysvars: bool = False,
130
- add_programs: bool = False,
131
53
  ignore_devices_on_create: list[str] | None = None,
54
+ include_properties_in_mocks: set[str] | None = None,
55
+ interface_configs: set[InterfaceConfig] | None = None,
132
56
  un_ignore_list: list[str] | None = None,
133
- ignore_custom_device_definition_models: list[str] | None = None,
134
- ) -> tuple[CentralUnit, Client | Mock]:
135
- """Return a central based on give address_device_translation."""
136
- central, client = await self.get_unpatched_default_central(
137
- port=port,
57
+ ) -> None:
58
+ """Init the central factory."""
59
+ self._player = player
60
+ self.init(
138
61
  address_device_translation=address_device_translation,
139
- do_mock_client=True,
62
+ do_mock_client=do_mock_client,
63
+ exclude_methods_from_mocks=exclude_methods_from_mocks,
64
+ ignore_custom_device_definition_models=ignore_custom_device_definition_models,
140
65
  ignore_devices_on_create=ignore_devices_on_create,
66
+ include_properties_in_mocks=include_properties_in_mocks,
67
+ interface_configs=interface_configs,
141
68
  un_ignore_list=un_ignore_list,
142
- ignore_custom_device_definition_models=ignore_custom_device_definition_models,
143
69
  )
70
+ self.system_event_mock = MagicMock()
71
+ self.ha_event_mock = MagicMock()
144
72
 
145
- patch("aiohomematic.central.CentralUnit._get_primary_client", return_value=client).start()
146
- patch("aiohomematic.client.ClientConfig.create_client", return_value=client).start()
147
- patch(
148
- "aiohomematic_test_support.client_local.ClientLocal.get_all_system_variables",
149
- return_value=const.SYSVAR_DATA if add_sysvars else [],
150
- ).start()
151
- patch(
152
- "aiohomematic_test_support.client_local.ClientLocal.get_all_programs",
153
- return_value=const.PROGRAM_DATA if add_programs else [],
154
- ).start()
155
- patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST).start()
156
-
157
- await central.start()
158
- if new_device_addresses := central._check_for_new_device_addresses():
159
- await central._create_devices(new_device_addresses=new_device_addresses, source=SourceOfDeviceCreation.INIT)
160
- await central._init_hub()
161
-
162
- assert central
163
- assert client
164
- return central, client
165
-
166
-
167
- class FactoryWithClient:
168
- """Factory for a central with one local client."""
169
-
170
- def __init__(
73
+ def init(
171
74
  self,
172
75
  *,
173
- recorder: SessionPlayer,
174
- address_device_translation: dict[str, str] | None = None,
76
+ address_device_translation: set[str] | None = None,
77
+ do_mock_client: bool = True,
78
+ exclude_methods_from_mocks: set[str] | None = None,
79
+ ignore_custom_device_definition_models: list[str] | None = None,
175
80
  ignore_devices_on_create: list[str] | None = None,
176
- ):
81
+ include_properties_in_mocks: set[str] | None = None,
82
+ interface_configs: set[InterfaceConfig] | None = None,
83
+ un_ignore_list: list[str] | None = None,
84
+ ) -> Self:
177
85
  """Init the central factory."""
86
+ self._address_device_translation = address_device_translation
87
+ self._do_mock_client = do_mock_client
88
+ self._exclude_methods_from_mocks = exclude_methods_from_mocks
89
+ self._ignore_custom_device_definition_models = ignore_custom_device_definition_models
90
+ self._ignore_devices_on_create = ignore_devices_on_create
91
+ self._include_properties_in_mocks = include_properties_in_mocks
92
+ self._interface_configs = (
93
+ interface_configs
94
+ if interface_configs is not None
95
+ else {
96
+ InterfaceConfig(
97
+ central_name=const.CENTRAL_NAME,
98
+ interface=Interface.BIDCOS_RF,
99
+ port=2001,
100
+ )
101
+ }
102
+ )
103
+ self._un_ignore_list = frozenset(un_ignore_list or [])
178
104
  self._client_session = _get_client_session(
179
- recorder=recorder,
180
- address_device_translation=address_device_translation,
181
- ignore_devices_on_create=ignore_devices_on_create,
105
+ player=self._player,
106
+ address_device_translation=self._address_device_translation,
107
+ ignore_devices_on_create=self._ignore_devices_on_create,
182
108
  )
183
109
  self._xml_proxy = _get_xml_rpc_proxy(
184
- recorder=recorder,
185
- address_device_translation=address_device_translation,
186
- ignore_devices_on_create=ignore_devices_on_create,
110
+ player=self._player,
111
+ address_device_translation=self._address_device_translation,
112
+ ignore_devices_on_create=self._ignore_devices_on_create,
187
113
  )
188
- self.system_event_mock = MagicMock()
189
- self.ha_event_mock = MagicMock()
114
+ return self
190
115
 
191
- async def get_raw_central(
192
- self,
193
- *,
194
- interface_config: InterfaceConfig | None,
195
- un_ignore_list: list[str] | None = None,
196
- ignore_custom_device_definition_models: list[str] | None = None,
197
- ) -> CentralUnit:
116
+ async def get_raw_central(self) -> CentralUnit:
198
117
  """Return a central based on give address_device_translation."""
199
- interface_configs = {interface_config} if interface_config else set()
118
+ interface_configs = self._interface_configs if self._interface_configs else set()
200
119
  central = CentralConfig(
201
120
  name=const.CENTRAL_NAME,
202
121
  host=const.CCU_HOST,
@@ -205,81 +124,57 @@ class FactoryWithClient:
205
124
  central_id="test1234",
206
125
  interface_configs=interface_configs,
207
126
  client_session=self._client_session,
208
- un_ignore_list=frozenset(un_ignore_list or []),
209
- ignore_custom_device_definition_models=frozenset(ignore_custom_device_definition_models or []),
127
+ un_ignore_list=self._un_ignore_list,
128
+ ignore_custom_device_definition_models=frozenset(self._ignore_custom_device_definition_models or []),
210
129
  start_direct=True,
211
130
  ).create_central()
212
131
 
213
132
  central.register_backend_system_callback(cb=self.system_event_mock)
214
133
  central.register_homematic_callback(cb=self.ha_event_mock)
215
134
 
216
- return central
217
-
218
- async def get_unpatched_default_central(
219
- self,
220
- *,
221
- un_ignore_list: list[str] | None = None,
222
- ignore_custom_device_definition_models: list[str] | None = None,
223
- ) -> CentralUnit:
224
- """Return a central based on give address_device_translation."""
225
- interface_config = InterfaceConfig(
226
- central_name=const.CENTRAL_NAME,
227
- interface=Interface.BIDCOS_RF,
228
- port=2001,
229
- )
230
-
231
- central = await self.get_raw_central(
232
- interface_config=interface_config,
233
- un_ignore_list=un_ignore_list,
234
- ignore_custom_device_definition_models=ignore_custom_device_definition_models,
235
- )
236
-
237
135
  assert central
238
136
  self._client_session.set_central(central=central) # type: ignore[attr-defined]
239
137
  self._xml_proxy.set_central(central=central)
240
138
  return central
241
139
 
242
- async def get_default_central(
243
- self,
244
- *,
245
- do_mock_client: bool = True,
246
- un_ignore_list: list[str] | None = None,
247
- ignore_custom_device_definition_models: list[str] | None = None,
248
- ) -> tuple[CentralUnit, Client | Mock]:
140
+ async def get_default_central(self, *, start: bool = True) -> CentralUnit:
249
141
  """Return a central based on give address_device_translation."""
250
- central = await self.get_unpatched_default_central(
251
- un_ignore_list=un_ignore_list,
252
- ignore_custom_device_definition_models=ignore_custom_device_definition_models,
253
- )
142
+ central = await self.get_raw_central()
254
143
 
255
144
  await self._xml_proxy.do_init()
256
145
  patch("aiohomematic.client.ClientConfig._create_xml_rpc_proxy", return_value=self._xml_proxy).start()
257
146
  patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST).start()
258
147
 
259
148
  # Optionally patch client creation to return a mocked client
260
- if do_mock_client:
149
+ if self._do_mock_client:
261
150
  _orig_create_client = ClientConfig.create_client
262
151
 
263
- async def _mocked_create_client(self: ClientConfig) -> Client | Mock:
264
- real_client = await _orig_create_client(self)
265
- return cast(Mock, get_mock(real_client))
152
+ async def _mocked_create_client(config: ClientConfig) -> Client | Mock:
153
+ real_client = await _orig_create_client(config)
154
+ return cast(
155
+ Mock,
156
+ get_mock(
157
+ instance=real_client,
158
+ exclude_methods=self._exclude_methods_from_mocks,
159
+ include_properties=self._include_properties_in_mocks,
160
+ ),
161
+ )
266
162
 
267
163
  patch("aiohomematic.client.ClientConfig.create_client", _mocked_create_client).start()
268
164
 
269
- await central.start()
270
-
271
- client = central.primary_client
165
+ if start:
166
+ await central.start()
167
+ await central._init_hub()
272
168
  assert central
273
- assert client
274
- return central, client
169
+ return central
275
170
 
276
171
 
277
- def _get_not_mockable_method_names(instance: Any) -> set[str]:
172
+ def _get_not_mockable_method_names(instance: Any, exclude_methods: set[str]) -> set[str]:
278
173
  """Return all relevant method names for mocking."""
279
174
  methods: set[str] = set(_get_properties(data_object=instance, decorator=property))
280
175
 
281
176
  for method in dir(instance):
282
- if method in EXCLUDE_METHODS_FROM_MOCKS:
177
+ if method in exclude_methods:
283
178
  methods.add(method)
284
179
  return methods
285
180
 
@@ -306,26 +201,26 @@ def _load_json_file(anchor: str, resource: str, file_name: str) -> Any | None:
306
201
  return orjson.loads(fptr.read())
307
202
 
308
203
 
309
- def _get_client_session(
204
+ def _get_client_session( # noqa: C901
310
205
  *,
311
- recorder: SessionPlayer,
312
- address_device_translation: dict[str, str] | None = None,
206
+ player: SessionPlayer,
207
+ address_device_translation: set[str] | None = None,
313
208
  ignore_devices_on_create: list[str] | None = None,
314
209
  ) -> ClientSession:
315
210
  """
316
- Provide a ClientSession-like fixture that answers via SimpleSessionRecorder (JSON-RPC).
211
+ Provide a ClientSession-like fixture that answers via SessionPlayer(JSON-RPC).
317
212
 
318
213
  Any POST request will be answered by looking up the latest recorded
319
- JSON-RPC response in the session recorder using the provided method and params.
214
+ JSON-RPC response in the session player using the provided method and params.
320
215
  """
321
216
 
322
217
  class _MockResponse:
323
218
  def __init__(self, json_data: dict | None) -> None:
324
219
  # If no match is found, emulate backend error payload
325
220
  self._json = json_data or {
326
- "result": None,
327
- "error": {"name": "-1", "code": -1, "message": "Not found in session recorder"},
328
- "id": 0,
221
+ _JsonKey.RESULT: None,
222
+ _JsonKey.ERROR: {"name": "-1", "code": -1, "message": "Not found in session player"},
223
+ _JsonKey.ID: 0,
329
224
  }
330
225
  self.status = 200
331
226
 
@@ -365,6 +260,32 @@ def _get_client_session(
365
260
  params = payload.get("params")
366
261
 
367
262
  if self._central:
263
+ if method in (
264
+ _JsonRpcMethod.PROGRAM_EXECUTE,
265
+ _JsonRpcMethod.SYSVAR_SET_BOOL,
266
+ _JsonRpcMethod.SYSVAR_SET_FLOAT,
267
+ _JsonRpcMethod.SESSION_LOGOUT,
268
+ ):
269
+ return _MockResponse({_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
270
+ if method == _JsonRpcMethod.SYSVAR_GET_ALL:
271
+ return _MockResponse(
272
+ {_JsonKey.ID: 0, _JsonKey.RESULT: const.SYSVAR_DATA_JSON, _JsonKey.ERROR: None}
273
+ )
274
+ if method == _JsonRpcMethod.PROGRAM_GET_ALL:
275
+ return _MockResponse(
276
+ {_JsonKey.ID: 0, _JsonKey.RESULT: const.PROGRAM_DATA_JSON, _JsonKey.ERROR: None}
277
+ )
278
+ if method == _JsonRpcMethod.REGA_RUN_SCRIPT:
279
+ if "get_program_descriptions" in params[_JsonKey.SCRIPT]:
280
+ return _MockResponse(
281
+ {_JsonKey.ID: 0, _JsonKey.RESULT: const.PROGRAM_DATA_JSON_DESCRIPTION, _JsonKey.ERROR: None}
282
+ )
283
+
284
+ if "get_system_variable_descriptions" in params[_JsonKey.SCRIPT]:
285
+ return _MockResponse(
286
+ {_JsonKey.ID: 0, _JsonKey.RESULT: const.SYSVAR_DATA_JSON_DESCRIPTION, _JsonKey.ERROR: None}
287
+ )
288
+
368
289
  if method == _JsonRpcMethod.INTERFACE_SET_VALUE:
369
290
  await self._central.data_point_event(
370
291
  interface_id=params[_JsonKey.INTERFACE],
@@ -372,7 +293,7 @@ def _get_client_session(
372
293
  parameter=params[_JsonKey.VALUE_KEY],
373
294
  value=params[_JsonKey.VALUE],
374
295
  )
375
- return _MockResponse({"result": "200"})
296
+ return _MockResponse({_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
376
297
  if method == _JsonRpcMethod.INTERFACE_PUT_PARAMSET:
377
298
  if params[_JsonKey.PARAMSET_KEY] == ParamsetKey.VALUES:
378
299
  interface_id = params[_JsonKey.INTERFACE]
@@ -385,19 +306,29 @@ def _get_client_session(
385
306
  parameter=param,
386
307
  value=value,
387
308
  )
388
- return _MockResponse({"result": "200"})
309
+ return _MockResponse({_JsonKey.RESULT: "200", _JsonKey.ERROR: None})
389
310
 
390
- json_data = recorder.get_latest_response_by_params(
311
+ json_data = player.get_latest_response_by_params(
391
312
  rpc_type=RPCType.JSON_RPC,
392
313
  method=str(method) if method is not None else "",
393
314
  params=params,
394
315
  )
395
- # if method == _JsonRpcMethod.INTERFACE_LIST_DEVICES:
396
- # if address_device_translation:
397
- # devices: dict[str, Any] = {}
398
- # for address, device in json_data["result"].items():
399
- # if address in address_device_translation:
400
- # device["ADDRESS"] = address_device_translation[address]
316
+ if method == _JsonRpcMethod.INTERFACE_LIST_DEVICES and (
317
+ ignore_devices_on_create is not None or address_device_translation is not None
318
+ ):
319
+ new_devices = []
320
+ for dd in json_data[_JsonKey.RESULT]:
321
+ if ignore_devices_on_create is not None and (
322
+ dd["address"] in ignore_devices_on_create or dd["parent"] in ignore_devices_on_create
323
+ ):
324
+ continue
325
+ if address_device_translation is not None:
326
+ if dd["address"] in address_device_translation or dd["parent"] in address_device_translation:
327
+ new_devices.append(dd)
328
+ else:
329
+ new_devices.append(dd)
330
+
331
+ json_data[_JsonKey.RESULT] = new_devices
401
332
  return _MockResponse(json_data)
402
333
 
403
334
  async def close(self) -> None: # compatibility
@@ -408,16 +339,16 @@ def _get_client_session(
408
339
 
409
340
  def _get_xml_rpc_proxy( # noqa: C901
410
341
  *,
411
- recorder: SessionPlayer,
412
- address_device_translation: dict[str, str] | None = None,
342
+ player: SessionPlayer,
343
+ address_device_translation: set[str] | None = None,
413
344
  ignore_devices_on_create: list[str] | None = None,
414
345
  ) -> BaseRpcProxy:
415
346
  """
416
- Provide an BaseRpcProxy-like fixture that answers via SimpleSessionRecorder (XML-RPC).
347
+ Provide an BaseRpcProxy-like fixture that answers via SessionPlayer (XML-RPC).
417
348
 
418
349
  Any method call like: await proxy.system.listMethods(...)
419
350
  will be answered by looking up the latest recorded XML-RPC response
420
- in the session recorder using the provided method and positional params.
351
+ in the session player using the provided method and positional params.
421
352
  """
422
353
 
423
354
  class _Method:
@@ -433,9 +364,9 @@ def _get_xml_rpc_proxy( # noqa: C901
433
364
  # Forward to caller with collected method name and positional params
434
365
  return await self._caller(self._name, *args)
435
366
 
436
- class _AioXmlRpcProxyFromRecorder:
367
+ class _AioXmlRpcProxyFromSession:
437
368
  def __init__(self) -> None:
438
- self._recorder = recorder
369
+ self._player = player
439
370
  self._supported_methods: tuple[str, ...] = ()
440
371
  self._central: CentralUnit | None = None
441
372
 
@@ -448,6 +379,20 @@ def _get_xml_rpc_proxy( # noqa: C901
448
379
  """Return the supported methods."""
449
380
  return self._supported_methods
450
381
 
382
+ async def getAllSystemVariables(self) -> dict[str, Any]:
383
+ """Return all system variables."""
384
+ return const.SYSVAR_DATA_XML
385
+
386
+ async def getParamset(self, channel_address: str, paramset: str) -> Any:
387
+ """Set a value."""
388
+ if self._central:
389
+ result = self._player.get_latest_response_by_params(
390
+ rpc_type=RPCType.XML_RPC,
391
+ method="getParamset",
392
+ params=(channel_address, paramset),
393
+ )
394
+ return result if result else {}
395
+
451
396
  async def setValue(self, channel_address: str, parameter: str, value: Any, rx_mode: Any | None = None) -> None:
452
397
  """Set a value."""
453
398
  if self._central:
@@ -485,7 +430,7 @@ def _get_xml_rpc_proxy( # noqa: C901
485
430
 
486
431
  async def listDevices(self) -> list[Any]:
487
432
  """Return a list of devices."""
488
- devices = self._recorder.get_latest_response_by_params(
433
+ devices = self._player.get_latest_response_by_params(
489
434
  rpc_type=RPCType.XML_RPC,
490
435
  method="listDevices",
491
436
  params="()",
@@ -495,19 +440,16 @@ def _get_xml_rpc_proxy( # noqa: C901
495
440
  if ignore_devices_on_create is None and address_device_translation is None:
496
441
  return cast(list[Any], devices)
497
442
 
498
- for device in devices:
443
+ for dd in devices:
499
444
  if ignore_devices_on_create is not None and (
500
- device["ADDRESS"] in ignore_devices_on_create or device["PARENT"] in ignore_devices_on_create
445
+ dd["ADDRESS"] in ignore_devices_on_create or dd["PARENT"] in ignore_devices_on_create
501
446
  ):
502
447
  continue
503
448
  if address_device_translation is not None:
504
- if (
505
- device["ADDRESS"] in address_device_translation
506
- or device["PARENT"] in address_device_translation
507
- ):
508
- new_devices.append(device)
449
+ if dd["ADDRESS"] in address_device_translation or dd["PARENT"] in address_device_translation:
450
+ new_devices.append(dd)
509
451
  else:
510
- new_devices.append(device)
452
+ new_devices.append(dd)
511
453
 
512
454
  return new_devices
513
455
 
@@ -517,7 +459,7 @@ def _get_xml_rpc_proxy( # noqa: C901
517
459
 
518
460
  async def _invoke(self, method: str, *args: Any) -> Any:
519
461
  params = tuple(args)
520
- return self._recorder.get_latest_response_by_params(
462
+ return self._player.get_latest_response_by_params(
521
463
  rpc_type=RPCType.XML_RPC,
522
464
  method=method,
523
465
  params=params,
@@ -533,12 +475,12 @@ def _get_xml_rpc_proxy( # noqa: C901
533
475
  supported_methods.append(_RpcMethod.PING)
534
476
  self._supported_methods = tuple(supported_methods)
535
477
 
536
- return cast(BaseRpcProxy, _AioXmlRpcProxyFromRecorder())
478
+ return cast(BaseRpcProxy, _AioXmlRpcProxyFromSession())
537
479
 
538
480
 
539
481
  async def get_central_client_factory(
540
- recorder: SessionPlayer,
541
- address_device_translation: dict[str, str],
482
+ player: SessionPlayer,
483
+ address_device_translation: set[str],
542
484
  do_mock_client: bool,
543
485
  ignore_devices_on_create: list[str] | None,
544
486
  ignore_custom_device_definition_models: list[str] | None,
@@ -546,16 +488,16 @@ async def get_central_client_factory(
546
488
  ) -> AsyncGenerator[tuple[CentralUnit, Client | Mock, FactoryWithClient]]:
547
489
  """Return central factory."""
548
490
  factory = FactoryWithClient(
549
- recorder=recorder,
491
+ player=player,
550
492
  address_device_translation=address_device_translation,
551
- ignore_devices_on_create=ignore_devices_on_create,
552
- )
553
- central_client = await factory.get_default_central(
554
493
  do_mock_client=do_mock_client,
555
494
  ignore_custom_device_definition_models=ignore_custom_device_definition_models,
495
+ ignore_devices_on_create=ignore_devices_on_create,
556
496
  un_ignore_list=un_ignore_list,
557
497
  )
558
- central, client = central_client
498
+ central = await factory.get_default_central()
499
+ client = central.primary_client
500
+ assert client
559
501
  try:
560
502
  yield central, client, factory
561
503
  finally:
@@ -563,8 +505,15 @@ async def get_central_client_factory(
563
505
  await central.clear_files()
564
506
 
565
507
 
566
- def get_mock(instance: Any, **kwargs: Any) -> Any:
508
+ def get_mock(
509
+ instance: Any, exclude_methods: set[str] | None = None, include_properties: set[str] | None = None, **kwargs: Any
510
+ ) -> Any:
567
511
  """Create a mock and copy instance attributes over mock."""
512
+ if exclude_methods is None:
513
+ exclude_methods = set()
514
+ if include_properties is None:
515
+ include_properties = set()
516
+
568
517
  if isinstance(instance, Mock):
569
518
  instance.__dict__.update(instance._mock_wraps.__dict__)
570
519
  return instance
@@ -573,8 +522,8 @@ def get_mock(instance: Any, **kwargs: Any) -> Any:
573
522
  try:
574
523
  for method_name in [
575
524
  prop
576
- for prop in _get_not_mockable_method_names(instance)
577
- if prop not in INCLUDE_PROPERTIES_IN_MOCKS and prop not in kwargs
525
+ for prop in _get_not_mockable_method_names(instance=instance, exclude_methods=exclude_methods)
526
+ if prop not in include_properties and prop not in kwargs
578
527
  ]:
579
528
  setattr(mock, method_name, getattr(instance, method_name))
580
529
  except Exception:
@@ -632,6 +581,13 @@ async def get_pydev_ccu_central_unit_full(
632
581
  return central
633
582
 
634
583
 
584
+ def load_device_description(file_name: str) -> Any:
585
+ """Load device description."""
586
+ dev_desc = _load_json_file(anchor="pydevccu", resource="device_descriptions", file_name=file_name)
587
+ assert dev_desc
588
+ return dev_desc
589
+
590
+
635
591
  async def get_session_player(*, file_name: str) -> SessionPlayer:
636
592
  """Provide a SessionPlayer preloaded from the randomized full session JSON file."""
637
593
  player = SessionPlayer(file_id=file_name)
@@ -640,13 +596,6 @@ async def get_session_player(*, file_name: str) -> SessionPlayer:
640
596
  return player
641
597
 
642
598
 
643
- def load_device_description(file_name: str) -> Any:
644
- """Load device description."""
645
- dev_desc = _load_json_file(anchor="pydevccu", resource="device_descriptions", file_name=file_name)
646
- assert dev_desc
647
- return dev_desc
648
-
649
-
650
599
  class SessionPlayer:
651
600
  """Player for sessions."""
652
601
 
@@ -655,7 +604,7 @@ class SessionPlayer:
655
604
  )
656
605
 
657
606
  def __init__(self, *, file_id: str) -> None:
658
- """Initialize the session recorder."""
607
+ """Initialize the session player."""
659
608
  self._file_id = file_id
660
609
 
661
610
  async def load(self, *, file_path: str) -> DataOperationResult:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic-test-support
3
- Version: 2025.10.14
3
+ Version: 2025.10.16
4
4
  Summary: Support-only package for AioHomematic (tests/dev). Not part of production builds.
5
5
  Author-email: SukramJ <sukramj@icloud.com>
6
6
  Project-URL: Homepage, https://github.com/SukramJ/aiohomematic
@@ -8,4 +8,5 @@ Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Intended Audience :: Developers
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
11
12
  Requires-Python: >=3.13
@@ -0,0 +1,11 @@
1
+ aiohomematic_test_support/__init__.py,sha256=AH2c33h4vDkVdRr4jNak1cH5T-nLvnncMF2pbsCqJJo,93
2
+ aiohomematic_test_support/client_local.py,sha256=gpWkbyt_iCQCwxsvKahYl4knFrzyBku5WwbR83_mgM8,12825
3
+ aiohomematic_test_support/const.py,sha256=vatushPkwtueWUMdGkEJMTU6UB0Enxy4u7IXjUrXmJo,19468
4
+ aiohomematic_test_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ aiohomematic_test_support/support.py,sha256=xFb7NRALsxE2sVo17DC_WdV6WmQWNRJhMUoHqpyHeKk,27610
6
+ aiohomematic_test_support/data/full_session_randomized_ccu.zip,sha256=oN7g0CB_0kQX3qk-RSu-Rt28yW7BcplVqPYZCDqr0EU,734626
7
+ aiohomematic_test_support/data/full_session_randomized_pydevccu.zip,sha256=_QFWSP03dkiMFdD_w-R98DS6ur4PYDQXw-DCkbJEGg4,1293240
8
+ aiohomematic_test_support-2025.10.16.dist-info/METADATA,sha256=d0IB2LBdxzCAPQsaLD_mthR-Z_VtX9Srtkm1w0yqCMY,536
9
+ aiohomematic_test_support-2025.10.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ aiohomematic_test_support-2025.10.16.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
11
+ aiohomematic_test_support-2025.10.16.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- aiohomematic_test_support/__init__.py,sha256=ZINGEtZ2Mugy2m8EN8ZhHt3y3Mfk7lppvTBAcGIozV0,93
2
- aiohomematic_test_support/client_local.py,sha256=gpWkbyt_iCQCwxsvKahYl4knFrzyBku5WwbR83_mgM8,12825
3
- aiohomematic_test_support/const.py,sha256=YBaJheW3MpX8rgwS6YKWquhghcHCTiQv4Wp6t3XGHxQ,18333
4
- aiohomematic_test_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- aiohomematic_test_support/support.py,sha256=q6hhNF6d0tyEqSzhxY8AxekFQ1sWVCx3DdwPkf4Slls,28671
6
- aiohomematic_test_support/data/full_session_randomized_ccu.zip,sha256=oN7g0CB_0kQX3qk-RSu-Rt28yW7BcplVqPYZCDqr0EU,734626
7
- aiohomematic_test_support/data/full_session_randomized_pydevccu.zip,sha256=_QFWSP03dkiMFdD_w-R98DS6ur4PYDQXw-DCkbJEGg4,1293240
8
- aiohomematic_test_support-2025.10.14.dist-info/METADATA,sha256=sgsF-eqONESqL-cUNn8tsXi-Ol46-iRAKcYRaj1AD08,485
9
- aiohomematic_test_support-2025.10.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- aiohomematic_test_support-2025.10.14.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
11
- aiohomematic_test_support-2025.10.14.dist-info/RECORD,,