aiohomematic 2025.10.9__py3-none-any.whl → 2025.10.11__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.

@@ -96,8 +96,10 @@ from aiohomematic.const import (
96
96
  DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS,
97
97
  DEFAULT_INTERFACES_REQUIRING_PERIODIC_REFRESH,
98
98
  DEFAULT_MAX_READ_WORKERS,
99
+ DEFAULT_OPTIONAL_SETTINGS,
99
100
  DEFAULT_PERIODIC_REFRESH_INTERVAL,
100
101
  DEFAULT_PROGRAM_MARKERS,
102
+ DEFAULT_SESSION_RECORDER_START_FOR_SECONDS,
101
103
  DEFAULT_STORAGE_DIRECTORY,
102
104
  DEFAULT_SYS_SCAN_INTERVAL,
103
105
  DEFAULT_SYSVAR_MARKERS,
@@ -130,6 +132,7 @@ from aiohomematic.const import (
130
132
  Interface,
131
133
  InterfaceEventType,
132
134
  Operations,
135
+ OptionalSettings,
133
136
  Parameter,
134
137
  ParamsetKey,
135
138
  ProxyInitState,
@@ -223,7 +226,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
223
226
  self._paramset_descriptions: Final = ParamsetDescriptionCache(central=self)
224
227
  self._parameter_visibility: Final = ParameterVisibilityCache(central=self)
225
228
  self._recorder: Final = SessionRecorder(
226
- central=self, default_ttl_seconds=600, active=central_config.start_recorder
229
+ central=self, ttl_seconds=600, active=central_config.session_recorder_start
227
230
  )
228
231
  self._primary_client: hmcl.Client | None = None
229
232
  # {interface_id, client}
@@ -493,14 +496,14 @@ class CentralUnit(LogContextMixin, PayloadMixin):
493
496
  _LOGGER.debug("START: Central %s already started", self.name)
494
497
  return
495
498
 
496
- if self._config.start_recorder:
499
+ if self._config.session_recorder_start:
497
500
  await self._recorder.deactivate(
498
- delay=self._config.start_recorder_for_minutes * 60,
501
+ delay=self._config.session_recorder_start_for_seconds,
499
502
  auto_save=True,
500
- randomize_output=True,
501
- use_ts_in_filename=False,
503
+ randomize_output=self._config.session_recorder_randomize_output,
504
+ use_ts_in_file_name=False,
502
505
  )
503
- _LOGGER.debug("START: Starting Recorder for %s minutes", self._config.start_recorder_for_minutes)
506
+ _LOGGER.debug("START: Starting Recorder for %s seconds", self._config.session_recorder_start_for_seconds)
504
507
 
505
508
  self._state = CentralUnitState.INITIALIZING
506
509
  _LOGGER.debug("START: Initializing Central %s", self.name)
@@ -2009,10 +2012,10 @@ class CentralConfig:
2009
2012
  listen_ip_addr: str | None = None,
2010
2013
  listen_port_xml_rpc: int | None = None,
2011
2014
  max_read_workers: int = DEFAULT_MAX_READ_WORKERS,
2015
+ optional_settings: tuple[OptionalSettings | str, ...] = DEFAULT_OPTIONAL_SETTINGS,
2012
2016
  periodic_refresh_interval: int = DEFAULT_PERIODIC_REFRESH_INTERVAL,
2013
2017
  program_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_PROGRAM_MARKERS,
2014
2018
  start_direct: bool = False,
2015
- start_recorder_for_minutes: int = 0,
2016
2019
  storage_directory: str = DEFAULT_STORAGE_DIRECTORY,
2017
2020
  sys_scan_interval: int = DEFAULT_SYS_SCAN_INTERVAL,
2018
2021
  sysvar_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_SYSVAR_MARKERS,
@@ -2023,6 +2026,7 @@ class CentralConfig:
2023
2026
  ) -> None:
2024
2027
  """Init the client config."""
2025
2028
  self._interface_configs: Final = interface_configs
2029
+ self._optional_settings: Final = frozenset(optional_settings or ())
2026
2030
  self.requires_xml_rpc_server: Final = any(
2027
2031
  ic for ic in interface_configs if ic.rpc_server == RpcServerType.XML_RPC
2028
2032
  )
@@ -2048,8 +2052,15 @@ class CentralConfig:
2048
2052
  self.periodic_refresh_interval = periodic_refresh_interval
2049
2053
  self.program_markers: Final = program_markers
2050
2054
  self.start_direct: Final = start_direct
2051
- self.start_recorder_for_minutes: Final = start_recorder_for_minutes
2052
- self.start_recorder = start_recorder_for_minutes > 0
2055
+ self.session_recorder_randomize_output = (
2056
+ OptionalSettings.SR_DISABLE_RANDOMIZE_OUTPUT not in self._optional_settings
2057
+ )
2058
+ self.session_recorder_start_for_seconds: Final = (
2059
+ DEFAULT_SESSION_RECORDER_START_FOR_SECONDS
2060
+ if OptionalSettings.SR_RECORD_SYSTEM_INIT in self._optional_settings
2061
+ else 0
2062
+ )
2063
+ self.session_recorder_start = self.session_recorder_start_for_seconds > 0
2053
2064
  self.storage_directory: Final = storage_directory
2054
2065
  self.sys_scan_interval: Final = sys_scan_interval
2055
2066
  self.sysvar_markers: Final = sysvar_markers
@@ -18,7 +18,7 @@ from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
18
18
  from aiohomematic import central as hmcu
19
19
  from aiohomematic.central.decorators import callback_backend_system
20
20
  from aiohomematic.const import IP_ANY_V4, PORT_ANY, BackendSystemEvent
21
- from aiohomematic.support import find_free_port, log_boundary_error
21
+ from aiohomematic.support import log_boundary_error
22
22
 
23
23
  _LOGGER: Final = logging.getLogger(__name__)
24
24
 
@@ -177,23 +177,19 @@ class RpcServer(threading.Thread):
177
177
  _initialized: bool = False
178
178
  _instances: Final[dict[tuple[str, int], RpcServer]] = {}
179
179
 
180
- def __init__(
181
- self,
182
- *,
183
- ip_addr: str,
184
- port: int,
185
- ) -> None:
180
+ def __init__(self, *, server: SimpleXMLRPCServer) -> None:
186
181
  """Init XmlRPC server."""
187
- if self._initialized:
188
- return
182
+ self._server = server
183
+ self._server.register_introspection_functions()
184
+ self._server.register_multicall_functions()
185
+ self._server.register_instance(RPCFunctions(rpc_server=self), allow_dotted_names=True)
189
186
  self._initialized = True
190
- self._listen_ip_addr: Final = ip_addr
191
- self._listen_port: Final[int] = find_free_port() if port == PORT_ANY else port
192
- self._address: Final[tuple[str, int]] = (ip_addr, self._listen_port)
187
+ self._address: Final[tuple[str, int]] = cast(tuple[str, int], server.server_address)
188
+ self._listen_ip_addr: Final = self._address[0]
189
+ self._listen_port: Final = self._address[1]
193
190
  self._centrals: Final[dict[str, hmcu.CentralUnit]] = {}
194
- self._simple_rpc_server: SimpleXMLRPCServer
195
191
  self._instances[self._address] = self
196
- threading.Thread.__init__(self, name=f"RpcServer {ip_addr}:{self._listen_port}")
192
+ threading.Thread.__init__(self, name=f"RpcServer {self._listen_ip_addr}:{self._listen_port}")
197
193
 
198
194
  def run(self) -> None:
199
195
  """Run the RPC-Server thread."""
@@ -202,15 +198,15 @@ class RpcServer(threading.Thread):
202
198
  self._listen_ip_addr,
203
199
  self._listen_port,
204
200
  )
