aiohomematic-test-support 2025.10.16__tar.gz → 2025.10.17__tar.gz

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

Potentially problematic release.


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

Files changed (18) hide show
  1. {aiohomematic_test_support-2025.10.16/aiohomematic_test_support.egg-info → aiohomematic_test_support-2025.10.17}/PKG-INFO +1 -1
  2. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/__init__.py +1 -1
  3. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17/aiohomematic_test_support.egg-info}/PKG-INFO +1 -1
  4. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/aiohomematic_test_support.egg-info/SOURCES.txt +6 -4
  5. aiohomematic_test_support-2025.10.17/factory.py +218 -0
  6. aiohomematic_test_support-2025.10.17/helper.py +45 -0
  7. aiohomematic_test_support-2025.10.16/support.py → aiohomematic_test_support-2025.10.17/mock.py +7 -242
  8. aiohomematic_test_support-2025.10.16/client_local.py +0 -361
  9. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/MANIFEST.in +0 -0
  10. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/README.md +0 -0
  11. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/aiohomematic_test_support.egg-info/dependency_links.txt +0 -0
  12. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/aiohomematic_test_support.egg-info/top_level.txt +0 -0
  13. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/const.py +0 -0
  14. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/data/full_session_randomized_ccu.zip +0 -0
  15. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/data/full_session_randomized_pydevccu.zip +0 -0
  16. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/py.typed +0 -0
  17. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/pyproject.toml +0 -0
  18. {aiohomematic_test_support-2025.10.16 → aiohomematic_test_support-2025.10.17}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic-test-support
