aiohomematic 2025.10.5__py3-none-any.whl → 2025.10.7__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/caches/visibility.py +2 -2
- aiohomematic/central/__init__.py +49 -45
- aiohomematic/central/decorators.py +2 -2
- aiohomematic/central/{xml_rpc_server.py → rpc_server.py} +66 -49
- aiohomematic/client/__init__.py +118 -78
- aiohomematic/client/json_rpc.py +1 -1
- aiohomematic/client/{xml_rpc.py → rpc_proxy.py} +78 -43
- aiohomematic/const.py +31 -4
- aiohomematic/model/custom/switch.py +1 -0
- aiohomematic/support.py +3 -3
- {aiohomematic-2025.10.5.dist-info → aiohomematic-2025.10.7.dist-info}/METADATA +1 -1
- {aiohomematic-2025.10.5.dist-info → aiohomematic-2025.10.7.dist-info}/RECORD +15 -15
- {aiohomematic-2025.10.5.dist-info → aiohomematic-2025.10.7.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.10.5.dist-info → aiohomematic-2025.10.7.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.10.5.dist-info → aiohomematic-2025.10.7.dist-info}/top_level.txt +0 -0
|
@@ -171,8 +171,8 @@ _IGNORED_PARAMETERS: Final[frozenset[TParameterName]] = frozenset(
|
|
|
171
171
|
"CLEAR_ERROR",
|
|
172
172
|
"CLEAR_WINDOW_OPEN_SYMBOL",
|
|
173
173
|
"CLOCK",
|
|
174
|
-
"CMD_RETL", #
|
|
175
|
-
"CMD_RETS", #
|
|
174
|
+
"CMD_RETL", # CUxD
|
|
175
|
+
"CMD_RETS", # CUxD
|
|
176
176
|
"CONTROL_DIFFERENTIAL_TEMPERATURE",
|
|
177
177
|
"DATE_TIME_UNKNOWN",
|
|
178
178
|
"DECISION_VALUE",
|
aiohomematic/central/__init__.py
CHANGED
|
@@ -82,10 +82,10 @@ from aiohomematic.async_support import Looper, loop_check
|
|
|
82
82
|
from aiohomematic.caches.dynamic import CentralDataCache, DeviceDetailsCache
|
|
83
83
|
from aiohomematic.caches.persistent import DeviceDescriptionCache, ParamsetDescriptionCache
|
|
84
84
|
from aiohomematic.caches.visibility import ParameterVisibilityCache
|
|
85
|
-
from aiohomematic.central import
|
|
85
|
+
from aiohomematic.central import rpc_server as rpc
|
|
86
86
|
from aiohomematic.central.decorators import callback_backend_system, callback_event
|
|
87
|
-
from aiohomematic.client.json_rpc import
|
|
88
|
-
from aiohomematic.client.
|
|
87
|
+
from aiohomematic.client.json_rpc import AioJsonRpcAioHttpClient
|
|
88
|
+
from aiohomematic.client.rpc_proxy import AioXmlRpcProxy
|
|
89
89
|
from aiohomematic.const import (
|
|
90
90
|
CALLBACK_TYPE,
|
|
91
91
|
CATEGORIES,
|
|
@@ -98,6 +98,7 @@ from aiohomematic.const import (
|
|
|
98
98
|
DEFAULT_ENABLE_SYSVAR_SCAN,
|
|
99
99
|
DEFAULT_HM_MASTER_POLL_AFTER_SEND_INTERVALS,
|
|
100
100
|
DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS,
|
|
101
|
+
DEFAULT_INTERFACES_REQUIRING_PERIODIC_REFRESH,
|
|
101
102
|
DEFAULT_MAX_READ_WORKERS,
|
|
102
103
|
DEFAULT_PERIODIC_REFRESH_INTERVAL,
|
|
103
104
|
DEFAULT_PROGRAM_MARKERS,
|
|
@@ -112,7 +113,6 @@ from aiohomematic.const import (
|
|
|
112
113
|
DEVICE_FIRMWARE_DELIVERING_CHECK_INTERVAL,
|
|
113
114
|
DEVICE_FIRMWARE_UPDATING_CHECK_INTERVAL,
|
|
114
115
|
IGNORE_FOR_UN_IGNORE_PARAMETERS,
|
|
115
|
-
INTERFACES_REQUIRING_PERIODIC_REFRESH,
|
|
116
116
|
IP_ANY_V4,
|
|
117
117
|
LOCAL_HOST,
|
|
118
118
|
PORT_ANY,
|
|
@@ -137,6 +137,7 @@ from aiohomematic.const import (
|
|
|
137
137
|
Parameter,
|
|
138
138
|
ParamsetKey,
|
|
139
139
|
ProxyInitState,
|
|
140
|
+
RpcServerType,
|
|
140
141
|
SourceOfDeviceCreation,
|
|
141
142
|
SystemInformation,
|
|
142
143
|
)
|
|
@@ -180,7 +181,7 @@ _LOGGER_EVENT: Final = logging.getLogger(f"{__package__}.event")
|
|
|
180
181
|
|
|
181
182
|
# {central_name, central}
|
|
182
183
|
CENTRAL_INSTANCES: Final[dict[str, CentralUnit]] = {}
|
|
183
|
-
ConnectionProblemIssuer =
|
|
184
|
+
ConnectionProblemIssuer = AioJsonRpcAioHttpClient | AioXmlRpcProxy
|
|
184
185
|
|
|
185
186
|
INTERFACE_EVENT_SCHEMA = vol.Schema(
|
|
186
187
|
{
|
|
@@ -208,8 +209,8 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
208
209
|
self._url: Final = self._config.create_central_url()
|
|
209
210
|
self._model: str | None = None
|
|
210
211
|
self._looper = Looper()
|
|
211
|
-
self._xml_rpc_server:
|
|
212
|
-
self._json_rpc_client:
|
|
212
|
+
self._xml_rpc_server: rpc.XmlRpcServer | None = None
|
|
213
|
+
self._json_rpc_client: AioJsonRpcAioHttpClient | None = None
|
|
213
214
|
|
|
214
215
|
# Caches for the backend data
|
|
215
216
|
self._data_cache: Final = CentralDataCache(central=self)
|
|
@@ -248,9 +249,9 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
248
249
|
self._version: str | None = None
|
|
249
250
|
# store last event received datetime by interface_id
|
|
250
251
|
self._last_event_seen_for_interface: Final[dict[str, datetime]] = {}
|
|
251
|
-
self.
|
|
252
|
+
self._rpc_callback_ip: str = IP_ANY_V4
|
|
252
253
|
self._listen_ip_addr: str = IP_ANY_V4
|
|
253
|
-
self.
|
|
254
|
+
self._listen_port_xml_rpc: int = PORT_ANY
|
|
254
255
|
|
|
255
256
|
@property
|
|
256
257
|
def available(self) -> bool:
|
|
@@ -260,7 +261,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
260
261
|
@property
|
|
261
262
|
def callback_ip_addr(self) -> str:
|
|
262
263
|
"""Return the xml rpc server callback ip address."""
|
|
263
|
-
return self.
|
|
264
|
+
return self._rpc_callback_ip
|
|
264
265
|
|
|
265
266
|
@info_property(log_context=True)
|
|
266
267
|
def url(self) -> str:
|
|
@@ -327,7 +328,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
327
328
|
return all(client.is_callback_alive() for client in self._clients.values())
|
|
328
329
|
|
|
329
330
|
@property
|
|
330
|
-
def json_rpc_client(self) ->
|
|
331
|
+
def json_rpc_client(self) -> AioJsonRpcAioHttpClient:
|
|
331
332
|
"""Return the json rpc client."""
|
|
332
333
|
if not self._json_rpc_client:
|
|
333
334
|
self._json_rpc_client = self._config.create_json_rpc_client(central=self)
|
|
@@ -363,9 +364,9 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
363
364
|
return self._listen_ip_addr
|
|
364
365
|
|
|
365
366
|
@property
|
|
366
|
-
def
|
|
367
|
+
def listen_port_xml_rpc(self) -> int:
|
|
367
368
|
"""Return the xml rpc listening server port."""
|
|
368
|
-
return self.
|
|
369
|
+
return self._listen_port_xml_rpc
|
|
369
370
|
|
|
370
371
|
@property
|
|
371
372
|
def looper(self) -> Looper:
|
|
@@ -483,22 +484,22 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
483
484
|
if self._config.enabled_interface_configs and (
|
|
484
485
|
ip_addr := await self._identify_ip_addr(port=self._config.connection_check_port)
|
|
485
486
|
):
|
|
486
|
-
self.
|
|
487
|
+
self._rpc_callback_ip = ip_addr
|
|
487
488
|
self._listen_ip_addr = self._config.listen_ip_addr if self._config.listen_ip_addr else ip_addr
|
|
488
489
|
|
|
489
|
-
|
|
490
|
-
self._config.
|
|
491
|
-
if self._config.
|
|
492
|
-
else self._config.
|
|
490
|
+
port_xml_rpc: int = (
|
|
491
|
+
self._config.listen_port_xml_rpc
|
|
492
|
+
if self._config.listen_port_xml_rpc
|
|
493
|
+
else self._config.callback_port_xml_rpc or self._config.default_callback_port_xml_rpc
|
|
493
494
|
)
|
|
494
495
|
try:
|
|
495
496
|
if (
|
|
496
|
-
xml_rpc_server :=
|
|
497
|
-
if self._config.
|
|
497
|
+
xml_rpc_server := rpc.create_xml_rpc_server(ip_addr=self._listen_ip_addr, port=port_xml_rpc)
|
|
498
|
+
if self._config.enable_xml_rpc_server
|
|
498
499
|
else None
|
|
499
500
|
):
|
|
500
501
|
self._xml_rpc_server = xml_rpc_server
|
|
501
|
-
self.
|
|
502
|
+
self._listen_port_xml_rpc = xml_rpc_server.listen_port
|
|
502
503
|
self._xml_rpc_server.add_central(central=self)
|
|
503
504
|
except OSError as oserr:
|
|
504
505
|
self._state = CentralUnitState.STOPPED_BY_ERROR
|
|
@@ -514,7 +515,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
514
515
|
)
|
|
515
516
|
else:
|
|
516
517
|
self._clients_started = await self._start_clients()
|
|
517
|
-
if self._config.
|
|
518
|
+
if self._config.enable_xml_rpc_server:
|
|
518
519
|
self._start_scheduler()
|
|
519
520
|
|
|
520
521
|
self._state = CentralUnitState.RUNNING
|
|
@@ -1331,7 +1332,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
1331
1332
|
self._data_point_key_event_subscriptions[data_point.dpk] = []
|
|
1332
1333
|
self._data_point_key_event_subscriptions[data_point.dpk].append(data_point.event)
|
|
1333
1334
|
if (
|
|
1334
|
-
not data_point.channel.device.client.
|
|
1335
|
+
not data_point.channel.device.client.supports_rpc_callback
|
|
1335
1336
|
and data_point.state_path not in self._data_point_path_event_subscriptions
|
|
1336
1337
|
):
|
|
1337
1338
|
self._data_point_path_event_subscriptions[data_point.state_path] = data_point.dpk
|
|
@@ -1971,18 +1972,18 @@ class CentralConfig:
|
|
|
1971
1972
|
username: str,
|
|
1972
1973
|
client_session: ClientSession | None = None,
|
|
1973
1974
|
callback_host: str | None = None,
|
|
1974
|
-
|
|
1975
|
-
|
|
1975
|
+
callback_port_xml_rpc: int | None = None,
|
|
1976
|
+
default_callback_port_xml_rpc: int = PORT_ANY,
|
|
1976
1977
|
delay_new_device_creation: bool = DEFAULT_DELAY_NEW_DEVICE_CREATION,
|
|
1977
1978
|
enable_device_firmware_check: bool = DEFAULT_ENABLE_DEVICE_FIRMWARE_CHECK,
|
|
1978
1979
|
enable_program_scan: bool = DEFAULT_ENABLE_PROGRAM_SCAN,
|
|
1979
1980
|
enable_sysvar_scan: bool = DEFAULT_ENABLE_SYSVAR_SCAN,
|
|
1980
1981
|
hm_master_poll_after_send_intervals: tuple[int, ...] = DEFAULT_HM_MASTER_POLL_AFTER_SEND_INTERVALS,
|
|
1981
1982
|
ignore_custom_device_definition_models: frozenset[str] = DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS,
|
|
1982
|
-
interfaces_requiring_periodic_refresh: frozenset[Interface] =
|
|
1983
|
+
interfaces_requiring_periodic_refresh: frozenset[Interface] = DEFAULT_INTERFACES_REQUIRING_PERIODIC_REFRESH,
|
|
1983
1984
|
json_port: int | None = None,
|
|
1984
1985
|
listen_ip_addr: str | None = None,
|
|
1985
|
-
|
|
1986
|
+
listen_port_xml_rpc: int | None = None,
|
|
1986
1987
|
max_read_workers: int = DEFAULT_MAX_READ_WORKERS,
|
|
1987
1988
|
periodic_refresh_interval: int = DEFAULT_PERIODIC_REFRESH_INTERVAL,
|
|
1988
1989
|
program_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_PROGRAM_MARKERS,
|
|
@@ -1997,11 +1998,14 @@ class CentralConfig:
|
|
|
1997
1998
|
) -> None:
|
|
1998
1999
|
"""Init the client config."""
|
|
1999
2000
|
self._interface_configs: Final = interface_configs
|
|
2001
|
+
self.requires_xml_rpc_server: Final = any(
|
|
2002
|
+
ic for ic in interface_configs if ic.rpc_server == RpcServerType.XML_RPC
|
|
2003
|
+
)
|
|
2000
2004
|
self.callback_host: Final = callback_host
|
|
2001
|
-
self.
|
|
2005
|
+
self.callback_port_xml_rpc: Final = callback_port_xml_rpc
|
|
2002
2006
|
self.central_id: Final = central_id
|
|
2003
2007
|
self.client_session: Final = client_session
|
|
2004
|
-
self.
|
|
2008
|
+
self.default_callback_port_xml_rpc: Final = default_callback_port_xml_rpc
|
|
2005
2009
|
self.delay_new_device_creation: Final = delay_new_device_creation
|
|
2006
2010
|
self.enable_device_firmware_check: Final = enable_device_firmware_check
|
|
2007
2011
|
self.enable_program_scan: Final = enable_program_scan
|
|
@@ -2012,7 +2016,7 @@ class CentralConfig:
|
|
|
2012
2016
|
self.interfaces_requiring_periodic_refresh: Final = frozenset(interfaces_requiring_periodic_refresh or ())
|
|
2013
2017
|
self.json_port: Final = json_port
|
|
2014
2018
|
self.listen_ip_addr: Final = listen_ip_addr
|
|
2015
|
-
self.
|
|
2019
|
+
self.listen_port_xml_rpc: Final = listen_port_xml_rpc
|
|
2016
2020
|
self.max_read_workers = max_read_workers
|
|
2017
2021
|
self.name: Final = name
|
|
2018
2022
|
self.password: Final = password
|
|
@@ -2029,9 +2033,9 @@ class CentralConfig:
|
|
|
2029
2033
|
self.verify_tls: Final = verify_tls
|
|
2030
2034
|
|
|
2031
2035
|
@property
|
|
2032
|
-
def
|
|
2036
|
+
def enable_xml_rpc_server(self) -> bool:
|
|
2033
2037
|
"""Return if server and connection checker should be started."""
|
|
2034
|
-
return self.start_direct is False
|
|
2038
|
+
return self.requires_xml_rpc_server and self.start_direct is False
|
|
2035
2039
|
|
|
2036
2040
|
@property
|
|
2037
2041
|
def load_un_ignore(self) -> bool:
|
|
@@ -2066,7 +2070,7 @@ class CentralConfig:
|
|
|
2066
2070
|
password=self.password,
|
|
2067
2071
|
storage_folder=self.storage_folder,
|
|
2068
2072
|
callback_host=self.callback_host,
|
|
2069
|
-
|
|
2073
|
+
callback_port_xml_rpc=self.callback_port_xml_rpc,
|
|
2070
2074
|
json_port=self.json_port,
|
|
2071
2075
|
interface_configs=self._interface_configs,
|
|
2072
2076
|
):
|
|
@@ -2091,9 +2095,9 @@ class CentralConfig:
|
|
|
2091
2095
|
url = f"{url}:{self.json_port}"
|
|
2092
2096
|
return f"{url}"
|
|
2093
2097
|
|
|
2094
|
-
def create_json_rpc_client(self, *, central: CentralUnit) ->
|
|
2098
|
+
def create_json_rpc_client(self, *, central: CentralUnit) -> AioJsonRpcAioHttpClient:
|
|
2095
2099
|
"""Create a json rpc client."""
|
|
2096
|
-
return
|
|
2100
|
+
return AioJsonRpcAioHttpClient(
|
|
2097
2101
|
username=self.username,
|
|
2098
2102
|
password=self.password,
|
|
2099
2103
|
device_url=central.url,
|
|
@@ -2110,38 +2114,38 @@ class CentralConnectionState:
|
|
|
2110
2114
|
def __init__(self) -> None:
|
|
2111
2115
|
"""Init the CentralConnectionStatus."""
|
|
2112
2116
|
self._json_issues: Final[list[str]] = []
|
|
2113
|
-
self.
|
|
2117
|
+
self._rpc_proxy_issues: Final[list[str]] = []
|
|
2114
2118
|
|
|
2115
2119
|
def add_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
|
|
2116
2120
|
"""Add issue to collection."""
|
|
2117
|
-
if isinstance(issuer,
|
|
2121
|
+
if isinstance(issuer, AioJsonRpcAioHttpClient) and iid not in self._json_issues:
|
|
2118
2122
|
self._json_issues.append(iid)
|
|
2119
2123
|
_LOGGER.debug("add_issue: add issue [%s] for JsonRpcAioHttpClient", iid)
|
|
2120
2124
|
return True
|
|
2121
|
-
if isinstance(issuer,
|
|
2122
|
-
self.
|
|
2125
|
+
if isinstance(issuer, AioXmlRpcProxy) and iid not in self._rpc_proxy_issues:
|
|
2126
|
+
self._rpc_proxy_issues.append(iid)
|
|
2123
2127
|
_LOGGER.debug("add_issue: add issue [%s] for %s", iid, issuer.interface_id)
|
|
2124
2128
|
return True
|
|
2125
2129
|
return False
|
|
2126
2130
|
|
|
2127
2131
|
def remove_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
|
|
2128
2132
|
"""Add issue to collection."""
|
|
2129
|
-
if isinstance(issuer,
|
|
2133
|
+
if isinstance(issuer, AioJsonRpcAioHttpClient) and iid in self._json_issues:
|
|
2130
2134
|
self._json_issues.remove(iid)
|
|
2131
2135
|
_LOGGER.debug("remove_issue: removing issue [%s] for JsonRpcAioHttpClient", iid)
|
|
2132
2136
|
return True
|
|
2133
|
-
if isinstance(issuer,
|
|
2134
|
-
self.
|
|
2137
|
+
if isinstance(issuer, AioXmlRpcProxy) and issuer.interface_id in self._rpc_proxy_issues:
|
|
2138
|
+
self._rpc_proxy_issues.remove(iid)
|
|
2135
2139
|
_LOGGER.debug("remove_issue: removing issue [%s] for %s", iid, issuer.interface_id)
|
|
2136
2140
|
return True
|
|
2137
2141
|
return False
|
|
2138
2142
|
|
|
2139
2143
|
def has_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
|
|
2140
2144
|
"""Add issue to collection."""
|
|
2141
|
-
if isinstance(issuer,
|
|
2145
|
+
if isinstance(issuer, AioJsonRpcAioHttpClient):
|
|
2142
2146
|
return iid in self._json_issues
|
|
2143
|
-
if isinstance(issuer,
|
|
2144
|
-
return iid in self.
|
|
2147
|
+
if isinstance(issuer, (AioXmlRpcProxy)):
|
|
2148
|
+
return iid in self._rpc_proxy_issues
|
|
2145
2149
|
|
|
2146
2150
|
def handle_exception_log(
|
|
2147
2151
|
self,
|
|
@@ -12,7 +12,7 @@ import logging
|
|
|
12
12
|
from typing import Any, Final, cast
|
|
13
13
|
|
|
14
14
|
from aiohomematic import central as hmcu, client as hmcl
|
|
15
|
-
from aiohomematic.central import
|
|
15
|
+
from aiohomematic.central import rpc_server as rpc
|
|
16
16
|
from aiohomematic.const import BackendSystemEvent
|
|
17
17
|
from aiohomematic.exceptions import AioHomematicException
|
|
18
18
|
from aiohomematic.support import extract_exc_args
|
|
@@ -48,7 +48,7 @@ def callback_backend_system(system_event: BackendSystemEvent) -> Callable:
|
|
|
48
48
|
central: hmcu.CentralUnit | None = None
|
|
49
49
|
if isinstance(unit, hmcu.CentralUnit):
|
|
50
50
|
central = unit
|
|
51
|
-
if central is None and isinstance(unit,
|
|
51
|
+
if central is None and isinstance(unit, rpc.RPCFunctions):
|
|
52
52
|
central = unit.get_central(interface_id=str(args[1]))
|
|
53
53
|
if central:
|
|
54
54
|
central.looper.create_task(
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
import contextlib
|
|
13
13
|
import logging
|
|
14
14
|
import threading
|
|
15
|
-
from typing import Any, Final
|
|
15
|
+
from typing import Any, Final, cast
|
|
16
16
|
from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
|
|
17
17
|
|
|
18
18
|
from aiohomematic import central as hmcu
|
|
@@ -25,14 +25,14 @@ _LOGGER: Final = logging.getLogger(__name__)
|
|
|
25
25
|
|
|
26
26
|
# pylint: disable=invalid-name
|
|
27
27
|
class RPCFunctions:
|
|
28
|
-
"""The
|
|
28
|
+
"""The RPC functions the backend will expect."""
|
|
29
29
|
|
|
30
|
-
# Disable kw-only linter
|
|
30
|
+
# Disable kw-only linter
|
|
31
31
|
__kwonly_check__ = False
|
|
32
32
|
|
|
33
|
-
def __init__(self, *,
|
|
33
|
+
def __init__(self, *, rpc_server: RpcServer) -> None:
|
|
34
34
|
"""Init RPCFunctions."""
|
|
35
|
-
self.
|
|
35
|
+
self._rpc_server: Final = rpc_server
|
|
36
36
|
|
|
37
37
|
def event(self, interface_id: str, channel_address: str, parameter: str, value: Any, /) -> None:
|
|
38
38
|
"""If a device emits some sort event, we will handle it here."""
|
|
@@ -50,13 +50,13 @@ class RPCFunctions:
|
|
|
50
50
|
@callback_backend_system(system_event=BackendSystemEvent.ERROR)
|
|
51
51
|
def error(self, interface_id: str, error_code: str, msg: str, /) -> None:
|
|
52
52
|
"""When some error occurs the backend will send its error message here."""
|
|
53
|
-
# Structured boundary log (warning level).
|
|
53
|
+
# Structured boundary log (warning level). RPC server received error notification.
|
|
54
54
|
try:
|
|
55
55
|
raise RuntimeError(str(msg))
|
|
56
56
|
except RuntimeError as err:
|
|
57
57
|
log_boundary_error(
|
|
58
58
|
logger=_LOGGER,
|
|
59
|
-
boundary="
|
|
59
|
+
boundary="rpc-server",
|
|
60
60
|
action="error",
|
|
61
61
|
err=err,
|
|
62
62
|
level=logging.WARNING,
|
|
@@ -137,7 +137,7 @@ class RPCFunctions:
|
|
|
137
137
|
|
|
138
138
|
def get_central(self, *, interface_id: str) -> hmcu.CentralUnit | None:
|
|
139
139
|
"""Return the central by interface_id."""
|
|
140
|
-
return self.
|
|
140
|
+
return self._rpc_server.get_central(interface_id=interface_id)
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
# Restrict to specific paths.
|
|
@@ -150,7 +150,7 @@ class RequestHandler(SimpleXMLRPCRequestHandler):
|
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
|
|
153
|
-
class
|
|
153
|
+
class HomematicXMLRPCServer(SimpleXMLRPCServer):
|
|
154
154
|
"""
|
|
155
155
|
Simple XML-RPC server.
|
|
156
156
|
|
|
@@ -171,11 +171,11 @@ class AioHomematicXMLRPCServer(SimpleXMLRPCServer):
|
|
|
171
171
|
return SimpleXMLRPCServer.system_listMethods(self)
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
class
|
|
175
|
-
"""
|
|
174
|
+
class RpcServer(threading.Thread):
|
|
175
|
+
"""RPC server thread to handle messages from the backend."""
|
|
176
176
|
|
|
177
177
|
_initialized: bool = False
|
|
178
|
-
_instances: Final[dict[tuple[str, int],
|
|
178
|
+
_instances: Final[dict[tuple[str, int], RpcServer]] = {}
|
|
179
179
|
|
|
180
180
|
def __init__(
|
|
181
181
|
self,
|
|
@@ -190,46 +190,31 @@ class XmlRpcServer(threading.Thread):
|
|
|
190
190
|
self._listen_ip_addr: Final = ip_addr
|
|
191
191
|
self._listen_port: Final[int] = find_free_port() if port == PORT_ANY else port
|
|
192
192
|
self._address: Final[tuple[str, int]] = (ip_addr, self._listen_port)
|
|
193
|
-
self._instances[self._address] = self
|
|
194
|
-
threading.Thread.__init__(self, name=f"XmlRpcServer {ip_addr}:{self._listen_port}")
|
|
195
|
-
self._simple_xml_rpc_server = AioHomematicXMLRPCServer(
|
|
196
|
-
addr=self._address,
|
|
197
|
-
requestHandler=RequestHandler,
|
|
198
|
-
logRequests=False,
|
|
199
|
-
allow_none=True,
|
|
200
|
-
)
|
|
201
|
-
self._simple_xml_rpc_server.register_introspection_functions()
|
|
202
|
-
self._simple_xml_rpc_server.register_multicall_functions()
|
|
203
|
-
self._simple_xml_rpc_server.register_instance(RPCFunctions(xml_rpc_server=self), allow_dotted_names=True)
|
|
204
193
|
self._centrals: Final[dict[str, hmcu.CentralUnit]] = {}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"
|
|
208
|
-
if (xml_rpc := cls._instances.get((ip_addr, port))) is None:
|
|
209
|
-
_LOGGER.debug("Creating XmlRpc server")
|
|
210
|
-
return super().__new__(cls)
|
|
211
|
-
return xml_rpc
|
|
194
|
+
self._simple_rpc_server: SimpleXMLRPCServer
|
|
195
|
+
self._instances[self._address] = self
|
|
196
|
+
threading.Thread.__init__(self, name=f"RpcServer {ip_addr}:{self._listen_port}")
|
|
212
197
|
|
|
213
198
|
def run(self) -> None:
|
|
214
|
-
"""Run the
|
|
199
|
+
"""Run the RPC-Server thread."""
|
|
215
200
|
_LOGGER.debug(
|
|
216
|
-
"RUN: Starting
|
|
201
|
+
"RUN: Starting RPC-Server listening on %s:%i",
|
|
217
202
|
self._listen_ip_addr,
|
|
218
203
|
self._listen_port,
|
|
219
204
|
)
|
|
220
|
-
if self.
|
|
221
|
-
self.
|
|
205
|
+
if self._simple_rpc_server:
|
|
206
|
+
self._simple_rpc_server.serve_forever()
|
|
222
207
|
|
|
223
208
|
def stop(self) -> None:
|
|
224
|
-
"""Stop the
|
|
225
|
-
_LOGGER.debug("STOP: Shutting down
|
|
226
|
-
self.
|
|
227
|
-
_LOGGER.debug("STOP: Stopping
|
|
228
|
-
self.
|
|
209
|
+
"""Stop the RPC-Server."""
|
|
210
|
+
_LOGGER.debug("STOP: Shutting down RPC-Server")
|
|
211
|
+
self._simple_rpc_server.shutdown()
|
|
212
|
+
_LOGGER.debug("STOP: Stopping RPC-Server")
|
|
213
|
+
self._simple_rpc_server.server_close()
|
|
229
214
|
# Ensure the server thread has actually terminated to avoid slow teardown
|
|
230
215
|
with contextlib.suppress(RuntimeError):
|
|
231
216
|
self.join(timeout=1.0)
|
|
232
|
-
_LOGGER.debug("STOP:
|
|
217
|
+
_LOGGER.debug("STOP: RPC-Server stopped")
|
|
233
218
|
if self._address in self._instances:
|
|
234
219
|
del self._instances[self._address]
|
|
235
220
|
|
|
@@ -249,12 +234,12 @@ class XmlRpcServer(threading.Thread):
|
|
|
249
234
|
return self._started.is_set() is True # type: ignore[attr-defined]
|
|
250
235
|
|
|
251
236
|
def add_central(self, *, central: hmcu.CentralUnit) -> None:
|
|
252
|
-
"""Register a central in the
|
|
237
|
+
"""Register a central in the RPC-Server."""
|
|
253
238
|
if not self._centrals.get(central.name):
|
|
254
239
|
self._centrals[central.name] = central
|
|
255
240
|
|
|
256
241
|
def remove_central(self, *, central: hmcu.CentralUnit) -> None:
|
|
257
|
-
"""Unregister a central from
|
|
242
|
+
"""Unregister a central from RPC-Server."""
|
|
258
243
|
if self._centrals.get(central.name):
|
|
259
244
|
del self._centrals[central.name]
|
|
260
245
|
|
|
@@ -271,14 +256,46 @@ class XmlRpcServer(threading.Thread):
|
|
|
271
256
|
return len(self._centrals) == 0
|
|
272
257
|
|
|
273
258
|
|
|
259
|
+
class XmlRpcServer(RpcServer):
|
|
260
|
+
"""XML-RPC server thread to handle messages from the backend."""
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
*,
|
|
265
|
+
ip_addr: str,
|
|
266
|
+
port: int,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Init XmlRPC server."""
|
|
269
|
+
|
|
270
|
+
if self._initialized:
|
|
271
|
+
return
|
|
272
|
+
super().__init__(ip_addr=ip_addr, port=port)
|
|
273
|
+
self._simple_rpc_server = HomematicXMLRPCServer(
|
|
274
|
+
addr=self._address,
|
|
275
|
+
requestHandler=RequestHandler,
|
|
276
|
+
logRequests=False,
|
|
277
|
+
allow_none=True,
|
|
278
|
+
)
|
|
279
|
+
self._simple_rpc_server.register_introspection_functions()
|
|
280
|
+
self._simple_rpc_server.register_multicall_functions()
|
|
281
|
+
self._simple_rpc_server.register_instance(RPCFunctions(rpc_server=self), allow_dotted_names=True)
|
|
282
|
+
|
|
283
|
+
def __new__(cls, ip_addr: str, port: int) -> XmlRpcServer: # noqa: PYI034 # kwonly: disable
|
|
284
|
+
"""Create new RPC server."""
|
|
285
|
+
if (rpc := cls._instances.get((ip_addr, port))) is None:
|
|
286
|
+
_LOGGER.debug("Creating XmlRpc server")
|
|
287
|
+
return super().__new__(cls)
|
|
288
|
+
return cast(XmlRpcServer, rpc)
|
|
289
|
+
|
|
290
|
+
|
|
274
291
|
def create_xml_rpc_server(*, ip_addr: str = IP_ANY_V4, port: int = PORT_ANY) -> XmlRpcServer:
|
|
275
|
-
"""Register the
|
|
276
|
-
|
|
277
|
-
if not
|
|
278
|
-
|
|
292
|
+
"""Register the rpc server."""
|
|
293
|
+
rpc = XmlRpcServer(ip_addr=ip_addr, port=port)
|
|
294
|
+
if not rpc.started:
|
|
295
|
+
rpc.start()
|
|
279
296
|
_LOGGER.debug(
|
|
280
297
|
"CREATE_XML_RPC_SERVER: Starting XmlRPC-Server listening on %s:%i",
|
|
281
|
-
|
|
282
|
-
|
|
298
|
+
rpc.listen_ip_addr,
|
|
299
|
+
rpc.listen_port,
|
|
283
300
|
)
|
|
284
|
-
return
|
|
301
|
+
return rpc
|
aiohomematic/client/__init__.py
CHANGED
|
@@ -56,7 +56,7 @@ from typing import Any, Final, cast
|
|
|
56
56
|
|
|
57
57
|
from aiohomematic import central as hmcu
|
|
58
58
|
from aiohomematic.caches.dynamic import CommandCache, PingPongCache
|
|
59
|
-
from aiohomematic.client.
|
|
59
|
+
from aiohomematic.client.rpc_proxy import AioXmlRpcProxy, BaseRpcProxy
|
|
60
60
|
from aiohomematic.const import (
|
|
61
61
|
CALLBACK_WARN_INTERVAL,
|
|
62
62
|
DATETIME_FORMAT_MILLIS,
|
|
@@ -65,8 +65,10 @@ from aiohomematic.const import (
|
|
|
65
65
|
DP_KEY_VALUE,
|
|
66
66
|
DUMMY_SERIAL,
|
|
67
67
|
INIT_DATETIME,
|
|
68
|
+
INTERFACE_RPC_SERVER_TYPE,
|
|
69
|
+
INTERFACES_REQUIRING_JSON_RPC_CLIENT,
|
|
68
70
|
INTERFACES_SUPPORTING_FIRMWARE_UPDATES,
|
|
69
|
-
|
|
71
|
+
INTERFACES_SUPPORTING_RPC_CALLBACK,
|
|
70
72
|
RECONNECT_WAIT,
|
|
71
73
|
VIRTUAL_REMOTE_MODELS,
|
|
72
74
|
WAIT_FOR_CALLBACK,
|
|
@@ -86,6 +88,7 @@ from aiohomematic.const import (
|
|
|
86
88
|
ProductGroup,
|
|
87
89
|
ProgramData,
|
|
88
90
|
ProxyInitState,
|
|
91
|
+
RpcServerType,
|
|
89
92
|
SystemInformation,
|
|
90
93
|
SystemVariableData,
|
|
91
94
|
)
|
|
@@ -132,7 +135,6 @@ class Client(ABC, LogContextMixin):
|
|
|
132
135
|
def __init__(self, *, client_config: _ClientConfig) -> None:
|
|
133
136
|
"""Initialize the Client."""
|
|
134
137
|
self._config: Final = client_config
|
|
135
|
-
self._supports_xml_rpc = self.interface in INTERFACES_SUPPORTING_XML_RPC
|
|
136
138
|
self._last_value_send_cache = CommandCache(interface_id=client_config.interface_id)
|
|
137
139
|
self._available: bool = True
|
|
138
140
|
self._connection_error_count: int = 0
|
|
@@ -141,8 +143,8 @@ class Client(ABC, LogContextMixin):
|
|
|
141
143
|
self._ping_pong_cache: Final = PingPongCache(
|
|
142
144
|
central=client_config.central, interface_id=client_config.interface_id
|
|
143
145
|
)
|
|
144
|
-
self._proxy:
|
|
145
|
-
self._proxy_read:
|
|
146
|
+
self._proxy: BaseRpcProxy
|
|
147
|
+
self._proxy_read: BaseRpcProxy
|
|
146
148
|
self._system_information: SystemInformation
|
|
147
149
|
self.modified_at: datetime = INIT_DATETIME
|
|
148
150
|
|
|
@@ -150,11 +152,16 @@ class Client(ABC, LogContextMixin):
|
|
|
150
152
|
async def init_client(self) -> None:
|
|
151
153
|
"""Init the client."""
|
|
152
154
|
self._system_information = await self._get_system_information()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
if self.supports_rpc_callback:
|
|
156
|
+
self._proxy = await self._config.create_rpc_proxy(
|
|
157
|
+
interface=self.interface,
|
|
158
|
+
auth_enabled=self.system_information.auth_enabled,
|
|
159
|
+
)
|
|
160
|
+
self._proxy_read = await self._config.create_rpc_proxy(
|
|
161
|
+
interface=self.interface,
|
|
162
|
+
auth_enabled=self.system_information.auth_enabled,
|
|
163
|
+
max_workers=self._config.max_read_workers,
|
|
164
|
+
)
|
|
158
165
|
|
|
159
166
|
@property
|
|
160
167
|
def available(self) -> bool:
|
|
@@ -197,9 +204,9 @@ class Client(ABC, LogContextMixin):
|
|
|
197
204
|
return self._ping_pong_cache
|
|
198
205
|
|
|
199
206
|
@property
|
|
200
|
-
def
|
|
201
|
-
"""Return if interface support
|
|
202
|
-
return self.
|
|
207
|
+
def supports_rpc_callback(self) -> bool:
|
|
208
|
+
"""Return if interface support rpc callback."""
|
|
209
|
+
return self._config.supports_rpc_callback
|
|
203
210
|
|
|
204
211
|
@property
|
|
205
212
|
def system_information(self) -> SystemInformation:
|
|
@@ -233,24 +240,24 @@ class Client(ABC, LogContextMixin):
|
|
|
233
240
|
return ProductGroup.UNKNOWN
|
|
234
241
|
|
|
235
242
|
@property
|
|
236
|
-
@abstractmethod
|
|
237
243
|
def supports_ping_pong(self) -> bool:
|
|
238
244
|
"""Return the supports_ping_pong info of the backend."""
|
|
245
|
+
return self.interface in INTERFACES_SUPPORTING_RPC_CALLBACK
|
|
239
246
|
|
|
240
247
|
@property
|
|
241
248
|
def supports_push_updates(self) -> bool:
|
|
242
249
|
"""Return the client supports push update."""
|
|
243
|
-
return self.
|
|
250
|
+
return self._config.supports_push_updates
|
|
244
251
|
|
|
245
252
|
@property
|
|
246
253
|
def supports_firmware_updates(self) -> bool:
|
|
247
254
|
"""Return the supports_ping_pong info of the backend."""
|
|
248
|
-
return self.
|
|
255
|
+
return self._config.supports_firmware_updates
|
|
249
256
|
|
|
250
257
|
async def initialize_proxy(self) -> ProxyInitState:
|
|
251
258
|
"""Init the proxy has to tell the backend where to send the events."""
|
|
252
259
|
|
|
253
|
-
if not self.
|
|
260
|
+
if not self.supports_rpc_callback:
|
|
254
261
|
if device_descriptions := await self.list_devices():
|
|
255
262
|
await self.central.add_new_devices(
|
|
256
263
|
interface_id=self.interface_id, device_descriptions=device_descriptions
|
|
@@ -278,7 +285,7 @@ class Client(ABC, LogContextMixin):
|
|
|
278
285
|
|
|
279
286
|
async def deinitialize_proxy(self) -> ProxyInitState:
|
|
280
287
|
"""De-init to stop the backend from sending events for this remote."""
|
|
281
|
-
if not self.
|
|
288
|
+
if not self.supports_rpc_callback:
|
|
282
289
|
return ProxyInitState.DE_INIT_SUCCESS
|
|
283
290
|
|
|
284
291
|
if self.modified_at == INIT_DATETIME:
|
|
@@ -348,7 +355,7 @@ class Client(ABC, LogContextMixin):
|
|
|
348
355
|
|
|
349
356
|
async def stop(self) -> None:
|
|
350
357
|
"""Stop depending services."""
|
|
351
|
-
if not self.
|
|
358
|
+
if not self.supports_rpc_callback:
|
|
352
359
|
return
|
|
353
360
|
await self._proxy.stop()
|
|
354
361
|
await self._proxy_read.stop()
|
|
@@ -422,15 +429,17 @@ class Client(ABC, LogContextMixin):
|
|
|
422
429
|
async def check_connection_availability(self, *, handle_ping_pong: bool) -> bool:
|
|
423
430
|
"""Send ping to the backend to generate PONG event."""
|
|
424
431
|
|
|
425
|
-
@abstractmethod
|
|
426
432
|
@inspector
|
|
427
433
|
async def execute_program(self, *, pid: str) -> bool:
|
|
428
434
|
"""Execute a program on the backend."""
|
|
435
|
+
_LOGGER.debug("EXECUTE_PROGRAM: not usable for %s.", self.interface_id)
|
|
436
|
+
return True
|
|
429
437
|
|
|
430
|
-
@abstractmethod
|
|
431
438
|
@inspector
|
|
432
439
|
async def set_program_state(self, *, pid: str, state: bool) -> bool:
|
|
433
440
|
"""Set the program state on the backend."""
|
|
441
|
+
_LOGGER.debug("SET_PROGRAM_STATE: not usable for %s.", self.interface_id)
|
|
442
|
+
return True
|
|
434
443
|
|
|
435
444
|
@abstractmethod
|
|
436
445
|
@inspector(measure_performance=True)
|
|
@@ -454,20 +463,23 @@ class Client(ABC, LogContextMixin):
|
|
|
454
463
|
) -> tuple[SystemVariableData, ...] | None:
|
|
455
464
|
"""Get all system variables from the backend."""
|
|
456
465
|
|
|
457
|
-
@abstractmethod
|
|
458
466
|
@inspector(re_raise=False)
|
|
459
467
|
async def get_all_programs(self, *, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...] | None:
|
|
460
468
|
"""Get all programs, if available."""
|
|
469
|
+
_LOGGER.debug("GET_ALL_PROGRAMS: not usable for %s.", self.interface_id)
|
|
470
|
+
return None
|
|
461
471
|
|
|
462
|
-
@abstractmethod
|
|
463
472
|
@inspector(re_raise=False, no_raise_return={})
|
|
464
473
|
async def get_all_rooms(self) -> dict[str, set[str]]:
|
|
465
474
|
"""Get all rooms, if available."""
|
|
475
|
+
_LOGGER.debug("GET_ALL_ROOMS: not usable for %s.", self.interface_id)
|
|
476
|
+
return {}
|
|
466
477
|
|
|
467
|
-
@abstractmethod
|
|
468
478
|
@inspector(re_raise=False, no_raise_return={})
|
|
469
479
|
async def get_all_functions(self) -> dict[str, set[str]]:
|
|
470
480
|
"""Get all functions, if available."""
|
|
481
|
+
_LOGGER.debug("GET_ALL_FUNCTIONS: not usable for %s.", self.interface_id)
|
|
482
|
+
return {}
|
|
471
483
|
|
|
472
484
|
@abstractmethod
|
|
473
485
|
async def _get_system_information(self) -> SystemInformation:
|
|
@@ -1094,11 +1106,6 @@ class ClientCCU(Client):
|
|
|
1094
1106
|
"""Return the model of the backend."""
|
|
1095
1107
|
return Backend.CCU
|
|
1096
1108
|
|
|
1097
|
-
@property
|
|
1098
|
-
def supports_ping_pong(self) -> bool:
|
|
1099
|
-
"""Return the supports_ping_pong info of the backend."""
|
|
1100
|
-
return True
|
|
1101
|
-
|
|
1102
1109
|
@inspector(re_raise=False, measure_performance=True)
|
|
1103
1110
|
async def fetch_device_details(self) -> None:
|
|
1104
1111
|
"""Get all names via JSON-RPS and store in data.NAMES."""
|
|
@@ -1252,23 +1259,13 @@ class ClientCCU(Client):
|
|
|
1252
1259
|
|
|
1253
1260
|
|
|
1254
1261
|
class ClientJsonCCU(ClientCCU):
|
|
1255
|
-
"""Client implementation for CCU-like backend (CCU-Jack
|
|
1256
|
-
|
|
1257
|
-
@inspector
|
|
1258
|
-
async def init_client(self) -> None:
|
|
1259
|
-
"""Init the client."""
|
|
1260
|
-
self._system_information = await self._get_system_information()
|
|
1262
|
+
"""Client implementation for CCU-like backend (CCU-Jack)."""
|
|
1261
1263
|
|
|
1262
1264
|
@inspector(re_raise=False, no_raise_return=False)
|
|
1263
1265
|
async def check_connection_availability(self, *, handle_ping_pong: bool) -> bool:
|
|
1264
1266
|
"""Check if proxy is still initialized."""
|
|
1265
1267
|
return await self._json_rpc_client.is_present(interface=self.interface)
|
|
1266
1268
|
|
|
1267
|
-
@property
|
|
1268
|
-
def supports_ping_pong(self) -> bool:
|
|
1269
|
-
"""Return the supports_ping_pong info of the backend."""
|
|
1270
|
-
return False
|
|
1271
|
-
|
|
1272
1269
|
@inspector(re_raise=False)
|
|
1273
1270
|
async def get_device_description(self, *, device_address: str) -> DeviceDescription | None:
|
|
1274
1271
|
"""Get device descriptions from the backend."""
|
|
@@ -1401,7 +1398,7 @@ class ClientJsonCCU(ClientCCU):
|
|
|
1401
1398
|
channel_address=channel_address, parameter=parameter, value=value, rx_mode=rx_mode
|
|
1402
1399
|
)
|
|
1403
1400
|
|
|
1404
|
-
#
|
|
1401
|
+
# Doesn't work. put_paramset not supported
|
|
1405
1402
|
# if (
|
|
1406
1403
|
# value_type := self._get_parameter_type(
|
|
1407
1404
|
# channel_address=channel_address,
|
|
@@ -1459,6 +1456,52 @@ class ClientJsonCCU(ClientCCU):
|
|
|
1459
1456
|
serial=f"{self.interface}_{DUMMY_SERIAL}",
|
|
1460
1457
|
)
|
|
1461
1458
|
|
|
1459
|
+
@inspector
|
|
1460
|
+
async def add_link(self, *, sender_address: str, receiver_address: str, name: str, description: str) -> None:
|
|
1461
|
+
"""Return a list of links."""
|
|
1462
|
+
_LOGGER.debug("ADD_LINK: not usable for %s.", self.interface_id)
|
|
1463
|
+
|
|
1464
|
+
@inspector
|
|
1465
|
+
async def remove_link(self, *, sender_address: str, receiver_address: str) -> None:
|
|
1466
|
+
"""Return a list of links."""
|
|
1467
|
+
_LOGGER.debug("REMOVE_LINK: not usable for %s.", self.interface_id)
|
|
1468
|
+
|
|
1469
|
+
@inspector
|
|
1470
|
+
async def get_link_peers(self, *, address: str) -> tuple[str, ...] | None:
|
|
1471
|
+
"""Return a list of link pers."""
|
|
1472
|
+
_LOGGER.debug("GET_LINK_PEERS: not usable for %s.", self.interface_id)
|
|
1473
|
+
return None
|
|
1474
|
+
|
|
1475
|
+
@inspector
|
|
1476
|
+
async def get_links(self, *, address: str, flags: int) -> dict[str, Any]:
|
|
1477
|
+
"""Return a list of links."""
|
|
1478
|
+
_LOGGER.debug("GET_LINKS: not usable for %s.", self.interface_id)
|
|
1479
|
+
return {}
|
|
1480
|
+
|
|
1481
|
+
@inspector
|
|
1482
|
+
async def get_metadata(self, *, address: str, data_id: str) -> dict[str, Any]:
|
|
1483
|
+
"""Return the metadata for an object."""
|
|
1484
|
+
_LOGGER.debug("GET_METADATA: not usable for %s.", self.interface_id)
|
|
1485
|
+
return {}
|
|
1486
|
+
|
|
1487
|
+
@inspector
|
|
1488
|
+
async def set_metadata(self, *, address: str, data_id: str, value: dict[str, Any]) -> dict[str, Any]:
|
|
1489
|
+
"""Write the metadata for an object."""
|
|
1490
|
+
_LOGGER.debug("SET_METADATA: not usable for %s.", self.interface_id)
|
|
1491
|
+
return {}
|
|
1492
|
+
|
|
1493
|
+
@inspector
|
|
1494
|
+
async def report_value_usage(self, *, address: str, value_id: str, ref_counter: int) -> bool:
|
|
1495
|
+
"""Report value usage."""
|
|
1496
|
+
_LOGGER.debug("REPORT_VALUE_USAGE: not usable for %s.", self.interface_id)
|
|
1497
|
+
return True
|
|
1498
|
+
|
|
1499
|
+
@inspector
|
|
1500
|
+
async def update_device_firmware(self, *, device_address: str) -> bool:
|
|
1501
|
+
"""Update the firmware of a Homematic device."""
|
|
1502
|
+
_LOGGER.debug("UPDATE_DEVICE_FIRMWARE: not usable for %s.", self.interface_id)
|
|
1503
|
+
return True
|
|
1504
|
+
|
|
1462
1505
|
|
|
1463
1506
|
class ClientHomegear(Client):
|
|
1464
1507
|
"""Client implementation for Homegear backend."""
|
|
@@ -1515,16 +1558,6 @@ class ClientHomegear(Client):
|
|
|
1515
1558
|
self.modified_at = INIT_DATETIME
|
|
1516
1559
|
return False
|
|
1517
1560
|
|
|
1518
|
-
@inspector
|
|
1519
|
-
async def execute_program(self, *, pid: str) -> bool:
|
|
1520
|
-
"""Execute a program on the backend."""
|
|
1521
|
-
return True
|
|
1522
|
-
|
|
1523
|
-
@inspector
|
|
1524
|
-
async def set_program_state(self, *, pid: str, state: bool) -> bool:
|
|
1525
|
-
"""Set the program state on the backend."""
|
|
1526
|
-
return True
|
|
1527
|
-
|
|
1528
1561
|
@inspector(measure_performance=True)
|
|
1529
1562
|
async def set_system_variable(self, *, legacy_name: str, value: Any) -> bool:
|
|
1530
1563
|
"""Set a system variable on the backend."""
|
|
@@ -1553,21 +1586,6 @@ class ClientHomegear(Client):
|
|
|
1553
1586
|
variables.append(SystemVariableData(vid=name, legacy_name=name, value=value))
|
|
1554
1587
|
return tuple(variables)
|
|
1555
1588
|
|
|
1556
|
-
@inspector(re_raise=False)
|
|
1557
|
-
async def get_all_programs(self, *, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...] | None:
|
|
1558
|
-
"""Get all programs, if available."""
|
|
1559
|
-
return ()
|
|
1560
|
-
|
|
1561
|
-
@inspector(re_raise=False, no_raise_return={})
|
|
1562
|
-
async def get_all_rooms(self) -> dict[str, set[str]]:
|
|
1563
|
-
"""Get all rooms from the backend."""
|
|
1564
|
-
return {}
|
|
1565
|
-
|
|
1566
|
-
@inspector(re_raise=False, no_raise_return={})
|
|
1567
|
-
async def get_all_functions(self) -> dict[str, set[str]]:
|
|
1568
|
-
"""Get all functions from the backend."""
|
|
1569
|
-
return {}
|
|
1570
|
-
|
|
1571
1589
|
async def _get_system_information(self) -> SystemInformation:
|
|
1572
1590
|
"""Get system information of the backend."""
|
|
1573
1591
|
return SystemInformation(available_interfaces=(Interface.BIDCOS_RF,), serial=f"{self.interface}_{DUMMY_SERIAL}")
|
|
@@ -1590,9 +1608,20 @@ class _ClientConfig:
|
|
|
1590
1608
|
self.interface_id: Final = interface_config.interface_id
|
|
1591
1609
|
self.max_read_workers: Final[int] = central.config.max_read_workers
|
|
1592
1610
|
self.has_credentials: Final[bool] = central.config.username is not None and central.config.password is not None
|
|
1593
|
-
self.
|
|
1611
|
+
self.supports_firmware_updates: Final = self.interface in INTERFACES_SUPPORTING_FIRMWARE_UPDATES
|
|
1612
|
+
self.supports_push_updates: Final = self.interface not in central.config.interfaces_requiring_periodic_refresh
|
|
1613
|
+
self.supports_rpc_callback: Final = self.interface in INTERFACES_SUPPORTING_RPC_CALLBACK
|
|
1614
|
+
callback_host: Final = (
|
|
1594
1615
|
central.config.callback_host if central.config.callback_host else central.callback_ip_addr
|
|
1595
|
-
|
|
1616
|
+
)
|
|
1617
|
+
callback_port = (
|
|
1618
|
+
central.config.callback_port_xml_rpc
|
|
1619
|
+
if central.config.callback_port_xml_rpc
|
|
1620
|
+
else central.listen_port_xml_rpc
|
|
1621
|
+
)
|
|
1622
|
+
init_url = f"{callback_host}:{callback_port}"
|
|
1623
|
+
self.init_url: Final = f"http://{init_url}"
|
|
1624
|
+
|
|
1596
1625
|
self.xml_rpc_uri: Final = build_xml_rpc_uri(
|
|
1597
1626
|
host=central.config.host,
|
|
1598
1627
|
port=interface_config.port,
|
|
@@ -1607,7 +1636,7 @@ class _ClientConfig:
|
|
|
1607
1636
|
client: Client | None
|
|
1608
1637
|
if self.interface == Interface.BIDCOS_RF and ("Homegear" in self.version or "pydevccu" in self.version):
|
|
1609
1638
|
client = ClientHomegear(client_config=self)
|
|
1610
|
-
elif self.interface in
|
|
1639
|
+
elif self.interface in INTERFACES_REQUIRING_JSON_RPC_CLIENT:
|
|
1611
1640
|
client = ClientJsonCCU(client_config=self)
|
|
1612
1641
|
else:
|
|
1613
1642
|
client = ClientCCU(client_config=self)
|
|
@@ -1624,9 +1653,9 @@ class _ClientConfig:
|
|
|
1624
1653
|
|
|
1625
1654
|
async def _get_version(self) -> str:
|
|
1626
1655
|
"""Return the version of the the backend."""
|
|
1627
|
-
if self.interface in
|
|
1656
|
+
if self.interface in INTERFACES_REQUIRING_JSON_RPC_CLIENT:
|
|
1628
1657
|
return "0"
|
|
1629
|
-
check_proxy = await self.
|
|
1658
|
+
check_proxy = await self._create_simple_rpc_proxy(interface=self.interface)
|
|
1630
1659
|
try:
|
|
1631
1660
|
if (methods := check_proxy.supported_methods) and "getVersion" in methods:
|
|
1632
1661
|
# BidCos-Wired does not support getVersion()
|
|
@@ -1635,9 +1664,14 @@ class _ClientConfig:
|
|
|
1635
1664
|
raise NoConnectionException(f"Unable to connect {extract_exc_args(exc=exc)}.") from exc
|
|
1636
1665
|
return "0"
|
|
1637
1666
|
|
|
1638
|
-
async def
|
|
1667
|
+
async def create_rpc_proxy(
|
|
1668
|
+
self, *, interface: Interface, auth_enabled: bool | None = None, max_workers: int = DEFAULT_MAX_WORKERS
|
|
1669
|
+
) -> BaseRpcProxy:
|
|
1670
|
+
return await self._create_xml_rpc_proxy(auth_enabled=auth_enabled, max_workers=max_workers)
|
|
1671
|
+
|
|
1672
|
+
async def _create_xml_rpc_proxy(
|
|
1639
1673
|
self, *, auth_enabled: bool | None = None, max_workers: int = DEFAULT_MAX_WORKERS
|
|
1640
|
-
) ->
|
|
1674
|
+
) -> AioXmlRpcProxy:
|
|
1641
1675
|
"""Return a XmlRPC proxy for the backend communication."""
|
|
1642
1676
|
config = self.central.config
|
|
1643
1677
|
xml_rpc_headers = (
|
|
@@ -1648,7 +1682,7 @@ class _ClientConfig:
|
|
|
1648
1682
|
if auth_enabled
|
|
1649
1683
|
else []
|
|
1650
1684
|
)
|
|
1651
|
-
xml_proxy =
|
|
1685
|
+
xml_proxy = AioXmlRpcProxy(
|
|
1652
1686
|
max_workers=max_workers,
|
|
1653
1687
|
interface_id=self.interface_id,
|
|
1654
1688
|
connection_state=self.central.connection_state,
|
|
@@ -1660,9 +1694,13 @@ class _ClientConfig:
|
|
|
1660
1694
|
await xml_proxy.do_init()
|
|
1661
1695
|
return xml_proxy
|
|
1662
1696
|
|
|
1663
|
-
async def
|
|
1697
|
+
async def _create_simple_rpc_proxy(self, *, interface: Interface) -> BaseRpcProxy:
|
|
1698
|
+
"""Return a RPC proxy for the backend communication."""
|
|
1699
|
+
return await self._create_xml_rpc_proxy()
|
|
1700
|
+
|
|
1701
|
+
async def _create_simple_xml_rpc_proxy(self) -> AioXmlRpcProxy:
|
|
1664
1702
|
"""Return a XmlRPC proxy for the backend communication."""
|
|
1665
|
-
return await self.
|
|
1703
|
+
return await self._create_xml_rpc_proxy(auth_enabled=True, max_workers=0)
|
|
1666
1704
|
|
|
1667
1705
|
|
|
1668
1706
|
class InterfaceConfig:
|
|
@@ -1673,11 +1711,13 @@ class InterfaceConfig:
|
|
|
1673
1711
|
*,
|
|
1674
1712
|
central_name: str,
|
|
1675
1713
|
interface: Interface,
|
|
1676
|
-
port: int
|
|
1714
|
+
port: int,
|
|
1677
1715
|
remote_path: str | None = None,
|
|
1678
1716
|
) -> None:
|
|
1679
1717
|
"""Init the interface config."""
|
|
1680
1718
|
self.interface: Final[Interface] = interface
|
|
1719
|
+
|
|
1720
|
+
self.rpc_server: Final[RpcServerType] = INTERFACE_RPC_SERVER_TYPE[interface]
|
|
1681
1721
|
self.interface_id: Final[str] = f"{central_name}-{self.interface}"
|
|
1682
1722
|
self.port: Final = port
|
|
1683
1723
|
self.remote_path: Final = remote_path
|
|
@@ -1686,7 +1726,7 @@ class InterfaceConfig:
|
|
|
1686
1726
|
|
|
1687
1727
|
def _init_validate(self) -> None:
|
|
1688
1728
|
"""Validate the client_config."""
|
|
1689
|
-
if not self.port and self.interface in
|
|
1729
|
+
if not self.port and self.interface in INTERFACES_SUPPORTING_RPC_CALLBACK:
|
|
1690
1730
|
raise ClientException(f"VALIDATE interface config failed: Port must defined for interface{self.interface}")
|
|
1691
1731
|
|
|
1692
1732
|
@property
|
aiohomematic/client/json_rpc.py
CHANGED
|
@@ -21,13 +21,14 @@ Notes
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
24
25
|
import asyncio
|
|
25
|
-
from collections.abc import Mapping
|
|
26
|
+
from collections.abc import Callable, Mapping
|
|
26
27
|
from concurrent.futures import ThreadPoolExecutor
|
|
27
28
|
from enum import Enum, IntEnum, StrEnum
|
|
28
29
|
import errno
|
|
29
30
|
import logging
|
|
30
|
-
from ssl import SSLError
|
|
31
|
+
from ssl import SSLContext, SSLError
|
|
31
32
|
from typing import Any, Final
|
|
32
33
|
import xmlrpc.client
|
|
33
34
|
|
|
@@ -51,7 +52,7 @@ _TLS: Final = "tls"
|
|
|
51
52
|
_VERIFY_TLS: Final = "verify_tls"
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
class
|
|
55
|
+
class _RpcMethod(StrEnum):
|
|
55
56
|
"""Enum for Homematic json rpc methods types."""
|
|
56
57
|
|
|
57
58
|
GET_VERSION = "getVersion"
|
|
@@ -61,12 +62,12 @@ class _XmlRpcMethod(StrEnum):
|
|
|
61
62
|
SYSTEM_LIST_METHODS = "system.listMethods"
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
_VALID_RPC_COMMANDS_ON_NO_CONNECTION: Final[tuple[str, ...]] = (
|
|
66
|
+
_RpcMethod.GET_VERSION,
|
|
67
|
+
_RpcMethod.HOMEGEAR_INIT,
|
|
68
|
+
_RpcMethod.INIT,
|
|
69
|
+
_RpcMethod.PING,
|
|
70
|
+
_RpcMethod.SYSTEM_LIST_METHODS,
|
|
70
71
|
)
|
|
71
72
|
|
|
72
73
|
_SSL_ERROR_CODES: Final[dict[int, str]] = {
|
|
@@ -83,7 +84,7 @@ _OS_ERROR_CODES: Final[dict[int, str]] = {
|
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
# noinspection PyProtectedMember,PyUnresolvedReferences
|
|
86
|
-
class
|
|
87
|
+
class BaseRpcProxy(ABC):
|
|
87
88
|
"""ServerProxy implementation with ThreadPoolExecutor when request is executing."""
|
|
88
89
|
|
|
89
90
|
def __init__(
|
|
@@ -92,59 +93,96 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
92
93
|
max_workers: int,
|
|
93
94
|
interface_id: str,
|
|
94
95
|
connection_state: hmcu.CentralConnectionState,
|
|
95
|
-
|
|
96
|
-
headers: list[tuple[str, str]],
|
|
96
|
+
magic_method: Callable,
|
|
97
97
|
tls: bool = False,
|
|
98
98
|
verify_tls: bool = False,
|
|
99
99
|
) -> None:
|
|
100
100
|
"""Initialize new proxy for server and get local ip."""
|
|
101
101
|
self._interface_id: Final = interface_id
|
|
102
102
|
self._connection_state: Final = connection_state
|
|
103
|
+
self._magic_method: Final = magic_method
|
|
103
104
|
self._looper: Final = Looper()
|
|
104
105
|
self._proxy_executor: Final = (
|
|
105
106
|
ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix=interface_id) if max_workers > 0 else None
|
|
106
107
|
)
|
|
107
|
-
self._tls: Final[bool] = tls
|
|
108
|
-
self._verify_tls: Final[bool] = verify_tls
|
|
108
|
+
self._tls: Final[bool | SSLContext] = get_tls_context(verify_tls=verify_tls) if tls else False
|
|
109
109
|
self._supported_methods: tuple[str, ...] = ()
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
|
|
110
|
+
self._kwargs: dict[str, Any] = {}
|
|
111
|
+
if tls:
|
|
112
|
+
self._kwargs[_CONTEXT] = self._tls
|
|
113
113
|
# Due to magic method the log_context must be defined manually.
|
|
114
|
-
self.log_context: Final[Mapping[str, Any]] = {"interface_id": self._interface_id, "tls":
|
|
115
|
-
xmlrpc.client.ServerProxy.__init__(
|
|
116
|
-
self,
|
|
117
|
-
uri=uri,
|
|
118
|
-
encoding=ISO_8859_1,
|
|
119
|
-
headers=headers,
|
|
120
|
-
**kwargs,
|
|
121
|
-
)
|
|
114
|
+
self.log_context: Final[Mapping[str, Any]] = {"interface_id": self._interface_id, "tls": tls}
|
|
122
115
|
|
|
116
|
+
@abstractmethod
|
|
123
117
|
async def do_init(self) -> None:
|
|
124
|
-
"""Init the
|
|
125
|
-
if supported_methods := await self.system.listMethods():
|
|
126
|
-
# ping is missing in VirtualDevices interface but can be used.
|
|
127
|
-
supported_methods.append(_XmlRpcMethod.PING)
|
|
128
|
-
self._supported_methods = tuple(supported_methods)
|
|
118
|
+
"""Init the rpc proxy."""
|
|
129
119
|
|
|
130
120
|
@property
|
|
131
121
|
def supported_methods(self) -> tuple[str, ...]:
|
|
132
122
|
"""Return the supported methods."""
|
|
133
123
|
return self._supported_methods
|
|
134
124
|
|
|
135
|
-
async def
|
|
125
|
+
async def stop(self) -> None:
|
|
126
|
+
"""Stop depending services."""
|
|
127
|
+
await self._looper.block_till_done()
|
|
128
|
+
if self._proxy_executor:
|
|
129
|
+
self._proxy_executor.shutdown()
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
async def _async_request(self, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
133
|
+
"""Call method on server side."""
|
|
134
|
+
|
|
135
|
+
def __getattr__(self, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
136
|
+
"""Magic method dispatcher."""
|
|
137
|
+
return self._magic_method(self._async_request, *args, **kwargs)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# noinspection PyProtectedMember,PyUnresolvedReferences
|
|
141
|
+
class AioXmlRpcProxy(BaseRpcProxy, xmlrpc.client.ServerProxy):
|
|
142
|
+
"""ServerProxy implementation with ThreadPoolExecutor when request is executing."""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
*,
|
|
147
|
+
max_workers: int,
|
|
148
|
+
interface_id: str,
|
|
149
|
+
connection_state: hmcu.CentralConnectionState,
|
|
150
|
+
uri: str,
|
|
151
|
+
headers: list[tuple[str, str]],
|
|
152
|
+
tls: bool = False,
|
|
153
|
+
verify_tls: bool = False,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Initialize new proxy for server and get local ip."""
|
|
156
|
+
super().__init__(
|
|
157
|
+
max_workers=max_workers,
|
|
158
|
+
interface_id=interface_id,
|
|
159
|
+
connection_state=connection_state,
|
|
160
|
+
magic_method=xmlrpc.client._Method,
|
|
161
|
+
tls=tls,
|
|
162
|
+
verify_tls=verify_tls,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
xmlrpc.client.ServerProxy.__init__(
|
|
166
|
+
self,
|
|
167
|
+
uri=uri,
|
|
168
|
+
encoding=ISO_8859_1,
|
|
169
|
+
headers=headers,
|
|
170
|
+
**self._kwargs,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def _async_request(self, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
136
174
|
"""Call method on server side."""
|
|
137
175
|
parent = xmlrpc.client.ServerProxy
|
|
138
176
|
try:
|
|
139
177
|
method = args[0]
|
|
140
178
|
if self._supported_methods and method not in self._supported_methods:
|
|
141
|
-
raise UnsupportedException(f"__ASYNC_REQUEST: method '{method} not supported by the backend.")
|
|
179
|
+
raise UnsupportedException(f"XmlRPC.__ASYNC_REQUEST: method '{method} not supported by the backend.")
|
|
142
180
|
|
|
143
|
-
if method in
|
|
181
|
+
if method in _VALID_RPC_COMMANDS_ON_NO_CONNECTION or not self._connection_state.has_issue(
|
|
144
182
|
issuer=self, iid=self._interface_id
|
|
145
183
|
):
|
|
146
184
|
args = _cleanup_args(*args)
|
|
147
|
-
_LOGGER.debug("__ASYNC_REQUEST: %s", args)
|
|
185
|
+
_LOGGER.debug("XmlRPC.__ASYNC_REQUEST: %s", args)
|
|
148
186
|
result = await asyncio.shield(
|
|
149
187
|
self._looper.async_add_executor_job(
|
|
150
188
|
# pylint: disable=protected-access
|
|
@@ -212,15 +250,12 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
212
250
|
except Exception as exc:
|
|
213
251
|
raise ClientException(exc) from exc
|
|
214
252
|
|
|
215
|
-
def
|
|
216
|
-
"""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
await self._looper.block_till_done()
|
|
222
|
-
if self._proxy_executor:
|
|
223
|
-
self._proxy_executor.shutdown()
|
|
253
|
+
async def do_init(self) -> None:
|
|
254
|
+
"""Init the xml rpc proxy."""
|
|
255
|
+
if supported_methods := await self.system.listMethods():
|
|
256
|
+
# ping is missing in VirtualDevices interface but can be used.
|
|
257
|
+
supported_methods.append(_RpcMethod.PING)
|
|
258
|
+
self._supported_methods = tuple(supported_methods)
|
|
224
259
|
|
|
225
260
|
|
|
226
261
|
def _cleanup_args(*args: Any) -> Any:
|
aiohomematic/const.py
CHANGED
|
@@ -19,7 +19,7 @@ import sys
|
|
|
19
19
|
from types import MappingProxyType
|
|
20
20
|
from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
|
|
21
21
|
|
|
22
|
-
VERSION: Final = "2025.10.
|
|
22
|
+
VERSION: Final = "2025.10.7"
|
|
23
23
|
|
|
24
24
|
# Detect test speedup mode via environment
|
|
25
25
|
_TEST_SPEEDUP: Final = (
|
|
@@ -573,6 +573,13 @@ class ParameterType(StrEnum):
|
|
|
573
573
|
EMPTY = ""
|
|
574
574
|
|
|
575
575
|
|
|
576
|
+
class RpcServerType(StrEnum):
|
|
577
|
+
"""Enum for Homematic rpc server types."""
|
|
578
|
+
|
|
579
|
+
XML_RPC = "xml_rpc"
|
|
580
|
+
NONE = "none"
|
|
581
|
+
|
|
582
|
+
|
|
576
583
|
CLICK_EVENTS: Final[frozenset[Parameter]] = frozenset(
|
|
577
584
|
{
|
|
578
585
|
Parameter.PRESS,
|
|
@@ -689,7 +696,7 @@ INTERFACES_SUPPORTING_FIRMWARE_UPDATES: Final[frozenset[Interface]] = frozenset(
|
|
|
689
696
|
}
|
|
690
697
|
)
|
|
691
698
|
|
|
692
|
-
|
|
699
|
+
INTERFACES_REQUIRING_XML_RPC: Final[frozenset[Interface]] = frozenset(
|
|
693
700
|
{
|
|
694
701
|
Interface.BIDCOS_RF,
|
|
695
702
|
Interface.BIDCOS_WIRED,
|
|
@@ -698,13 +705,33 @@ INTERFACES_SUPPORTING_XML_RPC: Final[frozenset[Interface]] = frozenset(
|
|
|
698
705
|
}
|
|
699
706
|
)
|
|
700
707
|
|
|
701
|
-
|
|
708
|
+
|
|
709
|
+
INTERFACES_SUPPORTING_RPC_CALLBACK: Final[frozenset[Interface]] = frozenset(INTERFACES_REQUIRING_XML_RPC)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
INTERFACES_REQUIRING_JSON_RPC_CLIENT: Final[frozenset[Interface]] = frozenset(
|
|
702
713
|
{
|
|
703
|
-
Interface.CCU_JACK,
|
|
704
714
|
Interface.CUXD,
|
|
715
|
+
Interface.CCU_JACK,
|
|
705
716
|
}
|
|
706
717
|
)
|
|
707
718
|
|
|
719
|
+
DEFAULT_INTERFACES_REQUIRING_PERIODIC_REFRESH: Final[frozenset[Interface]] = frozenset(
|
|
720
|
+
INTERFACES_REQUIRING_JSON_RPC_CLIENT - INTERFACES_REQUIRING_XML_RPC
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
INTERFACE_RPC_SERVER_TYPE: Final[Mapping[Interface, RpcServerType]] = MappingProxyType(
|
|
724
|
+
{
|
|
725
|
+
Interface.BIDCOS_RF: RpcServerType.XML_RPC,
|
|
726
|
+
Interface.BIDCOS_WIRED: RpcServerType.XML_RPC,
|
|
727
|
+
Interface.HMIP_RF: RpcServerType.XML_RPC,
|
|
728
|
+
Interface.VIRTUAL_DEVICES: RpcServerType.XML_RPC,
|
|
729
|
+
Interface.CUXD: RpcServerType.NONE,
|
|
730
|
+
Interface.CCU_JACK: RpcServerType.NONE,
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
|
|
708
735
|
DEFAULT_USE_PERIODIC_SCAN_FOR_INTERFACES: Final = True
|
|
709
736
|
|
|
710
737
|
IGNORE_FOR_UN_IGNORE_PARAMETERS: Final[frozenset[Parameter]] = frozenset(
|
|
@@ -110,6 +110,7 @@ def make_ip_switch(
|
|
|
110
110
|
# HomeBrew (HB-) devices are always listed as HM-.
|
|
111
111
|
DEVICES: Mapping[str, CustomConfig | tuple[CustomConfig, ...]] = {
|
|
112
112
|
"ELV-SH-BS2": CustomConfig(make_ce_func=make_ip_switch, channels=(4, 8)),
|
|
113
|
+
"ELV-SH-PSMCI": CustomConfig(make_ce_func=make_ip_switch, channels=(3,)),
|
|
113
114
|
"ELV-SH-SW1-BAT": CustomConfig(make_ce_func=make_ip_switch, channels=(3,)),
|
|
114
115
|
"HmIP-BS2": CustomConfig(make_ce_func=make_ip_switch, channels=(4, 8)),
|
|
115
116
|
"HmIP-BSL": CustomConfig(make_ce_func=make_ip_switch, channels=(4,)),
|
aiohomematic/support.py
CHANGED
|
@@ -100,7 +100,7 @@ def check_config(
|
|
|
100
100
|
password: str,
|
|
101
101
|
storage_folder: str,
|
|
102
102
|
callback_host: str | None,
|
|
103
|
-
|
|
103
|
+
callback_port_xml_rpc: int | None,
|
|
104
104
|
json_port: int | None,
|
|
105
105
|
interface_configs: AbstractSet[hmcl.InterfaceConfig] | None = None,
|
|
106
106
|
) -> list[str]:
|
|
@@ -123,8 +123,8 @@ def check_config(
|
|
|
123
123
|
config_failures.append(extract_exc_args(exc=bhexc)[0])
|
|
124
124
|
if callback_host and not (is_hostname(hostname=callback_host) or is_ipv4_address(address=callback_host)):
|
|
125
125
|
config_failures.append("Invalid callback hostname or ipv4 address")
|
|
126
|
-
if
|
|
127
|
-
config_failures.append("Invalid callback port")
|
|
126
|
+
if callback_port_xml_rpc and not is_port(port=callback_port_xml_rpc):
|
|
127
|
+
config_failures.append("Invalid xml rpc callback port")
|
|
128
128
|
if json_port and not is_port(port=json_port):
|
|
129
129
|
config_failures.append("Invalid json port")
|
|
130
130
|
if interface_configs and not has_primary_client(interface_configs=interface_configs):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.7
|
|
4
4
|
Summary: Homematic interface for Home Assistant running on Python 3.
|
|
5
5
|
Home-page: https://github.com/sukramj/aiohomematic
|
|
6
6
|
Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
aiohomematic/__init__.py,sha256=ngULK_anZQwwUUCVcberBdVjguYfboiuG9VoueKy9fA,2283
|
|
2
2
|
aiohomematic/async_support.py,sha256=BeNKaDrFsRA5-_uAFzmyyKPqlImfSs58C22Nqd5dZAg,7887
|
|
3
|
-
aiohomematic/const.py,sha256=
|
|
3
|
+
aiohomematic/const.py,sha256=0akDDWSDmPgfp-ynHluHHIKL3_1RamHINEec2jE9qC0,26643
|
|
4
4
|
aiohomematic/context.py,sha256=M7gkA7KFT0dp35gzGz2dzKVXu1PP0sAnepgLlmjyRS4,451
|
|
5
5
|
aiohomematic/converter.py,sha256=gaNHe-WEiBStZMuuRz9iGn3Mo_CGz1bjgLtlYBJJAko,3624
|
|
6
6
|
aiohomematic/decorators.py,sha256=M4n_VSyqmsUgQQQv_-3JWQxYPbS6KEkhCS8OzAfaVKo,11060
|
|
@@ -8,19 +8,19 @@ aiohomematic/exceptions.py,sha256=8Uu3rADawhYlAz6y4J52aJ-wKok8Z7YbUYUwWeGMKhs,50
|
|
|
8
8
|
aiohomematic/hmcli.py,sha256=qNstNDX6q8t3mJFCGlXlmRVobGabntrPtFi3kchf1Eg,4933
|
|
9
9
|
aiohomematic/property_decorators.py,sha256=56lHGATgRtaFkIK_IXcR2tBW9mIVITcCwH5KOw575GA,17162
|
|
10
10
|
aiohomematic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
aiohomematic/support.py,sha256=
|
|
11
|
+
aiohomematic/support.py,sha256=7FTIDvRZvGFMfN3i_zBnHtJQd-vDqTMTq2i1G5GmW3Y,22834
|
|
12
12
|
aiohomematic/validator.py,sha256=HUikmo-SFksehFBAdZmBv4ajy0XkjgvXvcCfbexnzZo,3563
|
|
13
13
|
aiohomematic/caches/__init__.py,sha256=_gI30tbsWgPRaHvP6cRxOQr6n9bYZzU-jp1WbHhWg-A,470
|
|
14
14
|
aiohomematic/caches/dynamic.py,sha256=0hOu-WoYUc9_3fofMeg_OjlYS-quD4uTyDI6zd5W4Do,22553
|
|
15
15
|
aiohomematic/caches/persistent.py,sha256=xUMjvu5Vthz9W0LLllSbcqTADZvVV025b4VnPzrPnis,20604
|
|
16
|
-
aiohomematic/caches/visibility.py,sha256=
|
|
17
|
-
aiohomematic/central/__init__.py,sha256=
|
|
18
|
-
aiohomematic/central/decorators.py,sha256=
|
|
19
|
-
aiohomematic/central/
|
|
20
|
-
aiohomematic/client/__init__.py,sha256=
|
|
16
|
+
aiohomematic/caches/visibility.py,sha256=8lTO-jfAUzd90atUOK8rKMrzRa__m083RAoEovg0Q0o,31676
|
|
17
|
+
aiohomematic/central/__init__.py,sha256=_ft-2HXfn0pF_LTrNyV_mZ7cHkHuRgeprBJZx5MlK0I,92659
|
|
18
|
+
aiohomematic/central/decorators.py,sha256=NUMSsQ_Or6gno4LzagrNMXeBtmbBbYyoIlMI0TFp1_E,6908
|
|
19
|
+
aiohomematic/central/rpc_server.py,sha256=wf2KG-cj_wIdgfRHY3GIFFzOenJbz8MfUGLdF1drd3k,10971
|
|
20
|
+
aiohomematic/client/__init__.py,sha256=w7ns0JZNroKNy9Yw1YM1ssxhPwXUoVNpPo5RLAbgK7E,73857
|
|
21
21
|
aiohomematic/client/_rpc_errors.py,sha256=-NPtGvkQPJ4V2clDxv1tKy09M9JZm61pUCeki9DDh6s,2984
|
|
22
|
-
aiohomematic/client/json_rpc.py,sha256=
|
|
23
|
-
aiohomematic/client/
|
|
22
|
+
aiohomematic/client/json_rpc.py,sha256=7p8j6uhS0y2LuJVtobQqwtpOA_AsC5HqEdGB0T8ZSu4,50177
|
|
23
|
+
aiohomematic/client/rpc_proxy.py,sha256=v0YyhfQ_qylQpqGvGtylJtG3_tIk9PN6tWMHkki4D48,10705
|
|
24
24
|
aiohomematic/model/__init__.py,sha256=KO7gas_eEzm67tODKqWTs0617CSGeKKjOWOlDbhRo_Q,5458
|
|
25
25
|
aiohomematic/model/data_point.py,sha256=Ml8AOQ1RcRezTYWiGBlIXwcTLolQMX5Cyb-O7GtNDm4,41586
|
|
26
26
|
aiohomematic/model/device.py,sha256=15z5G2X3jSJaj-yz7jX_tnirzipRIGBJPymObY3Dmjk,52942
|
|
@@ -42,7 +42,7 @@ aiohomematic/model/custom/light.py,sha256=2UxQOoupwTpQ-5iwY51gL_B815sgDXNW-HG-Qh
|
|
|
42
42
|
aiohomematic/model/custom/lock.py,sha256=ndzZ0hp7FBohw7T_qR0jPobwlcwxus9M1DuDu_7vfPw,11996
|
|
43
43
|
aiohomematic/model/custom/siren.py,sha256=DT8RoOCl7FqstgRSBK-RWRcY4T29LuEdnlhaWCB6ATk,9785
|
|
44
44
|
aiohomematic/model/custom/support.py,sha256=UvencsvCwgpm4iqRNRt5KRs560tyw1NhYP5ZaqmCT2k,1453
|
|
45
|
-
aiohomematic/model/custom/switch.py,sha256=
|
|
45
|
+
aiohomematic/model/custom/switch.py,sha256=tIAd501_yqQB9dd1pcTTmF7tEhFqqj3gfcSgBYN_2_8,6963
|
|
46
46
|
aiohomematic/model/custom/valve.py,sha256=u9RYzeJ8FNmpFO6amlLElXTQdAeqac5yo7NbZYS6Z9U,4242
|
|
47
47
|
aiohomematic/model/generic/__init__.py,sha256=-ho8m9gFlORBGNPn2i8c9i5-GVLLFvTlf5FFpaTJbFw,7675
|
|
48
48
|
aiohomematic/model/generic/action.py,sha256=niJPvTs43b9GiKomdBaBKwjOwtmNxR_YRhj5Fpje9NU,997
|
|
@@ -69,10 +69,10 @@ aiohomematic/rega_scripts/get_serial.fn,sha256=t1oeo-sB_EuVeiY24PLcxFSkdQVgEWGXz
|
|
|
69
69
|
aiohomematic/rega_scripts/get_system_variable_descriptions.fn,sha256=UKXvC0_5lSApdQ2atJc0E5Stj5Zt3lqh0EcliokYu2c,849
|
|
70
70
|
aiohomematic/rega_scripts/set_program_state.fn,sha256=0bnv7lUj8FMjDZBz325tDVP61m04cHjVj4kIOnUUgpY,279
|
|
71
71
|
aiohomematic/rega_scripts/set_system_variable.fn,sha256=sTmr7vkPTPnPkor5cnLKlDvfsYRbGO1iq2z_2pMXq5E,383
|
|
72
|
-
aiohomematic-2025.10.
|
|
72
|
+
aiohomematic-2025.10.7.dist-info/licenses/LICENSE,sha256=q-B0xpREuZuvKsmk3_iyVZqvZ-vJcWmzMZpeAd0RqtQ,1083
|
|
73
73
|
aiohomematic_support/__init__.py,sha256=_0YtF4lTdC_k6-zrM2IefI0u0LMr_WA61gXAyeGLgbY,66
|
|
74
74
|
aiohomematic_support/client_local.py,sha256=nFeYkoX_EXXIwbrpL_5peYQG-934D0ASN6kflYp0_4I,12819
|
|
75
|
-
aiohomematic-2025.10.
|
|
76
|
-
aiohomematic-2025.10.
|
|
77
|
-
aiohomematic-2025.10.
|
|
78
|
-
aiohomematic-2025.10.
|
|
75
|
+
aiohomematic-2025.10.7.dist-info/METADATA,sha256=EP3Y37kiLdfx6DP4M988pY_gDoL3lKF_9LE3miXFxro,7603
|
|
76
|
+
aiohomematic-2025.10.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
77
|
+
aiohomematic-2025.10.7.dist-info/top_level.txt,sha256=5TDRlUWQPThIUwQjOj--aUo4UA-ow4m0sNhnoCBi5n8,34
|
|
78
|
+
aiohomematic-2025.10.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|