aiohomematic 2025.11.3__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 +61 -0
- aiohomematic/async_support.py +212 -0
- aiohomematic/central/__init__.py +2309 -0
- aiohomematic/central/decorators.py +155 -0
- aiohomematic/central/rpc_server.py +295 -0
- aiohomematic/client/__init__.py +1848 -0
- aiohomematic/client/_rpc_errors.py +81 -0
- aiohomematic/client/json_rpc.py +1326 -0
- aiohomematic/client/rpc_proxy.py +311 -0
- aiohomematic/const.py +1127 -0
- aiohomematic/context.py +18 -0
- aiohomematic/converter.py +108 -0
- aiohomematic/decorators.py +302 -0
- aiohomematic/exceptions.py +164 -0
- aiohomematic/hmcli.py +186 -0
- aiohomematic/model/__init__.py +140 -0
- aiohomematic/model/calculated/__init__.py +84 -0
- aiohomematic/model/calculated/climate.py +290 -0
- aiohomematic/model/calculated/data_point.py +327 -0
- aiohomematic/model/calculated/operating_voltage_level.py +299 -0
- aiohomematic/model/calculated/support.py +234 -0
- aiohomematic/model/custom/__init__.py +177 -0
- aiohomematic/model/custom/climate.py +1532 -0
- aiohomematic/model/custom/cover.py +792 -0
- aiohomematic/model/custom/data_point.py +334 -0
- aiohomematic/model/custom/definition.py +871 -0
- aiohomematic/model/custom/light.py +1128 -0
- aiohomematic/model/custom/lock.py +394 -0
- aiohomematic/model/custom/siren.py +275 -0
- aiohomematic/model/custom/support.py +41 -0
- aiohomematic/model/custom/switch.py +175 -0
- aiohomematic/model/custom/valve.py +114 -0
- aiohomematic/model/data_point.py +1123 -0
- aiohomematic/model/device.py +1445 -0
- aiohomematic/model/event.py +208 -0
- aiohomematic/model/generic/__init__.py +217 -0
- aiohomematic/model/generic/action.py +34 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +27 -0
- aiohomematic/model/generic/data_point.py +171 -0
- aiohomematic/model/generic/dummy.py +147 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +39 -0
- aiohomematic/model/generic/sensor.py +74 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +29 -0
- aiohomematic/model/hub/__init__.py +333 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/data_point.py +340 -0
- aiohomematic/model/hub/number.py +39 -0
- aiohomematic/model/hub/select.py +49 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +44 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/support.py +586 -0
- aiohomematic/model/update.py +143 -0
- aiohomematic/property_decorators.py +496 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -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/store/__init__.py +34 -0
- aiohomematic/store/dynamic.py +551 -0
- aiohomematic/store/persistent.py +988 -0
- aiohomematic/store/visibility.py +812 -0
- aiohomematic/support.py +664 -0
- aiohomematic/validator.py +112 -0
- aiohomematic-2025.11.3.dist-info/METADATA +144 -0
- aiohomematic-2025.11.3.dist-info/RECORD +77 -0
- aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
- aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
- aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2025.11.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""Decorators for central used within aiohomematic."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from functools import wraps
|
|
10
|
+
import inspect
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Final, cast
|
|
13
|
+
|
|
14
|
+
from aiohomematic import central as hmcu, client as hmcl
|
|
15
|
+
from aiohomematic.central import rpc_server as rpc
|
|
16
|
+
from aiohomematic.const import BackendSystemEvent
|
|
17
|
+
from aiohomematic.exceptions import AioHomematicException
|
|
18
|
+
from aiohomematic.support import extract_exc_args
|
|
19
|
+
|
|
20
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
21
|
+
_INTERFACE_ID: Final = "interface_id"
|
|
22
|
+
_CHANNEL_ADDRESS: Final = "channel_address"
|
|
23
|
+
_PARAMETER: Final = "parameter"
|
|
24
|
+
_VALUE: Final = "value"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def callback_backend_system(system_event: BackendSystemEvent) -> Callable:
|
|
28
|
+
"""Check if backend_system_callback is set and call it AFTER original function."""
|
|
29
|
+
|
|
30
|
+
def decorator_backend_system_callback[**P, R](
|
|
31
|
+
func: Callable[P, R | Awaitable[R]],
|
|
32
|
+
) -> Callable[P, R | Awaitable[R]]:
|
|
33
|
+
"""Decorate callback system events."""
|
|
34
|
+
|
|
35
|
+
@wraps(func)
|
|
36
|
+
async def async_wrapper_backend_system_callback(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
37
|
+
"""Wrap async callback system events."""
|
|
38
|
+
return_value = cast(R, await func(*args, **kwargs)) # type: ignore[misc]
|
|
39
|
+
await _exec_backend_system_callback(*args, **kwargs)
|
|
40
|
+
return return_value
|
|
41
|
+
|
|
42
|
+
@wraps(func)
|
|
43
|
+
def wrapper_backend_system_callback(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
44
|
+
"""Wrap callback system events."""
|
|
45
|
+
return_value = cast(R, func(*args, **kwargs))
|
|
46
|
+
try:
|
|
47
|
+
unit = args[0]
|
|
48
|
+
central: hmcu.CentralUnit | None = None
|
|
49
|
+
if isinstance(unit, hmcu.CentralUnit):
|
|
50
|
+
central = unit
|
|
51
|
+
if central is None and isinstance(unit, rpc.RPCFunctions):
|
|
52
|
+
central = unit.get_central(interface_id=str(args[1]))
|
|
53
|
+
if central:
|
|
54
|
+
central.looper.create_task(
|
|
55
|
+
target=lambda: _exec_backend_system_callback(*args, **kwargs),
|
|
56
|
+
name="wrapper_backend_system_callback",
|
|
57
|
+
)
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
_LOGGER.warning(
|
|
60
|
+
"EXEC_BACKEND_SYSTEM_CALLBACK failed: Problem with identifying central: %s",
|
|
61
|
+
extract_exc_args(exc=exc),
|
|
62
|
+
)
|
|
63
|
+
return return_value
|
|
64
|
+
|
|
65
|
+
async def _exec_backend_system_callback(*args: Any, **kwargs: Any) -> None:
|
|
66
|
+
"""Execute the callback for a system event."""
|
|
67
|
+
|
|
68
|
+
if not ((len(args) > 1 and not kwargs) or (len(args) == 1 and kwargs)):
|
|
69
|
+
_LOGGER.warning("EXEC_BACKEND_SYSTEM_CALLBACK failed: *args not supported for callback_system_event")
|
|
70
|
+
try:
|
|
71
|
+
args = args[1:]
|
|
72
|
+
interface_id: str = args[0] if len(args) > 0 else str(kwargs[_INTERFACE_ID])
|
|
73
|
+
if client := hmcl.get_client(interface_id=interface_id):
|
|
74
|
+
client.modified_at = datetime.now()
|
|
75
|
+
client.central.emit_backend_system_callback(system_event=system_event, **kwargs)
|
|
76
|
+
except Exception as exc: # pragma: no cover
|
|
77
|
+
_LOGGER.warning(
|
|
78
|
+
"EXEC_BACKEND_SYSTEM_CALLBACK failed: Unable to reduce kwargs for backend_system_callback"
|
|
79
|
+
)
|
|
80
|
+
raise AioHomematicException(
|
|
81
|
+
f"args-exception backend_system_callback [{extract_exc_args(exc=exc)}]"
|
|
82
|
+
) from exc
|
|
83
|
+
|
|
84
|
+
if inspect.iscoroutinefunction(func):
|
|
85
|
+
return async_wrapper_backend_system_callback
|
|
86
|
+
return wrapper_backend_system_callback
|
|
87
|
+
|
|
88
|
+
return decorator_backend_system_callback
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def callback_event[**P, R](func: Callable[P, R]) -> Callable:
|
|
92
|
+
"""Check if event_callback is set and call it AFTER original function."""
|
|
93
|
+
|
|
94
|
+
def _exec_event_callback(*args: Any, **kwargs: Any) -> None:
|
|
95
|
+
"""Execute the callback for a data_point event."""
|
|
96
|
+
try:
|
|
97
|
+
# Expected signature: (self, interface_id, channel_address, parameter, value)
|
|
98
|
+
interface_id: str
|
|
99
|
+
if len(args) > 1:
|
|
100
|
+
interface_id = cast(str, args[1])
|
|
101
|
+
channel_address = cast(str, args[2])
|
|
102
|
+
parameter = cast(str, args[3])
|
|
103
|
+
value = args[4] if len(args) > 4 else kwargs.get(_VALUE)
|
|
104
|
+
else:
|
|
105
|
+
interface_id = cast(str, kwargs[_INTERFACE_ID])
|
|
106
|
+
channel_address = cast(str, kwargs[_CHANNEL_ADDRESS])
|
|
107
|
+
parameter = cast(str, kwargs[_PARAMETER])
|
|
108
|
+
value = kwargs[_VALUE]
|
|
109
|
+
|
|
110
|
+
if client := hmcl.get_client(interface_id=interface_id):
|
|
111
|
+
client.modified_at = datetime.now()
|
|
112
|
+
client.central.emit_backend_parameter_callback(
|
|
113
|
+
interface_id=interface_id, channel_address=channel_address, parameter=parameter, value=value
|
|
114
|
+
)
|
|
115
|
+
except Exception as exc: # pragma: no cover
|
|
116
|
+
_LOGGER.warning("EXEC_DATA_POINT_EVENT_CALLBACK failed: Unable to process args/kwargs for event_callback")
|
|
117
|
+
raise AioHomematicException(f"args-exception event_callback [{extract_exc_args(exc=exc)}]") from exc
|
|
118
|
+
|
|
119
|
+
def _schedule_or_exec(*args: Any, **kwargs: Any) -> None:
|
|
120
|
+
"""Schedule event callback on central looper when possible, else execute inline."""
|
|
121
|
+
try:
|
|
122
|
+
# Prefer scheduling on the CentralUnit looper when available to avoid blocking hot path
|
|
123
|
+
unit = args[0]
|
|
124
|
+
if isinstance(unit, hmcu.CentralUnit):
|
|
125
|
+
unit.looper.create_task(
|
|
126
|
+
target=lambda: _async_wrap_sync(_exec_event_callback, *args, **kwargs),
|
|
127
|
+
name="wrapper_event_callback",
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
except Exception:
|
|
131
|
+
# Fall through to inline execution on any error
|
|
132
|
+
pass
|
|
133
|
+
_exec_event_callback(*args, **kwargs)
|
|
134
|
+
|
|
135
|
+
@wraps(func)
|
|
136
|
+
async def async_wrapper_event_callback(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
137
|
+
"""Wrap async callback events."""
|
|
138
|
+
return_value = cast(R, await func(*args, **kwargs)) # type: ignore[misc]
|
|
139
|
+
_schedule_or_exec(*args, **kwargs)
|
|
140
|
+
return return_value
|
|
141
|
+
|
|
142
|
+
@wraps(func)
|
|
143
|
+
def wrapper_event_callback(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
144
|
+
"""Wrap sync callback events."""
|
|
145
|
+
return_value = func(*args, **kwargs)
|
|
146
|
+
_schedule_or_exec(*args, **kwargs)
|
|
147
|
+
return return_value
|
|
148
|
+
|
|
149
|
+
# Helper to create a trivial coroutine from a sync callable
|
|
150
|
+
async def _async_wrap_sync(cb: Callable[..., None], *a: Any, **kw: Any) -> None:
|
|
151
|
+
cb(*a, **kw)
|
|
152
|
+
|
|
153
|
+
if inspect.iscoroutinefunction(func):
|
|
154
|
+
return async_wrapper_event_callback
|
|
155
|
+
return wrapper_event_callback
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""
|
|
4
|
+
XML-RPC server module.
|
|
5
|
+
|
|
6
|
+
Provides the XML-RPC server which handles communication
|
|
7
|
+
with the backend.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
from typing import Any, Final, cast
|
|
16
|
+
from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
|
|
17
|
+
|
|
18
|
+
from aiohomematic import central as hmcu
|
|
19
|
+
from aiohomematic.central.decorators import callback_backend_system
|
|
20
|
+
from aiohomematic.const import IP_ANY_V4, PORT_ANY, BackendSystemEvent
|
|
21
|
+
from aiohomematic.support import log_boundary_error
|
|
22
|
+
|
|
23
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# pylint: disable=invalid-name
|
|
27
|
+
class RPCFunctions:
|
|
28
|
+
"""The RPC functions the backend will expect."""
|
|
29
|
+
|
|
30
|
+
# Disable kw-only linter
|
|
31
|
+
__kwonly_check__ = False
|
|
32
|
+
|
|
33
|
+
def __init__(self, *, rpc_server: RpcServer) -> None:
|
|
34
|
+
"""Init RPCFunctions."""
|
|
35
|
+
self._rpc_server: Final = rpc_server
|
|
36
|
+
|
|
37
|
+
def event(self, interface_id: str, channel_address: str, parameter: str, value: Any, /) -> None:
|
|
38
|
+
"""If a device emits some sort event, we will handle it here."""
|
|
39
|
+
if central := self.get_central(interface_id=interface_id):
|
|
40
|
+
central.looper.create_task(
|
|
41
|
+
target=central.data_point_event(
|
|
42
|
+
interface_id=interface_id,
|
|
43
|
+
channel_address=channel_address,
|
|
44
|
+
parameter=parameter,
|
|
45
|
+
value=value,
|
|
46
|
+
),
|
|
47
|
+
name=f"event-{interface_id}-{channel_address}-{parameter}",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@callback_backend_system(system_event=BackendSystemEvent.ERROR)
|
|
51
|
+
def error(self, interface_id: str, error_code: str, msg: str, /) -> None:
|
|
52
|
+
"""When some error occurs the backend will send its error message here."""
|
|
53
|
+
# Structured boundary log (warning level). RPC server received error notification.
|
|
54
|
+
try:
|
|
55
|
+
raise RuntimeError(str(msg))
|
|
56
|
+
except RuntimeError as err:
|
|
57
|
+
log_boundary_error(
|
|
58
|
+
logger=_LOGGER,
|
|
59
|
+
boundary="rpc-server",
|
|
60
|
+
action="error",
|
|
61
|
+
err=err,
|
|
62
|
+
level=logging.WARNING,
|
|
63
|
+
log_context={"interface_id": interface_id, "error_code": int(error_code)},
|
|
64
|
+
)
|
|
65
|
+
_LOGGER.warning(
|
|
66
|
+
"ERROR failed: interface_id = %s, error_code = %i, message = %s",
|
|
67
|
+
interface_id,
|
|
68
|
+
int(error_code),
|
|
69
|
+
str(msg),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def listDevices(self, interface_id: str, /) -> list[dict[str, Any]]:
|
|
73
|
+
"""Return already existing devices to the backend."""
|
|
74
|
+
if central := self.get_central(interface_id=interface_id):
|
|
75
|
+
return [dict(device_description) for device_description in central.list_devices(interface_id=interface_id)]
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
def newDevices(self, interface_id: str, device_descriptions: list[dict[str, Any]], /) -> None:
|
|
79
|
+
"""Add new devices send from the backend."""
|
|
80
|
+
central: hmcu.CentralUnit | None
|
|
81
|
+
if central := self.get_central(interface_id=interface_id):
|
|
82
|
+
central.looper.create_task(
|
|
83
|
+
target=central.add_new_devices(
|
|
84
|
+
interface_id=interface_id, device_descriptions=tuple(device_descriptions)
|
|
85
|
+
),
|
|
86
|
+
name=f"newDevices-{interface_id}",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def deleteDevices(self, interface_id: str, addresses: list[str], /) -> None:
|
|
90
|
+
"""Delete devices send from the backend."""
|
|
91
|
+
central: hmcu.CentralUnit | None
|
|
92
|
+
if central := self.get_central(interface_id=interface_id):
|
|
93
|
+
central.looper.create_task(
|
|
94
|
+
target=central.delete_devices(interface_id=interface_id, addresses=tuple(addresses)),
|
|
95
|
+
name=f"deleteDevices-{interface_id}",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@callback_backend_system(system_event=BackendSystemEvent.UPDATE_DEVICE)
|
|
99
|
+
def updateDevice(self, interface_id: str, address: str, hint: int, /) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Update a device.
|
|
102
|
+
|
|
103
|
+
Irrelevant, as currently only changes to link
|
|
104
|
+
partners are reported.
|
|
105
|
+
"""
|
|
106
|
+
_LOGGER.debug(
|
|
107
|
+
"UPDATEDEVICE: interface_id = %s, address = %s, hint = %s",
|
|
108
|
+
interface_id,
|
|
109
|
+
address,
|
|
110
|
+
str(hint),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@callback_backend_system(system_event=BackendSystemEvent.REPLACE_DEVICE)
|
|
114
|
+
def replaceDevice(self, interface_id: str, old_device_address: str, new_device_address: str, /) -> None:
|
|
115
|
+
"""Replace a device. Probably irrelevant for us."""
|
|
116
|
+
_LOGGER.debug(
|
|
117
|
+
"REPLACEDEVICE: interface_id = %s, oldDeviceAddress = %s, newDeviceAddress = %s",
|
|
118
|
+
interface_id,
|
|
119
|
+
old_device_address,
|
|
120
|
+
new_device_address,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@callback_backend_system(system_event=BackendSystemEvent.RE_ADDED_DEVICE)
|
|
124
|
+
def readdedDevice(self, interface_id: str, addresses: list[str], /) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Re-Add device from the backend.
|
|
127
|
+
|
|
128
|
+
Probably irrelevant for us.
|
|
129
|
+
Gets called when a known devices is put into learn-mode
|
|
130
|
+
while installation mode is active.
|
|
131
|
+
"""
|
|
132
|
+
_LOGGER.debug(
|
|
133
|
+
"READDEDDEVICES: interface_id = %s, addresses = %s",
|
|
134
|
+
interface_id,
|
|
135
|
+
str(addresses),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def get_central(self, *, interface_id: str) -> hmcu.CentralUnit | None:
|
|
139
|
+
"""Return the central by interface_id."""
|
|
140
|
+
return self._rpc_server.get_central(interface_id=interface_id)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Restrict to specific paths.
|
|
144
|
+
class RequestHandler(SimpleXMLRPCRequestHandler):
|
|
145
|
+
"""We handle requests to / and /RPC2."""
|
|
146
|
+
|
|
147
|
+
rpc_paths = (
|
|
148
|
+
"/",
|
|
149
|
+
"/RPC2",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class HomematicXMLRPCServer(SimpleXMLRPCServer):
|
|
154
|
+
"""
|
|
155
|
+
Simple XML-RPC server.
|
|
156
|
+
|
|
157
|
+
Simple XML-RPC server that allows functions and a single instance
|
|
158
|
+
to be installed to handle requests. The default implementation
|
|
159
|
+
attempts to dispatch XML-RPC calls to the functions or instance
|
|
160
|
+
installed in the server. Override the _dispatch method inherited
|
|
161
|
+
from SimpleXMLRPCDispatcher to change this behavior.
|
|
162
|
+
|
|
163
|
+
This implementation adds an additional method:
|
|
164
|
+
system_listMethods(self, interface_id: str.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
__kwonly_check__ = False
|
|
168
|
+
|
|
169
|
+
def system_listMethods(self, interface_id: str | None = None, /) -> list[str]:
|
|
170
|
+
"""Return a list of the methods supported by the server."""
|
|
171
|
+
return SimpleXMLRPCServer.system_listMethods(self)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class RpcServer(threading.Thread):
|
|
175
|
+
"""RPC server thread to handle messages from the backend."""
|
|
176
|
+
|
|
177
|
+
_initialized: bool = False
|
|
178
|
+
_instances: Final[dict[tuple[str, int], RpcServer]] = {}
|
|
179
|
+
|
|
180
|
+
def __init__(self, *, server: SimpleXMLRPCServer) -> None:
|
|
181
|
+
"""Init XmlRPC server."""
|
|
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)
|
|
186
|
+
self._initialized = True
|
|
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]
|
|
190
|
+
self._centrals: Final[dict[str, hmcu.CentralUnit]] = {}
|
|
191
|
+
self._instances[self._address] = self
|
|
192
|
+
threading.Thread.__init__(self, name=f"RpcServer {self._listen_ip_addr}:{self._listen_port}")
|
|
193
|
+
|
|
194
|
+
def run(self) -> None:
|
|
195
|
+
"""Run the RPC-Server thread."""
|
|
196
|
+
_LOGGER.debug(
|
|
197
|
+
"RUN: Starting RPC-Server listening on %s:%i",
|
|
198
|
+
self._listen_ip_addr,
|
|
199
|
+
self._listen_port,
|
|
200
|
+
)
|
|
201
|
+
if self._server:
|
|
202
|
+
self._server.serve_forever()
|
|
203
|
+
|
|
204
|
+
def stop(self) -> None:
|
|
205
|
+
"""Stop the RPC-Server."""
|
|
206
|
+
_LOGGER.debug("STOP: Shutting down RPC-Server")
|
|
207
|
+
self._server.shutdown()
|
|
208
|
+
_LOGGER.debug("STOP: Stopping RPC-Server")
|
|
209
|
+
self._server.server_close()
|
|
210
|
+
# Ensure the server thread has actually terminated to avoid slow teardown
|
|
211
|
+
with contextlib.suppress(RuntimeError):
|
|
212
|
+
self.join(timeout=1.0)
|
|
213
|
+
_LOGGER.debug("STOP: RPC-Server stopped")
|
|
214
|
+
if self._address in self._instances:
|
|
215
|
+
del self._instances[self._address]
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def listen_ip_addr(self) -> str:
|
|
219
|
+
"""Return the local ip address."""
|
|
220
|
+
return self._listen_ip_addr
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def listen_port(self) -> int:
|
|
224
|
+
"""Return the local port."""
|
|
225
|
+
return self._listen_port
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def started(self) -> bool:
|
|
229
|
+
"""Return if thread is active."""
|
|
230
|
+
return self._started.is_set() is True # type: ignore[attr-defined]
|
|
231
|
+
|
|
232
|
+
def add_central(self, *, central: hmcu.CentralUnit) -> None:
|
|
233
|
+
"""Register a central in the RPC-Server."""
|
|
234
|
+
if not self._centrals.get(central.name):
|
|
235
|
+
self._centrals[central.name] = central
|
|
236
|
+
|
|
237
|
+
def remove_central(self, *, central: hmcu.CentralUnit) -> None:
|
|
238
|
+
"""Unregister a central from RPC-Server."""
|
|
239
|
+
if self._centrals.get(central.name):
|
|
240
|
+
del self._centrals[central.name]
|
|
241
|
+
|
|
242
|
+
def get_central(self, *, interface_id: str) -> hmcu.CentralUnit | None:
|
|
243
|
+
"""Return a central by interface_id."""
|
|
244
|
+
for central in self._centrals.values():
|
|
245
|
+
if central.has_client(interface_id=interface_id):
|
|
246
|
+
return central
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def no_central_assigned(self) -> bool:
|
|
251
|
+
"""Return if no central is assigned."""
|
|
252
|
+
return len(self._centrals) == 0
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class XmlRpcServer(RpcServer):
|
|
256
|
+
"""XML-RPC server thread to handle messages from the backend."""
|
|
257
|
+
|
|
258
|
+
def __init__(
|
|
259
|
+
self,
|
|
260
|
+
*,
|
|
261
|
+
ip_addr: str,
|
|
262
|
+
port: int,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Init XmlRPC server."""
|
|
265
|
+
|
|
266
|
+
if self._initialized:
|
|
267
|
+
return
|
|
268
|
+
super().__init__(
|
|
269
|
+
server=HomematicXMLRPCServer(
|
|
270
|
+
addr=(ip_addr, port),
|
|
271
|
+
requestHandler=RequestHandler,
|
|
272
|
+
logRequests=False,
|
|
273
|
+
allow_none=True,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def __new__(cls, ip_addr: str, port: int) -> XmlRpcServer: # noqa: PYI034 # kwonly: disable
|
|
278
|
+
"""Create new RPC server."""
|
|
279
|
+
if (rpc := cls._instances.get((ip_addr, port))) is None:
|
|
280
|
+
_LOGGER.debug("Creating XmlRpc server")
|
|
281
|
+
return super().__new__(cls)
|
|
282
|
+
return cast(XmlRpcServer, rpc)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def create_xml_rpc_server(*, ip_addr: str = IP_ANY_V4, port: int = PORT_ANY) -> XmlRpcServer:
|
|
286
|
+
"""Register the rpc server."""
|
|
287
|
+
rpc = XmlRpcServer(ip_addr=ip_addr, port=port)
|
|
288
|
+
if not rpc.started:
|
|
289
|
+
rpc.start()
|
|
290
|
+
_LOGGER.debug(
|
|
291
|
+
"CREATE_XML_RPC_SERVER: Starting XmlRPC-Server listening on %s:%i",
|
|
292
|
+
rpc.listen_ip_addr,
|
|
293
|
+
rpc.listen_port,
|
|
294
|
+
)
|
|
295
|
+
return rpc
|