aiohomematic 2025.10.1__py3-none-any.whl → 2025.10.3__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/async_support.py +8 -8
- aiohomematic/caches/dynamic.py +31 -26
- aiohomematic/caches/persistent.py +34 -32
- aiohomematic/caches/visibility.py +19 -7
- aiohomematic/central/__init__.py +88 -75
- aiohomematic/central/decorators.py +2 -2
- aiohomematic/central/xml_rpc_server.py +33 -25
- aiohomematic/client/__init__.py +72 -56
- aiohomematic/client/_rpc_errors.py +3 -3
- aiohomematic/client/json_rpc.py +33 -25
- aiohomematic/client/xml_rpc.py +14 -9
- aiohomematic/const.py +2 -1
- aiohomematic/converter.py +19 -19
- aiohomematic/exceptions.py +2 -1
- aiohomematic/model/__init__.py +4 -3
- aiohomematic/model/calculated/__init__.py +1 -1
- aiohomematic/model/calculated/climate.py +9 -9
- aiohomematic/model/calculated/data_point.py +13 -7
- aiohomematic/model/calculated/operating_voltage_level.py +2 -2
- aiohomematic/model/calculated/support.py +7 -7
- aiohomematic/model/custom/__init__.py +3 -3
- aiohomematic/model/custom/climate.py +57 -34
- aiohomematic/model/custom/cover.py +32 -18
- aiohomematic/model/custom/data_point.py +9 -7
- aiohomematic/model/custom/definition.py +23 -17
- aiohomematic/model/custom/light.py +52 -23
- aiohomematic/model/custom/lock.py +16 -12
- aiohomematic/model/custom/siren.py +8 -3
- aiohomematic/model/custom/switch.py +3 -2
- aiohomematic/model/custom/valve.py +3 -2
- aiohomematic/model/data_point.py +63 -49
- aiohomematic/model/device.py +48 -42
- aiohomematic/model/event.py +6 -5
- aiohomematic/model/generic/__init__.py +6 -4
- aiohomematic/model/generic/action.py +1 -1
- aiohomematic/model/generic/data_point.py +8 -6
- aiohomematic/model/generic/number.py +3 -3
- aiohomematic/model/generic/select.py +1 -1
- aiohomematic/model/generic/sensor.py +2 -2
- aiohomematic/model/generic/switch.py +3 -3
- aiohomematic/model/hub/__init__.py +17 -16
- aiohomematic/model/hub/data_point.py +12 -7
- aiohomematic/model/hub/number.py +3 -3
- aiohomematic/model/hub/select.py +3 -3
- aiohomematic/model/hub/text.py +2 -2
- aiohomematic/model/support.py +10 -9
- aiohomematic/model/update.py +6 -6
- aiohomematic/property_decorators.py +2 -0
- aiohomematic/support.py +44 -38
- aiohomematic/validator.py +6 -6
- {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.3.dist-info}/METADATA +1 -1
- aiohomematic-2025.10.3.dist-info/RECORD +78 -0
- aiohomematic_support/client_local.py +26 -14
- aiohomematic-2025.10.1.dist-info/RECORD +0 -78
- {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.3.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.3.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.3.dist-info}/top_level.txt +0 -0
aiohomematic/support.py
CHANGED
|
@@ -54,7 +54,7 @@ from aiohomematic.property_decorators import Kind, get_hm_property_by_kind, get_
|
|
|
54
54
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def extract_exc_args(exc: Exception) -> tuple[Any, ...] | Any:
|
|
57
|
+
def extract_exc_args(*, exc: Exception) -> tuple[Any, ...] | Any:
|
|
58
58
|
"""Return the first arg, if there is only one arg."""
|
|
59
59
|
if exc.args:
|
|
60
60
|
return exc.args[0] if len(exc.args) == 1 else exc.args
|
|
@@ -62,6 +62,7 @@ def extract_exc_args(exc: Exception) -> tuple[Any, ...] | Any:
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def build_xml_rpc_uri(
|
|
65
|
+
*,
|
|
65
66
|
host: str,
|
|
66
67
|
port: int | None,
|
|
67
68
|
path: str | None,
|
|
@@ -80,6 +81,7 @@ def build_xml_rpc_uri(
|
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
def build_xml_rpc_headers(
|
|
84
|
+
*,
|
|
83
85
|
username: str,
|
|
84
86
|
password: str,
|
|
85
87
|
) -> list[tuple[str, str]]:
|
|
@@ -90,6 +92,7 @@ def build_xml_rpc_headers(
|
|
|
90
92
|
|
|
91
93
|
|
|
92
94
|
def check_config(
|
|
95
|
+
*,
|
|
93
96
|
central_name: str,
|
|
94
97
|
host: str,
|
|
95
98
|
username: str,
|
|
@@ -111,10 +114,10 @@ def check_config(
|
|
|
111
114
|
config_failures.append("Username must not be empty")
|
|
112
115
|
if not password:
|
|
113
116
|
config_failures.append("Password is required")
|
|
114
|
-
if not check_password(password):
|
|
117
|
+
if not check_password(password=password):
|
|
115
118
|
config_failures.append("Password is not valid")
|
|
116
119
|
try:
|
|
117
|
-
check_or_create_directory(storage_folder)
|
|
120
|
+
check_or_create_directory(directory=storage_folder)
|
|
118
121
|
except BaseHomematicException as bhexc:
|
|
119
122
|
config_failures.append(extract_exc_args(exc=bhexc)[0])
|
|
120
123
|
if callback_host and not (is_hostname(hostname=callback_host) or is_ipv4_address(address=callback_host)):
|
|
@@ -129,7 +132,7 @@ def check_config(
|
|
|
129
132
|
return config_failures
|
|
130
133
|
|
|
131
134
|
|
|
132
|
-
def has_primary_client(interface_configs: AbstractSet[hmcl.InterfaceConfig]) -> bool:
|
|
135
|
+
def has_primary_client(*, interface_configs: AbstractSet[hmcl.InterfaceConfig]) -> bool:
|
|
133
136
|
"""Check if all configured clients exists in central."""
|
|
134
137
|
for interface_config in interface_configs:
|
|
135
138
|
if interface_config.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES:
|
|
@@ -137,7 +140,7 @@ def has_primary_client(interface_configs: AbstractSet[hmcl.InterfaceConfig]) ->
|
|
|
137
140
|
return False
|
|
138
141
|
|
|
139
142
|
|
|
140
|
-
def delete_file(folder: str, file_name: str) -> None:
|
|
143
|
+
def delete_file(*, folder: str, file_name: str) -> None:
|
|
141
144
|
"""Delete the file."""
|
|
142
145
|
file_path = os.path.join(folder, file_name)
|
|
143
146
|
if (
|
|
@@ -148,7 +151,7 @@ def delete_file(folder: str, file_name: str) -> None:
|
|
|
148
151
|
os.unlink(file_path)
|
|
149
152
|
|
|
150
153
|
|
|
151
|
-
def check_or_create_directory(directory: str) -> bool:
|
|
154
|
+
def check_or_create_directory(*, directory: str) -> bool:
|
|
152
155
|
"""Check / create directory."""
|
|
153
156
|
if not directory:
|
|
154
157
|
return False
|
|
@@ -162,12 +165,12 @@ def check_or_create_directory(directory: str) -> bool:
|
|
|
162
165
|
return True
|
|
163
166
|
|
|
164
167
|
|
|
165
|
-
def parse_sys_var(data_type: SysvarType | None, raw_value: Any) -> Any:
|
|
168
|
+
def parse_sys_var(*, data_type: SysvarType | None, raw_value: Any) -> Any:
|
|
166
169
|
"""Parse system variables to fix type."""
|
|
167
170
|
if not data_type:
|
|
168
171
|
return raw_value
|
|
169
172
|
if data_type in (SysvarType.ALARM, SysvarType.LOGIC):
|
|
170
|
-
return to_bool(raw_value)
|
|
173
|
+
return to_bool(value=raw_value)
|
|
171
174
|
if data_type == SysvarType.FLOAT:
|
|
172
175
|
return float(raw_value)
|
|
173
176
|
if data_type in (SysvarType.INTEGER, SysvarType.LIST):
|
|
@@ -175,7 +178,7 @@ def parse_sys_var(data_type: SysvarType | None, raw_value: Any) -> Any:
|
|
|
175
178
|
return raw_value
|
|
176
179
|
|
|
177
180
|
|
|
178
|
-
def to_bool(value: Any) -> bool:
|
|
181
|
+
def to_bool(*, value: Any) -> bool:
|
|
179
182
|
"""Convert defined string values to bool."""
|
|
180
183
|
if isinstance(value, bool):
|
|
181
184
|
return value
|
|
@@ -186,7 +189,7 @@ def to_bool(value: Any) -> bool:
|
|
|
186
189
|
return value.lower() in ["y", "yes", "t", "true", "on", "1"]
|
|
187
190
|
|
|
188
191
|
|
|
189
|
-
def check_password(password: str | None) -> bool:
|
|
192
|
+
def check_password(*, password: str | None) -> bool:
|
|
190
193
|
"""Check password."""
|
|
191
194
|
if password is None:
|
|
192
195
|
return False
|
|
@@ -200,7 +203,7 @@ def check_password(password: str | None) -> bool:
|
|
|
200
203
|
return True
|
|
201
204
|
|
|
202
205
|
|
|
203
|
-
def regular_to_default_dict_hook(origin: dict) -> defaultdict[Any, Any]:
|
|
206
|
+
def regular_to_default_dict_hook(origin: dict, /) -> defaultdict[Any, Any]:
|
|
204
207
|
"""Use defaultdict in json.loads object_hook."""
|
|
205
208
|
new_dict: Callable = lambda: defaultdict(new_dict)
|
|
206
209
|
new_instance = new_dict()
|
|
@@ -208,7 +211,7 @@ def regular_to_default_dict_hook(origin: dict) -> defaultdict[Any, Any]:
|
|
|
208
211
|
return cast(defaultdict[Any, Any], new_instance)
|
|
209
212
|
|
|
210
213
|
|
|
211
|
-
def _create_tls_context(verify_tls: bool) -> ssl.SSLContext:
|
|
214
|
+
def _create_tls_context(*, verify_tls: bool) -> ssl.SSLContext:
|
|
212
215
|
"""Create tls verified/unverified context."""
|
|
213
216
|
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
214
217
|
if not verify_tls:
|
|
@@ -225,48 +228,48 @@ _DEFAULT_NO_VERIFY_SSL_CONTEXT = _create_tls_context(verify_tls=False)
|
|
|
225
228
|
_DEFAULT_SSL_CONTEXT = _create_tls_context(verify_tls=True)
|
|
226
229
|
|
|
227
230
|
|
|
228
|
-
def get_tls_context(verify_tls: bool) -> ssl.SSLContext:
|
|
231
|
+
def get_tls_context(*, verify_tls: bool) -> ssl.SSLContext:
|
|
229
232
|
"""Return tls verified/unverified context."""
|
|
230
233
|
return _DEFAULT_SSL_CONTEXT if verify_tls else _DEFAULT_NO_VERIFY_SSL_CONTEXT
|
|
231
234
|
|
|
232
235
|
|
|
233
|
-
def get_channel_address(device_address: str, channel_no: int | None) -> str:
|
|
236
|
+
def get_channel_address(*, device_address: str, channel_no: int | None) -> str:
|
|
234
237
|
"""Return the channel address."""
|
|
235
238
|
return device_address if channel_no is None else f"{device_address}:{channel_no}"
|
|
236
239
|
|
|
237
240
|
|
|
238
|
-
def get_device_address(address: str) -> str:
|
|
241
|
+
def get_device_address(*, address: str) -> str:
|
|
239
242
|
"""Return the device part of an address."""
|
|
240
243
|
return get_split_channel_address(channel_address=address)[0]
|
|
241
244
|
|
|
242
245
|
|
|
243
|
-
def get_channel_no(address: str) -> int | None:
|
|
246
|
+
def get_channel_no(*, address: str) -> int | None:
|
|
244
247
|
"""Return the channel part of an address."""
|
|
245
248
|
return get_split_channel_address(channel_address=address)[1]
|
|
246
249
|
|
|
247
250
|
|
|
248
|
-
def is_address(address: str) -> bool:
|
|
251
|
+
def is_address(*, address: str) -> bool:
|
|
249
252
|
"""Check if it is a address."""
|
|
250
253
|
return is_device_address(address=address) or is_channel_address(address=address)
|
|
251
254
|
|
|
252
255
|
|
|
253
|
-
def is_channel_address(address: str) -> bool:
|
|
256
|
+
def is_channel_address(*, address: str) -> bool:
|
|
254
257
|
"""Check if it is a channel address."""
|
|
255
258
|
return CHANNEL_ADDRESS_PATTERN.match(address) is not None
|
|
256
259
|
|
|
257
260
|
|
|
258
|
-
def is_device_address(address: str) -> bool:
|
|
261
|
+
def is_device_address(*, address: str) -> bool:
|
|
259
262
|
"""Check if it is a device address."""
|
|
260
263
|
return DEVICE_ADDRESS_PATTERN.match(address) is not None
|
|
261
264
|
|
|
262
265
|
|
|
263
|
-
def is_paramset_key(paramset_key: ParamsetKey | str) -> bool:
|
|
266
|
+
def is_paramset_key(*, paramset_key: ParamsetKey | str) -> bool:
|
|
264
267
|
"""Check if it is a paramset key."""
|
|
265
268
|
return isinstance(paramset_key, ParamsetKey) or (isinstance(paramset_key, str) and paramset_key in ParamsetKey)
|
|
266
269
|
|
|
267
270
|
|
|
268
271
|
@lru_cache(maxsize=4096)
|
|
269
|
-
def get_split_channel_address(channel_address: str) -> tuple[str, int | None]:
|
|
272
|
+
def get_split_channel_address(*, channel_address: str) -> tuple[str, int | None]:
|
|
270
273
|
"""
|
|
271
274
|
Return the device part of an address.
|
|
272
275
|
|
|
@@ -281,7 +284,7 @@ def get_split_channel_address(channel_address: str) -> tuple[str, int | None]:
|
|
|
281
284
|
return channel_address, None
|
|
282
285
|
|
|
283
286
|
|
|
284
|
-
def changed_within_seconds(last_change: datetime, max_age: int = MAX_CACHE_AGE) -> bool:
|
|
287
|
+
def changed_within_seconds(*, last_change: datetime, max_age: int = MAX_CACHE_AGE) -> bool:
|
|
285
288
|
"""DataPoint has been modified within X minutes."""
|
|
286
289
|
if last_change == INIT_DATETIME:
|
|
287
290
|
return False
|
|
@@ -297,7 +300,7 @@ def find_free_port() -> int:
|
|
|
297
300
|
return int(sock.getsockname()[1])
|
|
298
301
|
|
|
299
302
|
|
|
300
|
-
def get_ip_addr(host: str, port: int) -> str | None:
|
|
303
|
+
def get_ip_addr(host: str, port: int, /) -> str | None:
|
|
301
304
|
"""Get local_ip from socket."""
|
|
302
305
|
try:
|
|
303
306
|
socket.gethostbyname(host)
|
|
@@ -314,7 +317,7 @@ def get_ip_addr(host: str, port: int) -> str | None:
|
|
|
314
317
|
return local_ip
|
|
315
318
|
|
|
316
319
|
|
|
317
|
-
def is_hostname(hostname: str | None) -> bool:
|
|
320
|
+
def is_hostname(*, hostname: str | None) -> bool:
|
|
318
321
|
"""Return True if hostname is valid."""
|
|
319
322
|
if not hostname:
|
|
320
323
|
return False
|
|
@@ -333,7 +336,7 @@ def is_hostname(hostname: str | None) -> bool:
|
|
|
333
336
|
return all(ALLOWED_HOSTNAME_PATTERN.match(label) for label in labels)
|
|
334
337
|
|
|
335
338
|
|
|
336
|
-
def is_ipv4_address(address: str | None) -> bool:
|
|
339
|
+
def is_ipv4_address(*, address: str | None) -> bool:
|
|
337
340
|
"""Return True if ipv4_address is valid."""
|
|
338
341
|
if not address:
|
|
339
342
|
return False
|
|
@@ -344,12 +347,13 @@ def is_ipv4_address(address: str | None) -> bool:
|
|
|
344
347
|
return True
|
|
345
348
|
|
|
346
349
|
|
|
347
|
-
def is_port(port: int) -> bool:
|
|
350
|
+
def is_port(*, port: int) -> bool:
|
|
348
351
|
"""Return True if port is valid."""
|
|
349
352
|
return 0 <= port <= 65535
|
|
350
353
|
|
|
351
354
|
|
|
352
355
|
def element_matches_key(
|
|
356
|
+
*,
|
|
353
357
|
search_elements: str | Collection[str],
|
|
354
358
|
compare_with: str | None,
|
|
355
359
|
search_key: str | None = None,
|
|
@@ -401,7 +405,7 @@ def element_matches_key(
|
|
|
401
405
|
return False
|
|
402
406
|
|
|
403
407
|
|
|
404
|
-
def _get_search_key(search_elements: Collection[str], search_key: str) -> str | None:
|
|
408
|
+
def _get_search_key(*, search_elements: Collection[str], search_key: str) -> str | None:
|
|
405
409
|
"""Search for a matching key in a collection."""
|
|
406
410
|
for element in search_elements:
|
|
407
411
|
if search_key.startswith(element):
|
|
@@ -446,7 +450,7 @@ def debug_enabled() -> bool:
|
|
|
446
450
|
return False
|
|
447
451
|
|
|
448
452
|
|
|
449
|
-
def hash_sha256(value: Any) -> str:
|
|
453
|
+
def hash_sha256(*, value: Any) -> str:
|
|
450
454
|
"""
|
|
451
455
|
Hash a value with sha256.
|
|
452
456
|
|
|
@@ -459,26 +463,26 @@ def hash_sha256(value: Any) -> str:
|
|
|
459
463
|
data = orjson.dumps(value, option=orjson.OPT_SORT_KEYS | orjson.OPT_NON_STR_KEYS)
|
|
460
464
|
except Exception:
|
|
461
465
|
# Fallback: convert to a hashable representation and use repr()
|
|
462
|
-
data = repr(_make_value_hashable(value)).encode()
|
|
466
|
+
data = repr(_make_value_hashable(value=value)).encode()
|
|
463
467
|
hasher.update(data)
|
|
464
468
|
return base64.b64encode(hasher.digest()).decode()
|
|
465
469
|
|
|
466
470
|
|
|
467
|
-
def _make_value_hashable(value: Any) -> Any:
|
|
471
|
+
def _make_value_hashable(*, value: Any) -> Any:
|
|
468
472
|
"""Make a hashable object."""
|
|
469
473
|
if isinstance(value, tuple | list):
|
|
470
|
-
return tuple(_make_value_hashable(e) for e in value)
|
|
474
|
+
return tuple(_make_value_hashable(value=e) for e in value)
|
|
471
475
|
|
|
472
476
|
if isinstance(value, dict):
|
|
473
|
-
return tuple(sorted((k, _make_value_hashable(v)) for k, v in value.items()))
|
|
477
|
+
return tuple(sorted((k, _make_value_hashable(value=v)) for k, v in value.items()))
|
|
474
478
|
|
|
475
479
|
if isinstance(value, set | frozenset):
|
|
476
|
-
return tuple(sorted(_make_value_hashable(e) for e in value))
|
|
480
|
+
return tuple(sorted(_make_value_hashable(value=e) for e in value))
|
|
477
481
|
|
|
478
482
|
return value
|
|
479
483
|
|
|
480
484
|
|
|
481
|
-
def get_rx_modes(mode: int) -> tuple[RxMode, ...]:
|
|
485
|
+
def get_rx_modes(*, mode: int) -> tuple[RxMode, ...]:
|
|
482
486
|
"""Convert int to rx modes."""
|
|
483
487
|
rx_modes: set[RxMode] = set()
|
|
484
488
|
if mode & RxMode.LAZY_CONFIG:
|
|
@@ -498,14 +502,14 @@ def get_rx_modes(mode: int) -> tuple[RxMode, ...]:
|
|
|
498
502
|
return tuple(rx_modes)
|
|
499
503
|
|
|
500
504
|
|
|
501
|
-
def supports_rx_mode(command_rx_mode: CommandRxMode, rx_modes: tuple[RxMode, ...]) -> bool:
|
|
505
|
+
def supports_rx_mode(*, command_rx_mode: CommandRxMode, rx_modes: tuple[RxMode, ...]) -> bool:
|
|
502
506
|
"""Check if rx mode is supported."""
|
|
503
507
|
return (command_rx_mode == CommandRxMode.BURST and RxMode.BURST in rx_modes) or (
|
|
504
508
|
command_rx_mode == CommandRxMode.WAKEUP and RxMode.WAKEUP in rx_modes
|
|
505
509
|
)
|
|
506
510
|
|
|
507
511
|
|
|
508
|
-
def cleanup_text_from_html_tags(text: str) -> str:
|
|
512
|
+
def cleanup_text_from_html_tags(*, text: str) -> str:
|
|
509
513
|
"""Cleanup text from html tags."""
|
|
510
514
|
return re.sub(HTMLTAG_PATTERN, "", text)
|
|
511
515
|
|
|
@@ -515,7 +519,7 @@ def cleanup_text_from_html_tags(text: str) -> str:
|
|
|
515
519
|
_BOUNDARY_MSG = "error_boundary"
|
|
516
520
|
|
|
517
521
|
|
|
518
|
-
def _safe_log_context(context: Mapping[str, Any] | None) -> dict[str, Any]:
|
|
522
|
+
def _safe_log_context(*, context: Mapping[str, Any] | None) -> dict[str, Any]:
|
|
519
523
|
"""Extract safe context from a mapping."""
|
|
520
524
|
ctx: dict[str, Any] = {}
|
|
521
525
|
if not context:
|
|
@@ -565,7 +569,9 @@ def log_boundary_error(
|
|
|
565
569
|
log_message += f" {message}"
|
|
566
570
|
|
|
567
571
|
if log_context:
|
|
568
|
-
log_message +=
|
|
572
|
+
log_message += (
|
|
573
|
+
f" ctx={orjson.dumps(_safe_log_context(context=log_context), option=orjson.OPT_SORT_KEYS).decode()}"
|
|
574
|
+
)
|
|
569
575
|
|
|
570
576
|
# Choose level if not provided:
|
|
571
577
|
if (chosen_level := level) is None:
|
aiohomematic/validator.py
CHANGED
|
@@ -28,42 +28,42 @@ positive_int = vol.All(vol.Coerce(int), vol.Range(min=0))
|
|
|
28
28
|
wait_for = vol.All(vol.Coerce(int), vol.Range(min=1, max=MAX_WAIT_FOR_CALLBACK))
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def channel_address(value: str) -> str:
|
|
31
|
+
def channel_address(value: str, /) -> str:
|
|
32
32
|
"""Validate channel_address."""
|
|
33
33
|
if is_channel_address(address=value):
|
|
34
34
|
return value
|
|
35
35
|
raise vol.Invalid("channel_address is invalid")
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def device_address(value: str) -> str:
|
|
38
|
+
def device_address(value: str, /) -> str:
|
|
39
39
|
"""Validate channel_address."""
|
|
40
40
|
if is_device_address(address=value):
|
|
41
41
|
return value
|
|
42
42
|
raise vol.Invalid("device_address is invalid")
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def hostname(value: str) -> str:
|
|
45
|
+
def hostname(value: str, /) -> str:
|
|
46
46
|
"""Validate hostname."""
|
|
47
47
|
if is_hostname(hostname=value):
|
|
48
48
|
return value
|
|
49
49
|
raise vol.Invalid("hostname is invalid")
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def ipv4_address(value: str) -> str:
|
|
52
|
+
def ipv4_address(value: str, /) -> str:
|
|
53
53
|
"""Validate ipv4_address."""
|
|
54
54
|
if is_ipv4_address(address=value):
|
|
55
55
|
return value
|
|
56
56
|
raise vol.Invalid("ipv4_address is invalid")
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def password(value: str) -> str:
|
|
59
|
+
def password(value: str, /) -> str:
|
|
60
60
|
"""Validate password."""
|
|
61
61
|
if check_password(password=value):
|
|
62
62
|
return value
|
|
63
63
|
raise vol.Invalid("password is invalid")
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def paramset_key(value: str) -> str:
|
|
66
|
+
def paramset_key(value: str, /) -> str:
|
|
67
67
|
"""Validate paramset_key."""
|
|
68
68
|
if is_paramset_key(paramset_key=value):
|
|
69
69
|
return value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.3
|
|
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>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
aiohomematic/__init__.py,sha256=ngULK_anZQwwUUCVcberBdVjguYfboiuG9VoueKy9fA,2283
|
|
2
|
+
aiohomematic/async_support.py,sha256=EEzOwI3LAWjl16Q2QgtTLS7P9ktodBvs8Cw3ZKsjyxw,6153
|
|
3
|
+
aiohomematic/const.py,sha256=mJmfDFhm3NFQP4qkRlb63d89qi-pDidoHUq0RYYOM6I,25565
|
|
4
|
+
aiohomematic/context.py,sha256=M7gkA7KFT0dp35gzGz2dzKVXu1PP0sAnepgLlmjyRS4,451
|
|
5
|
+
aiohomematic/converter.py,sha256=gaNHe-WEiBStZMuuRz9iGn3Mo_CGz1bjgLtlYBJJAko,3624
|
|
6
|
+
aiohomematic/decorators.py,sha256=oHEFSJoJVd-h9RYb6NLTJ60erkpRHPYv7EEipKn1a9Q,10636
|
|
7
|
+
aiohomematic/exceptions.py,sha256=8Uu3rADawhYlAz6y4J52aJ-wKok8Z7YbUYUwWeGMKhs,5028
|
|
8
|
+
aiohomematic/hmcli.py,sha256=qNstNDX6q8t3mJFCGlXlmRVobGabntrPtFi3kchf1Eg,4933
|
|
9
|
+
aiohomematic/property_decorators.py,sha256=tAsmcSDnLsUiWlD1KUEe5fvYoIGpk5syYB0DrGuN4QU,16231
|
|
10
|
+
aiohomematic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
aiohomematic/support.py,sha256=w05YlO1IrelF2a1zKkUGWyr5E-nDJ-knh0LQqOHpR34,20514
|
|
12
|
+
aiohomematic/validator.py,sha256=HUikmo-SFksehFBAdZmBv4ajy0XkjgvXvcCfbexnzZo,3563
|
|
13
|
+
aiohomematic/caches/__init__.py,sha256=_gI30tbsWgPRaHvP6cRxOQr6n9bYZzU-jp1WbHhWg-A,470
|
|
14
|
+
aiohomematic/caches/dynamic.py,sha256=vyQsjBc2uGoNCCrWPMGmNBlUQ9GYvgVuupA9bhqH5hI,21466
|
|
15
|
+
aiohomematic/caches/persistent.py,sha256=2bgASru4zbrFEd8rOn2YcReeHPiPJE8hMgMUPAbGh68,20117
|
|
16
|
+
aiohomematic/caches/visibility.py,sha256=SThfEO3LKbIIERD4Novyj4ZZUkcYrBuvYdNQfPO29JQ,31676
|
|
17
|
+
aiohomematic/central/__init__.py,sha256=nXbA9IYHSn-Gm59U_T9gqMeKwQxmliv3jdrkXnMtpXs,86940
|
|
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=XWHg8VTtiahu7jfHwq4yOVXOxUy8nw80JF9uyY7QNJI,70457
|
|
21
|
+
aiohomematic/client/_rpc_errors.py,sha256=-NPtGvkQPJ4V2clDxv1tKy09M9JZm61pUCeki9DDh6s,2984
|
|
22
|
+
aiohomematic/client/json_rpc.py,sha256=k5euUJkDzIQXdOX5JJxkua_7AUUTFspK8rTGnVKoCf8,49568
|
|
23
|
+
aiohomematic/client/xml_rpc.py,sha256=9PEOWoTG0EMUAMyqiInF4iA_AduHv_hhjJW3YhYjYqU,9614
|
|
24
|
+
aiohomematic/model/__init__.py,sha256=KO7gas_eEzm67tODKqWTs0617CSGeKKjOWOlDbhRo_Q,5458
|
|
25
|
+
aiohomematic/model/data_point.py,sha256=Ml8AOQ1RcRezTYWiGBlIXwcTLolQMX5Cyb-O7GtNDm4,41586
|
|
26
|
+
aiohomematic/model/device.py,sha256=15z5G2X3jSJaj-yz7jX_tnirzipRIGBJPymObY3Dmjk,52942
|
|
27
|
+
aiohomematic/model/event.py,sha256=82H8M_QNMCCC29mP3R16alJyKWS3Hb3aqY_aFMSqCvo,6874
|
|
28
|
+
aiohomematic/model/support.py,sha256=l5E9Oon20nkGWOSEmbYtqQbpbh6-H4rIk8xtEtk5Fcg,19657
|
|
29
|
+
aiohomematic/model/update.py,sha256=5F39xNz9B2GKJ8TvJHPMC-Wu97HfkiiMawjnHEYMnoA,5156
|
|
30
|
+
aiohomematic/model/calculated/__init__.py,sha256=UGLePgKDH8JpLqjhPBgvBzjggI34omcaCPsc6tcM8Xs,2811
|
|
31
|
+
aiohomematic/model/calculated/climate.py,sha256=GXBsC5tnrC_BvnFBkJ9KUqE7uVcGD1KTU_6-OleF5H4,8545
|
|
32
|
+
aiohomematic/model/calculated/data_point.py,sha256=oTN8y3B9weh7CX3ZFiDyZFgvX77iUwge-acg49pd1sI,11609
|
|
33
|
+
aiohomematic/model/calculated/operating_voltage_level.py,sha256=ZrOPdNoWQ5QLr4yzMRsoPG3UuJKRkBUHfchIrpKZU4o,13527
|
|
34
|
+
aiohomematic/model/calculated/support.py,sha256=vOxTvWe8SBCwJpLzcVA8ibtfw4eP8yTUZj4Jt9hWt9k,6695
|
|
35
|
+
aiohomematic/model/custom/__init__.py,sha256=UzczqjsUqWvS9ZaqKeb6elbjb2y5W3cgFPB0YQUHaeM,6095
|
|
36
|
+
aiohomematic/model/custom/climate.py,sha256=zSLQUY_tU7tDlbM-vW15BGuyWRjcR_DyqOwSg1_Vmfw,57217
|
|
37
|
+
aiohomematic/model/custom/const.py,sha256=Kh1pnab6nmwbaY43CfXQy3yrWpPwsrQdl1Ea2aZ6aw0,4961
|
|
38
|
+
aiohomematic/model/custom/cover.py,sha256=hlIeQD0cZpq7X222J7ygm6kD4AE6h5IN-wcqzvZCLFA,29057
|
|
39
|
+
aiohomematic/model/custom/data_point.py,sha256=l2pTz7Fu5jGCstXHK1cWCFfBWIJeKmtt37qdGLmrQhA,14155
|
|
40
|
+
aiohomematic/model/custom/definition.py,sha256=9kSdqVOHQs65Q2Op5QknNQv5lLmZkZlGCUUCRGicOaw,35662
|
|
41
|
+
aiohomematic/model/custom/light.py,sha256=2UxQOoupwTpQ-5iwY51gL_B815sgDXNW-HG-QhAFb9E,44448
|
|
42
|
+
aiohomematic/model/custom/lock.py,sha256=ndzZ0hp7FBohw7T_qR0jPobwlcwxus9M1DuDu_7vfPw,11996
|
|
43
|
+
aiohomematic/model/custom/siren.py,sha256=DT8RoOCl7FqstgRSBK-RWRcY4T29LuEdnlhaWCB6ATk,9785
|
|
44
|
+
aiohomematic/model/custom/support.py,sha256=UvencsvCwgpm4iqRNRt5KRs560tyw1NhYP5ZaqmCT2k,1453
|
|
45
|
+
aiohomematic/model/custom/switch.py,sha256=VKknWPJOtSwIzV-Ag_8Zg1evtkyjKh768Be_quU_R54,6885
|
|
46
|
+
aiohomematic/model/custom/valve.py,sha256=u9RYzeJ8FNmpFO6amlLElXTQdAeqac5yo7NbZYS6Z9U,4242
|
|
47
|
+
aiohomematic/model/generic/__init__.py,sha256=-ho8m9gFlORBGNPn2i8c9i5-GVLLFvTlf5FFpaTJbFw,7675
|
|
48
|
+
aiohomematic/model/generic/action.py,sha256=niJPvTs43b9GiKomdBaBKwjOwtmNxR_YRhj5Fpje9NU,997
|
|
49
|
+
aiohomematic/model/generic/binary_sensor.py,sha256=U5GvfRYbhwe0jRmaedD4LVZ_24SyyPbVr74HEfZXoxE,887
|
|
50
|
+
aiohomematic/model/generic/button.py,sha256=6jZ49woI9gYJEx__PjguDNbc5vdE1P-YcLMZZFkYRCg,740
|
|
51
|
+
aiohomematic/model/generic/data_point.py,sha256=2NvdU802JUo4NZh0v6oMI-pVtlNluSFse7ISMGqo70g,6084
|
|
52
|
+
aiohomematic/model/generic/number.py,sha256=nJgOkMZwNfPtzBrX2o5RAjBt-o8KrKuqtDa9LBj0Jw0,2678
|
|
53
|
+
aiohomematic/model/generic/select.py,sha256=vWfLUdQBjZLG-q-WZMxHk9Klawg_iNOEeSoVHrvG35I,1538
|
|
54
|
+
aiohomematic/model/generic/sensor.py,sha256=wCnQ8IoC8uPTN29R250pfJa4x6y9sh4c3vxQ4Km8Clg,2262
|
|
55
|
+
aiohomematic/model/generic/switch.py,sha256=VIMwIVok9kSRoSb-s5saYRHeiZcNWH4J5FyMSxUAbpw,1842
|
|
56
|
+
aiohomematic/model/generic/text.py,sha256=vtNV7YxZuxF6LzNRKRAeOtSQtPQxPaEd560OFaVR13U,854
|
|
57
|
+
aiohomematic/model/hub/__init__.py,sha256=g2m-5rba6SNCfGrlxwqYa8mlP5-N2obFAvyHJV8i4FY,13525
|
|
58
|
+
aiohomematic/model/hub/binary_sensor.py,sha256=Z4o-zghHSc83ZHUUCtHqWEGueD9K1Fe0JEt_xJNdx_Y,752
|
|
59
|
+
aiohomematic/model/hub/button.py,sha256=XMnoImnz5vDybxfrP4GWDp2M5gEMG71d8ba1YzVYAnE,890
|
|
60
|
+
aiohomematic/model/hub/data_point.py,sha256=E6qn1gVhZ4meti-Tpd03f-YyfKnkUh-FpLbwBaa-d1c,10653
|
|
61
|
+
aiohomematic/model/hub/number.py,sha256=12BK6mBOJn4aP7DWuWvMYfiaLmDX7ejd5tDCoHa2bUk,1237
|
|
62
|
+
aiohomematic/model/hub/select.py,sha256=C9ke_8U_pzI0fctIeOZHwq-fvWj5s64jW6-DcZT-SrU,1674
|
|
63
|
+
aiohomematic/model/hub/sensor.py,sha256=cIr-7bsbOLBxnH33EHQ6D42vc61abO4QYLWLzkm1T10,1192
|
|
64
|
+
aiohomematic/model/hub/switch.py,sha256=510Vrlyak5TRsMvURrGYMkad-CbGUKWh20jjHQGOios,1390
|
|
65
|
+
aiohomematic/model/hub/text.py,sha256=JOy-hfDSu95NbxP5JeGgga9NGrR8JLNmz_h4k8J1GXU,999
|
|
66
|
+
aiohomematic/rega_scripts/fetch_all_device_data.fn,sha256=7uxhHoelAOsH6yYr1n1M1XwIRgDmItiHnWIMhDYEimk,4373
|
|
67
|
+
aiohomematic/rega_scripts/get_program_descriptions.fn,sha256=pGmj377MkqbZi6j-UBKQAsXTphwh1kDwDKqXij8zUBE,835
|
|
68
|
+
aiohomematic/rega_scripts/get_serial.fn,sha256=t1oeo-sB_EuVeiY24PLcxFSkdQVgEWGXzpemJQZFybY,1079
|
|
69
|
+
aiohomematic/rega_scripts/get_system_variable_descriptions.fn,sha256=UKXvC0_5lSApdQ2atJc0E5Stj5Zt3lqh0EcliokYu2c,849
|
|
70
|
+
aiohomematic/rega_scripts/set_program_state.fn,sha256=0bnv7lUj8FMjDZBz325tDVP61m04cHjVj4kIOnUUgpY,279
|
|
71
|
+
aiohomematic/rega_scripts/set_system_variable.fn,sha256=sTmr7vkPTPnPkor5cnLKlDvfsYRbGO1iq2z_2pMXq5E,383
|
|
72
|
+
aiohomematic-2025.10.3.dist-info/licenses/LICENSE,sha256=q-B0xpREuZuvKsmk3_iyVZqvZ-vJcWmzMZpeAd0RqtQ,1083
|
|
73
|
+
aiohomematic_support/__init__.py,sha256=_0YtF4lTdC_k6-zrM2IefI0u0LMr_WA61gXAyeGLgbY,66
|
|
74
|
+
aiohomematic_support/client_local.py,sha256=CGZ6N_JCQ12_u2h56M_WCZ5SbcOK4xBsPYjLH2eyqlo,12820
|
|
75
|
+
aiohomematic-2025.10.3.dist-info/METADATA,sha256=npEp8wqV-PrVJzhabx_yKlOicBz3Q7pnqrLf1nqqlLg,7603
|
|
76
|
+
aiohomematic-2025.10.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
77
|
+
aiohomematic-2025.10.3.dist-info/top_level.txt,sha256=5TDRlUWQPThIUwQjOj--aUo4UA-ow4m0sNhnoCBi5n8,34
|
|
78
|
+
aiohomematic-2025.10.3.dist-info/RECORD,,
|
|
@@ -41,7 +41,7 @@ BACKEND_LOCAL: Final = "Local CCU"
|
|
|
41
41
|
class ClientLocal(Client): # pragma: no cover
|
|
42
42
|
"""Local client object to provide access to locally stored files."""
|
|
43
43
|
|
|
44
|
-
def __init__(self, client_config: _ClientConfig, local_resources: LocalRessources) -> None:
|
|
44
|
+
def __init__(self, *, client_config: _ClientConfig, local_resources: LocalRessources) -> None:
|
|
45
45
|
"""Initialize the Client."""
|
|
46
46
|
super().__init__(client_config=client_config)
|
|
47
47
|
self._local_resources = local_resources
|
|
@@ -63,7 +63,7 @@ class ClientLocal(Client): # pragma: no cover
|
|
|
63
63
|
"""Return the model of the backend."""
|
|
64
64
|
return BACKEND_LOCAL
|
|
65
65
|
|
|
66
|
-
def get_product_group(self, model: str) -> ProductGroup:
|
|
66
|
+
def get_product_group(self, *, model: str) -> ProductGroup:
|
|
67
67
|
"""Return the product group."""
|
|
68
68
|
l_model = model.lower()
|
|
69
69
|
if l_model.startswith("hmipw"):
|
|
@@ -120,46 +120,46 @@ class ClientLocal(Client): # pragma: no cover
|
|
|
120
120
|
return True
|
|
121
121
|
|
|
122
122
|
@inspector(re_raise=False, no_raise_return=False)
|
|
123
|
-
async def check_connection_availability(self, handle_ping_pong: bool) -> bool:
|
|
123
|
+
async def check_connection_availability(self, *, handle_ping_pong: bool) -> bool:
|
|
124
124
|
"""Send ping to the backend to generate PONG event."""
|
|
125
125
|
if handle_ping_pong and self.supports_ping_pong:
|
|
126
126
|
self._ping_pong_cache.handle_send_ping(ping_ts=datetime.now())
|
|
127
127
|
return True
|
|
128
128
|
|
|
129
129
|
@inspector
|
|
130
|
-
async def execute_program(self, pid: str) -> bool:
|
|
130
|
+
async def execute_program(self, *, pid: str) -> bool:
|
|
131
131
|
"""Execute a program on the backend."""
|
|
132
132
|
return True
|
|
133
133
|
|
|
134
134
|
@inspector
|
|
135
|
-
async def set_program_state(self, pid: str, state: bool) -> bool:
|
|
135
|
+
async def set_program_state(self, *, pid: str, state: bool) -> bool:
|
|
136
136
|
"""Set the program state on the backend."""
|
|
137
137
|
return True
|
|
138
138
|
|
|
139
139
|
@inspector(measure_performance=True)
|
|
140
|
-
async def set_system_variable(self, legacy_name: str, value: Any) -> bool:
|
|
140
|
+
async def set_system_variable(self, *, legacy_name: str, value: Any) -> bool:
|
|
141
141
|
"""Set a system variable on the backend."""
|
|
142
142
|
return True
|
|
143
143
|
|
|
144
144
|
@inspector
|
|
145
|
-
async def delete_system_variable(self, name: str) -> bool:
|
|
145
|
+
async def delete_system_variable(self, *, name: str) -> bool:
|
|
146
146
|
"""Delete a system variable from the backend."""
|
|
147
147
|
return True
|
|
148
148
|
|
|
149
149
|
@inspector
|
|
150
|
-
async def get_system_variable(self, name: str) -> str:
|
|
150
|
+
async def get_system_variable(self, *, name: str) -> str:
|
|
151
151
|
"""Get single system variable from the backend."""
|
|
152
152
|
return "Empty"
|
|
153
153
|
|
|
154
154
|
@inspector(re_raise=False)
|
|
155
155
|
async def get_all_system_variables(
|
|
156
|
-
self, markers: tuple[DescriptionMarker | str, ...]
|
|
156
|
+
self, *, markers: tuple[DescriptionMarker | str, ...]
|
|
157
157
|
) -> tuple[SystemVariableData, ...]:
|
|
158
158
|
"""Get all system variables from the backend."""
|
|
159
159
|
return ()
|
|
160
160
|
|
|
161
161
|
@inspector(re_raise=False)
|
|
162
|
-
async def get_all_programs(self, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...]:
|
|
162
|
+
async def get_all_programs(self, *, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...]:
|
|
163
163
|
"""Get all programs, if available."""
|
|
164
164
|
return ()
|
|
165
165
|
|
|
@@ -203,6 +203,7 @@ class ClientLocal(Client): # pragma: no cover
|
|
|
203
203
|
@inspector(log_level=logging.NOTSET)
|
|
204
204
|
async def get_value(
|
|
205
205
|
self,
|
|
206
|
+
*,
|
|
206
207
|
channel_address: str,
|
|
207
208
|
paramset_key: ParamsetKey,
|
|
208
209
|
parameter: str,
|
|
@@ -214,6 +215,7 @@ class ClientLocal(Client): # pragma: no cover
|
|
|
214
215
|
@inspector(re_raise=False, no_raise_return=set())
|
|
215
216
|
async def set_value(
|
|
216
217
|
self,
|
|
218
|
+
*,
|
|
217
219
|
channel_address: str,
|
|
218
220
|
paramset_key: ParamsetKey,
|
|
219
221
|
parameter: str,
|
|
@@ -228,12 +230,15 @@ class ClientLocal(Client): # pragma: no cover
|
|
|
228
230
|
channel_address=channel_address, parameter=parameter, value=value
|
|
229
231
|
)
|
|
230
232
|
# fire an event to fake the state change for a simple parameter
|
|
231
|
-
await self.central.data_point_event(
|
|
233
|
+
await self.central.data_point_event(
|
|
234
|
+
interface_id=self.interface_id, channel_address=channel_address, parameter=parameter, value=value
|
|
235
|
+
)
|
|
232
236
|
return result
|
|
233
237
|
|
|
234
238
|
@inspector
|
|
235
239
|
async def get_paramset(
|
|
236
240
|
self,
|
|
241
|
+
*,
|
|
237
242
|
address: str,
|
|
238
243
|
paramset_key: ParamsetKey | str,
|
|
239
244
|
call_source: CallSource = CallSource.MANUAL_OR_SCHEDULED,
|
|
@@ -247,7 +252,7 @@ class ClientLocal(Client): # pragma: no cover
|
|
|
247
252
|
return {}
|
|
248
253
|
|
|
249
254
|
async def _get_paramset_description(
|
|
250
|
-
self, address: str, paramset_key: ParamsetKey
|
|
255
|
+
self, *, address: str, paramset_key: ParamsetKey
|
|
251
256
|
) -> dict[str, ParameterData] | None:
|
|
252
257
|
"""Get paramset description from the backend."""
|
|
253
258
|
if not self._local_resources:
|
|
@@ -275,6 +280,7 @@ class ClientLocal(Client): # pragma: no cover
|
|
|
275
280
|
@inspector(measure_performance=True)
|
|
276
281
|
async def put_paramset(
|
|
277
282
|
self,
|
|
283
|
+
*,
|
|
278
284
|
channel_address: str,
|
|
279
285
|
paramset_key_or_link_address: ParamsetKey | str,
|
|
280
286
|
values: Any,
|
|
@@ -300,11 +306,17 @@ class ClientLocal(Client): # pragma: no cover
|
|
|
300
306
|
|
|
301
307
|
# fire an event to fake the state change for the content of a paramset
|
|
302
308
|
for parameter in values:
|
|
303
|
-
await self.central.data_point_event(
|
|
309
|
+
await self.central.data_point_event(
|
|
310
|
+
interface_id=self.interface_id,
|
|
311
|
+
channel_address=channel_address,
|
|
312
|
+
parameter=parameter,
|
|
313
|
+
value=values[parameter],
|
|
314
|
+
)
|
|
304
315
|
return result
|
|
305
316
|
|
|
306
317
|
async def _load_all_json_files(
|
|
307
318
|
self,
|
|
319
|
+
*,
|
|
308
320
|
anchor: str,
|
|
309
321
|
resource: str,
|
|
310
322
|
include_list: list[str] | None = None,
|
|
@@ -324,7 +336,7 @@ class ClientLocal(Client): # pragma: no cover
|
|
|
324
336
|
result.append(file_content)
|
|
325
337
|
return result
|
|
326
338
|
|
|
327
|
-
async def _load_json_file(self, anchor: str, resource: str, filename: str) -> Any | None:
|
|
339
|
+
async def _load_json_file(self, *, anchor: str, resource: str, filename: str) -> Any | None:
|
|
328
340
|
"""Load json file from disk into dict."""
|
|
329
341
|
package_path = str(importlib.resources.files(anchor))
|
|
330
342
|
|