aiohomematic 2025.10.5__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.

@@ -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", # CuXD
175
- "CMD_RETS", # CuXD
174
+ "CMD_RETL", # CUxD
175
+ "CMD_RETS", # CUxD
176
176
  "CONTROL_DIFFERENTIAL_TEMPERATURE",
177
177
  "DATE_TIME_UNKNOWN",
178
178
  "DECISION_VALUE",
@@ -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 xml_rpc_server as xmlrpc
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 JsonRpcAioHttpClient
88
- from aiohomematic.client.xml_rpc import XmlRpcProxy
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 = JsonRpcAioHttpClient | XmlRpcProxy
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: xmlrpc.XmlRpcServer | None = None
212
- self._json_rpc_client: JsonRpcAioHttpClient | None = None
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._xml_rpc_callback_ip: str = IP_ANY_V4
252
+ self._rpc_callback_ip: str = IP_ANY_V4
252
253
  self._listen_ip_addr: str = IP_ANY_V4
253
- self._listen_port: int = PORT_ANY
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._xml_rpc_callback_ip
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) -> JsonRpcAioHttpClient:
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 listen_port(self) -> int:
367
+ def listen_port_xml_rpc(self) -> int:
367
368
  """Return the xml rpc listening server port."""
368
- return self._listen_port
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._xml_rpc_callback_ip = ip_addr
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
- listen_port: int = (
490
- self._config.listen_port
491
- if self._config.listen_port
492
- else self._config.callback_port or self._config.default_callback_port
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 := xmlrpc.create_xml_rpc_server(ip_addr=self._listen_ip_addr, port=listen_port)
497
- if self._config.enable_server
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._listen_port = xml_rpc_server.listen_port
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.enable_server:
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.supports_xml_rpc
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
- callback_port: int | None = None,
1975
- default_callback_port: int = PORT_ANY,
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] = INTERFACES_REQUIRING_PERIODIC_REFRESH,
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
- listen_port: int | None = None,
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.callback_port: Final = callback_port
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.default_callback_port: Final = default_callback_port
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.listen_port: Final = listen_port
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 enable_server(self) -> bool:
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
- callback_port=self.callback_port,
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) -> JsonRpcAioHttpClient:
2098
+ def create_json_rpc_client(self, *, central: CentralUnit) -> AioJsonRpcAioHttpClient:
2095
2099
  """Create a json rpc client."""
2096
- return JsonRpcAioHttpClient(
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._xml_proxy_issues: Final[list[str]] = []
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, JsonRpcAioHttpClient) and iid not in self._json_issues:
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, XmlRpcProxy) and iid not in self._xml_proxy_issues:
2122
- self._xml_proxy_issues.append(iid)
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, JsonRpcAioHttpClient) and iid in self._json_issues:
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, XmlRpcProxy) and issuer.interface_id in self._xml_proxy_issues:
2134
- self._xml_proxy_issues.remove(iid)
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, JsonRpcAioHttpClient):
2145
+ if isinstance(issuer, AioJsonRpcAioHttpClient):
2142
2146
  return iid in self._json_issues
2143
- if isinstance(issuer, XmlRpcProxy):
2144
- return iid in self._xml_proxy_issues
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 xml_rpc_server as xmlrpc
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, xmlrpc.RPCFunctions):
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 XML-RPC functions the backend will expect."""
28
+ """The RPC functions the backend will expect."""
29
29
 
30
- # Disable kw-only linter for this class since XML-RPC signatures are positional by protocol
30
+ # Disable kw-only linter
31
31
  __kwonly_check__ = False
32
32
 
33
- def __init__(self, *, xml_rpc_server: XmlRpcServer) -> None:
33
+ def __init__(self, *, rpc_server: RpcServer) -> None:
34
34
  """Init RPCFunctions."""
35
- self._xml_rpc_server: Final = xml_rpc_server
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). XML-RPC server received error notification.
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="xml-rpc-server",
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._xml_rpc_server.get_central(interface_id=interface_id)
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 AioHomematicXMLRPCServer(SimpleXMLRPCServer):
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 XmlRpcServer(threading.Thread):
175
- """XML-RPC server thread to handle messages from the backend."""
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
- 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)
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 XmlRPC-Server thread."""
198
+ """Run the RPC-Server thread."""
215
199
  _LOGGER.debug(
216
- "RUN: Starting XmlRPC-Server listening on http://%s:%i",
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._simple_xml_rpc_server:
221
- self._simple_xml_rpc_server.serve_forever()
204
+ if self._simple_rpc_server:
205
+ self._simple_rpc_server.serve_forever()
222
206
 
223
207
  def stop(self) -> None:
224
- """Stop the XmlRPC-Server."""
225
- _LOGGER.debug("STOP: Shutting down XmlRPC-Server")
226
- self._simple_xml_rpc_server.shutdown()
227
- _LOGGER.debug("STOP: Stopping XmlRPC-Server")
228
- self._simple_xml_rpc_server.server_close()
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: XmlRPC-Server stopped")
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 XmlRPC-Server."""
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 XmlRPC-Server."""
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 xml rpc server."""
276
- xml_rpc = XmlRpcServer(ip_addr=ip_addr, port=port)
277
- if not xml_rpc.started:
278
- xml_rpc.start()
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
- xml_rpc.listen_ip_addr,
282
- xml_rpc.listen_port,
296
+ rpc.listen_ip_addr,
297
+ rpc.listen_port,
283
298
  )
284
- return xml_rpc
299
+ return rpc
@@ -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.xml_rpc import XmlRpcProxy
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
- INTERFACES_SUPPORTING_XML_RPC,
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: XmlRpcProxy
145
- self._proxy_read: XmlRpcProxy
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
- self._proxy = await self._config.create_xml_rpc_proxy(auth_enabled=self.system_information.auth_enabled)
154
- self._proxy_read = await self._config.create_xml_rpc_proxy(
155
- auth_enabled=self.system_information.auth_enabled,
156
- max_workers=self._config.max_read_workers,
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 supports_xml_rpc(self) -> bool:
201
- """Return if interface support xml rpc."""
202
- return self._supports_xml_rpc
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.interface not in self.central.config.interfaces_requiring_periodic_refresh
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.interface in INTERFACES_SUPPORTING_FIRMWARE_UPDATES
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.supports_xml_rpc:
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.supports_xml_rpc:
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.supports_xml_rpc:
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, CuXD)."""
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
- # Funktioniert nicht
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.init_url: Final[str] = f"http://{
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
- }:{central.config.callback_port if central.config.callback_port else central.listen_port}"
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 (Interface.CCU_JACK, Interface.CUXD):
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 (Interface.CCU_JACK, Interface.CUXD):
1656
+ if self.interface in INTERFACES_REQUIRING_JSON_RPC_CLIENT:
1628
1657
  return "0"
