aiohomematic-test-support 2025.10.16__py3-none-any.whl → 2025.10.18__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.
- aiohomematic_test_support/__init__.py +1 -1
- aiohomematic_test_support/const.py +5 -0
- aiohomematic_test_support/factory.py +220 -0
- aiohomematic_test_support/helper.py +45 -0
- aiohomematic_test_support/{support.py → mock.py} +97 -264
- {aiohomematic_test_support-2025.10.16.dist-info → aiohomematic_test_support-2025.10.18.dist-info}/METADATA +1 -1
- aiohomematic_test_support-2025.10.18.dist-info/RECORD +12 -0
- aiohomematic_test_support/client_local.py +0 -361
- aiohomematic_test_support-2025.10.16.dist-info/RECORD +0 -11
- {aiohomematic_test_support-2025.10.16.dist-info → aiohomematic_test_support-2025.10.18.dist-info}/WHEEL +0 -0
- {aiohomematic_test_support-2025.10.16.dist-info → aiohomematic_test_support-2025.10.18.dist-info}/top_level.txt +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "2025.10.
|
|
1
|
+
__version__ = "2025.10.18"
|
|
2
2
|
"""Module to support aiohomematic testing with a local client."""
|
|
@@ -16,6 +16,11 @@ INTERFACE_ID = f"{CENTRAL_NAME}-{Interface.BIDCOS_RF}"
|
|
|
16
16
|
FULL_SESSION_RANDOMIZED_PYDEVCCU = "full_session_randomized_pydevccu.zip"
|
|
17
17
|
FULL_SESSION_RANDOMIZED_CCU = "full_session_randomized_ccu.zip"
|
|
18
18
|
|
|
19
|
+
ALL_SESSION_FILES = [
|
|
20
|
+
FULL_SESSION_RANDOMIZED_PYDEVCCU,
|
|
21
|
+
FULL_SESSION_RANDOMIZED_CCU,
|
|
22
|
+
]
|
|
23
|
+
|
|
19
24
|
|
|
20
25
|
SYSVAR_DATA: list[SystemVariableData] = [
|
|
21
26
|
SystemVariableData(
|
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
*,
|
|
156
|
+
player: SessionPlayer,
|
|
157
|
+
address_device_translation: set[str],
|
|
158
|
+
do_mock_client: bool,
|
|
159
|
+
ignore_devices_on_create: list[str] | None,
|
|
160
|
+
ignore_custom_device_definition_models: list[str] | None,
|
|
161
|
+
un_ignore_list: list[str] | None,
|
|
162
|
+
) -> AsyncGenerator[tuple[CentralUnit, Client | Mock, FactoryWithClient]]:
|
|
163
|
+
"""Return central factory."""
|
|
164
|
+
factory = FactoryWithClient(
|
|
165
|
+
player=player,
|
|
166
|
+
address_device_translation=address_device_translation,
|
|
167
|
+
do_mock_client=do_mock_client,
|
|
168
|
+
ignore_custom_device_definition_models=ignore_custom_device_definition_models,
|
|
169
|
+
ignore_devices_on_create=ignore_devices_on_create,
|
|
170
|
+
un_ignore_list=un_ignore_list,
|
|
171
|
+
)
|
|
172
|
+
central = await factory.get_default_central()
|
|
173
|
+
client = central.primary_client
|
|
174
|
+
assert client
|
|
175
|
+
try:
|
|
176
|
+
yield central, client, factory
|
|
177
|
+
finally:
|
|
178
|
+
await central.stop()
|
|
179
|
+
await central.clear_files()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def get_pydev_ccu_central_unit_full(
|
|
183
|
+
*,
|
|
184
|
+
port: int,
|
|
185
|
+
client_session: ClientSession | None = None,
|
|
186
|
+
) -> CentralUnit:
|
|
187
|
+
"""Create and yield central, after all devices have been created."""
|
|
188
|
+
device_event = asyncio.Event()
|
|
189
|
+
|
|
190
|
+
def systemcallback(system_event: Any, *args: Any, **kwargs: Any) -> None:
|
|
191
|
+
if system_event == BackendSystemEvent.DEVICES_CREATED:
|
|
192
|
+
device_event.set()
|
|
193
|
+
|
|
194
|
+
interface_configs = {
|
|
195
|
+
InterfaceConfig(
|
|
196
|
+
central_name=const.CENTRAL_NAME,
|
|
197
|
+
interface=Interface.BIDCOS_RF,
|
|
198
|
+
port=port,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
central = CentralConfig(
|
|
203
|
+
name=const.CENTRAL_NAME,
|
|
204
|
+
host=const.CCU_HOST,
|
|
205
|
+
username=const.CCU_USERNAME,
|
|
206
|
+
password=const.CCU_PASSWORD,
|
|
207
|
+
central_id="test1234",
|
|
208
|
+
interface_configs=interface_configs,
|
|
209
|
+
client_session=client_session,
|
|
210
|
+
program_markers=(),
|
|
211
|
+
sysvar_markers=(),
|
|
212
|
+
).create_central()
|
|
213
|
+
central.register_backend_system_callback(cb=systemcallback)
|
|
214
|
+
await central.start()
|
|
215
|
+
|
|
216
|
+
# Wait up to 60 seconds for the DEVICES_CREATED event which signals that all devices are available
|
|
217
|
+
with contextlib.suppress(TimeoutError):
|
|
218
|
+
await asyncio.wait_for(device_event.wait(), timeout=60)
|
|
219
|
+
|
|
220
|
+
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,172 +4,30 @@ 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,
|
|
14
|
-
from unittest.mock import MagicMock, Mock
|
|
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
|
|
21
|
-
from aiohomematic.client import BaseRpcProxy
|
|
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
|
-
def _get_not_mockable_method_names(instance: Any, exclude_methods: set[str]) -> set[str]:
|
|
30
|
+
def _get_not_mockable_method_names(*, instance: Any, exclude_methods: set[str]) -> set[str]:
|
|
173
31
|
"""Return all relevant method names for mocking."""
|
|
174
32
|
methods: set[str] = set(_get_properties(data_object=instance, decorator=property))
|
|
175
33
|
|
|
@@ -179,7 +37,7 @@ def _get_not_mockable_method_names(instance: Any, exclude_methods: set[str]) ->
|
|
|
179
37
|
return methods
|
|
180
38
|
|
|
181
39
|
|
|
182
|
-
def _get_properties(data_object: Any, decorator: Any) -> set[str]:
|
|
40
|
+
def _get_properties(*, data_object: Any, decorator: Any) -> set[str]:
|
|
183
41
|
"""Return the object attributes by decorator."""
|
|
184
42
|
cls = data_object.__class__
|
|
185
43
|
|
|
@@ -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
|
|
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,
|
|
@@ -215,7 +63,7 @@ def _get_client_session( # noqa: C901
|
|
|
215
63
|
"""
|
|
216
64
|
|
|
217
65
|
class _MockResponse:
|
|
218
|
-
def __init__(self, json_data: dict | None) -> None:
|
|
66
|
+
def __init__(self, *, json_data: dict | None) -> None:
|
|
219
67
|
# If no match is found, emulate backend error payload
|
|
220
68
|
self._json = json_data or {
|
|
221
69
|
_JsonKey.RESULT: None,
|
|
@@ -224,7 +72,7 @@ def _get_client_session( # noqa: C901
|
|
|
224
72
|
}
|
|
225
73
|
self.status = 200
|
|
226
74
|
|
|
227
|
-
async def json(self, encoding: str | None = None) -> dict[str, Any]: # mimic aiohttp API
|
|
75
|
+
async def json(self, *, encoding: str | None = None) -> dict[str, Any]: # mimic aiohttp API
|
|
228
76
|
return self._json
|
|
229
77
|
|
|
230
78
|
async def read(self) -> bytes:
|
|
@@ -266,24 +114,32 @@ def _get_client_session( # noqa: C901
|
|
|
266
114
|
_JsonRpcMethod.SYSVAR_SET_FLOAT,
|
|
267
115
|
_JsonRpcMethod.SESSION_LOGOUT,
|
|
268
116
|
):
|
|
269
|
-
return _MockResponse({_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
117
|
+
return _MockResponse(json_data={_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
270
118
|
if method == _JsonRpcMethod.SYSVAR_GET_ALL:
|
|
271
119
|
return _MockResponse(
|
|
272
|
-
{_JsonKey.ID: 0, _JsonKey.RESULT: const.SYSVAR_DATA_JSON, _JsonKey.ERROR: None}
|
|
120
|
+
json_data={_JsonKey.ID: 0, _JsonKey.RESULT: const.SYSVAR_DATA_JSON, _JsonKey.ERROR: None}
|
|
273
121
|
)
|
|
274
122
|
if method == _JsonRpcMethod.PROGRAM_GET_ALL:
|
|
275
123
|
return _MockResponse(
|
|
276
|
-
{_JsonKey.ID: 0, _JsonKey.RESULT: const.PROGRAM_DATA_JSON, _JsonKey.ERROR: None}
|
|
124
|
+
json_data={_JsonKey.ID: 0, _JsonKey.RESULT: const.PROGRAM_DATA_JSON, _JsonKey.ERROR: None}
|
|
277
125
|
)
|
|
278
126
|
if method == _JsonRpcMethod.REGA_RUN_SCRIPT:
|
|
279
127
|
if "get_program_descriptions" in params[_JsonKey.SCRIPT]:
|
|
280
128
|
return _MockResponse(
|
|
281
|
-
{
|
|
129
|
+
json_data={
|
|
130
|
+
_JsonKey.ID: 0,
|
|
131
|
+
_JsonKey.RESULT: const.PROGRAM_DATA_JSON_DESCRIPTION,
|
|
132
|
+
_JsonKey.ERROR: None,
|
|
133
|
+
}
|
|
282
134
|
)
|
|
283
135
|
|
|
284
136
|
if "get_system_variable_descriptions" in params[_JsonKey.SCRIPT]:
|
|
285
137
|
return _MockResponse(
|
|
286
|
-
{
|
|
138
|
+
json_data={
|
|
139
|
+
_JsonKey.ID: 0,
|
|
140
|
+
_JsonKey.RESULT: const.SYSVAR_DATA_JSON_DESCRIPTION,
|
|
141
|
+
_JsonKey.ERROR: None,
|
|
142
|
+
}
|
|
287
143
|
)
|
|
288
144
|
|
|
289
145
|
if method == _JsonRpcMethod.INTERFACE_SET_VALUE:
|
|
@@ -293,7 +149,7 @@ def _get_client_session( # noqa: C901
|
|
|
293
149
|
parameter=params[_JsonKey.VALUE_KEY],
|
|
294
150
|
value=params[_JsonKey.VALUE],
|
|
295
151
|
)
|
|
296
|
-
return _MockResponse({_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
152
|
+
return _MockResponse(json_data={_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
297
153
|
if method == _JsonRpcMethod.INTERFACE_PUT_PARAMSET:
|
|
298
154
|
if params[_JsonKey.PARAMSET_KEY] == ParamsetKey.VALUES:
|
|
299
155
|
interface_id = params[_JsonKey.INTERFACE]
|
|
@@ -306,7 +162,7 @@ def _get_client_session( # noqa: C901
|
|
|
306
162
|
parameter=param,
|
|
307
163
|
value=value,
|
|
308
164
|
)
|
|
309
|
-
return _MockResponse({_JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
165
|
+
return _MockResponse(json_data={_JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
310
166
|
|
|
311
167
|
json_data = player.get_latest_response_by_params(
|
|
312
168
|
rpc_type=RPCType.JSON_RPC,
|
|
@@ -329,7 +185,7 @@ def _get_client_session( # noqa: C901
|
|
|
329
185
|
new_devices.append(dd)
|
|
330
186
|
|
|
331
187
|
json_data[_JsonKey.RESULT] = new_devices
|
|
332
|
-
return _MockResponse(json_data)
|
|
188
|
+
return _MockResponse(json_data=json_data)
|
|
333
189
|
|
|
334
190
|
async def close(self) -> None: # compatibility
|
|
335
191
|
return None
|
|
@@ -337,7 +193,7 @@ def _get_client_session( # noqa: C901
|
|
|
337
193
|
return cast(ClientSession, _MockClientSession())
|
|
338
194
|
|
|
339
195
|
|
|
340
|
-
def
|
|
196
|
+
def get_xml_rpc_proxy( # noqa: C901
|
|
341
197
|
*,
|
|
342
198
|
player: SessionPlayer,
|
|
343
199
|
address_device_translation: set[str] | None = None,
|
|
@@ -478,35 +334,8 @@ def _get_xml_rpc_proxy( # noqa: C901
|
|
|
478
334
|
return cast(BaseRpcProxy, _AioXmlRpcProxyFromSession())
|
|
479
335
|
|
|
480
336
|
|
|
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
337
|
def get_mock(
|
|
509
|
-
instance: Any, exclude_methods: set[str] | None = None, include_properties: set[str] | None = None, **kwargs: Any
|
|
338
|
+
*, instance: Any, exclude_methods: set[str] | None = None, include_properties: set[str] | None = None, **kwargs: Any
|
|
510
339
|
) -> Any:
|
|
511
340
|
"""Create a mock and copy instance attributes over mock."""
|
|
512
341
|
if exclude_methods is None:
|
|
@@ -532,67 +361,15 @@ def get_mock(
|
|
|
532
361
|
return mock
|
|
533
362
|
|
|
534
363
|
|
|
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
364
|
async def get_session_player(*, file_name: str) -> SessionPlayer:
|
|
592
365
|
"""Provide a SessionPlayer preloaded from the randomized full session JSON file."""
|
|
593
366
|
player = SessionPlayer(file_id=file_name)
|
|
594
|
-
|
|
595
|
-
|
|
367
|
+
if player.supports_file_id(file_id=file_name):
|
|
368
|
+
return player
|
|
369
|
+
|
|
370
|
+
for load_fn in const.ALL_SESSION_FILES:
|
|
371
|
+
file_path = os.path.join(os.path.dirname(__file__), "data", load_fn)
|
|
372
|
+
await player.load(file_path=file_path, file_id=load_fn)
|
|
596
373
|
return player
|
|
597
374
|
|
|
598
375
|
|
|
@@ -607,7 +384,16 @@ class SessionPlayer:
|
|
|
607
384
|
"""Initialize the session player."""
|
|
608
385
|
self._file_id = file_id
|
|
609
386
|
|
|
610
|
-
|
|
387
|
+
@property
|
|
388
|
+
def _secondary_file_ids(self) -> list[str]:
|
|
389
|
+
"""Return the secondary store for the given file_id."""
|
|
390
|
+
return [fid for fid in self._store if fid != self._file_id]
|
|
391
|
+
|
|
392
|
+
def supports_file_id(self, *, file_id: str) -> bool:
|
|
393
|
+
"""Return whether the session player supports the given file_id."""
|
|
394
|
+
return file_id in self._store
|
|
395
|
+
|
|
396
|
+
async def load(self, *, file_path: str, file_id: str) -> DataOperationResult:
|
|
611
397
|
"""
|
|
612
398
|
Load data from disk into the dictionary.
|
|
613
399
|
|
|
@@ -616,7 +402,7 @@ class SessionPlayer:
|
|
|
616
402
|
will be loaded.
|
|
617
403
|
"""
|
|
618
404
|
|
|
619
|
-
if self.
|
|
405
|
+
if self.supports_file_id(file_id=file_id):
|
|
620
406
|
return DataOperationResult.NO_LOAD
|
|
621
407
|
|
|
622
408
|
if not os.path.exists(file_path):
|
|
@@ -635,7 +421,7 @@ class SessionPlayer:
|
|
|
635
421
|
with open(file=file_path, encoding=UTF_8) as file_pointer:
|
|
636
422
|
data = json.loads(file_pointer.read())
|
|
637
423
|
|
|
638
|
-
self._store[
|
|
424
|
+
self._store[file_id] = data
|
|
639
425
|
except (json.JSONDecodeError, zipfile.BadZipFile, UnicodeDecodeError, OSError):
|
|
640
426
|
return DataOperationResult.LOAD_FAIL
|
|
641
427
|
return DataOperationResult.LOAD_SUCCESS
|
|
@@ -643,11 +429,13 @@ class SessionPlayer:
|
|
|
643
429
|
loop = asyncio.get_running_loop()
|
|
644
430
|
return await loop.run_in_executor(None, _perform_load)
|
|
645
431
|
|
|
646
|
-
def
|
|
432
|
+
def get_latest_response_by_method_for_file_id(
|
|
433
|
+
self, *, file_id: str, rpc_type: str, method: str
|
|
434
|
+
) -> list[tuple[Any, Any]]:
|
|
647
435
|
"""Return latest non-expired responses for a given (rpc_type, method)."""
|
|
648
436
|
result: list[Any] = []
|
|
649
437
|
# Access store safely to avoid side effects from creating buckets.
|
|
650
|
-
if not (bucket_by_method := self._store[
|
|
438
|
+
if not (bucket_by_method := self._store[file_id].get(rpc_type)):
|
|
651
439
|
return result
|
|
652
440
|
if not (bucket_by_parameter := bucket_by_method.get(method)):
|
|
653
441
|
return result
|
|
@@ -665,16 +453,35 @@ class SessionPlayer:
|
|
|
665
453
|
result.append((params, resp))
|
|
666
454
|
return result
|
|
667
455
|
|
|
668
|
-
def
|
|
456
|
+
def get_latest_response_by_method(self, *, rpc_type: str, method: str) -> list[tuple[Any, Any]]:
|
|
457
|
+
"""Return latest non-expired responses for a given (rpc_type, method)."""
|
|
458
|
+
if pri_result := self.get_latest_response_by_method_for_file_id(
|
|
459
|
+
file_id=self._file_id,
|
|
460
|
+
rpc_type=rpc_type,
|
|
461
|
+
method=method,
|
|
462
|
+
):
|
|
463
|
+
return pri_result
|
|
464
|
+
|
|
465
|
+
for secondary_file_id in self._secondary_file_ids:
|
|
466
|
+
if sec_result := self.get_latest_response_by_method_for_file_id(
|
|
467
|
+
file_id=secondary_file_id,
|
|
468
|
+
rpc_type=rpc_type,
|
|
469
|
+
method=method,
|
|
470
|
+
):
|
|
471
|
+
return sec_result
|
|
472
|
+
return pri_result
|
|
473
|
+
|
|
474
|
+
def get_latest_response_by_params_for_file_id(
|
|
669
475
|
self,
|
|
670
476
|
*,
|
|
477
|
+
file_id: str,
|
|
671
478
|
rpc_type: str,
|
|
672
479
|
method: str,
|
|
673
480
|
params: Any,
|
|
674
481
|
) -> Any:
|
|
675
482
|
"""Return latest non-expired responses for a given (rpc_type, method, params)."""
|
|
676
483
|
# Access store safely to avoid side effects from creating buckets.
|
|
677
|
-
if not (bucket_by_method := self._store[
|
|
484
|
+
if not (bucket_by_method := self._store[file_id].get(rpc_type)):
|
|
678
485
|
return None
|
|
679
486
|
if not (bucket_by_parameter := bucket_by_method.get(method)):
|
|
680
487
|
return None
|
|
@@ -689,3 +496,29 @@ class SessionPlayer:
|
|
|
689
496
|
return bucket_by_ts[latest_ts]
|
|
690
497
|
except ValueError:
|
|
691
498
|
return None
|
|
499
|
+
|
|
500
|
+
def get_latest_response_by_params(
|
|
501
|
+
self,
|
|
502
|
+
*,
|
|
503
|
+
rpc_type: str,
|
|
504
|
+
method: str,
|
|
505
|
+
params: Any,
|
|
506
|
+
) -> Any:
|
|
507
|
+
"""Return latest non-expired responses for a given (rpc_type, method, params)."""
|
|
508
|
+
if pri_result := self.get_latest_response_by_params_for_file_id(
|
|
509
|
+
file_id=self._file_id,
|
|
510
|
+
rpc_type=rpc_type,
|
|
511
|
+
method=method,
|
|
512
|
+
params=params,
|
|
513
|
+
):
|
|
514
|
+
return pri_result
|
|
515
|
+
|
|
516
|
+
for secondary_file_id in self._secondary_file_ids:
|
|
517
|
+
if sec_result := self.get_latest_response_by_params_for_file_id(
|
|
518
|
+
file_id=secondary_file_id,
|
|
519
|
+
rpc_type=rpc_type,
|
|
520
|
+
method=method,
|
|
521
|
+
params=params,
|
|
522
|
+
):
|
|
523
|
+
return sec_result
|
|
524
|
+
return pri_result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic-test-support
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.18
|
|
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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
aiohomematic_test_support/__init__.py,sha256=vd4sacVZP3Omyq1nAHonJJ-mE7chdIn9ue0j2NNSUmY,93
|
|
2
|
+
aiohomematic_test_support/const.py,sha256=4AGTK0gMaXPeje0Z2Hy9q11kxKW8TxiD5nIAygypXc0,19564
|
|
3
|
+
aiohomematic_test_support/factory.py,sha256=fzkhkI4LqHbzlH0GkwpAWgRjAnLpKd0dbq3APiEecFk,8541
|
|
4
|
+
aiohomematic_test_support/helper.py,sha256=pE687jG-KkrXZUFXnHTiydmhAvlGTjlejQoPeaH9w5w,1304
|
|
5
|
+
aiohomematic_test_support/mock.py,sha256=E3lMlLR_by2ZLRV_zJtI0Ru-B7adLNp9YY43vBPi8_E,21023
|
|
6
|
+
aiohomematic_test_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
aiohomematic_test_support/data/full_session_randomized_ccu.zip,sha256=oN7g0CB_0kQX3qk-RSu-Rt28yW7BcplVqPYZCDqr0EU,734626
|
|
8
|
+
aiohomematic_test_support/data/full_session_randomized_pydevccu.zip,sha256=_QFWSP03dkiMFdD_w-R98DS6ur4PYDQXw-DCkbJEGg4,1293240
|
|
9
|
+
aiohomematic_test_support-2025.10.18.dist-info/METADATA,sha256=w_d31xurGNK8VhWJIc9zjEP_Z6p4HFcJBPq0_9qIqls,536
|
|
10
|
+
aiohomematic_test_support-2025.10.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
aiohomematic_test_support-2025.10.18.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
|
|
12
|
+
aiohomematic_test_support-2025.10.18.dist-info/RECORD,,
|
|
@@ -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"
|
|
@@ -1,11 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|