aiohomematic 2026.1.29__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.
- aiohomematic/__init__.py +110 -0
- aiohomematic/_log_context_protocol.py +29 -0
- aiohomematic/api.py +410 -0
- aiohomematic/async_support.py +250 -0
- aiohomematic/backend_detection.py +462 -0
- aiohomematic/central/__init__.py +103 -0
- aiohomematic/central/async_rpc_server.py +760 -0
- aiohomematic/central/central_unit.py +1152 -0
- aiohomematic/central/config.py +463 -0
- aiohomematic/central/config_builder.py +772 -0
- aiohomematic/central/connection_state.py +160 -0
- aiohomematic/central/coordinators/__init__.py +38 -0
- aiohomematic/central/coordinators/cache.py +414 -0
- aiohomematic/central/coordinators/client.py +480 -0
- aiohomematic/central/coordinators/connection_recovery.py +1141 -0
- aiohomematic/central/coordinators/device.py +1166 -0
- aiohomematic/central/coordinators/event.py +514 -0
- aiohomematic/central/coordinators/hub.py +532 -0
- aiohomematic/central/decorators.py +184 -0
- aiohomematic/central/device_registry.py +229 -0
- aiohomematic/central/events/__init__.py +104 -0
- aiohomematic/central/events/bus.py +1392 -0
- aiohomematic/central/events/integration.py +424 -0
- aiohomematic/central/events/types.py +194 -0
- aiohomematic/central/health.py +762 -0
- aiohomematic/central/rpc_server.py +353 -0
- aiohomematic/central/scheduler.py +794 -0
- aiohomematic/central/state_machine.py +391 -0
- aiohomematic/client/__init__.py +203 -0
- aiohomematic/client/_rpc_errors.py +187 -0
- aiohomematic/client/backends/__init__.py +48 -0
- aiohomematic/client/backends/base.py +335 -0
- aiohomematic/client/backends/capabilities.py +138 -0
- aiohomematic/client/backends/ccu.py +487 -0
- aiohomematic/client/backends/factory.py +116 -0
- aiohomematic/client/backends/homegear.py +294 -0
- aiohomematic/client/backends/json_ccu.py +252 -0
- aiohomematic/client/backends/protocol.py +316 -0
- aiohomematic/client/ccu.py +1857 -0
- aiohomematic/client/circuit_breaker.py +459 -0
- aiohomematic/client/config.py +64 -0
- aiohomematic/client/handlers/__init__.py +40 -0
- aiohomematic/client/handlers/backup.py +157 -0
- aiohomematic/client/handlers/base.py +79 -0
- aiohomematic/client/handlers/device_ops.py +1085 -0
- aiohomematic/client/handlers/firmware.py +144 -0
- aiohomematic/client/handlers/link_mgmt.py +199 -0
- aiohomematic/client/handlers/metadata.py +436 -0
- aiohomematic/client/handlers/programs.py +144 -0
- aiohomematic/client/handlers/sysvars.py +100 -0
- aiohomematic/client/interface_client.py +1304 -0
- aiohomematic/client/json_rpc.py +2068 -0
- aiohomematic/client/request_coalescer.py +282 -0
- aiohomematic/client/rpc_proxy.py +629 -0
- aiohomematic/client/state_machine.py +324 -0
- aiohomematic/const.py +2207 -0
- aiohomematic/context.py +275 -0
- aiohomematic/converter.py +270 -0
- aiohomematic/decorators.py +390 -0
- aiohomematic/exceptions.py +185 -0
- aiohomematic/hmcli.py +997 -0
- aiohomematic/i18n.py +193 -0
- aiohomematic/interfaces/__init__.py +407 -0
- aiohomematic/interfaces/central.py +1067 -0
- aiohomematic/interfaces/client.py +1096 -0
- aiohomematic/interfaces/coordinators.py +63 -0
- aiohomematic/interfaces/model.py +1921 -0
- aiohomematic/interfaces/operations.py +217 -0
- aiohomematic/logging_context.py +134 -0
- aiohomematic/metrics/__init__.py +125 -0
- aiohomematic/metrics/_protocols.py +140 -0
- aiohomematic/metrics/aggregator.py +534 -0
- aiohomematic/metrics/dataclasses.py +489 -0
- aiohomematic/metrics/emitter.py +292 -0
- aiohomematic/metrics/events.py +183 -0
- aiohomematic/metrics/keys.py +300 -0
- aiohomematic/metrics/observer.py +563 -0
- aiohomematic/metrics/stats.py +172 -0
- aiohomematic/model/__init__.py +189 -0
- aiohomematic/model/availability.py +65 -0
- aiohomematic/model/calculated/__init__.py +89 -0
- aiohomematic/model/calculated/climate.py +276 -0
- aiohomematic/model/calculated/data_point.py +315 -0
- aiohomematic/model/calculated/field.py +147 -0
- aiohomematic/model/calculated/operating_voltage_level.py +286 -0
- aiohomematic/model/calculated/support.py +232 -0
- aiohomematic/model/custom/__init__.py +214 -0
- aiohomematic/model/custom/capabilities/__init__.py +67 -0
- aiohomematic/model/custom/capabilities/climate.py +41 -0
- aiohomematic/model/custom/capabilities/light.py +87 -0
- aiohomematic/model/custom/capabilities/lock.py +44 -0
- aiohomematic/model/custom/capabilities/siren.py +63 -0
- aiohomematic/model/custom/climate.py +1130 -0
- aiohomematic/model/custom/cover.py +722 -0
- aiohomematic/model/custom/data_point.py +360 -0
- aiohomematic/model/custom/definition.py +300 -0
- aiohomematic/model/custom/field.py +89 -0
- aiohomematic/model/custom/light.py +1174 -0
- aiohomematic/model/custom/lock.py +322 -0
- aiohomematic/model/custom/mixins.py +445 -0
- aiohomematic/model/custom/profile.py +945 -0
- aiohomematic/model/custom/registry.py +251 -0
- aiohomematic/model/custom/siren.py +462 -0
- aiohomematic/model/custom/switch.py +195 -0
- aiohomematic/model/custom/text_display.py +289 -0
- aiohomematic/model/custom/valve.py +78 -0
- aiohomematic/model/data_point.py +1416 -0
- aiohomematic/model/device.py +1840 -0
- aiohomematic/model/event.py +216 -0
- aiohomematic/model/generic/__init__.py +327 -0
- aiohomematic/model/generic/action.py +40 -0
- aiohomematic/model/generic/action_select.py +62 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +31 -0
- aiohomematic/model/generic/data_point.py +177 -0
- aiohomematic/model/generic/dummy.py +150 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +56 -0
- aiohomematic/model/generic/sensor.py +76 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +33 -0
- aiohomematic/model/hub/__init__.py +100 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/connectivity.py +190 -0
- aiohomematic/model/hub/data_point.py +342 -0
- aiohomematic/model/hub/hub.py +864 -0
- aiohomematic/model/hub/inbox.py +135 -0
- aiohomematic/model/hub/install_mode.py +393 -0
- aiohomematic/model/hub/metrics.py +208 -0
- aiohomematic/model/hub/number.py +42 -0
- aiohomematic/model/hub/select.py +52 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +43 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/hub/update.py +221 -0
- aiohomematic/model/support.py +592 -0
- aiohomematic/model/update.py +140 -0
- aiohomematic/model/week_profile.py +1827 -0
- aiohomematic/property_decorators.py +719 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
- aiohomematic/rega_scripts/create_backup_start.fn +28 -0
- aiohomematic/rega_scripts/create_backup_status.fn +89 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
- aiohomematic/rega_scripts/get_backend_info.fn +25 -0
- aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_service_messages.fn +83 -0
- aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
- aiohomematic/rega_scripts/set_program_state.fn +17 -0
- aiohomematic/rega_scripts/set_system_variable.fn +19 -0
- aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
- aiohomematic/schemas.py +256 -0
- aiohomematic/store/__init__.py +55 -0
- aiohomematic/store/dynamic/__init__.py +43 -0
- aiohomematic/store/dynamic/command.py +250 -0
- aiohomematic/store/dynamic/data.py +175 -0
- aiohomematic/store/dynamic/details.py +187 -0
- aiohomematic/store/dynamic/ping_pong.py +416 -0
- aiohomematic/store/persistent/__init__.py +71 -0
- aiohomematic/store/persistent/base.py +285 -0
- aiohomematic/store/persistent/device.py +233 -0
- aiohomematic/store/persistent/incident.py +380 -0
- aiohomematic/store/persistent/paramset.py +241 -0
- aiohomematic/store/persistent/session.py +556 -0
- aiohomematic/store/serialization.py +150 -0
- aiohomematic/store/storage.py +689 -0
- aiohomematic/store/types.py +526 -0
- aiohomematic/store/visibility/__init__.py +40 -0
- aiohomematic/store/visibility/parser.py +141 -0
- aiohomematic/store/visibility/registry.py +722 -0
- aiohomematic/store/visibility/rules.py +307 -0
- aiohomematic/strings.json +237 -0
- aiohomematic/support.py +706 -0
- aiohomematic/tracing.py +236 -0
- aiohomematic/translations/de.json +237 -0
- aiohomematic/translations/en.json +237 -0
- aiohomematic/type_aliases.py +51 -0
- aiohomematic/validator.py +128 -0
- aiohomematic-2026.1.29.dist-info/METADATA +296 -0
- aiohomematic-2026.1.29.dist-info/RECORD +188 -0
- aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
- aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
- aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2026.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
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 i18n
|
|
19
|
+
from aiohomematic.central.decorators import callback_backend_system
|
|
20
|
+
from aiohomematic.const import IP_ANY_V4, PORT_ANY, SystemEventType, UpdateDeviceHint
|
|
21
|
+
from aiohomematic.interfaces.central import RpcServerCentralProtocol, RpcServerTaskSchedulerProtocol
|
|
22
|
+
from aiohomematic.property_decorators import DelegatedProperty
|
|
23
|
+
from aiohomematic.schemas import normalize_device_description
|
|
24
|
+
from aiohomematic.support import get_device_address, log_boundary_error
|
|
25
|
+
|
|
26
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# pylint: disable=invalid-name
|
|
30
|
+
class RPCFunctions:
|
|
31
|
+
"""The RPC functions the backend will expect."""
|
|
32
|
+
|
|
33
|
+
# Disable kw-only linter
|
|
34
|
+
__kwonly_check__ = False
|
|
35
|
+
|
|
36
|
+
def __init__(self, *, rpc_server: RpcServer) -> None:
|
|
37
|
+
"""Initialize RPCFunctions."""
|
|
38
|
+
self._rpc_server: Final = rpc_server
|
|
39
|
+
|
|
40
|
+
def deleteDevices(self, interface_id: str, addresses: list[str], /) -> None:
|
|
41
|
+
"""Delete devices send from the backend."""
|
|
42
|
+
if entry := self.get_central_entry(interface_id=interface_id):
|
|
43
|
+
entry.looper.create_task(
|
|
44
|
+
target=lambda: entry.central.device_coordinator.delete_devices(
|
|
45
|
+
interface_id=interface_id, addresses=tuple(addresses)
|
|
46
|
+
),
|
|
47
|
+
name=f"deleteDevices-{interface_id}",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@callback_backend_system(system_event=SystemEventType.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.error(
|
|
66
|
+
i18n.tr(
|
|
67
|
+
key="log.central.rpc_server.error",
|
|
68
|
+
interface_id=interface_id,
|
|
69
|
+
error_code=int(error_code),
|
|
70
|
+
msg=str(msg),
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def event(self, interface_id: str, channel_address: str, parameter: str, value: Any, /) -> None:
|
|
75
|
+
"""If a device publishes some sort event, we will handle it here."""
|
|
76
|
+
if entry := self.get_central_entry(interface_id=interface_id):
|
|
77
|
+
entry.looper.create_task(
|
|
78
|
+
target=lambda: entry.central.event_coordinator.data_point_event(
|
|
79
|
+
interface_id=interface_id,
|
|
80
|
+
channel_address=channel_address,
|
|
81
|
+
parameter=parameter,
|
|
82
|
+
value=value,
|
|
83
|
+
),
|
|
84
|
+
name=f"event-{interface_id}-{channel_address}-{parameter}",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def get_central_entry(self, *, interface_id: str) -> _CentralEntry | None:
|
|
88
|
+
"""Return the central entry by interface_id."""
|
|
89
|
+
return self._rpc_server.get_central_entry(interface_id=interface_id)
|
|
90
|
+
|
|
91
|
+
def listDevices(self, interface_id: str, /) -> list[dict[str, Any]]:
|
|
92
|
+
"""Return already existing devices to the backend."""
|
|
93
|
+
# No normalization needed here - data is already normalized in cache
|
|
94
|
+
if entry := self.get_central_entry(interface_id=interface_id):
|
|
95
|
+
return [
|
|
96
|
+
dict(device_description)
|
|
97
|
+
for device_description in entry.central.device_coordinator.list_devices(interface_id=interface_id)
|
|
98
|
+
]
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
def newDevices(self, interface_id: str, device_descriptions: list[dict[str, Any]], /) -> None:
|
|
102
|
+
"""Add new devices send from the backend (normalized)."""
|
|
103
|
+
if entry := self.get_central_entry(interface_id=interface_id):
|
|
104
|
+
# Normalize at callback entry point
|
|
105
|
+
normalized = tuple(normalize_device_description(device_description=desc) for desc in device_descriptions)
|
|
106
|
+
entry.looper.create_task(
|
|
107
|
+
target=entry.central.device_coordinator.add_new_devices(
|
|
108
|
+
interface_id=interface_id, device_descriptions=normalized
|
|
109
|
+
),
|
|
110
|
+
name=f"newDevices-{interface_id}",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def readdedDevice(self, interface_id: str, addresses: list[str], /) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Handle re-added device after re-pairing in learn mode.
|
|
116
|
+
|
|
117
|
+
Gets called when a known device is put into learn-mode while installation
|
|
118
|
+
mode is active. The device parameters may have changed, so we refresh
|
|
119
|
+
the device data.
|
|
120
|
+
"""
|
|
121
|
+
_LOGGER.debug(
|
|
122
|
+
"READDEDDEVICES: interface_id = %s, addresses = %s",
|
|
123
|
+
interface_id,
|
|
124
|
+
str(addresses),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Filter to device addresses only (exclude channel addresses)
|
|
128
|
+
if (entry := self.get_central_entry(interface_id=interface_id)) and (
|
|
129
|
+
device_addresses := tuple(addr for addr in addresses if ":" not in addr)
|
|
130
|
+
):
|
|
131
|
+
entry.looper.create_task(
|
|
132
|
+
target=lambda: entry.central.device_coordinator.readd_device(
|
|
133
|
+
interface_id=interface_id, device_addresses=device_addresses
|
|
134
|
+
),
|
|
135
|
+
name=f"readdedDevice-{interface_id}",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def replaceDevice(self, interface_id: str, old_device_address: str, new_device_address: str, /) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Handle device replacement from CCU.
|
|
141
|
+
|
|
142
|
+
Gets called when a user replaces a broken device with a new one using the
|
|
143
|
+
CCU's "Replace device" function. The old device is removed and the new
|
|
144
|
+
device is created with fresh descriptions.
|
|
145
|
+
"""
|
|
146
|
+
_LOGGER.debug(
|
|
147
|
+
"REPLACEDEVICE: interface_id = %s, oldDeviceAddress = %s, newDeviceAddress = %s",
|
|
148
|
+
interface_id,
|
|
149
|
+
old_device_address,
|
|
150
|
+
new_device_address,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if entry := self.get_central_entry(interface_id=interface_id):
|
|
154
|
+
entry.looper.create_task(
|
|
155
|
+
target=lambda: entry.central.device_coordinator.replace_device(
|
|
156
|
+
interface_id=interface_id,
|
|
157
|
+
old_device_address=old_device_address,
|
|
158
|
+
new_device_address=new_device_address,
|
|
159
|
+
),
|
|
160
|
+
name=f"replaceDevice-{interface_id}-{old_device_address}-{new_device_address}",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def updateDevice(self, interface_id: str, address: str, hint: int, /) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Update a device after firmware update or link partner change.
|
|
166
|
+
|
|
167
|
+
When hint=0 (firmware update), this method triggers cache invalidation
|
|
168
|
+
and reloading of device/paramset descriptions. When hint=1 (link partner
|
|
169
|
+
change), it refreshes the link peer information for all channels.
|
|
170
|
+
"""
|
|
171
|
+
_LOGGER.debug(
|
|
172
|
+
"UPDATEDEVICE: interface_id = %s, address = %s, hint = %s",
|
|
173
|
+
interface_id,
|
|
174
|
+
address,
|
|
175
|
+
str(hint),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if entry := self.get_central_entry(interface_id=interface_id):
|
|
179
|
+
device_address = get_device_address(address=address)
|
|
180
|
+
if hint == UpdateDeviceHint.FIRMWARE:
|
|
181
|
+
# Firmware update: invalidate cache and reload device
|
|
182
|
+
entry.looper.create_task(
|
|
183
|
+
target=lambda: entry.central.device_coordinator.update_device(
|
|
184
|
+
interface_id=interface_id, device_address=device_address
|
|
185
|
+
),
|
|
186
|
+
name=f"updateDevice-firmware-{interface_id}-{device_address}",
|
|
187
|
+
)
|
|
188
|
+
elif hint == UpdateDeviceHint.LINKS:
|
|
189
|
+
# Link partner change: refresh link peer information
|
|
190
|
+
entry.looper.create_task(
|
|
191
|
+
target=lambda: entry.central.device_coordinator.refresh_device_link_peers(
|
|
192
|
+
device_address=device_address
|
|
193
|
+
),
|
|
194
|
+
name=f"updateDevice-links-{interface_id}-{device_address}",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Restrict to specific paths.
|
|
199
|
+
class RequestHandler(SimpleXMLRPCRequestHandler):
|
|
200
|
+
"""We handle requests to / and /RPC2."""
|
|
201
|
+
|
|
202
|
+
rpc_paths = (
|
|
203
|
+
"/",
|
|
204
|
+
"/RPC2",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class HomematicXMLRPCServer(SimpleXMLRPCServer):
|
|
209
|
+
"""
|
|
210
|
+
Simple XML-RPC server.
|
|
211
|
+
|
|
212
|
+
Simple XML-RPC server that allows functions and a single instance
|
|
213
|
+
to be installed to handle requests. The default implementation
|
|
214
|
+
attempts to dispatch XML-RPC calls to the functions or instance
|
|
215
|
+
installed in the server. Override the _dispatch method inherited
|
|
216
|
+
from SimpleXMLRPCDispatcher to change this behavior.
|
|
217
|
+
|
|
218
|
+
This implementation adds an additional method:
|
|
219
|
+
system_listMethods(self, interface_id: str.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
__kwonly_check__ = False
|
|
223
|
+
|
|
224
|
+
def system_listMethods(self, interface_id: str | None = None, /) -> list[str]:
|
|
225
|
+
"""Return a list of the methods supported by the server."""
|
|
226
|
+
return SimpleXMLRPCServer.system_listMethods(self)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class _CentralEntry:
|
|
230
|
+
"""Container for central unit with its task scheduler."""
|
|
231
|
+
|
|
232
|
+
__slots__ = ("central", "looper")
|
|
233
|
+
|
|
234
|
+
def __init__(self, *, central: RpcServerCentralProtocol, looper: RpcServerTaskSchedulerProtocol) -> None:
|
|
235
|
+
"""Initialize central entry."""
|
|
236
|
+
self.central: Final = central
|
|
237
|
+
self.looper: Final = looper
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class RpcServer(threading.Thread):
|
|
241
|
+
"""RPC server thread to handle messages from the backend."""
|
|
242
|
+
|
|
243
|
+
_initialized: bool = False
|
|
244
|
+
_instances: Final[dict[tuple[str, int], RpcServer]] = {}
|
|
245
|
+
|
|
246
|
+
def __init__(self, *, server: SimpleXMLRPCServer) -> None:
|
|
247
|
+
"""Initialize XmlRPC server."""
|
|
248
|
+
self._server = server
|
|
249
|
+
self._server.register_introspection_functions()
|
|
250
|
+
self._server.register_multicall_functions()
|
|
251
|
+
self._server.register_instance(RPCFunctions(rpc_server=self), allow_dotted_names=True)
|
|
252
|
+
self._initialized = True
|
|
253
|
+
self._address: Final[tuple[str, int]] = cast(tuple[str, int], server.server_address)
|
|
254
|
+
self._listen_ip_addr: Final = self._address[0]
|
|
255
|
+
self._listen_port: Final = self._address[1]
|
|
256
|
+
self._centrals: Final[dict[str, _CentralEntry]] = {}
|
|
257
|
+
self._instances[self._address] = self
|
|
258
|
+
threading.Thread.__init__(self, name=f"RpcServer {self._listen_ip_addr}:{self._listen_port}")
|
|
259
|
+
|
|
260
|
+
listen_ip_addr: Final = DelegatedProperty[str](path="_listen_ip_addr")
|
|
261
|
+
listen_port: Final = DelegatedProperty[int](path="_listen_port")
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def no_central_assigned(self) -> bool:
|
|
265
|
+
"""Return if no central is assigned."""
|
|
266
|
+
return len(self._centrals) == 0
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def started(self) -> bool:
|
|
270
|
+
"""Return if thread is active."""
|
|
271
|
+
return self._started.is_set() is True # type: ignore[attr-defined]
|
|
272
|
+
|
|
273
|
+
def add_central(self, *, central: RpcServerCentralProtocol, looper: RpcServerTaskSchedulerProtocol) -> None:
|
|
274
|
+
"""Register a central in the RPC-Server."""
|
|
275
|
+
if not self._centrals.get(central.name):
|
|
276
|
+
self._centrals[central.name] = _CentralEntry(central=central, looper=looper)
|
|
277
|
+
|
|
278
|
+
def get_central_entry(self, *, interface_id: str) -> _CentralEntry | None:
|
|
279
|
+
"""Return a central entry by interface_id."""
|
|
280
|
+
for entry in self._centrals.values():
|
|
281
|
+
if entry.central.client_coordinator.has_client(interface_id=interface_id):
|
|
282
|
+
return entry
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
def remove_central(self, *, central: RpcServerCentralProtocol) -> None:
|
|
286
|
+
"""Unregister a central from RPC-Server."""
|
|
287
|
+
if self._centrals.get(central.name):
|
|
288
|
+
del self._centrals[central.name]
|
|
289
|
+
|
|
290
|
+
def run(self) -> None:
|
|
291
|
+
"""Run the RPC-Server thread."""
|
|
292
|
+
_LOGGER.debug(
|
|
293
|
+
"RUN: Starting RPC-Server listening on %s:%i",
|
|
294
|
+
self._listen_ip_addr,
|
|
295
|
+
self._listen_port,
|
|
296
|
+
)
|
|
297
|
+
if self._server:
|
|
298
|
+
self._server.serve_forever()
|
|
299
|
+
|
|
300
|
+
def stop(self) -> None:
|
|
301
|
+
"""Stop the RPC-Server."""
|
|
302
|
+
_LOGGER.debug("STOP: Shutting down RPC-Server")
|
|
303
|
+
self._server.shutdown()
|
|
304
|
+
_LOGGER.debug("STOP: Stopping RPC-Server")
|
|
305
|
+
self._server.server_close()
|
|
306
|
+
# Ensure the server thread has actually terminated to avoid slow teardown
|
|
307
|
+
with contextlib.suppress(RuntimeError):
|
|
308
|
+
self.join(timeout=1.0)
|
|
309
|
+
_LOGGER.debug("STOP: RPC-Server stopped")
|
|
310
|
+
if self._address in self._instances:
|
|
311
|
+
del self._instances[self._address]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class XmlRpcServer(RpcServer):
|
|
315
|
+
"""XML-RPC server thread to handle messages from the backend."""
|
|
316
|
+
|
|
317
|
+
def __init__(
|
|
318
|
+
self,
|
|
319
|
+
*,
|
|
320
|
+
ip_addr: str,
|
|
321
|
+
port: int,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Initialize XmlRPC server."""
|
|
324
|
+
if self._initialized:
|
|
325
|
+
return
|
|
326
|
+
super().__init__(
|
|
327
|
+
server=HomematicXMLRPCServer(
|
|
328
|
+
addr=(ip_addr, port),
|
|
329
|
+
requestHandler=RequestHandler,
|
|
330
|
+
logRequests=False,
|
|
331
|
+
allow_none=True,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def __new__(cls, ip_addr: str, port: int) -> XmlRpcServer: # noqa: PYI034 # kwonly: disable
|
|
336
|
+
"""Create new RPC server."""
|
|
337
|
+
if (rpc := cls._instances.get((ip_addr, port))) is None:
|
|
338
|
+
_LOGGER.debug("Creating XmlRpc server")
|
|
339
|
+
return super().__new__(cls)
|
|
340
|
+
return cast(XmlRpcServer, rpc)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def create_xml_rpc_server(*, ip_addr: str = IP_ANY_V4, port: int = PORT_ANY) -> XmlRpcServer:
|
|
344
|
+
"""Register the rpc server."""
|
|
345
|
+
rpc = XmlRpcServer(ip_addr=ip_addr, port=port)
|
|
346
|
+
if not rpc.started:
|
|
347
|
+
rpc.start()
|
|
348
|
+
_LOGGER.debug(
|
|
349
|
+
"CREATE_XML_RPC_SERVER: Starting XmlRPC-Server listening on %s:%i",
|
|
350
|
+
rpc.listen_ip_addr,
|
|
351
|
+
rpc.listen_port,
|
|
352
|
+
)
|
|
353
|
+
return rpc
|