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,110 @@
1
+ """Decorators for central used within aiohomematic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from datetime import datetime
7
+ from functools import wraps
8
+ import inspect
9
+ import logging
10
+ from typing import Any, Final, cast
11
+
12
+ from aiohomematic import central as hmcu, client as hmcl
13
+ from aiohomematic.central import xml_rpc_server as xmlrpc
14
+ from aiohomematic.const import BackendSystemEvent
15
+ from aiohomematic.exceptions import AioHomematicException
16
+ from aiohomematic.support import extract_exc_args
17
+
18
+ _LOGGER: Final = logging.getLogger(__name__)
19
+ _INTERFACE_ID: Final = "interface_id"
20
+
21
+
22
+ def callback_backend_system(system_event: BackendSystemEvent) -> Callable:
23
+ """Check if backend_system_callback is set and call it AFTER original function."""
24
+
25
+ def decorator_backend_system_callback[**P, R](
26
+ func: Callable[P, R | Awaitable[R]],
27
+ ) -> Callable[P, R | Awaitable[R]]:
28
+ """Decorate callback system events."""
29
+
30
+ @wraps(func)
31
+ async def async_wrapper_backend_system_callback(*args: P.args, **kwargs: P.kwargs) -> R:
32
+ """Wrap async callback system events."""
33
+ return_value = cast(R, await func(*args, **kwargs)) # type: ignore[misc]
34
+ await _exec_backend_system_callback(*args, **kwargs)
35
+ return return_value
36
+
37
+ @wraps(func)
38
+ def wrapper_backend_system_callback(*args: P.args, **kwargs: P.kwargs) -> R:
39
+ """Wrap callback system events."""
40
+ return_value = cast(R, func(*args, **kwargs))
41
+ try:
42
+ unit = args[0]
43
+ central: hmcu.CentralUnit | None = None
44
+ if isinstance(unit, hmcu.CentralUnit):
45
+ central = unit
46
+ if central is None and isinstance(unit, xmlrpc.RPCFunctions):
47
+ central = unit.get_central(interface_id=str(args[1]))
48
+ if central:
49
+ central.looper.create_task(
50
+ _exec_backend_system_callback(*args, **kwargs),
51
+ name="wrapper_backend_system_callback",
52
+ )
53
+ except Exception as exc:
54
+ _LOGGER.warning(
55
+ "EXEC_BACKEND_SYSTEM_CALLBACK failed: Problem with identifying central: %s",
56
+ extract_exc_args(exc=exc),
57
+ )
58
+ return return_value
59
+
60
+ async def _exec_backend_system_callback(*args: Any, **kwargs: Any) -> None:
61
+ """Execute the callback for a system event."""
62
+
63
+ if not ((len(args) > 1 and not kwargs) or (len(args) == 1 and kwargs)):
64
+ _LOGGER.warning("EXEC_BACKEND_SYSTEM_CALLBACK failed: *args not supported for callback_system_event")
65
+ try:
66
+ args = args[1:]
67
+ interface_id: str = args[0] if len(args) > 0 else str(kwargs[_INTERFACE_ID])
68
+ if client := hmcl.get_client(interface_id=interface_id):
69
+ client.modified_at = datetime.now()
70
+ client.central.fire_backend_system_callback(system_event=system_event, **kwargs)
71
+ except Exception as exc: # pragma: no cover
72
+ _LOGGER.warning(
73
+ "EXEC_BACKEND_SYSTEM_CALLBACK failed: Unable to reduce kwargs for backend_system_callback"
74
+ )
75
+ raise AioHomematicException(
76
+ f"args-exception backend_system_callback [{extract_exc_args(exc=exc)}]"
77
+ ) from exc
78
+
79
+ if inspect.iscoroutinefunction(func):
80
+ return async_wrapper_backend_system_callback
81
+ return wrapper_backend_system_callback
82
+
83
+ return decorator_backend_system_callback
84
+
85
+
86
+ def callback_event[**P, R](
87
+ func: Callable[P, R],
88
+ ) -> Callable:
89
+ """Check if event_callback is set and call it AFTER original function."""
90
+
91
+ @wraps(func)
92
+ async def async_wrapper_event_callback(*args: P.args, **kwargs: P.kwargs) -> R:
93
+ """Wrap callback events."""
94
+ return_value = cast(R, await func(*args, **kwargs)) # type: ignore[misc]
95
+ _exec_event_callback(*args, **kwargs)
96
+ return return_value
97
+
98
+ def _exec_event_callback(*args: Any, **kwargs: Any) -> None:
99
+ """Execute the callback for a data_point event."""
100
+ try:
101
+ args = args[1:]
102
+ interface_id: str = args[0] if len(args) > 1 else str(kwargs[_INTERFACE_ID])
103
+ if client := hmcl.get_client(interface_id=interface_id):
104
+ client.modified_at = datetime.now()
105
+ client.central.fire_backend_parameter_callback(*args, **kwargs)
106
+ except Exception as exc: # pragma: no cover
107
+ _LOGGER.warning("EXEC_DATA_POINT_EVENT_CALLBACK failed: Unable to reduce kwargs for event_callback")
108
+ raise AioHomematicException(f"args-exception event_callback [{extract_exc_args(exc=exc)}]") from exc
109
+
110
+ return async_wrapper_event_callback
@@ -0,0 +1,267 @@
1
+ """
2
+ XML-RPC server module.
3
+
4
+ Provides the XML-RPC server which handles communication
5
+ with the CCU or Homegear.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ import logging
12
+ import threading
13
+ from typing import Any, Final
14
+ from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
15
+
16
+ from aiohomematic import central as hmcu
17
+ from aiohomematic.central.decorators import callback_backend_system
18
+ from aiohomematic.const import IP_ANY_V4, PORT_ANY, BackendSystemEvent
19
+ from aiohomematic.support import find_free_port
20
+
21
+ _LOGGER: Final = logging.getLogger(__name__)
22
+
23
+
24
+ # pylint: disable=invalid-name
25
+ class RPCFunctions:
26
+ """The XML-RPC functions the CCU or Homegear will expect."""
27
+
28
+ def __init__(self, xml_rpc_server: XmlRpcServer) -> None:
29
+ """Init RPCFunctions."""
30
+ self._xml_rpc_server: Final = xml_rpc_server
31
+
32
+ def event(self, interface_id: str, channel_address: str, parameter: str, value: Any) -> None:
33
+ """If a device emits some sort event, we will handle it here."""
34
+ if central := self.get_central(interface_id):
35
+ central.looper.create_task(
36
+ central.data_point_event(
37
+ interface_id=interface_id,
38
+ channel_address=channel_address,
39
+ parameter=parameter,
40
+ value=value,
41
+ ),
42
+ name=f"event-{interface_id}-{channel_address}-{parameter}",
43
+ )
44
+
45
+ @callback_backend_system(system_event=BackendSystemEvent.ERROR)
46
+ def error(self, interface_id: str, error_code: str, msg: str) -> None:
47
+ """When some error occurs the CCU / Homegear will send its error message here."""
48
+ _LOGGER.warning(
49
+ "ERROR failed: interface_id = %s, error_code = %i, message = %s",
50
+ interface_id,
51
+ int(error_code),
52
+ str(msg),
53
+ )
54
+
55
+ def listDevices(self, interface_id: str) -> list[dict[str, Any]]:
56
+ """Return already existing devices to CCU / Homegear."""
57
+ if central := self.get_central(interface_id):
58
+ return [dict(device_description) for device_description in central.list_devices(interface_id=interface_id)]
59
+ return []
60
+
61
+ def newDevices(self, interface_id: str, device_descriptions: list[dict[str, Any]]) -> None:
62
+ """Add new devices send from backend."""
63
+ central: hmcu.CentralUnit | None
64
+ if central := self.get_central(interface_id):
65
+ central.looper.create_task(
66
+ central.add_new_devices(interface_id=interface_id, device_descriptions=tuple(device_descriptions)),
67
+ name=f"newDevices-{interface_id}",
68
+ )
69
+
70
+ def deleteDevices(self, interface_id: str, addresses: list[str]) -> None:
71
+ """Delete devices send from backend."""
72
+ central: hmcu.CentralUnit | None
73
+ if central := self.get_central(interface_id):
74
+ central.looper.create_task(
75
+ central.delete_devices(interface_id=interface_id, addresses=tuple(addresses)),
76
+ name=f"deleteDevices-{interface_id}",
77
+ )
78
+
79
+ @callback_backend_system(system_event=BackendSystemEvent.UPDATE_DEVICE)
80
+ def updateDevice(self, interface_id: str, address: str, hint: int) -> None:
81
+ """
82
+ Update a device.
83
+
84
+ Irrelevant, as currently only changes to link
85
+ partners are reported.
86
+ """
87
+ _LOGGER.debug(
88
+ "UPDATEDEVICE: interface_id = %s, address = %s, hint = %s",
89
+ interface_id,
90
+ address,
91
+ str(hint),
92
+ )
93
+
94
+ @callback_backend_system(system_event=BackendSystemEvent.REPLACE_DEVICE)
95
+ def replaceDevice(self, interface_id: str, old_device_address: str, new_device_address: str) -> None:
96
+ """Replace a device. Probably irrelevant for us."""
97
+ _LOGGER.debug(
98
+ "REPLACEDEVICE: interface_id = %s, oldDeviceAddress = %s, newDeviceAddress = %s",
99
+ interface_id,
100
+ old_device_address,
101
+ new_device_address,
102
+ )
103
+
104
+ @callback_backend_system(system_event=BackendSystemEvent.RE_ADDED_DEVICE)
105
+ def readdedDevice(self, interface_id: str, addresses: list[str]) -> None:
106
+ """
107
+ Re-Add device from backend.
108
+
109
+ Probably irrelevant for us.
110
+ Gets called when a known devices is put into learn-mode
111
+ while installation mode is active.
112
+ """
113
+ _LOGGER.debug(
114
+ "READDEDDEVICES: interface_id = %s, addresses = %s",
115
+ interface_id,
116
+ str(addresses),
117
+ )
118
+
119
+ def get_central(self, interface_id: str) -> hmcu.CentralUnit | None:
120
+ """Return the central by interface_id."""
121
+ return self._xml_rpc_server.get_central(interface_id)
122
+
123
+
124
+ # Restrict to specific paths.
125
+ class RequestHandler(SimpleXMLRPCRequestHandler):
126
+ """We handle requests to / and /RPC2."""
127
+
128
+ rpc_paths = (
129
+ "/",
130
+ "/RPC2",
131
+ )
132
+
133
+
134
+ class AioHomematicXMLRPCServer(SimpleXMLRPCServer):
135
+ """
136
+ Simple XML-RPC server.
137
+
138
+ Simple XML-RPC server that allows functions and a single instance
139
+ to be installed to handle requests. The default implementation
140
+ attempts to dispatch XML-RPC calls to the functions or instance
141
+ installed in the server. Override the _dispatch method inherited
142
+ from SimpleXMLRPCDispatcher to change this behavior.
143
+
144
+ This implementation adds an additional method:
145
+ system_listMethods(self, interface_id: str.
146
+ """
147
+
148
+ def system_listMethods(self, interface_id: str | None = None) -> list[str]:
149
+ """
150
+ Return a list of the methods supported by the server.
151
+
152
+ system.listMethods() => ['add', 'subtract', 'multiple']
153
+ Required for HomeMatic CCU usage.
154
+ """
155
+ return SimpleXMLRPCServer.system_listMethods(self)
156
+
157
+
158
+ class XmlRpcServer(threading.Thread):
159
+ """XML-RPC server thread to handle messages from CCU / Homegear."""
160
+
161
+ _initialized: bool = False
162
+ _instances: Final[dict[tuple[str, int], XmlRpcServer]] = {}
163
+
164
+ def __init__(
165
+ self,
166
+ ip_addr: str,
167
+ port: int,
168
+ ) -> None:
169
+ """Init XmlRPC server."""
170
+ if self._initialized:
171
+ return
172
+ self._initialized = True
173
+ self._listen_ip_addr: Final = ip_addr
174
+ self._listen_port: Final[int] = find_free_port() if port == PORT_ANY else port
175
+ self._address: Final[tuple[str, int]] = (ip_addr, self._listen_port)
176
+ self._instances[self._address] = self
177
+ threading.Thread.__init__(self, name=f"XmlRpcServer {ip_addr}:{self._listen_port}")
178
+ self._simple_xml_rpc_server = AioHomematicXMLRPCServer(
179
+ addr=self._address,
180
+ requestHandler=RequestHandler,
181
+ logRequests=False,
182
+ allow_none=True,
183
+ )
184
+ self._simple_xml_rpc_server.register_introspection_functions()
185
+ self._simple_xml_rpc_server.register_multicall_functions()
186
+ self._simple_xml_rpc_server.register_instance(RPCFunctions(self), allow_dotted_names=True)
187
+ self._centrals: Final[dict[str, hmcu.CentralUnit]] = {}
188
+
189
+ def __new__(cls, ip_addr: str, port: int) -> XmlRpcServer: # noqa: PYI034
190
+ """Create new XmlRPC server."""
191
+ if (xml_rpc := cls._instances.get((ip_addr, port))) is None:
192
+ _LOGGER.debug("Creating XmlRpc server")
193
+ return super().__new__(cls)
194
+ return xml_rpc
195
+
196
+ def run(self) -> None:
197
+ """Run the XmlRPC-Server thread."""
198
+ _LOGGER.debug(
199
+ "RUN: Starting XmlRPC-Server listening on http://%s:%i",
200
+ self._listen_ip_addr,
201
+ self._listen_port,
202
+ )
203
+ if self._simple_xml_rpc_server:
204
+ self._simple_xml_rpc_server.serve_forever()
205
+
206
+ def stop(self) -> None:
207
+ """Stop the XmlRPC-Server."""
208
+ _LOGGER.debug("STOP: Shutting down XmlRPC-Server")
209
+ self._simple_xml_rpc_server.shutdown()
210
+ _LOGGER.debug("STOP: Stopping XmlRPC-Server")
211
+ self._simple_xml_rpc_server.server_close()
212
+ # Ensure the server thread has actually terminated to avoid slow teardown
213
+ with contextlib.suppress(RuntimeError):
214
+ self.join(timeout=1.0)
215
+ _LOGGER.debug("STOP: XmlRPC-Server stopped")
216
+ if self._address in self._instances:
217
+ del self._instances[self._address]
218
+
219
+ @property
220
+ def listen_ip_addr(self) -> str:
221
+ """Return the local ip address."""
222
+ return self._listen_ip_addr
223
+
224
+ @property
225
+ def listen_port(self) -> int:
226
+ """Return the local port."""
227
+ return self._listen_port
228
+
229
+ @property
230
+ def started(self) -> bool:
231
+ """Return if thread is active."""
232
+ return self._started.is_set() is True # type: ignore[attr-defined]
233
+
234
+ def add_central(self, central: hmcu.CentralUnit) -> None:
235
+ """Register a central in the XmlRPC-Server."""
236
+ if not self._centrals.get(central.name):
237
+ self._centrals[central.name] = central
238
+
239
+ def remove_central(self, central: hmcu.CentralUnit) -> None:
240
+ """Unregister a central from XmlRPC-Server."""
241
+ if self._centrals.get(central.name):
242
+ del self._centrals[central.name]
243
+
244
+ def get_central(self, interface_id: str) -> hmcu.CentralUnit | None:
245
+ """Return a central by interface_id."""
246
+ for central in self._centrals.values():
247
+ if central.has_client(interface_id=interface_id):
248
+ return central
249
+ return None
250
+
251
+ @property
252
+ def no_central_assigned(self) -> bool:
253
+ """Return if no central is assigned."""
254
+ return len(self._centrals) == 0
255
+
256
+
257
+ def create_xml_rpc_server(ip_addr: str = IP_ANY_V4, port: int = PORT_ANY) -> XmlRpcServer:
258
+ """Register the xml rpc server."""
259
+ xml_rpc = XmlRpcServer(ip_addr=ip_addr, port=port)
260
+ if not xml_rpc.started:
261
+ xml_rpc.start()
262
+ _LOGGER.debug(
263
+ "CREATE_XML_RPC_SERVER: Starting XmlRPC-Server listening on %s:%i",
264
+ xml_rpc.listen_ip_addr,
265
+ xml_rpc.listen_port,
266
+ )
267
+ return xml_rpc