aiohomematic 2025.10.4__py3-none-any.whl → 2025.10.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/caches/visibility.py +2 -2
- aiohomematic/central/__init__.py +51 -46
- aiohomematic/central/decorators.py +2 -2
- aiohomematic/central/{xml_rpc_server.py → rpc_server.py} +62 -47
- 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 +4 -4
- {aiohomematic-2025.10.4.dist-info → aiohomematic-2025.10.6.dist-info}/METADATA +1 -1
- {aiohomematic-2025.10.4.dist-info → aiohomematic-2025.10.6.dist-info}/RECORD +15 -15
- {aiohomematic-2025.10.4.dist-info → aiohomematic-2025.10.6.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.10.4.dist-info → aiohomematic-2025.10.6.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.10.4.dist-info → aiohomematic-2025.10.6.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
|
|
@@ -1166,7 +1167,8 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
1166
1167
|
return tuple(
|
|
1167
1168
|
dev_desc
|
|
1168
1169
|
for dev_desc in device_descriptions
|
|
1169
|
-
if (dev_desc["ADDRESS"] if
|
|
1170
|
+
if (dev_desc["ADDRESS"] if not (parent_address := dev_desc.get("PARENT")) else parent_address)
|
|
1171
|
+
not in known_addresses
|
|
1170
1172
|
)
|
|
1171
1173
|
|
|
1172
1174
|
def _check_for_new_device_addresses(self, *, interface_id: str | None = None) -> Mapping[str, set[str]]:
|
|
@@ -1330,7 +1332,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
|
|
|
1330
1332
|
self._data_point_key_event_subscriptions[data_point.dpk] = []
|
|
1331
1333
|
self._data_point_key_event_subscriptions[data_point.dpk].append(data_point.event)
|
|
1332
1334
|
if (
|
|
1333
|
-
not data_point.channel.device.client.
|
|
1335
|
+
not data_point.channel.device.client.supports_rpc_callback
|
|
1334
1336
|
and data_point.state_path not in self._data_point_path_event_subscriptions
|
|
1335
1337
|
):
|
|
1336
1338
|
self._data_point_path_event_subscriptions[data_point.state_path] = data_point.dpk
|
|
@@ -1970,18 +1972,18 @@ class CentralConfig:
|
|
|
1970
1972
|
username: str,
|
|
1971
1973
|
client_session: ClientSession | None = None,
|
|
1972
1974
|
callback_host: str | None = None,
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
+
callback_port_xml_rpc: int | None = None,
|
|
1976
|
+
default_callback_port_xml_rpc: int = PORT_ANY,
|
|
1975
1977
|
delay_new_device_creation: bool = DEFAULT_DELAY_NEW_DEVICE_CREATION,
|
|
1976
1978
|
enable_device_firmware_check: bool = DEFAULT_ENABLE_DEVICE_FIRMWARE_CHECK,
|
|
1977
1979
|
enable_program_scan: bool = DEFAULT_ENABLE_PROGRAM_SCAN,
|
|
1978
1980
|
enable_sysvar_scan: bool = DEFAULT_ENABLE_SYSVAR_SCAN,
|
|
1979
1981
|
hm_master_poll_after_send_intervals: tuple[int, ...] = DEFAULT_HM_MASTER_POLL_AFTER_SEND_INTERVALS,
|
|
1980
1982
|
ignore_custom_device_definition_models: frozenset[str] = DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS,
|
|
1981
|
-
interfaces_requiring_periodic_refresh: frozenset[Interface] =
|
|
1983
|
+
interfaces_requiring_periodic_refresh: frozenset[Interface] = DEFAULT_INTERFACES_REQUIRING_PERIODIC_REFRESH,
|
|
1982
1984
|
json_port: int | None = None,
|
|
1983
1985
|
listen_ip_addr: str | None = None,
|
|
1984
|
-
|
|
1986
|
+
listen_port_xml_rpc: int | None = None,
|
|
1985
1987
|
max_read_workers: int = DEFAULT_MAX_READ_WORKERS,
|
|
1986
1988
|
periodic_refresh_interval: int = DEFAULT_PERIODIC_REFRESH_INTERVAL,
|
|
1987
1989
|
program_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_PROGRAM_MARKERS,
|
|
@@ -1996,11 +1998,14 @@ class CentralConfig:
|
|
|
1996
1998
|
) -> None:
|
|
1997
1999
|
"""Init the client config."""
|
|
1998
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
|
+
)
|
|
1999
2004
|
self.callback_host: Final = callback_host
|
|
2000
|
-
self.
|
|
2005
|
+
self.callback_port_xml_rpc: Final = callback_port_xml_rpc
|
|
2001
2006
|
self.central_id: Final = central_id
|
|
2002
2007
|
self.client_session: Final = client_session
|
|
2003
|
-
self.
|
|
2008
|
+
self.default_callback_port_xml_rpc: Final = default_callback_port_xml_rpc
|
|
2004
2009
|
self.delay_new_device_creation: Final = delay_new_device_creation
|
|
2005
2010
|
self.enable_device_firmware_check: Final = enable_device_firmware_check
|
|
2006
2011
|
self.enable_program_scan: Final = enable_program_scan
|
|
@@ -2011,7 +2016,7 @@ class CentralConfig:
|
|
|
2011
2016
|
self.interfaces_requiring_periodic_refresh: Final = frozenset(interfaces_requiring_periodic_refresh or ())
|
|
2012
2017
|
self.json_port: Final = json_port
|
|
2013
2018
|
self.listen_ip_addr: Final = listen_ip_addr
|
|
2014
|
-
self.
|
|
2019
|
+
self.listen_port_xml_rpc: Final = listen_port_xml_rpc
|
|
2015
2020
|
self.max_read_workers = max_read_workers
|
|
2016
2021
|
self.name: Final = name
|
|
2017
2022
|
self.password: Final = password
|
|
@@ -2028,9 +2033,9 @@ class CentralConfig:
|
|
|
2028
2033
|
self.verify_tls: Final = verify_tls
|
|
2029
2034
|
|
|
2030
2035
|
@property
|
|
2031
|
-
def
|
|
2036
|
+
def enable_xml_rpc_server(self) -> bool:
|
|
2032
2037
|
"""Return if server and connection checker should be started."""
|
|
2033
|
-
return self.start_direct is False
|
|
2038
|
+
return self.requires_xml_rpc_server and self.start_direct is False
|
|
2034
2039
|
|
|
2035
2040
|
@property
|
|
2036
2041
|
def load_un_ignore(self) -> bool:
|
|
@@ -2065,7 +2070,7 @@ class CentralConfig:
|
|
|
2065
2070
|
password=self.password,
|
|
2066
2071
|
storage_folder=self.storage_folder,
|
|
2067
2072
|
callback_host=self.callback_host,
|
|
2068
|
-
|
|
2073
|
+
callback_port_xml_rpc=self.callback_port_xml_rpc,
|
|
2069
2074
|
json_port=self.json_port,
|
|
2070
2075
|
interface_configs=self._interface_configs,
|
|
2071
2076
|
):
|
|
@@ -2090,9 +2095,9 @@ class CentralConfig:
|
|
|
2090
2095
|
url = f"{url}:{self.json_port}"
|
|
2091
2096
|
return f"{url}"
|
|
2092
2097
|
|
|
2093
|
-
def create_json_rpc_client(self, *, central: CentralUnit) ->
|
|
2098
|
+
def create_json_rpc_client(self, *, central: CentralUnit) -> AioJsonRpcAioHttpClient:
|
|
2094
2099
|
"""Create a json rpc client."""
|
|
2095
|
-
return
|
|
2100
|
+
return AioJsonRpcAioHttpClient(
|
|
2096
2101
|
username=self.username,
|
|
2097
2102
|
password=self.password,
|
|
2098
2103
|
device_url=central.url,
|
|
@@ -2109,38 +2114,38 @@ class CentralConnectionState:
|
|
|
2109
2114
|
def __init__(self) -> None:
|
|
2110
2115
|
"""Init the CentralConnectionStatus."""
|
|
2111
2116
|
self._json_issues: Final[list[str]] = []
|
|
2112
|
-
self.
|
|
2117
|
+
self._rpc_proxy_issues: Final[list[str]] = []
|
|
2113
2118
|
|
|
2114
2119
|
def add_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
|
|
2115
2120
|
"""Add issue to collection."""
|
|
2116
|
-
if isinstance(issuer,
|
|
2121
|
+
if isinstance(issuer, AioJsonRpcAioHttpClient) and iid not in self._json_issues:
|
|
2117
2122
|
self._json_issues.append(iid)
|
|
2118
2123
|
_LOGGER.debug("add_issue: add issue [%s] for JsonRpcAioHttpClient", iid)
|
|
2119
2124
|
return True
|
|
2120
|
-
if isinstance(issuer,
|
|
2121
|
-
self.
|
|
2125
|
+
if isinstance(issuer, AioXmlRpcProxy) and iid not in self._rpc_proxy_issues:
|
|
2126
|
+
self._rpc_proxy_issues.append(iid)
|
|
2122
2127
|
_LOGGER.debug("add_issue: add issue [%s] for %s", iid, issuer.interface_id)
|
|
2123
2128
|
return True
|
|
2124
2129
|
return False
|
|
2125
2130
|
|
|
2126
2131
|
def remove_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
|
|
2127
2132
|
"""Add issue to collection."""
|
|
2128
|
-
if isinstance(issuer,
|
|
2133
|
+
if isinstance(issuer, AioJsonRpcAioHttpClient) and iid in self._json_issues:
|
|
2129
2134
|
self._json_issues.remove(iid)
|
|
2130
2135
|
_LOGGER.debug("remove_issue: removing issue [%s] for JsonRpcAioHttpClient", iid)
|
|
2131
2136
|
return True
|
|
2132
|
-
if isinstance(issuer,
|
|
2133
|
-
self.
|
|
2137
|
+
if isinstance(issuer, AioXmlRpcProxy) and issuer.interface_id in self._rpc_proxy_issues:
|
|
2138
|
+
self._rpc_proxy_issues.remove(iid)
|
|
2134
2139
|
_LOGGER.debug("remove_issue: removing issue [%s] for %s", iid, issuer.interface_id)
|
|
2135
2140
|
return True
|
|
2136
2141
|
return False
|
|
2137
2142
|
|
|
2138
2143
|
def has_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
|
|
2139
2144
|
"""Add issue to collection."""
|
|
2140
|
-
if isinstance(issuer,
|
|
2145
|
+
if isinstance(issuer, AioJsonRpcAioHttpClient):
|
|
2141
2146
|
return iid in self._json_issues
|
|
2142
|
-
if isinstance(issuer,
|
|
2143
|
-
return iid in self.
|
|
2147
|
+
if isinstance(issuer, (AioXmlRpcProxy)):
|
|
2148
|
+
return iid in self._rpc_proxy_issues
|
|
2144
2149
|
|
|
2145
2150
|
def handle_exception_log(
|
|
2146
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(
|
|
@@ -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,8 +171,8 @@ 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
178
|
_instances: Final[dict[tuple[str, int], XmlRpcServer]] = {}
|
|
@@ -190,46 +190,30 @@ 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
|
-
|
|
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)
|
|
193
|
+
threading.Thread.__init__(self, name=f"RpcServer {ip_addr}:{self._listen_port}")
|
|
204
194
|
self._centrals: Final[dict[str, hmcu.CentralUnit]] = {}
|
|
205
|
-
|
|
206
|
-
def __new__(cls, ip_addr: str, port: int) -> XmlRpcServer: # noqa: PYI034 # kwonly: disable
|
|
207
|
-
"""Create new XmlRPC server."""
|
|
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
|
|
195
|
+
self._simple_rpc_server: SimpleXMLRPCServer
|
|
212
196
|
|
|
213
197
|
def run(self) -> None:
|
|
214
|
-
"""Run the
|
|
198
|
+
"""Run the RPC-Server thread."""
|
|
215
199
|
_LOGGER.debug(
|
|
216
|
-
"RUN: Starting
|
|
200
|
+
"RUN: Starting RPC-Server listening on %s:%i",
|
|
217
201
|
self._listen_ip_addr,
|
|
218
202
|
self._listen_port,
|
|
219
203
|
)
|
|
220
|
-
if self.
|
|
221
|
-
self.
|
|
204
|
+
if self._simple_rpc_server:
|
|
205
|
+
self._simple_rpc_server.serve_forever()
|
|
222
206
|
|
|
223
207
|
def stop(self) -> None:
|
|
224
|
-
"""Stop the
|
|
225
|
-
_LOGGER.debug("STOP: Shutting down
|
|
226
|
-
self.
|
|
227
|
-
_LOGGER.debug("STOP: Stopping
|
|
228
|
-
self.
|
|
208
|
+
"""Stop the RPC-Server."""
|
|
209
|
+
_LOGGER.debug("STOP: Shutting down RPC-Server")
|
|
210
|
+
self._simple_rpc_server.shutdown()
|
|
211
|
+
_LOGGER.debug("STOP: Stopping RPC-Server")
|
|
212
|
+
self._simple_rpc_server.server_close()
|
|
229
213
|
# Ensure the server thread has actually terminated to avoid slow teardown
|
|
230
214
|
with contextlib.suppress(RuntimeError):
|
|
231
215
|
self.join(timeout=1.0)
|
|
232
|
-
_LOGGER.debug("STOP:
|
|
216
|
+
_LOGGER.debug("STOP: RPC-Server stopped")
|
|
233
217
|
if self._address in self._instances:
|
|
234
218
|
del self._instances[self._address]
|
|
235
219
|
|
|
@@ -249,12 +233,12 @@ class XmlRpcServer(threading.Thread):
|
|
|
249
233
|
return self._started.is_set() is True # type: ignore[attr-defined]
|
|
250
234
|
|
|
251
235
|
def add_central(self, *, central: hmcu.CentralUnit) -> None:
|
|
252
|
-
"""Register a central in the
|
|
236
|
+
"""Register a central in the RPC-Server."""
|
|
253
237
|
if not self._centrals.get(central.name):
|
|
254
238
|
self._centrals[central.name] = central
|
|
255
239
|
|
|
256
240
|
def remove_central(self, *, central: hmcu.CentralUnit) -> None:
|
|
257
|
-
"""Unregister a central from
|
|
241
|
+
"""Unregister a central from RPC-Server."""
|
|
258
242
|
if self._centrals.get(central.name):
|
|
259
243
|
del self._centrals[central.name]
|
|
260
244
|
|
|
@@ -271,14 +255,45 @@ class XmlRpcServer(threading.Thread):
|
|
|
271
255
|
return len(self._centrals) == 0
|
|
272
256
|
|
|
273
257
|
|
|
258
|
+
class XmlRpcServer(RpcServer):
|
|
259
|
+
"""XML-RPC server thread to handle messages from the backend."""
|
|
260
|
+
|
|
261
|
+
def __init__(
|
|
262
|
+
self,
|
|
263
|
+
*,
|
|
264
|
+
ip_addr: str,
|
|
265
|
+
port: int,
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Init XmlRPC server."""
|
|
268
|
+
|
|
269
|
+
super().__init__(ip_addr=ip_addr, port=port)
|
|
270
|
+
self._instances[self._address] = self
|
|
271
|
+
self._simple_rpc_server = HomematicXMLRPCServer(
|
|
272
|
+
addr=self._address,
|
|
273
|
+
requestHandler=RequestHandler,
|
|
274
|
+
logRequests=False,
|
|
275
|
+
allow_none=True,
|
|
276
|
+
)
|
|
277
|
+
self._simple_rpc_server.register_introspection_functions()
|
|
278
|
+
self._simple_rpc_server.register_multicall_functions()
|
|
279
|
+
self._simple_rpc_server.register_instance(RPCFunctions(rpc_server=self), allow_dotted_names=True)
|
|
280
|
+
|
|
281
|
+
def __new__(cls, ip_addr: str, port: int) -> XmlRpcServer: # noqa: PYI034 # kwonly: disable
|
|
282
|
+
"""Create new RPC server."""
|
|
283
|
+
if (rpc := cls._instances.get((ip_addr, port))) is None:
|
|
284
|
+
_LOGGER.debug("Creating XmlRpc server")
|
|
285
|
+
return super().__new__(cls)
|
|
286
|
+
return rpc
|
|
287
|
+
|
|
288
|
+
|
|
274
289
|
def create_xml_rpc_server(*, ip_addr: str = IP_ANY_V4, port: int = PORT_ANY) -> XmlRpcServer:
|
|
275
|
-
"""Register the
|
|
276
|
-
|
|
277
|
-
if not
|
|
278
|
-
|
|
290
|
+
"""Register the rpc server."""
|
|
291
|
+
rpc = XmlRpcServer(ip_addr=ip_addr, port=port)
|
|
292
|
+
if not rpc.started:
|
|
293
|
+
rpc.start()
|
|
279
294
|
_LOGGER.debug(
|
|
280
295
|
"CREATE_XML_RPC_SERVER: Starting XmlRPC-Server listening on %s:%i",
|
|
281
|
-
|
|
282
|
-
|
|
296
|
+
rpc.listen_ip_addr,
|
|
297
|
+
rpc.listen_port,
|
|
283
298
|
)
|
|
284
|
-
return
|
|
299
|
+
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.6"
|
|
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):
|
|
@@ -174,7 +174,7 @@ def extract_device_addresses_from_device_descriptions(
|
|
|
174
174
|
{
|
|
175
175
|
parent_address
|
|
176
176
|
for dev_desc in device_descriptions
|
|
177
|
-
if (parent_address := dev_desc
|
|
177
|
+
if (parent_address := dev_desc.get("PARENT")) and (is_device_address(address=parent_address))
|
|
178
178
|
}
|
|
179
179
|
)
|
|
180
180
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.6
|
|
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=7siFMot4SYI6EAdWTG-GTgsHsdYpue_0EA-29kyBEbk,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=V1H8RNs8ofDOXe_L0yK_GXA7N-39sp3NcywPWzHcJnQ,10899
|
|
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.6.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.6.dist-info/METADATA,sha256=IjdBD7NWGknN0DlQcM0K4gZeFEYAkC-iCnUOkRPu1tU,7603
|
|
76
|
+
aiohomematic-2025.10.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
77
|
+
aiohomematic-2025.10.6.dist-info/top_level.txt,sha256=5TDRlUWQPThIUwQjOj--aUo4UA-ow4m0sNhnoCBi5n8,34
|
|
78
|
+
aiohomematic-2025.10.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|