3
- Version: 2025.10.16
3
+ Version: 2025.10.17
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
@@ -1,2 +1,2 @@
1
- __version__ = "2025.10.16"
1
+ __version__ = "2025.10.17"
2
2
  """Module to support aiohomematic testing with a local client."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic-test-support
3
- Version: 2025.10.16
3
+ Version: 2025.10.17
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
@@ -1,16 +1,18 @@
1
1
  MANIFEST.in
2
2
  README.md
3
3
  __init__.py
4
- client_local.py
5
4
  const.py
5
+ factory.py
6
+ helper.py
7
+ mock.py
6
8
  py.typed
7
9
  pyproject.toml
8
- support.py
9
10
  ./__init__.py
10
- ./client_local.py
11
11
  ./const.py
12
+ ./factory.py
13
+ ./helper.py
14
+ ./mock.py
12
15
  ./py.typed
13
- ./support.py
14
16
  ./data/full_session_randomized_ccu.zip
15
17
  ./data/full_session_randomized_pydevccu.zip
16
18
  aiohomematic_test_support.egg-info/PKG-INFO
@@ -0,0 +1,218 @@
1
+ """Helpers for tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import AsyncGenerator
7
+ import contextlib
8
+ import logging
9
+ from typing import Any, Self, cast
10
+ from unittest.mock import MagicMock, Mock, patch
11
+
12
+ from aiohttp import ClientSession
13
+
14
+ from aiohomematic.central import CentralConfig, CentralUnit
15
+ from aiohomematic.client import Client, ClientConfig, InterfaceConfig
16
+ from aiohomematic.const import LOCAL_HOST, BackendSystemEvent, Interface
17
+ from aiohomematic_test_support import const
18
+ from aiohomematic_test_support.mock import SessionPlayer, get_client_session, get_mock, get_xml_rpc_proxy
19
+
20
+ _LOGGER = logging.getLogger(__name__)
21
+
22
+
23
+ # pylint: disable=protected-access
24
+ class FactoryWithClient:
25
+ """Factory for a central with one local client."""
26
+
27
+ def __init__(
28
+ self,
29
+ *,
30
+ player: SessionPlayer,
31
+ address_device_translation: set[str] | None = None,
32
+ do_mock_client: bool = True,
33
+ exclude_methods_from_mocks: set[str] | None = None,
34
+ ignore_custom_device_definition_models: list[str] | None = None,
35
+ ignore_devices_on_create: list[str] | None = None,
36
+ include_properties_in_mocks: set[str] | None = None,
37
+ interface_configs: set[InterfaceConfig] | None = None,
38
+ un_ignore_list: list[str] | None = None,
39
+ ) -> None:
40
+ """Init the central factory."""
41
+ self._player = player
42
+ self.init(
43
+ address_device_translation=address_device_translation,
44
+ do_mock_client=do_mock_client,
45
+ exclude_methods_from_mocks=exclude_methods_from_mocks,
46
+ ignore_custom_device_definition_models=ignore_custom_device_definition_models,
47
+ ignore_devices_on_create=ignore_devices_on_create,
48
+ include_properties_in_mocks=include_properties_in_mocks,
49
+ interface_configs=interface_configs,
50
+ un_ignore_list=un_ignore_list,
51
+ )
52
+ self.system_event_mock = MagicMock()
53
+ self.ha_event_mock = MagicMock()
54
+
55
+ def init(
56
+ self,
57
+ *,
58
+ address_device_translation: set[str] | None = None,
59
+ do_mock_client: bool = True,
60
+ exclude_methods_from_mocks: set[str] | None = None,
61
+ ignore_custom_device_definition_models: list[str] | None = None,
62
+ ignore_devices_on_create: list[str] | None = None,
63
+ include_properties_in_mocks: set[str] | None = None,
64
+ interface_configs: set[InterfaceConfig] | None = None,
65
+ un_ignore_list: list[str] | None = None,
66
+ ) -> Self:
67
+ """Init the central factory."""
68
+ self._address_device_translation = address_device_translation
69
+ self._do_mock_client = do_mock_client
70
+ self._exclude_methods_from_mocks = exclude_methods_from_mocks
71
+ self._ignore_custom_device_definition_models = ignore_custom_device_definition_models
72
+ self._ignore_devices_on_create = ignore_devices_on_create
73
+ self._include_properties_in_mocks = include_properties_in_mocks
74
+ self._interface_configs = (
75
+ interface_configs
76
+ if interface_configs is not None
77
+ else {
78
+ InterfaceConfig(
79
+ central_name=const.CENTRAL_NAME,
80
+ interface=Interface.BIDCOS_RF,
81
+ port=2001,
82
+ )
83
+ }
84
+ )
85
+ self._un_ignore_list = frozenset(un_ignore_list or [])
86
+ self._client_session = get_client_session(
87
+ player=self._player,
88
+ address_device_translation=self._address_device_translation,
89
+ ignore_devices_on_create=self._ignore_devices_on_create,
90
+ )
91
+ self._xml_proxy = get_xml_rpc_proxy(
92
+ player=self._player,
93
+ address_device_translation=self._address_device_translation,
94
+ ignore_devices_on_create=self._ignore_devices_on_create,
95
+ )
96
+ return self
97
+
98
+ async def get_raw_central(self) -> CentralUnit:
99
+ """Return a central based on give address_device_translation."""
100
+ interface_configs = self._interface_configs if self._interface_configs else set()
101
+ central = CentralConfig(
102
+ name=const.CENTRAL_NAME,
103
+ host=const.CCU_HOST,
104
+ username=const.CCU_USERNAME,
105
+ password=const.CCU_PASSWORD,
106
+ central_id="test1234",
107
+ interface_configs=interface_configs,
108
+ client_session=self._client_session,
109
+ un_ignore_list=self._un_ignore_list,
110
+ ignore_custom_device_definition_models=frozenset(self._ignore_custom_device_definition_models or []),
111
+ start_direct=True,
112
+ ).create_central()
113
+
114
+ central.register_backend_system_callback(cb=self.system_event_mock)
115
+ central.register_homematic_callback(cb=self.ha_event_mock)
116
+
117
+ assert central
118
+ self._client_session.set_central(central=central) # type: ignore[attr-defined]
119
+ self._xml_proxy.set_central(central=central)
120
+ return central
121
+
122
+ async def get_default_central(self, *, start: bool = True) -> CentralUnit:
123
+ """Return a central based on give address_device_translation."""
124
+ central = await self.get_raw_central()
125
+
126
+ await self._xml_proxy.do_init()
127
+ patch("aiohomematic.client.ClientConfig._create_xml_rpc_proxy", return_value=self._xml_proxy).start()
128
+ patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST).start()
129
+
130
+ # Optionally patch client creation to return a mocked client
131
+ if self._do_mock_client:
132
+ _orig_create_client = ClientConfig.create_client
133
+
134
+ async def _mocked_create_client(config: ClientConfig) -> Client | Mock:
135
+ real_client = await _orig_create_client(config)
136
+ return cast(
137
+ Mock,
138
+ get_mock(
139
+ instance=real_client,
140
+ exclude_methods=self._exclude_methods_from_mocks,
141
+ include_properties=self._include_properties_in_mocks,
142
+ ),
143
+ )
144
+
145
+ patch("aiohomematic.client.ClientConfig.create_client", _mocked_create_client).start()
146
+
147
+ if start:
148
+ await central.start()
149
+ await central._init_hub()
150
+ assert central
151
+ return central
152
+
153
+
154
+ async def get_central_client_factory(
155
+ player: SessionPlayer,
156
+ address_device_translation: set[str],
157
+ do_mock_client: bool,
158
+ ignore_devices_on_create: list[str] | None,
159
+ ignore_custom_device_definition_models: list[str] | None,
160
+ un_ignore_list: list[str] | None,
161
+ ) -> AsyncGenerator[tuple[CentralUnit, Client | Mock, FactoryWithClient]]:
162
+ """Return central factory."""
163
+ factory = FactoryWithClient(
164
+ player=player,
165
+ address_device_translation=address_device_translation,
166
+ do_mock_client=do_mock_client,
167
+ ignore_custom_device_definition_models=ignore_custom_device_definition_models,
168
+ ignore_devices_on_create=ignore_devices_on_create,
169
+ un_ignore_list=un_ignore_list,
170
+ )
171
+ central = await factory.get_default_central()
172
+ client = central.primary_client
173
+ assert client
174
+ try:
175
+ yield central, client, factory
176
+ finally:
177
+ await central.stop()
178
+ await central.clear_files()
179
+
180
+
181
+ async def get_pydev_ccu_central_unit_full(
182
+ port: int,
183
+ client_session: ClientSession | None = None,
184
+ ) -> CentralUnit:
185
+ """Create and yield central, after all devices have been created."""
186
+ device_event = asyncio.Event()
187
+
188
+ def systemcallback(system_event: Any, *args: Any, **kwargs: Any) -> None:
189
+ if system_event == BackendSystemEvent.DEVICES_CREATED:
190
+ device_event.set()
191
+
192
+ interface_configs = {
193
+ InterfaceConfig(
194
+ central_name=const.CENTRAL_NAME,
195
+ interface=Interface.BIDCOS_RF,
196
+ port=port,
197
+ )
198
+ }
199
+
200
+ central = CentralConfig(
201
+ name=const.CENTRAL_NAME,
202
+ host=const.CCU_HOST,
203
+ username=const.CCU_USERNAME,
204
+ password=const.CCU_PASSWORD,
205
+ central_id="test1234",
206
+ interface_configs=interface_configs,
207
+ client_session=client_session,
208
+ program_markers=(),
209
+ sysvar_markers=(),
210
+ ).create_central()
211
+ central.register_backend_system_callback(cb=systemcallback)
212
+ await central.start()
213
+
214
+ # Wait up to 60 seconds for the DEVICES_CREATED event which signals that all devices are available
215
+ with contextlib.suppress(TimeoutError):
216
+ await asyncio.wait_for(device_event.wait(), timeout=60)
217
+
218
+ return central
@@ -0,0 +1,45 @@
1
+ """Helpers for tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.resources
6
+ import logging
7
+ import os
8
+ from typing import Any
9
+
10
+ import orjson
11
+
12
+ from aiohomematic.central import CentralUnit
13
+ from aiohomematic.const import UTF_8
14
+ from aiohomematic.model.custom import CustomDataPoint
15
+
16
+ _LOGGER = logging.getLogger(__name__)
17
+
18
+
19
+ # pylint: disable=protected-access
20
+
21
+
22
+ def _load_json_file(anchor: str, resource: str, file_name: str) -> Any | None:
23
+ """Load json file from disk into dict."""
24
+ package_path = str(importlib.resources.files(anchor))
25
+ with open(
26
+ file=os.path.join(package_path, resource, file_name),
27
+ encoding=UTF_8,
28
+ ) as fptr:
29
+ return orjson.loads(fptr.read())
30
+
31
+
32
+ def get_prepared_custom_data_point(central: CentralUnit, address: str, channel_no: int) -> CustomDataPoint | None:
33
+ """Return the hm custom_data_point."""
34
+ if cdp := central.get_custom_data_point(address=address, channel_no=channel_no):
35
+ for dp in cdp._data_points.values():
36
+ dp._state_uncertain = False
37
+ return cdp
38
+ return None
39
+
40
+
41
+ def load_device_description(file_name: str) -> Any:
42
+ """Load device description."""
43
+ dev_desc = _load_json_file(anchor="pydevccu", resource="device_descriptions", file_name=file_name)
44
+ assert dev_desc
45
+ return dev_desc
@@ -4,169 +4,27 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  from collections import defaultdict
7
- from collections.abc import AsyncGenerator
8
- import contextlib
9
- import importlib.resources
10
7
  import json
11
8
  import logging
12
9
  import os
13
- from typing import Any, Self, cast
14
- from unittest.mock import MagicMock, Mock, patch
10
+ from typing import Any, cast
11
+ from unittest.mock import MagicMock, Mock
15
12
  import zipfile
16
13
 
17
14
  from aiohttp import ClientSession
18
15
  import orjson
19
16
 
20
- from aiohomematic.central import CentralConfig, CentralUnit
21
- from aiohomematic.client import BaseRpcProxy, Client, ClientConfig, InterfaceConfig
17
+ from aiohomematic.central import CentralUnit
18
+ from aiohomematic.client import BaseRpcProxy
22
19
  from aiohomematic.client.json_rpc import _JsonKey, _JsonRpcMethod
23
20
  from aiohomematic.client.rpc_proxy import _RpcMethod
24
- from aiohomematic.const import (
25
- LOCAL_HOST,
26
- UTF_8,
27
- BackendSystemEvent,
28
- DataOperationResult,
29
- Interface,
30
- Parameter,
31
- ParamsetKey,
32
- RPCType,
33
- )
34
- from aiohomematic.model.custom import CustomDataPoint
21
+ from aiohomematic.const import UTF_8, DataOperationResult, Parameter, ParamsetKey, RPCType
35
22
  from aiohomematic.store.persistent import _freeze_params, _unfreeze_params
36
23
  from aiohomematic_test_support import const
37
24
 
38
25
  _LOGGER = logging.getLogger(__name__)
39
26
 
40
-
41
27
  # pylint: disable=protected-access
42
- class FactoryWithClient:
43
- """Factory for a central with one local client."""
44
-
45
- def __init__(
46
- self,
47
- *,
48
- player: SessionPlayer,
49
- address_device_translation: set[str] | None = None,
50
- do_mock_client: bool = True,
51
- exclude_methods_from_mocks: set[str] | None = None,
52
- ignore_custom_device_definition_models: list[str] | None = None,
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,
56
- un_ignore_list: list[str] | None = None,
57
- ) -> None:
58
- """Init the central factory."""
59
- self._player = player
60
- self.init(
61
- address_device_translation=address_device_translation,
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,
65
- ignore_devices_on_create=ignore_devices_on_create,
66
- include_properties_in_mocks=include_properties_in_mocks,
67
- interface_configs=interface_configs,
68
- un_ignore_list=un_ignore_list,
69
- )
70
- self.system_event_mock = MagicMock()
71
- self.ha_event_mock = MagicMock()
72
-
73
- def init(
74
- self,
75
- *,
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,
80
- ignore_devices_on_create: list[str] | None = None,
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:
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 [])
104
- self._client_session = _get_client_session(
105
- player=self._player,
106
- address_device_translation=self._address_device_translation,
107
- ignore_devices_on_create=self._ignore_devices_on_create,
108
- )
109
- self._xml_proxy = _get_xml_rpc_proxy(
110
- player=self._player,
111
- address_device_translation=self._address_device_translation,
112
- ignore_devices_on_create=self._ignore_devices_on_create,
113
- )
114
- return self
115
-
116
- async def get_raw_central(self) -> CentralUnit:
117
- """Return a central based on give address_device_translation."""
118
- interface_configs = self._interface_configs if self._interface_configs else set()
119
- central = CentralConfig(
120
- name=const.CENTRAL_NAME,
121
- host=const.CCU_HOST,
122
- username=const.CCU_USERNAME,
123
- password=const.CCU_PASSWORD,
124
- central_id="test1234",
125
- interface_configs=interface_configs,
126
- client_session=self._client_session,
127
- un_ignore_list=self._un_ignore_list,
128
- ignore_custom_device_definition_models=frozenset(self._ignore_custom_device_definition_models or []),
129
- start_direct=True,
130
- ).create_central()
131
-
132
- central.register_backend_system_callback(cb=self.system_event_mock)
133
- central.register_homematic_callback(cb=self.ha_event_mock)
134
-
135
- assert central
136
- self._client_session.set_central(central=central) # type: ignore[attr-defined]
137
- self._xml_proxy.set_central(central=central)
138
- return central
139
-
140
- async def get_default_central(self, *, start: bool = True) -> CentralUnit:
141
- """Return a central based on give address_device_translation."""
142
- central = await self.get_raw_central()
143
-
144
- await self._xml_proxy.do_init()
145
- patch("aiohomematic.client.ClientConfig._create_xml_rpc_proxy", return_value=self._xml_proxy).start()
146
- patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST).start()
147
-
148
- # Optionally patch client creation to return a mocked client
149
- if self._do_mock_client:
150
- _orig_create_client = ClientConfig.create_client
151
-
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
- )
162
-
163
- patch("aiohomematic.client.ClientConfig.create_client", _mocked_create_client).start()
164
-
165
- if start:
166
- await central.start()
167
- await central._init_hub()
168
- assert central
169
- return central
170
28
 
171
29
 
172
30
  def _get_not_mockable_method_names(instance: Any, exclude_methods: set[str]) -> set[str]:
@@ -191,17 +49,7 @@ def _get_properties(data_object: Any, decorator: Any) -> set[str]:
191
49
  return {y for y in dir(cls) if isinstance(getattr(cls, y), resolved_decorator)}
192
50
 
193
51
 
194
- def _load_json_file(anchor: str, resource: str, file_name: str) -> Any | None:
195
- """Load json file from disk into dict."""
196
- package_path = str(importlib.resources.files(anchor))
197
- with open(
198
- file=os.path.join(package_path, resource, file_name),
199
- encoding=UTF_8,
200
- ) as fptr:
201
- return orjson.loads(fptr.read())
202
-
203
-
204
- def _get_client_session( # noqa: C901
52
+ def get_client_session( # noqa: C901
205
53
  *,
206
54
  player: SessionPlayer,
207
55
  address_device_translation: set[str] | None = None,
@@ -337,7 +185,7 @@ def _get_client_session( # noqa: C901
337
185
  return cast(ClientSession, _MockClientSession())
338
186
 
339
187
 
340
- def _get_xml_rpc_proxy( # noqa: C901
188
+ def get_xml_rpc_proxy( # noqa: C901
341
189
  *,
342
190
  player: SessionPlayer,
343
191
  address_device_translation: set[str] | None = None,
@@ -478,33 +326,6 @@ def _get_xml_rpc_proxy( # noqa: C901
478
326
  return cast(BaseRpcProxy, _AioXmlRpcProxyFromSession())
479
327
 
480
328
 
481
- async def get_central_client_factory(
482
- player: SessionPlayer,
483
- address_device_translation: set[str],
484
- do_mock_client: bool,
485
- ignore_devices_on_create: list[str] | None,
486
- ignore_custom_device_definition_models: list[str] | None,
487
- un_ignore_list: list[str] | None,
488
- ) -> AsyncGenerator[tuple[CentralUnit, Client | Mock, FactoryWithClient]]:
489
- """Return central factory."""
490
- factory = FactoryWithClient(
491
- player=player,
492
- address_device_translation=address_device_translation,
493
- do_mock_client=do_mock_client,
494
- ignore_custom_device_definition_models=ignore_custom_device_definition_models,
495
- ignore_devices_on_create=ignore_devices_on_create,
496
- un_ignore_list=un_ignore_list,
497
- )
498
- central = await factory.get_default_central()
499
- client = central.primary_client
500
- assert client
501
- try:
502
- yield central, client, factory
503
- finally:
504
- await central.stop()
505
- await central.clear_files()
506
-
507
-
508
329
  def get_mock(
509
330
  instance: Any, exclude_methods: set[str] | None = None, include_properties: set[str] | None = None, **kwargs: Any
510
331
  ) -> Any:
@@ -532,62 +353,6 @@ def get_mock(
532
353
  return mock
533
354
 
534
355
 
535
- def get_prepared_custom_data_point(central: CentralUnit, address: str, channel_no: int) -> CustomDataPoint | None:
536
- """Return the hm custom_data_point."""
537
- if cdp := central.get_custom_data_point(address=address, channel_no=channel_no):
538
- for dp in cdp._data_points.values():
539
- dp._state_uncertain = False
540
- return cdp
541
- return None
542
-
543
-
544
- async def get_pydev_ccu_central_unit_full(
545
- port: int,
546
- client_session: ClientSession | None = None,
547
- ) -> CentralUnit:
548
- """Create and yield central, after all devices have been created."""
549
- device_event = asyncio.Event()
550
-
551
- def systemcallback(system_event: Any, *args: Any, **kwargs: Any) -> None:
552
- if system_event == BackendSystemEvent.DEVICES_CREATED:
553
- device_event.set()
554
-
555
- interface_configs = {
556
- InterfaceConfig(
557
- central_name=const.CENTRAL_NAME,
558
- interface=Interface.BIDCOS_RF,
559
- port=port,
560
- )
561
- }
562
-
563
- central = CentralConfig(
564
- name=const.CENTRAL_NAME,
565
- host=const.CCU_HOST,
566
- username=const.CCU_USERNAME,
567
- password=const.CCU_PASSWORD,
568
- central_id="test1234",
569
- interface_configs=interface_configs,
570
- client_session=client_session,
571
- program_markers=(),
572
- sysvar_markers=(),
573
- ).create_central()
574
- central.register_backend_system_callback(cb=systemcallback)
575
- await central.start()
576
-
577
- # Wait up to 60 seconds for the DEVICES_CREATED event which signals that all devices are available
578
- with contextlib.suppress(TimeoutError):
579
- await asyncio.wait_for(device_event.wait(), timeout=60)
580
-
581
- return central
582
-
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
-
591
356
  async def get_session_player(*, file_name: str) -> SessionPlayer:
592
357
  """Provide a SessionPlayer preloaded from the randomized full session JSON file."""
593
358
  player = SessionPlayer(file_id=file_name)
@@ -1,361 +0,0 @@
1
- """The local client-object and its methods."""
2
-
3
- from __future__ import annotations
4
-
5
- from _collections import defaultdict
6
- from dataclasses import dataclass
7
- from datetime import datetime
8
- import importlib.resources
9
- import logging
10
- import os
11
- from typing import Any, Final, cast
12
-
13
- import orjson
14
-
15
- from aiohomematic.client import _LOGGER, Client, ClientConfig
16
- from aiohomematic.const import (
17
- ADDRESS_SEPARATOR,
18
- DP_KEY_VALUE,
19
- UTF_8,
20
- WAIT_FOR_CALLBACK,
21
- CallSource,
22
- CommandRxMode,
23
- DescriptionMarker,
24
- DeviceDescription,
25
- Interface,
26
- ParameterData,
27
- ParamsetKey,
28
- ProductGroup,
29
- ProgramData,
30
- ProxyInitState,
31
- SystemInformation,
32
- SystemVariableData,
33
- )
34
- from aiohomematic.decorators import inspector
35
- from aiohomematic.support import is_channel_address
36
-
37
- LOCAL_SERIAL: Final = "0815_4711"
38
- BACKEND_LOCAL: Final = "PyDevCCU"
39
-
40
-
41
- class ClientLocal(Client): # pragma: no cover
42
- """Local client object to provide access to locally stored files."""
43
-
44
- def __init__(self, *, client_config: ClientConfig, local_resources: LocalRessources) -> None:
45
- """Initialize the Client."""
46
- super().__init__(client_config=client_config)
47
- self._local_resources = local_resources
48
- self._paramset_descriptions_cache: dict[str, dict[ParamsetKey, dict[str, ParameterData]]] = defaultdict(
49
- lambda: defaultdict(dict)
50
- )
51
-
52
- async def init_client(self) -> None:
53
- """Init the client."""
54
- self._system_information = await self._get_system_information()
55
-
56
- @property
57
- def available(self) -> bool:
58
- """Return the availability of the client."""
59
- return True
60
-
61
- @property
62
- def model(self) -> str:
63
- """Return the model of the backend."""
64
- return BACKEND_LOCAL
65
-
66
- def get_product_group(self, *, model: str) -> ProductGroup:
67
- """Return the product group."""
68
- l_model = model.lower()
69
- if l_model.startswith("hmipw"):
70
- return ProductGroup.HMIPW
71
- if l_model.startswith("hmip"):
72
- return ProductGroup.HMIP
73
- if l_model.startswith("hmw"):
74
- return ProductGroup.HMW
75
- if l_model.startswith("hm"):
76
- return ProductGroup.HM
77
- return ProductGroup.UNKNOWN
78
-
79
- @property
80
- def supports_ping_pong(self) -> bool:
81
- """Return the supports_ping_pong info of the backend."""
82
- return True
83
-
84
- @property
85
- def supports_push_updates(self) -> bool:
86
- """Return the client supports push update."""
87
- return True
88
-
89
- async def initialize_proxy(self) -> ProxyInitState:
90
- """Init the proxy has to tell the backend where to send the events."""
91
- return ProxyInitState.INIT_SUCCESS
92
-
93
- async def deinitialize_proxy(self) -> ProxyInitState:
94
- """De-init to stop the backend from sending events for this remote."""
95
- return ProxyInitState.DE_INIT_SUCCESS
96
-
97
- async def stop(self) -> None:
98
- """Stop depending services."""
99
-
100
- @inspector(re_raise=False, measure_performance=True)
101
- async def fetch_all_device_data(self) -> None:
102
- """Fetch all device data from the backend."""
103
-
104
- @inspector(re_raise=False, measure_performance=True)
105
- async def fetch_device_details(self) -> None:
106
- """Fetch names from the backend."""
107
-
108
- @inspector(re_raise=False, no_raise_return=False)
109
- async def is_connected(self) -> bool:
110
- """
111
- Perform actions required for connectivity check.
112
-
113
- Connection is not connected, if three consecutive checks fail.
114
- Return connectivity state.
115
- """
116
- return True
117
-
118
- def is_callback_alive(self) -> bool:
119
- """Return if XmlRPC-Server is alive based on received events for this client."""
120
- return True
121
-
122
- @inspector(re_raise=False, no_raise_return=False)
123
- async def check_connection_availability(self, *, handle_ping_pong: bool) -> bool:
124
- """Send ping to the backend to generate PONG event."""
125
- if handle_ping_pong and self.supports_ping_pong:
126
- self._ping_pong_cache.handle_send_ping(ping_ts=datetime.now())
127
- return True
128
-
129
- @inspector
130
- async def execute_program(self, *, pid: str) -> bool:
131
- """Execute a program on the backend."""
132
- return True
133
-
134
- @inspector
135
- async def set_program_state(self, *, pid: str, state: bool) -> bool:
136
- """Set the program state on the backend."""
137
- return True
138
-
139
- @inspector(measure_performance=True)
140
- async def set_system_variable(self, *, legacy_name: str, value: Any) -> bool:
141
- """Set a system variable on the backend."""
142
- return True
143
-
144
- @inspector
145
- async def delete_system_variable(self, *, name: str) -> bool:
146
- """Delete a system variable from the backend."""
147
- return True
148
-
149
- @inspector
150
- async def get_system_variable(self, *, name: str) -> str:
151
- """Get single system variable from the backend."""
152
- return "Empty"
153
-
154
- @inspector(re_raise=False)
155
- async def get_all_system_variables(
156
- self, *, markers: tuple[DescriptionMarker | str, ...]
157
- ) -> tuple[SystemVariableData, ...]:
158
- """Get all system variables from the backend."""
159
- return ()
160
-
161
- @inspector(re_raise=False)
162
- async def get_all_programs(self, *, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...]:
163
- """Get all programs, if available."""
164
- return ()
165
-
166
- @inspector(re_raise=False, no_raise_return={})
167
- async def get_all_rooms(self) -> dict[str, set[str]]:
168
- """Get all rooms, if available."""
169
- return {}
170
-
171
- @inspector(re_raise=False, no_raise_return={})
172
- async def get_all_functions(self) -> dict[str, set[str]]:
173
- """Get all functions, if available."""
174
- return {}
175
-
176
- async def _get_system_information(self) -> SystemInformation:
177
- """Get system information of the backend."""
178
- return SystemInformation(available_interfaces=(Interface.BIDCOS_RF,), serial=LOCAL_SERIAL)
179
-
180
- @inspector(re_raise=False, measure_performance=True)
181
- async def list_devices(self) -> tuple[DeviceDescription, ...] | None:
182
- """Get device descriptions from the backend."""
183
- if not self._local_resources:
184
- _LOGGER.warning(
185
- "LIST_DEVICES: missing local_resources in config for %s",
186
- self.central.name,
187
- )
188
- return None
189
- device_descriptions: list[DeviceDescription] = []
190
- if local_device_descriptions := cast(
191
- list[Any],
192
- await self._load_all_json_files(
193
- anchor=self._local_resources.anchor,
194
- resource=self._local_resources.device_description_dir,
195
- include_list=list(self._local_resources.address_device_translation.values()),
196
- exclude_list=self._local_resources.ignore_devices_on_create,
197
- ),
198
- ):
199
- for device_description in local_device_descriptions:
200
- device_descriptions.extend(device_description)
201
- return tuple(device_descriptions)
202
-
203
- @inspector(log_level=logging.NOTSET)
204
- async def get_value(
205
- self,
206
- *,
207
- channel_address: str,
208
- paramset_key: ParamsetKey,
209
- parameter: str,
210
- call_source: CallSource = CallSource.MANUAL_OR_SCHEDULED,
211
- ) -> Any:
212
- """Return a value from the backend."""
213
- return
214
-
215
- @inspector(re_raise=False, no_raise_return=set())
216
- async def set_value(
217
- self,
218
- *,
219
- channel_address: str,
220
- paramset_key: ParamsetKey,
221
- parameter: str,
222
- value: Any,
223
- wait_for_callback: int | None = WAIT_FOR_CALLBACK,
224
- rx_mode: CommandRxMode | None = None,
225
- check_against_pd: bool = False,
226
- ) -> set[DP_KEY_VALUE]:
227
- """Set single value on paramset VALUES."""
228
- # store the send value in the last_value_send_cache
229
- result = self._last_value_send_cache.add_set_value(
230
- channel_address=channel_address, parameter=parameter, value=value
231
- )
232
- # fire an event to fake the state change for a simple parameter
233
- await self.central.data_point_event(
234
- interface_id=self.interface_id, channel_address=channel_address, parameter=parameter, value=value
235
- )
236
- return result
237
-
238
- @inspector
239
- async def get_paramset(
240
- self,
241
- *,
242
- address: str,
243
- paramset_key: ParamsetKey | str,
244
- call_source: CallSource = CallSource.MANUAL_OR_SCHEDULED,
245
- ) -> Any:
246
- """
247
- Return a paramset from the backend.
248
-
249
- Address is usually the channel_address,
250
- but for bidcos devices there is a master paramset at the device.
251
- """
252
- return {}
253
-
254
- async def _get_paramset_description(
255
- self, *, address: str, paramset_key: ParamsetKey
256
- ) -> dict[str, ParameterData] | None:
257
- """Get paramset description from the backend."""
258
- if not self._local_resources:
259
- _LOGGER.warning(
260
- "GET_PARAMSET_DESCRIPTION: missing local_resources in config for %s",
261
- self.central.name,
262
- )
263
- return None
264
-
265
- if (
266
- address not in self._paramset_descriptions_cache
267
- and (file_name := self._local_resources.address_device_translation.get(address.split(ADDRESS_SEPARATOR)[0]))
268
- and (
269
- data := await self._load_json_file(
270
- anchor=self._local_resources.anchor,
271
- resource=self._local_resources.paramset_description_dir,
272
- file_name=file_name,
273
- )
274
- )
275
- ):
276
- self._paramset_descriptions_cache.update(data)
277
-
278
- return self._paramset_descriptions_cache[address].get(paramset_key)
279
-
280
- @inspector(measure_performance=True)
281
- async def put_paramset(
282
- self,
283
- *,
284
- channel_address: str,
285
- paramset_key_or_link_address: ParamsetKey | str,
286
- values: Any,
287
- wait_for_callback: int | None = WAIT_FOR_CALLBACK,
288
- rx_mode: CommandRxMode | None = None,
289
- check_against_pd: bool = False,
290
- ) -> set[DP_KEY_VALUE]:
291
- """
292
- Set paramsets manually.
293
-
294
- Address is usually the channel_address,
295
- but for bidcos devices there is a master paramset at the device.
296
- """
297
- # store the send value in the last_value_send_cache
298
- if isinstance(paramset_key_or_link_address, str) and is_channel_address(address=paramset_key_or_link_address):
299
- result = set()
300
- else:
301
- result = self._last_value_send_cache.add_put_paramset(
302
- channel_address=channel_address,
303
- paramset_key=ParamsetKey(paramset_key_or_link_address),
304
- values=values,
305
- )
306
-
307
- # fire an event to fake the state change for the content of a paramset
308
- for parameter in values:
309
- await self.central.data_point_event(
310
- interface_id=self.interface_id,
311
- channel_address=channel_address,
312
- parameter=parameter,
313
- value=values[parameter],
314
- )
315
- return result
316
-
317
- async def _load_all_json_files(
318
- self,
319
- *,
320
- anchor: str,
321
- resource: str,
322
- include_list: list[str] | None = None,
323
- exclude_list: list[str] | None = None,
324
- ) -> list[Any] | None:
325
- """Load all json files from disk into dict."""
326
- if not include_list:
327
- return []
328
- if not exclude_list:
329
- exclude_list = []
330
- result: list[Any] = []
331
- resource_path = os.path.join(str(importlib.resources.files(anchor)), resource)
332
- for file_name in os.listdir(resource_path):
333
- if file_name not in include_list or file_name in exclude_list:
334
- continue
335
- if file_content := await self._load_json_file(anchor=anchor, resource=resource, file_name=file_name):
336
- result.append(file_content)
337
- return result
338
-
339
- async def _load_json_file(self, *, anchor: str, resource: str, file_name: str) -> Any | None:
340
- """Load json file from disk into dict."""
341
- package_path = str(importlib.resources.files(anchor))
342
-
343
- def _perform_load() -> Any | None:
344
- with open(
345
- file=os.path.join(package_path, resource, file_name),
346
- encoding=UTF_8,
347
- ) as fptr:
348
- return orjson.loads(fptr.read())
349
-
350
- return await self.central.looper.async_add_executor_job(_perform_load, name="load-json-file")
351
-
352
-
353
- @dataclass(frozen=True, kw_only=True, slots=True)
354
- class LocalRessources:
355
- """Dataclass with information for local client."""
356
-
357
- address_device_translation: dict[str, str]
358
- ignore_devices_on_create: list[str]
359
- anchor: str = "pydevccu"
360
- device_description_dir: str = "device_descriptions"
361
- paramset_description_dir: str = "paramset_descriptions"