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.

Files changed (77) hide show
  1. aiohomematic/__init__.py +47 -0
  2. aiohomematic/async_support.py +146 -0
  3. aiohomematic/caches/__init__.py +10 -0
  4. aiohomematic/caches/dynamic.py +554 -0
  5. aiohomematic/caches/persistent.py +459 -0
  6. aiohomematic/caches/visibility.py +774 -0
  7. aiohomematic/central/__init__.py +2034 -0
  8. aiohomematic/central/decorators.py +110 -0
  9. aiohomematic/central/xml_rpc_server.py +267 -0
  10. aiohomematic/client/__init__.py +1746 -0
  11. aiohomematic/client/json_rpc.py +1193 -0
  12. aiohomematic/client/xml_rpc.py +222 -0
  13. aiohomematic/const.py +795 -0
  14. aiohomematic/context.py +8 -0
  15. aiohomematic/converter.py +82 -0
  16. aiohomematic/decorators.py +188 -0
  17. aiohomematic/exceptions.py +145 -0
  18. aiohomematic/hmcli.py +159 -0
  19. aiohomematic/model/__init__.py +137 -0
  20. aiohomematic/model/calculated/__init__.py +65 -0
  21. aiohomematic/model/calculated/climate.py +230 -0
  22. aiohomematic/model/calculated/data_point.py +319 -0
  23. aiohomematic/model/calculated/operating_voltage_level.py +311 -0
  24. aiohomematic/model/calculated/support.py +174 -0
  25. aiohomematic/model/custom/__init__.py +175 -0
  26. aiohomematic/model/custom/climate.py +1334 -0
  27. aiohomematic/model/custom/const.py +146 -0
  28. aiohomematic/model/custom/cover.py +741 -0
  29. aiohomematic/model/custom/data_point.py +318 -0
  30. aiohomematic/model/custom/definition.py +861 -0
  31. aiohomematic/model/custom/light.py +1092 -0
  32. aiohomematic/model/custom/lock.py +389 -0
  33. aiohomematic/model/custom/siren.py +268 -0
  34. aiohomematic/model/custom/support.py +40 -0
  35. aiohomematic/model/custom/switch.py +172 -0
  36. aiohomematic/model/custom/valve.py +112 -0
  37. aiohomematic/model/data_point.py +1109 -0
  38. aiohomematic/model/decorators.py +173 -0
  39. aiohomematic/model/device.py +1347 -0
  40. aiohomematic/model/event.py +210 -0
  41. aiohomematic/model/generic/__init__.py +211 -0
  42. aiohomematic/model/generic/action.py +32 -0
  43. aiohomematic/model/generic/binary_sensor.py +28 -0
  44. aiohomematic/model/generic/button.py +25 -0
  45. aiohomematic/model/generic/data_point.py +162 -0
  46. aiohomematic/model/generic/number.py +73 -0
  47. aiohomematic/model/generic/select.py +36 -0
  48. aiohomematic/model/generic/sensor.py +72 -0
  49. aiohomematic/model/generic/switch.py +52 -0
  50. aiohomematic/model/generic/text.py +27 -0
  51. aiohomematic/model/hub/__init__.py +334 -0
  52. aiohomematic/model/hub/binary_sensor.py +22 -0
  53. aiohomematic/model/hub/button.py +26 -0
  54. aiohomematic/model/hub/data_point.py +332 -0
  55. aiohomematic/model/hub/number.py +37 -0
  56. aiohomematic/model/hub/select.py +47 -0
  57. aiohomematic/model/hub/sensor.py +35 -0
  58. aiohomematic/model/hub/switch.py +42 -0
  59. aiohomematic/model/hub/text.py +28 -0
  60. aiohomematic/model/support.py +599 -0
  61. aiohomematic/model/update.py +136 -0
  62. aiohomematic/py.typed +0 -0
  63. aiohomematic/rega_scripts/fetch_all_device_data.fn +75 -0
  64. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  65. aiohomematic/rega_scripts/get_serial.fn +44 -0
  66. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  67. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  68. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  69. aiohomematic/support.py +482 -0
  70. aiohomematic/validator.py +65 -0
  71. aiohomematic-2025.8.6.dist-info/METADATA +69 -0
  72. aiohomematic-2025.8.6.dist-info/RECORD +77 -0
  73. aiohomematic-2025.8.6.dist-info/WHEEL +5 -0
  74. aiohomematic-2025.8.6.dist-info/licenses/LICENSE +21 -0
  75. aiohomematic-2025.8.6.dist-info/top_level.txt +2 -0
  76. aiohomematic_support/__init__.py +1 -0
  77. 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