205
- if self._simple_rpc_server:
206
- self._simple_rpc_server.serve_forever()
201
+ if self._server:
202
+ self._server.serve_forever()
207
203
 
208
204
  def stop(self) -> None:
209
205
  """Stop the RPC-Server."""
210
206
  _LOGGER.debug("STOP: Shutting down RPC-Server")
211
- self._simple_rpc_server.shutdown()
207
+ self._server.shutdown()
212
208
  _LOGGER.debug("STOP: Stopping RPC-Server")
213
- self._simple_rpc_server.server_close()
209
+ self._server.server_close()
214
210
  # Ensure the server thread has actually terminated to avoid slow teardown
215
211
  with contextlib.suppress(RuntimeError):
216
212
  self.join(timeout=1.0)
@@ -269,16 +265,14 @@ class XmlRpcServer(RpcServer):
269
265
 
270
266
  if self._initialized:
271
267
  return
272
- super().__init__(ip_addr=ip_addr, port=port)
273
- self._simple_rpc_server = HomematicXMLRPCServer(
274
- addr=self._address,
275
- requestHandler=RequestHandler,
276
- logRequests=False,
277
- allow_none=True,
268
+ super().__init__(
269
+ server=HomematicXMLRPCServer(
270
+ addr=(ip_addr, port),
271
+ requestHandler=RequestHandler,
272
+ logRequests=False,
273
+ allow_none=True,
274
+ )
278
275
  )
279
- self._simple_rpc_server.register_introspection_functions()
280
- self._simple_rpc_server.register_multicall_functions()
281
- self._simple_rpc_server.register_instance(RPCFunctions(rpc_server=self), allow_dotted_names=True)
282
276
 
283
277
  def __new__(cls, ip_addr: str, port: int) -> XmlRpcServer: # noqa: PYI034 # kwonly: disable
284
278
  """Create new RPC server."""
@@ -60,7 +60,6 @@ from aiohomematic.client.rpc_proxy import AioXmlRpcProxy, BaseRpcProxy
60
60
  from aiohomematic.const import (
61
61
  CALLBACK_WARN_INTERVAL,
62
62
  DATETIME_FORMAT_MILLIS,
63
- DEFAULT_CUSTOM_ID,
64
63
  DEFAULT_MAX_WORKERS,
65
64
  DP_KEY_VALUE,
66
65
  DUMMY_SERIAL,
@@ -81,6 +80,7 @@ from aiohomematic.const import (
81
80
  ForcedDeviceAvailability,
82
81
  Interface,
83
82
  InterfaceEventType,
83
+ InternalCustomID,
84
84
  Operations,
85
85
  ParameterData,
86
86
  ParameterType,
@@ -1815,7 +1815,9 @@ async def _track_single_data_point_state_change_or_timeout(
1815
1815
  )
1816
1816
  return
1817
1817
  if (
1818
- unsub := dp.register_data_point_updated_callback(cb=_async_event_changed, custom_id=DEFAULT_CUSTOM_ID)
1818
+ unsub := dp.register_data_point_updated_callback(
1819
+ cb=_async_event_changed, custom_id=InternalCustomID.DEFAULT
1820
+ )
1819
1821
  ) is None:
1820
1822
  return
1821
1823
 
@@ -368,8 +368,8 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
368
368
  _LOGGER.debug("POST_SCRIPT: method: %s [%s]", method, script_name)
369
369
 
370
370
  try:
371
- if not response[_JsonKey.ERROR]:
372
- response[_JsonKey.RESULT] = orjson.loads(response[_JsonKey.RESULT])
371
+ if not response[_JsonKey.ERROR] and (resp := response[_JsonKey.RESULT]) and isinstance(resp, str):
372
+ response[_JsonKey.RESULT] = orjson.loads(resp)
373
373
  finally:
374
374
  if not keep_session:
375
375
  await self._do_logout(session_id=session_id)
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.9"
22
+ VERSION: Final = "2025.10.11"
23
23
 
24
24
  # Detect test speedup mode via environment
25
25
  _TEST_SPEEDUP: Final = (
@@ -27,8 +27,6 @@ _TEST_SPEEDUP: Final = (
27
27
  )
28
28
 
29
29
  # default
30
- DEFAULT_STORAGE_DIRECTORY: Final = "aiohomematic_storage"
31
- DEFAULT_CUSTOM_ID: Final = "custom_id"
32
30
  DEFAULT_DELAY_NEW_DEVICE_CREATION: Final = False
33
31
  DEFAULT_ENABLE_DEVICE_FIRMWARE_CHECK: Final = False
34
32
  DEFAULT_ENABLE_PROGRAM_SCAN: Final = True
@@ -40,8 +38,11 @@ DEFAULT_INCLUDE_INTERNAL_SYSVARS: Final = True
40
38
  DEFAULT_MAX_READ_WORKERS: Final = 1
41
39
  DEFAULT_MAX_WORKERS: Final = 1
42
40
  DEFAULT_MULTIPLIER: Final = 1.0
41
+ DEFAULT_OPTIONAL_SETTINGS: Final[tuple[OptionalSettings | str, ...]] = ()
43
42
  DEFAULT_PERIODIC_REFRESH_INTERVAL: Final = 15
44
43
  DEFAULT_PROGRAM_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
44
+ DEFAULT_SESSION_RECORDER_START_FOR_SECONDS: Final = 180
45
+ DEFAULT_STORAGE_DIRECTORY: Final = "aiohomematic_storage"
45
46
  DEFAULT_SYSVAR_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
46
47
  DEFAULT_SYS_SCAN_INTERVAL: Final = 30
47
48
  DEFAULT_TLS: Final = False
@@ -49,12 +50,6 @@ DEFAULT_UN_IGNORES: Final[frozenset[str]] = frozenset()
49
50
  DEFAULT_USE_GROUP_CHANNEL_FOR_COVER_STATE: Final = True
50
51
  DEFAULT_VERIFY_TLS: Final = False
51
52
 
52
- MANU_TEMP_CUSTOM_ID: Final = "manu_temp"
53
- INTERNAL_CUSTOM_IDS: Final[tuple[str, ...]] = (
54
- DEFAULT_CUSTOM_ID,
55
- MANU_TEMP_CUSTOM_ID,
56
- )
57
-
58
53
  # Default encoding for json service calls, persistent cache
59
54
  UTF_8: Final = "utf-8"
60
55
  # Default encoding for xmlrpc service calls and script files
@@ -214,6 +209,13 @@ class CommandRxMode(StrEnum):
214
209
  WAKEUP = "WAKEUP"
215
210
 
216
211
 
212
+ class InternalCustomID(StrEnum):
213
+ """Enum for Homematic internal custom IDs."""
214
+
215
+ DEFAULT = "cid_default"
216
+ MANU_TEMP = "cid_manu_temp"
217
+
218
+
217
219
  class DataOperationResult(Enum):
218
220
  """Enum with data operation results."""
219
221
 
@@ -352,6 +354,13 @@ class Operations(IntEnum):
352
354
  EVENT = 4
353
355
 
354
356
 
357
+ class OptionalSettings(StrEnum):
358
+ """Enum with aiohomematic optional settings."""
359
+
360
+ SR_DISABLE_RANDOMIZE_OUTPUT = "SR_DISABLE_RANDOMIZED_OUTPUT"
361
+ SR_RECORD_SYSTEM_INIT = "SR_RECORD_SYSTEM_INIT"
362
+
363
+
355
364
  class Parameter(StrEnum):
356
365
  """Enum with Homematic parameters."""
357
366
 
@@ -12,10 +12,10 @@ import logging
12
12
  from typing import Any, Final, cast
13
13
 
14
14
  from aiohomematic.const import (
15
- MANU_TEMP_CUSTOM_ID,
16
15
  SCHEDULER_PROFILE_PATTERN,
17
16
  SCHEDULER_TIME_PATTERN,
18
17
  DataPointCategory,
18
+ InternalCustomID,
19
19
  Parameter,
20
20
  ParamsetKey,
21
21
  ProductGroup,
@@ -235,7 +235,7 @@ class BaseCustomDpClimate(CustomDataPoint):
235
235
  )
236
236
  self._unregister_callbacks.append(
237
237
  self._dp_setpoint.register_data_point_updated_callback(
238
- cb=self._manu_temp_changed, custom_id=MANU_TEMP_CUSTOM_ID
238
+ cb=self._manu_temp_changed, custom_id=InternalCustomID.MANU_TEMP
239
239
  )
240
240
  )
241
241
 
@@ -802,7 +802,7 @@ class CustomDpRfThermostat(BaseCustomDpClimate):
802
802
 
803
803
  self._unregister_callbacks.append(
804
804
  self._dp_control_mode.register_data_point_updated_callback(
805
- cb=self._manu_temp_changed, custom_id=MANU_TEMP_CUSTOM_ID
805
+ cb=self._manu_temp_changed, custom_id=InternalCustomID.MANU_TEMP
806
806
  )
807
807
  )
808
808
 
@@ -1047,7 +1047,7 @@ class CustomDpIpThermostat(BaseCustomDpClimate):
1047
1047
 
1048
1048
  self._unregister_callbacks.append(
1049
1049
  self._dp_set_point_mode.register_data_point_updated_callback(
1050
- cb=self._manu_temp_changed, custom_id=MANU_TEMP_CUSTOM_ID
1050
+ cb=self._manu_temp_changed, custom_id=InternalCustomID.MANU_TEMP
1051
1051
  )
1052
1052
  )
1053
1053
 
@@ -39,11 +39,9 @@ from aiohomematic import central as hmcu, client as hmcl, support as hms, valida
39
39
  from aiohomematic.async_support import loop_check
40
40
  from aiohomematic.const import (
41
41
  CALLBACK_TYPE,
42
- DEFAULT_CUSTOM_ID,
43
42
  DEFAULT_MULTIPLIER,
44
43
  DP_KEY_VALUE,
45
44
  INIT_DATETIME,
46
- INTERNAL_CUSTOM_IDS,
47
45
  KEY_CHANNEL_OPERATION_MODE_VISIBILITY,
48
46
  KWARGS_ARG_CUSTOM_ID,
49
47
  KWARGS_ARG_DATA_POINT,
@@ -55,6 +53,7 @@ from aiohomematic.const import (
55
53
  DataPointUsage,
56
54
  EventKey,
57
55
  Flag,
56
+ InternalCustomID,
58
57
  Operations,
59
58
  Parameter,
60
59
  ParameterData,
@@ -309,11 +308,11 @@ class CallbackDataPoint(ABC, LogContextMixin):
309
308
 
310
309
  def register_internal_data_point_updated_callback(self, *, cb: Callable) -> CALLBACK_TYPE:
311
310
  """Register internal data_point updated callback."""
312
- return self.register_data_point_updated_callback(cb=cb, custom_id=DEFAULT_CUSTOM_ID)
311
+ return self.register_data_point_updated_callback(cb=cb, custom_id=InternalCustomID.DEFAULT)
313
312
 
314
313
  def register_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> CALLBACK_TYPE:
315
314
  """Register data_point updated callback."""
316
- if custom_id not in INTERNAL_CUSTOM_IDS:
315
+ if custom_id not in InternalCustomID:
317
316
  if self._custom_id is not None and self._custom_id != custom_id:
318
317
  raise AioHomematicException(
319
318
  f"REGISTER_data_point_updated_CALLBACK failed: hm_data_point: {self.full_name} is already registered by {self._custom_id}"
@@ -1293,7 +1293,7 @@ class _DefinitionExporter:
1293
1293
  str, dict[ParamsetKey, dict[str, ParameterData]]
1294
1294
  ] = await self._client.get_all_paramset_descriptions(device_descriptions=tuple(device_descriptions.values()))
1295
1295
  model = device_descriptions[self._device_address]["TYPE"]
1296
- filename = f"{model}.json"
1296
+ file_name = f"{model}.json"
1297
1297
 
1298
1298
  # anonymize device_descriptions
1299
1299
  anonymize_device_descriptions: list[DeviceDescription] = []
@@ -1316,14 +1316,14 @@ class _DefinitionExporter:
1316
1316
  # Save device_descriptions for device to file.
1317
1317
  await self._save(
1318
1318
  directory=f"{self._storage_directory}/{DEVICE_DESCRIPTIONS_DIR}",
1319
- filename=filename,
1319
+ file_name=file_name,
1320
1320
  data=anonymize_device_descriptions,
1321
1321
  )
1322
1322
 
1323
1323
  # Save device_descriptions for device to file.
1324
1324
  await self._save(
1325
1325
  directory=f"{self._storage_directory}/{PARAMSET_DESCRIPTIONS_DIR}",
1326
- filename=filename,
1326
+ file_name=file_name,
1327
1327
  data=anonymize_paramset_descriptions,
1328
1328
  )
1329
1329
 
@@ -1332,13 +1332,13 @@ class _DefinitionExporter:
1332
1332
  address_parts[0] = self._random_id
1333
1333
  return ADDRESS_SEPARATOR.join(address_parts)
1334
1334
 
1335
- async def _save(self, *, directory: str, filename: str, data: Any) -> DataOperationResult:
1335
+ async def _save(self, *, directory: str, file_name: str, data: Any) -> DataOperationResult:
1336
1336
  """Save file to disk."""
1337
1337
 
1338
1338
  def perform_save() -> DataOperationResult:
1339
1339
  if not check_or_create_directory(directory=directory):
1340
1340
  return DataOperationResult.NO_SAVE # pragma: no cover
1341
- with open(file=os.path.join(directory, filename), mode="wb") as fptr:
1341
+ with open(file=os.path.join(directory, file_name), mode="wb") as fptr:
1342
1342
  fptr.write(orjson.dumps(data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS))
1343
1343
  return DataOperationResult.SAVE_SUCCESS
1344
1344
 
@@ -11,11 +11,11 @@ from typing import Final
11
11
 
12
12
  from aiohomematic.const import (
13
13
  CALLBACK_TYPE,
14
- DEFAULT_CUSTOM_ID,
15
14
  HMIP_FIRMWARE_UPDATE_IN_PROGRESS_STATES,
16
15
  HMIP_FIRMWARE_UPDATE_READY_STATES,
17
16
  DataPointCategory,
18
17
  Interface,
18
+ InternalCustomID,
19
19
  )
20
20
  from aiohomematic.decorators import inspector
21
21
  from aiohomematic.exceptions import AioHomematicException
@@ -114,7 +114,7 @@ class DpUpdate(CallbackDataPoint, PayloadMixin):
114
114
 
115
115
  def register_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> CALLBACK_TYPE:
116
116
  """Register update callback."""
117
- if custom_id != DEFAULT_CUSTOM_ID:
117
+ if custom_id != InternalCustomID.DEFAULT:
118
118
  if self._custom_id is not None:
119
119
  raise AioHomematicException(
120
120
  f"REGISTER_UPDATE_CALLBACK failed: hm_data_point: {self.full_name} is already registered by {self._custom_id}"