aiohomematic 2025.8.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/__init__.py +47 -0
- aiohomematic/async_support.py +146 -0
- aiohomematic/caches/__init__.py +10 -0
- aiohomematic/caches/dynamic.py +554 -0
- aiohomematic/caches/persistent.py +459 -0
- aiohomematic/caches/visibility.py +774 -0
- aiohomematic/central/__init__.py +2034 -0
- aiohomematic/central/decorators.py +110 -0
- aiohomematic/central/xml_rpc_server.py +267 -0
- aiohomematic/client/__init__.py +1746 -0
- aiohomematic/client/json_rpc.py +1193 -0
- aiohomematic/client/xml_rpc.py +222 -0
- aiohomematic/const.py +795 -0
- aiohomematic/context.py +8 -0
- aiohomematic/converter.py +82 -0
- aiohomematic/decorators.py +188 -0
- aiohomematic/exceptions.py +145 -0
- aiohomematic/hmcli.py +159 -0
- aiohomematic/model/__init__.py +137 -0
- aiohomematic/model/calculated/__init__.py +65 -0
- aiohomematic/model/calculated/climate.py +230 -0
- aiohomematic/model/calculated/data_point.py +319 -0
- aiohomematic/model/calculated/operating_voltage_level.py +311 -0
- aiohomematic/model/calculated/support.py +174 -0
- aiohomematic/model/custom/__init__.py +175 -0
- aiohomematic/model/custom/climate.py +1334 -0
- aiohomematic/model/custom/const.py +146 -0
- aiohomematic/model/custom/cover.py +741 -0
- aiohomematic/model/custom/data_point.py +318 -0
- aiohomematic/model/custom/definition.py +861 -0
- aiohomematic/model/custom/light.py +1092 -0
- aiohomematic/model/custom/lock.py +389 -0
- aiohomematic/model/custom/siren.py +268 -0
- aiohomematic/model/custom/support.py +40 -0
- aiohomematic/model/custom/switch.py +172 -0
- aiohomematic/model/custom/valve.py +112 -0
- aiohomematic/model/data_point.py +1109 -0
- aiohomematic/model/decorators.py +173 -0
- aiohomematic/model/device.py +1347 -0
- aiohomematic/model/event.py +210 -0
- aiohomematic/model/generic/__init__.py +211 -0
- aiohomematic/model/generic/action.py +32 -0
- aiohomematic/model/generic/binary_sensor.py +28 -0
- aiohomematic/model/generic/button.py +25 -0
- aiohomematic/model/generic/data_point.py +162 -0
- aiohomematic/model/generic/number.py +73 -0
- aiohomematic/model/generic/select.py +36 -0
- aiohomematic/model/generic/sensor.py +72 -0
- aiohomematic/model/generic/switch.py +52 -0
- aiohomematic/model/generic/text.py +27 -0
- aiohomematic/model/hub/__init__.py +334 -0
- aiohomematic/model/hub/binary_sensor.py +22 -0
- aiohomematic/model/hub/button.py +26 -0
- aiohomematic/model/hub/data_point.py +332 -0
- aiohomematic/model/hub/number.py +37 -0
- aiohomematic/model/hub/select.py +47 -0
- aiohomematic/model/hub/sensor.py +35 -0
- aiohomematic/model/hub/switch.py +42 -0
- aiohomematic/model/hub/text.py +28 -0
- aiohomematic/model/support.py +599 -0
- aiohomematic/model/update.py +136 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +75 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
- aiohomematic/rega_scripts/set_program_state.fn +12 -0
- aiohomematic/rega_scripts/set_system_variable.fn +15 -0
- aiohomematic/support.py +482 -0
- aiohomematic/validator.py +65 -0
- aiohomematic-2025.8.6.dist-info/METADATA +69 -0
- aiohomematic-2025.8.6.dist-info/RECORD +77 -0
- aiohomematic-2025.8.6.dist-info/WHEEL +5 -0
- aiohomematic-2025.8.6.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2025.8.6.dist-info/top_level.txt +2 -0
- aiohomematic_support/__init__.py +1 -0
- aiohomematic_support/client_local.py +349 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XML-RPC transport proxy with concurrency control and connection awareness.
|
|
3
|
+
|
|
4
|
+
Overview
|
|
5
|
+
--------
|
|
6
|
+
XmlRpcProxy extends xmlrpc.client.ServerProxy to:
|
|
7
|
+
- Execute RPC calls in a thread pool to avoid blocking the event loop
|
|
8
|
+
- Integrate with CentralConnectionState to mark/report connection issues
|
|
9
|
+
- Optionally use TLS with configurable certificate verification
|
|
10
|
+
- Filter unsupported methods at runtime via system.listMethods
|
|
11
|
+
|
|
12
|
+
Notes
|
|
13
|
+
-----
|
|
14
|
+
- The proxy cleans and normalizes argument encodings for XML-RPC.
|
|
15
|
+
- Certain methods are allowed even when the connection is flagged down
|
|
16
|
+
(e.g., ping, init, getVersion) to support recovery.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections.abc import Mapping
|
|
23
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
24
|
+
from enum import Enum, IntEnum, StrEnum
|
|
25
|
+
import errno
|
|
26
|
+
import logging
|
|
27
|
+
from ssl import SSLError
|
|
28
|
+
from typing import Any, Final
|
|
29
|
+
import xmlrpc.client
|
|
30
|
+
|
|
31
|
+
from aiohomematic import central as hmcu
|
|
32
|
+
from aiohomematic.async_support import Looper
|
|
33
|
+
from aiohomematic.const import ISO_8859_1
|
|
34
|
+
from aiohomematic.exceptions import (
|
|
35
|
+
AuthFailure,
|
|
36
|
+
BaseHomematicException,
|
|
37
|
+
ClientException,
|
|
38
|
+
NoConnectionException,
|
|
39
|
+
UnsupportedException,
|
|
40
|
+
)
|
|
41
|
+
from aiohomematic.support import extract_exc_args, get_tls_context
|
|
42
|
+
|
|
43
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
_CONTEXT: Final = "context"
|
|
46
|
+
_TLS: Final = "tls"
|
|
47
|
+
_VERIFY_TLS: Final = "verify_tls"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _XmlRpcMethod(StrEnum):
|
|
51
|
+
"""Enum for homematic json rpc methods types."""
|
|
52
|
+
|
|
53
|
+
GET_VERSION = "getVersion"
|
|
54
|
+
HOMEGEAR_INIT = "clientServerInitialized"
|
|
55
|
+
INIT = "init"
|
|
56
|
+
PING = "ping"
|
|
57
|
+
SYSTEM_LIST_METHODS = "system.listMethods"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_VALID_XMLRPC_COMMANDS_ON_NO_CONNECTION: Final[tuple[str, ...]] = (
|
|
61
|
+
_XmlRpcMethod.GET_VERSION,
|
|
62
|
+
_XmlRpcMethod.HOMEGEAR_INIT,
|
|
63
|
+
_XmlRpcMethod.INIT,
|
|
64
|
+
_XmlRpcMethod.PING,
|
|
65
|
+
_XmlRpcMethod.SYSTEM_LIST_METHODS,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
_SSL_ERROR_CODES: Final[dict[int, str]] = {
|
|
69
|
+
errno.ENOEXEC: "EOF occurred in violation of protocol",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_OS_ERROR_CODES: Final[dict[int, str]] = {
|
|
73
|
+
errno.ECONNREFUSED: "Connection refused",
|
|
74
|
+
errno.EHOSTUNREACH: "No route to host",
|
|
75
|
+
errno.ENETUNREACH: "Network is unreachable",
|
|
76
|
+
errno.ENOEXEC: "Exec",
|
|
77
|
+
errno.ETIMEDOUT: "Operation timed out",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# noinspection PyProtectedMember,PyUnresolvedReferences
|
|
82
|
+
class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
83
|
+
"""ServerProxy implementation with ThreadPoolExecutor when request is executing."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
max_workers: int,
|
|
88
|
+
interface_id: str,
|
|
89
|
+
connection_state: hmcu.CentralConnectionState,
|
|
90
|
+
*args: Any,
|
|
91
|
+
**kwargs: Any,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Initialize new proxy for server and get local ip."""
|
|
94
|
+
self.interface_id: Final = interface_id
|
|
95
|
+
self._connection_state: Final = connection_state
|
|
96
|
+
self._looper: Final = Looper()
|
|
97
|
+
self._proxy_executor: Final = (
|
|
98
|
+
ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix=interface_id) if max_workers > 0 else None
|
|
99
|
+
)
|
|
100
|
+
self._tls: Final[bool] = kwargs.pop(_TLS, False)
|
|
101
|
+
self._verify_tls: Final[bool] = kwargs.pop(_VERIFY_TLS, True)
|
|
102
|
+
self._supported_methods: tuple[str, ...] = ()
|
|
103
|
+
if self._tls:
|
|
104
|
+
kwargs[_CONTEXT] = get_tls_context(self._verify_tls)
|
|
105
|
+
xmlrpc.client.ServerProxy.__init__( # type: ignore[misc]
|
|
106
|
+
self,
|
|
107
|
+
encoding=ISO_8859_1,
|
|
108
|
+
*args, # noqa: B026
|
|
109
|
+
**kwargs,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def do_init(self) -> None:
|
|
113
|
+
"""Init the xml rpc proxy."""
|
|
114
|
+
if supported_methods := await self.system.listMethods():
|
|
115
|
+
# ping is missing in VirtualDevices interface but can be used.
|
|
116
|
+
supported_methods.append(_XmlRpcMethod.PING)
|
|
117
|
+
self._supported_methods = tuple(supported_methods)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def supported_methods(self) -> tuple[str, ...]:
|
|
121
|
+
"""Return the supported methods."""
|
|
122
|
+
return self._supported_methods
|
|
123
|
+
|
|
124
|
+
async def __async_request(self, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
125
|
+
"""Call method on server side."""
|
|
126
|
+
parent = xmlrpc.client.ServerProxy
|
|
127
|
+
try:
|
|
128
|
+
method = args[0]
|
|
129
|
+
if self._supported_methods and method not in self._supported_methods:
|
|
130
|
+
raise UnsupportedException(f"__ASYNC_REQUEST: method '{method} not supported by backend.")
|
|
131
|
+
|
|
132
|
+
if method in _VALID_XMLRPC_COMMANDS_ON_NO_CONNECTION or not self._connection_state.has_issue(
|
|
133
|
+
issuer=self, iid=self.interface_id
|
|
134
|
+
):
|
|
135
|
+
args = _cleanup_args(*args)
|
|
136
|
+
_LOGGER.debug("__ASYNC_REQUEST: %s", args)
|
|
137
|
+
result = await self._looper.async_add_executor_job(
|
|
138
|
+
# pylint: disable=protected-access
|
|
139
|
+
parent._ServerProxy__request, # type: ignore[attr-defined]
|
|
140
|
+
self,
|
|
141
|
+
*args,
|
|
142
|
+
name="xmp_rpc_proxy",
|
|
143
|
+
executor=self._proxy_executor,
|
|
144
|
+
)
|
|
145
|
+
self._connection_state.remove_issue(issuer=self, iid=self.interface_id)
|
|
146
|
+
return result
|
|
147
|
+
raise NoConnectionException(f"No connection to {self.interface_id}")
|
|
148
|
+
except BaseHomematicException:
|
|
149
|
+
raise
|
|
150
|
+
except SSLError as sslerr:
|
|
151
|
+
message = f"SSLError on {self.interface_id}: {extract_exc_args(exc=sslerr)}"
|
|
152
|
+
if sslerr.args[0] in _SSL_ERROR_CODES:
|
|
153
|
+
_LOGGER.debug(message)
|
|
154
|
+
else:
|
|
155
|
+
_LOGGER.error(message)
|
|
156
|
+
raise NoConnectionException(message) from sslerr
|
|
157
|
+
except OSError as oserr:
|
|
158
|
+
message = f"OSError on {self.interface_id}: {extract_exc_args(exc=oserr)}"
|
|
159
|
+
if oserr.args[0] in _OS_ERROR_CODES:
|
|
160
|
+
if self._connection_state.add_issue(issuer=self, iid=self.interface_id):
|
|
161
|
+
_LOGGER.error(message)
|
|
162
|
+
else:
|
|
163
|
+
_LOGGER.debug(message)
|
|
164
|
+
else:
|
|
165
|
+
_LOGGER.error(message)
|
|
166
|
+
raise NoConnectionException(message) from oserr
|
|
167
|
+
except xmlrpc.client.Fault as flt:
|
|
168
|
+
raise ClientException(f"XMLRPC Fault from backend: {flt.faultCode} {flt.faultString}") from flt
|
|
169
|
+
except TypeError as terr:
|
|
170
|
+
raise ClientException(terr) from terr
|
|
171
|
+
except xmlrpc.client.ProtocolError as perr:
|
|
172
|
+
if not self._connection_state.has_issue(issuer=self, iid=self.interface_id):
|
|
173
|
+
if perr.errmsg == "Unauthorized":
|
|
174
|
+
raise AuthFailure(perr) from perr
|
|
175
|
+
raise NoConnectionException(perr.errmsg) from perr
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
raise ClientException(exc) from exc
|
|
178
|
+
|
|
179
|
+
def __getattr__(self, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
180
|
+
"""Magic method dispatcher."""
|
|
181
|
+
return xmlrpc.client._Method(self.__async_request, *args, **kwargs)
|
|
182
|
+
|
|
183
|
+
async def stop(self) -> None:
|
|
184
|
+
"""Stop depending services."""
|
|
185
|
+
await self._looper.block_till_done()
|
|
186
|
+
if self._proxy_executor:
|
|
187
|
+
self._proxy_executor.shutdown()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _cleanup_args(*args: Any) -> Any:
|
|
191
|
+
"""Cleanup the type of args."""
|
|
192
|
+
if len(args[1]) == 0:
|
|
193
|
+
return args
|
|
194
|
+
if len(args) == 2:
|
|
195
|
+
new_args: list[Any] = []
|
|
196
|
+
for data in args[1]:
|
|
197
|
+
if isinstance(data, dict):
|
|
198
|
+
new_args.append(_cleanup_paramset(paramset=data))
|
|
199
|
+
else:
|
|
200
|
+
new_args.append(_cleanup_item(item=data))
|
|
201
|
+
return (args[0], tuple(new_args))
|
|
202
|
+
_LOGGER.error("XmlRpcProxy command: Too many arguments")
|
|
203
|
+
return args
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _cleanup_item(item: Any) -> Any:
|
|
207
|
+
"""Cleanup a single item."""
|
|
208
|
+
if isinstance(item, StrEnum):
|
|
209
|
+
return str(item)
|
|
210
|
+
if isinstance(item, IntEnum):
|
|
211
|
+
return int(item)
|
|
212
|
+
if isinstance(item, Enum):
|
|
213
|
+
_LOGGER.error("XmlRpcProxy command: Enum is not supported as parameter value")
|
|
214
|
+
return item
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _cleanup_paramset(paramset: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
218
|
+
"""Cleanup a paramset."""
|
|
219
|
+
new_paramset: dict[str, Any] = {}
|
|
220
|
+
for name, value in paramset.items():
|
|
221
|
+
new_paramset[_cleanup_item(item=name)] = _cleanup_item(item=value)
|
|
222
|
+
return new_paramset
|