1629
- check_proxy = await self._create_simple_xml_rpc_proxy()
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 create_xml_rpc_proxy(
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
- ) -> XmlRpcProxy:
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 = XmlRpcProxy(
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 _create_simple_xml_rpc_proxy(self) -> XmlRpcProxy:
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.create_xml_rpc_proxy(auth_enabled=True, max_workers=0)
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 | None = None,
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 INTERFACES_SUPPORTING_XML_RPC:
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
@@ -179,7 +179,7 @@ _PARALLEL_EXECUTION_LIMITED_JSONRPC_METHODS: Final = (
179
179
  )
180
180
 
181
181
 
182
- class JsonRpcAioHttpClient(LogContextMixin):
182
+ class AioJsonRpcAioHttpClient(LogContextMixin):
183
183
  """Connection to CCU JSON-RPC Server."""
184
184
 
185
185
  def __init__(
@@ -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 _XmlRpcMethod(StrEnum):
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
- _VALID_XMLRPC_COMMANDS_ON_NO_CONNECTION: Final[tuple[str, ...]] = (
65
- _XmlRpcMethod.GET_VERSION,
66
- _XmlRpcMethod.HOMEGEAR_INIT,
67
- _XmlRpcMethod.INIT,
68
- _XmlRpcMethod.PING,
69
- _XmlRpcMethod.SYSTEM_LIST_METHODS,
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 XmlRpcProxy(xmlrpc.client.ServerProxy):
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
- uri: str,
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
- kwargs: dict[str, Any] = {}
111
- if self._tls:
112
- kwargs[_CONTEXT] = get_tls_context(verify_tls=self._verify_tls)
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": self._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 xml rpc proxy."""
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 __async_request(self, *args, **kwargs): # type: ignore[no-untyped-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 _VALID_XMLRPC_COMMANDS_ON_NO_CONNECTION or not self._connection_state.has_issue(
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 __getattr__(self, *args, **kwargs): # type: ignore[no-untyped-def]
216
- """Magic method dispatcher."""
217
- return xmlrpc.client._Method(self.__async_request, *args, **kwargs)
218
-
219
- async def stop(self) -> None:
220
- """Stop depending services."""
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.5"
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
- INTERFACES_SUPPORTING_XML_RPC: Final[frozenset[Interface]] = frozenset(
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
- INTERFACES_REQUIRING_PERIODIC_REFRESH: Final[frozenset[Interface]] = frozenset(
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
- callback_port: int | None,
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 callback_port and not is_port(port=callback_port):
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.5
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=d_9gr5SrIwInmN0UTV3RJZ76z-HcxlVIZ1OuirBck4Y,25840
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=PMiE_8MXDQBIqeJGw1GIS7O3jGY4sq3HT-u1MIyOG-M,22802
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=SThfEO3LKbIIERD4Novyj4ZZUkcYrBuvYdNQfPO29JQ,31676
17
- aiohomematic/central/__init__.py,sha256=Jh0CH_gj1zsBaYYBRm9Ue1-f8qf2TV62jcMWuYoX-xI,92241
18
- aiohomematic/central/decorators.py,sha256=v0gCa5QEQPNEvGy0O2YIOhoJy7OKiJZJWtK64OQm7y4,6918
19
- aiohomematic/central/xml_rpc_server.py,sha256=1Vr8BXakLSo-RC967IlRI9LPUCl4iXp2y-AXSZuDyPc,10777
20
- aiohomematic/client/__init__.py,sha256=KcqPEsqYF6L5RoW3LoKd83GqyM2F8W6LkA_cBG_sLiw,71519
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=DSTmKB7YdxVmOH5b7HKoN_H-fI837n0CpyPdJyurans,50174
23
- aiohomematic/client/xml_rpc.py,sha256=9PEOWoTG0EMUAMyqiInF4iA_AduHv_hhjJW3YhYjYqU,9614
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=VKknWPJOtSwIzV-Ag_8Zg1evtkyjKh768Be_quU_R54,6885
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.5.dist-info/licenses/LICENSE,sha256=q-B0xpREuZuvKsmk3_iyVZqvZ-vJcWmzMZpeAd0RqtQ,1083
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.5.dist-info/METADATA,sha256=8x8jCa_ZHMKfTyNBstzj2oqgTvdjgcS_v9E74Urd5aQ,7603
76
- aiohomematic-2025.10.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
- aiohomematic-2025.10.5.dist-info/top_level.txt,sha256=5TDRlUWQPThIUwQjOj--aUo4UA-ow4m0sNhnoCBi5n8,34
78
- aiohomematic-2025.10.5.dist-info/RECORD,,
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,,