aiohomematic 2025.10.10__tar.gz → 2025.10.11__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 might be problematic. Click here for more details.
- aiohomematic-2025.10.11/MANIFEST.in +14 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/PKG-INFO +1 -1
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/central/rpc_server.py +21 -27
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/client/json_rpc.py +2 -2
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/const.py +2 -2
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/store/persistent.py +27 -11
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/support.py +9 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic.egg-info/PKG-INFO +1 -1
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic.egg-info/SOURCES.txt +2 -34
- aiohomematic-2025.10.11/aiohomematic.egg-info/top_level.txt +1 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/pyproject.toml +2 -3
- aiohomematic-2025.10.10/aiohomematic.egg-info/top_level.txt +0 -2
- aiohomematic-2025.10.10/aiohomematic_support/__init__.py +0 -1
- aiohomematic-2025.10.10/aiohomematic_support/client_local.py +0 -361
- aiohomematic-2025.10.10/tests/test_action.py +0 -70
- aiohomematic-2025.10.10/tests/test_async_support.py +0 -171
- aiohomematic-2025.10.10/tests/test_binary_sensor.py +0 -103
- aiohomematic-2025.10.10/tests/test_button.py +0 -112
- aiohomematic-2025.10.10/tests/test_calculated_support.py +0 -168
- aiohomematic-2025.10.10/tests/test_central.py +0 -1052
- aiohomematic-2025.10.10/tests/test_central_pydevccu.py +0 -211
- aiohomematic-2025.10.10/tests/test_climate.py +0 -1181
- aiohomematic-2025.10.10/tests/test_cover.py +0 -1184
- aiohomematic-2025.10.10/tests/test_decorator.py +0 -92
- aiohomematic-2025.10.10/tests/test_device.py +0 -146
- aiohomematic-2025.10.10/tests/test_dynamic_caches.py +0 -150
- aiohomematic-2025.10.10/tests/test_entity.py +0 -231
- aiohomematic-2025.10.10/tests/test_event.py +0 -142
- aiohomematic-2025.10.10/tests/test_json_rpc.py +0 -53
- aiohomematic-2025.10.10/tests/test_json_rpc_client_integration.py +0 -34
- aiohomematic-2025.10.10/tests/test_kwonly_lint.py +0 -29
- aiohomematic-2025.10.10/tests/test_light.py +0 -1233
- aiohomematic-2025.10.10/tests/test_lock.py +0 -197
- aiohomematic-2025.10.10/tests/test_logging_support.py +0 -108
- aiohomematic-2025.10.10/tests/test_number.py +0 -227
- aiohomematic-2025.10.10/tests/test_select.py +0 -119
- aiohomematic-2025.10.10/tests/test_sensor.py +0 -168
- aiohomematic-2025.10.10/tests/test_session_recorder.py +0 -38
- aiohomematic-2025.10.10/tests/test_siren.py +0 -211
- aiohomematic-2025.10.10/tests/test_support.py +0 -622
- aiohomematic-2025.10.10/tests/test_support_extra.py +0 -88
- aiohomematic-2025.10.10/tests/test_switch.py +0 -206
- aiohomematic-2025.10.10/tests/test_text.py +0 -73
- aiohomematic-2025.10.10/tests/test_valve.py +0 -98
- aiohomematic-2025.10.10/tests/test_xml_rpc_proxy_integration.py +0 -33
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/LICENSE +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/README.md +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/__init__.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/async_support.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/central/__init__.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/central/decorators.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/client/__init__.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/client/_rpc_errors.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/client/rpc_proxy.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/context.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/converter.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/decorators.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/exceptions.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/hmcli.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/__init__.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/calculated/__init__.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/calculated/climate.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/calculated/data_point.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/calculated/operating_voltage_level.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/calculated/support.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/__init__.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/climate.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/const.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/cover.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/data_point.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/definition.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/light.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/lock.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/siren.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/support.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/switch.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/custom/valve.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/data_point.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/device.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/event.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/__init__.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/action.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/binary_sensor.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/button.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/data_point.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/number.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/select.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/sensor.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/switch.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/generic/text.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/hub/__init__.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/hub/binary_sensor.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/hub/button.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/hub/data_point.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/hub/number.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/hub/select.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/hub/sensor.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/hub/switch.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/hub/text.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/support.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/model/update.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/property_decorators.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/py.typed +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/rega_scripts/fetch_all_device_data.fn +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/rega_scripts/get_program_descriptions.fn +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/rega_scripts/get_serial.fn +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/rega_scripts/get_system_variable_descriptions.fn +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/rega_scripts/set_program_state.fn +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/rega_scripts/set_system_variable.fn +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/store/__init__.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/store/dynamic.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/store/visibility.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic/validator.py +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic.egg-info/dependency_links.txt +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/aiohomematic.egg-info/requires.txt +0 -0
- {aiohomematic-2025.10.10 → aiohomematic-2025.10.11}/setup.cfg +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# MANIFEST.in
|
|
2
|
+
|
|
3
|
+
# Include the main package and important files
|
|
4
|
+
graft aiohomematic
|
|
5
|
+
include README.md
|
|
6
|
+
include LICENSE
|
|
7
|
+
|
|
8
|
+
# Exclude tests and build artifacts
|
|
9
|
+
prune tests
|
|
10
|
+
prune build
|
|
11
|
+
prune dist
|
|
12
|
+
|
|
13
|
+
# IMPORTANT: exclude support package from the sdist tarball
|
|
14
|
+
prune aiohomematic_support
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.11
|
|
4
4
|
Summary: Homematic interface for Home Assistant running on Python 3.
|
|
5
5
|
Home-page: https://github.com/sukramj/aiohomematic
|
|
6
6
|
Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
|
|
@@ -18,7 +18,7 @@ from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
|
|
|
18
18
|
from aiohomematic import central as hmcu
|
|
19
19
|
from aiohomematic.central.decorators import callback_backend_system
|
|
20
20
|
from aiohomematic.const import IP_ANY_V4, PORT_ANY, BackendSystemEvent
|
|
21
|
-
from aiohomematic.support import
|
|
21
|
+
from aiohomematic.support import log_boundary_error
|
|
22
22
|
|
|
23
23
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
24
24
|
|
|
@@ -177,23 +177,19 @@ class RpcServer(threading.Thread):
|
|
|
177
177
|
_initialized: bool = False
|
|
178
178
|
_instances: Final[dict[tuple[str, int], RpcServer]] = {}
|
|
179
179
|
|
|
180
|
-
def __init__(
|
|
181
|
-
self,
|
|
182
|
-
*,
|
|
183
|
-
ip_addr: str,
|
|
184
|
-
port: int,
|
|
185
|
-
) -> None:
|
|
180
|
+
def __init__(self, *, server: SimpleXMLRPCServer) -> None:
|
|
186
181
|
"""Init XmlRPC server."""
|
|
187
|
-
|
|
188
|
-
|
|
182
|
+
self._server = server
|
|
183
|
+
self._server.register_introspection_functions()
|
|
184
|
+
self._server.register_multicall_functions()
|
|
185
|
+
self._server.register_instance(RPCFunctions(rpc_server=self), allow_dotted_names=True)
|
|
189
186
|
self._initialized = True
|
|
190
|
-
self.
|
|
191
|
-
self.
|
|
192
|
-
self.
|
|
187
|
+
self._address: Final[tuple[str, int]] = cast(tuple[str, int], server.server_address)
|
|
188
|
+
self._listen_ip_addr: Final = self._address[0]
|
|
189
|
+
self._listen_port: Final = self._address[1]
|
|
193
190
|
self._centrals: Final[dict[str, hmcu.CentralUnit]] = {}
|
|
194
|
-
self._simple_rpc_server: SimpleXMLRPCServer
|
|
195
191
|
self._instances[self._address] = self
|
|
196
|
-
threading.Thread.__init__(self, name=f"RpcServer {
|
|
192
|
+
threading.Thread.__init__(self, name=f"RpcServer {self._listen_ip_addr}:{self._listen_port}")
|
|
197
193
|
|
|
198
194
|
def run(self) -> None:
|
|
199
195
|
"""Run the RPC-Server thread."""
|
|
@@ -202,15 +198,15 @@ class RpcServer(threading.Thread):
|
|
|
202
198
|
self._listen_ip_addr,
|
|
203
199
|
self._listen_port,
|
|
204
200
|
)
|
|
205
|
-
if self.
|
|
206
|
-
self.
|
|
201
|
+
if self._server:
|
|
202
|
+
self._server.serve_forever()
|
|
207
203
|
|
|
208
204
|
def stop(self) -> None:
|
|
209
205
|
"""Stop the RPC-Server."""
|
|
210
206
|
_LOGGER.debug("STOP: Shutting down RPC-Server")
|
|
211
|
-
self.
|
|
207
|
+
self._server.shutdown()
|
|
212
208
|
_LOGGER.debug("STOP: Stopping RPC-Server")
|
|
213
|
-
self.
|
|
209
|
+
self._server.server_close()
|
|
214
210
|
# Ensure the server thread has actually terminated to avoid slow teardown
|
|
215
211
|
with contextlib.suppress(RuntimeError):
|
|
216
212
|
self.join(timeout=1.0)
|
|
@@ -269,16 +265,14 @@ class XmlRpcServer(RpcServer):
|
|
|
269
265
|
|
|
270
266
|
if self._initialized:
|
|
271
267
|
return
|
|
272
|
-
super().__init__(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
268
|
+
super().__init__(
|
|
269
|
+
server=HomematicXMLRPCServer(
|
|
270
|
+
addr=(ip_addr, port),
|
|
271
|
+
requestHandler=RequestHandler,
|
|
272
|
+
logRequests=False,
|
|
273
|
+
allow_none=True,
|
|
274
|
+
)
|
|
278
275
|
)
|
|
279
|
-
self._simple_rpc_server.register_introspection_functions()
|
|
280
|
-
self._simple_rpc_server.register_multicall_functions()
|
|
281
|
-
self._simple_rpc_server.register_instance(RPCFunctions(rpc_server=self), allow_dotted_names=True)
|
|
282
276
|
|
|
283
277
|
def __new__(cls, ip_addr: str, port: int) -> XmlRpcServer: # noqa: PYI034 # kwonly: disable
|
|
284
278
|
"""Create new RPC server."""
|
|
@@ -368,8 +368,8 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
|
|
|
368
368
|
_LOGGER.debug("POST_SCRIPT: method: %s [%s]", method, script_name)
|
|
369
369
|
|
|
370
370
|
try:
|
|
371
|
-
if not response[_JsonKey.ERROR]:
|
|
372
|
-
response[_JsonKey.RESULT] = orjson.loads(
|
|
371
|
+
if not response[_JsonKey.ERROR] and (resp := response[_JsonKey.RESULT]) and isinstance(resp, str):
|
|
372
|
+
response[_JsonKey.RESULT] = orjson.loads(resp)
|
|
373
373
|
finally:
|
|
374
374
|
if not keep_session:
|
|
375
375
|
await self._do_logout(session_id=session_id)
|
|
@@ -19,7 +19,7 @@ import sys
|
|
|
19
19
|
from types import MappingProxyType
|
|
20
20
|
from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
|
|
21
21
|
|
|
22
|
-
VERSION: Final = "2025.10.
|
|
22
|
+
VERSION: Final = "2025.10.11"
|
|
23
23
|
|
|
24
24
|
# Detect test speedup mode via environment
|
|
25
25
|
_TEST_SPEEDUP: Final = (
|
|
@@ -41,7 +41,7 @@ DEFAULT_MULTIPLIER: Final = 1.0
|
|
|
41
41
|
DEFAULT_OPTIONAL_SETTINGS: Final[tuple[OptionalSettings | str, ...]] = ()
|
|
42
42
|
DEFAULT_PERIODIC_REFRESH_INTERVAL: Final = 15
|
|
43
43
|
DEFAULT_PROGRAM_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
|
|
44
|
-
DEFAULT_SESSION_RECORDER_START_FOR_SECONDS: Final =
|
|
44
|
+
DEFAULT_SESSION_RECORDER_START_FOR_SECONDS: Final = 180
|
|
45
45
|
DEFAULT_STORAGE_DIRECTORY: Final = "aiohomematic_storage"
|
|
46
46
|
DEFAULT_SYSVAR_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
|
|
47
47
|
DEFAULT_SYS_SCAN_INTERVAL: Final = 30
|
|
@@ -43,6 +43,7 @@ import json
|
|
|
43
43
|
import logging
|
|
44
44
|
import os
|
|
45
45
|
from typing import Any, Final, Self
|
|
46
|
+
import zipfile
|
|
46
47
|
|
|
47
48
|
import orjson
|
|
48
49
|
from slugify import slugify
|
|
@@ -198,7 +199,13 @@ class BasePersistentFile(ABC):
|
|
|
198
199
|
)
|
|
199
200
|
|
|
200
201
|
async def load(self, *, file_path: str | None = None) -> DataOperationResult:
|
|
201
|
-
"""
|
|
202
|
+
"""
|
|
203
|
+
Load data from disk into the dictionary.
|
|
204
|
+
|
|
205
|
+
Supports plain JSON files and ZIP archives containing a JSON file.
|
|
206
|
+
When a ZIP archive is provided, the first JSON member inside the archive
|
|
207
|
+
will be loaded.
|
|
208
|
+
"""
|
|
202
209
|
if not file_path and not check_or_create_directory(directory=self._directory):
|
|
203
210
|
return DataOperationResult.NO_LOAD
|
|
204
211
|
|
|
@@ -206,16 +213,25 @@ class BasePersistentFile(ABC):
|
|
|
206
213
|
return DataOperationResult.NO_LOAD
|
|
207
214
|
|
|
208
215
|
def _perform_load() -> DataOperationResult:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
try:
|
|
217
|
+
if zipfile.is_zipfile(file_path):
|
|
218
|
+
with zipfile.ZipFile(file_path, mode="r") as zf:
|
|
219
|
+
# Prefer json files; pick the first .json entry if available
|
|
220
|
+
if not (json_members := [n for n in zf.namelist() if n.lower().endswith(".json")]):
|
|
221
|
+
return DataOperationResult.LOAD_FAIL
|
|
222
|
+
raw = zf.read(json_members[0]).decode(UTF_8)
|
|
223
|
+
data = json.loads(raw, object_hook=regular_to_default_dict_hook)
|
|
224
|
+
else:
|
|
225
|
+
with open(file=file_path, encoding=UTF_8) as file_pointer:
|
|
226
|
+
data = json.loads(file_pointer.read(), object_hook=regular_to_default_dict_hook)
|
|
227
|
+
|
|
228
|
+
if (converted_hash := hash_sha256(value=data)) == self.last_hash_saved:
|
|
229
|
+
return DataOperationResult.NO_LOAD
|
|
230
|
+
self._persistent_content.clear()
|
|
231
|
+
self._persistent_content.update(data)
|
|
232
|
+
self.last_hash_saved = converted_hash
|
|
233
|
+
except (json.JSONDecodeError, zipfile.BadZipFile, UnicodeDecodeError, OSError):
|
|
234
|
+
return DataOperationResult.LOAD_FAIL
|
|
219
235
|
return DataOperationResult.LOAD_SUCCESS
|
|
220
236
|
|
|
221
237
|
async with self._save_load_semaphore:
|
|
@@ -582,6 +582,15 @@ def create_random_device_addresses(*, addresses: list[str]) -> dict[str, str]:
|
|
|
582
582
|
return {adr: f"VCU{int(random.randint(1000000, 9999999))}" for adr in addresses}
|
|
583
583
|
|
|
584
584
|
|
|
585
|
+
def shrink_json_file(file_name: str) -> None:
|
|
586
|
+
"""Shrink a json file."""
|
|
587
|
+
with open(file_name, "rb") as f:
|
|
588
|
+
data = orjson.loads(f.read())
|
|
589
|
+
|
|
590
|
+
with open(file_name, "wb") as f:
|
|
591
|
+
f.write(orjson.dumps(data))
|
|
592
|
+
|
|
593
|
+
|
|
585
594
|
# --- Structured error boundary logging helpers ---
|
|
586
595
|
|
|
587
596
|
_BOUNDARY_MSG = "error_boundary"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.11
|
|
4
4
|
Summary: Homematic interface for Home Assistant running on Python 3.
|
|
5
5
|
Home-page: https://github.com/sukramj/aiohomematic
|
|
6
6
|
Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
2
3
|
README.md
|
|
3
4
|
pyproject.toml
|
|
4
5
|
setup.cfg
|
|
@@ -77,37 +78,4 @@ aiohomematic/rega_scripts/set_system_variable.fn
|
|
|
77
78
|
aiohomematic/store/__init__.py
|
|
78
79
|
aiohomematic/store/dynamic.py
|
|
79
80
|
aiohomematic/store/persistent.py
|
|
80
|
-
aiohomematic/store/visibility.py
|
|
81
|
-
aiohomematic_support/__init__.py
|
|
82
|
-
aiohomematic_support/client_local.py
|
|
83
|
-
tests/test_action.py
|
|
84
|
-
tests/test_async_support.py
|
|
85
|
-
tests/test_binary_sensor.py
|
|
86
|
-
tests/test_button.py
|
|
87
|
-
tests/test_calculated_support.py
|
|
88
|
-
tests/test_central.py
|
|
89
|
-
tests/test_central_pydevccu.py
|
|
90
|
-
tests/test_climate.py
|
|
91
|
-
tests/test_cover.py
|
|
92
|
-
tests/test_decorator.py
|
|
93
|
-
tests/test_device.py
|
|
94
|
-
tests/test_dynamic_caches.py
|
|
95
|
-
tests/test_entity.py
|
|
96
|
-
tests/test_event.py
|
|
97
|
-
tests/test_json_rpc.py
|
|
98
|
-
tests/test_json_rpc_client_integration.py
|
|
99
|
-
tests/test_kwonly_lint.py
|
|
100
|
-
tests/test_light.py
|
|
101
|
-
tests/test_lock.py
|
|
102
|
-
tests/test_logging_support.py
|
|
103
|
-
tests/test_number.py
|
|
104
|
-
tests/test_select.py
|
|
105
|
-
tests/test_sensor.py
|
|
106
|
-
tests/test_session_recorder.py
|
|
107
|
-
tests/test_siren.py
|
|
108
|
-
tests/test_support.py
|
|
109
|
-
tests/test_support_extra.py
|
|
110
|
-
tests/test_switch.py
|
|
111
|
-
tests/test_text.py
|
|
112
|
-
tests/test_valve.py
|
|
113
|
-
tests/test_xml_rpc_proxy_integration.py
|
|
81
|
+
aiohomematic/store/visibility.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiohomematic
|
|
@@ -46,8 +46,8 @@ include-package-data = true
|
|
|
46
46
|
version = {attr = "aiohomematic.const.VERSION"}
|
|
47
47
|
|
|
48
48
|
[tool.setuptools.packages.find]
|
|
49
|
-
include = ["aiohomematic
|
|
50
|
-
exclude = ["tests", "tests.*", "dist", "build"]
|
|
49
|
+
include = ["aiohomematic", "aiohomematic.*"]
|
|
50
|
+
exclude = ["aiohomematic_support", "aiohomematic_support.*", "tests", "tests.*", "dist", "build"]
|
|
51
51
|
|
|
52
52
|
[tool.setuptools.package-data]
|
|
53
53
|
aiohomematic = ["py.typed", "rega_scripts/*.fn"]
|
|
@@ -571,7 +571,6 @@ branch = true
|
|
|
571
571
|
source = ["aiohomematic",]
|
|
572
572
|
omit = [
|
|
573
573
|
"aiohomematic/hmcli.py",
|
|
574
|
-
"aiohomematic/client/__init__.py",
|
|
575
574
|
"aiohomematic/validator.py",
|
|
576
575
|
"aiohomematic/exceptions.py",
|
|
577
576
|
"aiohomematic/central/rpc_server.py",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Module to support aiohomematic testing with a local client."""
|
|
@@ -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"
|