aiohomematic 2025.8.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.

Files changed (77) hide show
  1. aiohomematic/__init__.py +47 -0
  2. aiohomematic/async_support.py +146 -0
  3. aiohomematic/caches/__init__.py +10 -0
  4. aiohomematic/caches/dynamic.py +554 -0
  5. aiohomematic/caches/persistent.py +459 -0
  6. aiohomematic/caches/visibility.py +774 -0
  7. aiohomematic/central/__init__.py +2034 -0
  8. aiohomematic/central/decorators.py +110 -0
  9. aiohomematic/central/xml_rpc_server.py +267 -0
  10. aiohomematic/client/__init__.py +1746 -0
  11. aiohomematic/client/json_rpc.py +1193 -0
  12. aiohomematic/client/xml_rpc.py +222 -0
  13. aiohomematic/const.py +795 -0
  14. aiohomematic/context.py +8 -0
  15. aiohomematic/converter.py +82 -0
  16. aiohomematic/decorators.py +188 -0
  17. aiohomematic/exceptions.py +145 -0
  18. aiohomematic/hmcli.py +159 -0
  19. aiohomematic/model/__init__.py +137 -0
  20. aiohomematic/model/calculated/__init__.py +65 -0
  21. aiohomematic/model/calculated/climate.py +230 -0
  22. aiohomematic/model/calculated/data_point.py +319 -0
  23. aiohomematic/model/calculated/operating_voltage_level.py +311 -0
  24. aiohomematic/model/calculated/support.py +174 -0
  25. aiohomematic/model/custom/__init__.py +175 -0
  26. aiohomematic/model/custom/climate.py +1334 -0
  27. aiohomematic/model/custom/const.py +146 -0
  28. aiohomematic/model/custom/cover.py +741 -0
  29. aiohomematic/model/custom/data_point.py +318 -0
  30. aiohomematic/model/custom/definition.py +861 -0
  31. aiohomematic/model/custom/light.py +1092 -0
  32. aiohomematic/model/custom/lock.py +389 -0
  33. aiohomematic/model/custom/siren.py +268 -0
  34. aiohomematic/model/custom/support.py +40 -0
  35. aiohomematic/model/custom/switch.py +172 -0
  36. aiohomematic/model/custom/valve.py +112 -0
  37. aiohomematic/model/data_point.py +1109 -0
  38. aiohomematic/model/decorators.py +173 -0
  39. aiohomematic/model/device.py +1347 -0
  40. aiohomematic/model/event.py +210 -0
  41. aiohomematic/model/generic/__init__.py +211 -0
  42. aiohomematic/model/generic/action.py +32 -0
  43. aiohomematic/model/generic/binary_sensor.py +28 -0
  44. aiohomematic/model/generic/button.py +25 -0
  45. aiohomematic/model/generic/data_point.py +162 -0
  46. aiohomematic/model/generic/number.py +73 -0
  47. aiohomematic/model/generic/select.py +36 -0
  48. aiohomematic/model/generic/sensor.py +72 -0
  49. aiohomematic/model/generic/switch.py +52 -0
  50. aiohomematic/model/generic/text.py +27 -0
  51. aiohomematic/model/hub/__init__.py +334 -0
  52. aiohomematic/model/hub/binary_sensor.py +22 -0
  53. aiohomematic/model/hub/button.py +26 -0
  54. aiohomematic/model/hub/data_point.py +332 -0
  55. aiohomematic/model/hub/number.py +37 -0
  56. aiohomematic/model/hub/select.py +47 -0
  57. aiohomematic/model/hub/sensor.py +35 -0
  58. aiohomematic/model/hub/switch.py +42 -0
  59. aiohomematic/model/hub/text.py +28 -0
  60. aiohomematic/model/support.py +599 -0
  61. aiohomematic/model/update.py +136 -0
  62. aiohomematic/py.typed +0 -0
  63. aiohomematic/rega_scripts/fetch_all_device_data.fn +75 -0
  64. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  65. aiohomematic/rega_scripts/get_serial.fn +44 -0
  66. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  67. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  68. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  69. aiohomematic/support.py +482 -0
  70. aiohomematic/validator.py +65 -0
  71. aiohomematic-2025.8.6.dist-info/METADATA +69 -0
  72. aiohomematic-2025.8.6.dist-info/RECORD +77 -0
  73. aiohomematic-2025.8.6.dist-info/WHEEL +5 -0
  74. aiohomematic-2025.8.6.dist-info/licenses/LICENSE +21 -0
  75. aiohomematic-2025.8.6.dist-info/top_level.txt +2 -0
  76. aiohomematic_support/__init__.py +1 -0
  77. aiohomematic_support/client_local.py +349 -0
@@ -0,0 +1,459 @@
1
+ """
2
+ Persistent caches used to persist HomeMatic metadata between runs.
3
+
4
+ This module provides on-disk caches that complement the short‑lived, in‑memory
5
+ caches from aiohomematic.caches.dynamic. The goal is to minimize expensive data
6
+ retrieval from the backend by storing stable metadata such as device and
7
+ paramset descriptions in JSON files inside a dedicated cache directory.
8
+
9
+ Overview
10
+ - BasePersistentCache: Abstract base for file‑backed caches. It encapsulates
11
+ file path resolution, change detection via hashing, and thread‑safe save/load
12
+ operations delegated to the CentralUnit looper.
13
+ - DeviceDescriptionCache: Persists device descriptions per interface, including
14
+ the mapping of device/channels and model metadata.
15
+ - ParamsetDescriptionCache: Persists paramset descriptions per interface and
16
+ channel, and offers helpers to query parameters, paramset keys and related
17
+ channel addresses.
18
+
19
+ Key behaviors
20
+ - Saves only if caches are enabled (CentralConfig.use_caches) and content has
21
+ changed (hash comparison), keeping I/O minimal and predictable.
22
+ - Uses orjson for fast binary writes and json for reads with a custom
23
+ object_hook to rebuild nested defaultdict structures.
24
+ - Save/load/clear operations are synchronized via a semaphore and executed via
25
+ the CentralUnit looper to avoid blocking the event loop.
26
+
27
+ Helper functions are provided to build cache paths and filenames and to
28
+ optionally clean up stale cache directories.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from abc import ABC
34
+ import asyncio
35
+ from collections import defaultdict
36
+ from collections.abc import Mapping
37
+ from datetime import datetime
38
+ import json
39
+ import logging
40
+ import os
41
+ from typing import Any, Final
42
+
43
+ import orjson
44
+ from slugify import slugify
45
+
46
+ from aiohomematic import central as hmcu
47
+ from aiohomematic.const import (
48
+ ADDRESS_SEPARATOR,
49
+ CACHE_PATH,
50
+ FILE_DEVICES,
51
+ FILE_PARAMSETS,
52
+ INIT_DATETIME,
53
+ UTF_8,
54
+ DataOperationResult,
55
+ DeviceDescription,
56
+ ParameterData,
57
+ ParamsetKey,
58
+ )
59
+ from aiohomematic.model.device import Device
60
+ from aiohomematic.support import (
61
+ check_or_create_directory,
62
+ delete_file,
63
+ get_device_address,
64
+ get_split_channel_address,
65
+ hash_sha256,
66
+ regular_to_default_dict_hook,
67
+ )
68
+
69
+ _LOGGER: Final = logging.getLogger(__name__)
70
+
71
+
72
+ class BasePersistentCache(ABC):
73
+ """Cache for files."""
74
+
75
+ __slots__ = (
76
+ "_cache_dir",
77
+ "_central",
78
+ "_file_postfix",
79
+ "_filename",
80
+ "_persistent_cache",
81
+ "_save_load_semaphore",
82
+ "last_hash_saved",
83
+ "last_save_triggered",
84
+ )
85
+
86
+ _file_postfix: str
87
+
88
+ def __init__(
89
+ self,
90
+ central: hmcu.CentralUnit,
91
+ persistent_cache: dict[str, Any],
92
+ ) -> None:
93
+ """Initialize the base class of the persistent cache."""
94
+ self._save_load_semaphore: Final = asyncio.Semaphore()
95
+ self._central: Final = central
96
+ self._cache_dir: Final = _get_cache_path(storage_folder=central.config.storage_folder)
97
+ self._filename: Final = _get_filename(central_name=central.name, file_name=self._file_postfix)
98
+ self._persistent_cache: Final = persistent_cache
99
+ self.last_save_triggered: datetime = INIT_DATETIME
100
+ self.last_hash_saved = hash_sha256(value=persistent_cache)
101
+
102
+ @property
103
+ def cache_hash(self) -> str:
104
+ """Return the hash of the cache."""
105
+ return hash_sha256(value=self._persistent_cache)
106
+
107
+ @property
108
+ def data_changed(self) -> bool:
109
+ """Return if the data has changed."""
110
+ return self.cache_hash != self.last_hash_saved
111
+
112
+ @property
113
+ def _file_path(self) -> str:
114
+ """Return the full file path."""
115
+ return os.path.join(self._cache_dir, self._filename)
116
+
117
+ async def save(self) -> DataOperationResult:
118
+ """Save current data to disk."""
119
+ if not self._should_save:
120
+ return DataOperationResult.NO_SAVE
121
+
122
+ def _perform_save() -> DataOperationResult:
123
+ try:
124
+ with open(file=self._file_path, mode="wb") as file_pointer:
125
+ file_pointer.write(
126
+ orjson.dumps(
127
+ self._persistent_cache,
128
+ option=orjson.OPT_NON_STR_KEYS,
129
+ )
130
+ )
131
+ self.last_hash_saved = self.cache_hash
132
+ except json.JSONDecodeError:
133
+ return DataOperationResult.SAVE_FAIL
134
+ return DataOperationResult.SAVE_SUCCESS
135
+
136
+ async with self._save_load_semaphore:
137
+ return await self._central.looper.async_add_executor_job(
138
+ _perform_save, name=f"save-persistent-cache-{self._filename}"
139
+ )
140
+
141
+ @property
142
+ def _should_save(self) -> bool:
143
+ """Determine if save operation should proceed."""
144
+ self.last_save_triggered = datetime.now()
145
+ return (
146
+ check_or_create_directory(self._cache_dir)
147
+ and self._central.config.use_caches
148
+ and self.cache_hash != self.last_hash_saved
149
+ )
150
+
151
+ async def load(self) -> DataOperationResult:
152
+ """Load data from disk into the dictionary."""
153
+ if not check_or_create_directory(self._cache_dir) or not os.path.exists(self._file_path):
154
+ return DataOperationResult.NO_LOAD
155
+
156
+ def _perform_load() -> DataOperationResult:
157
+ with open(file=self._file_path, encoding=UTF_8) as file_pointer:
158
+ try:
159
+ data = json.loads(file_pointer.read(), object_hook=regular_to_default_dict_hook)
160
+ if (converted_hash := hash_sha256(value=data)) == self.last_hash_saved:
161
+ return DataOperationResult.NO_LOAD
162
+ self._persistent_cache.clear()
163
+ self._persistent_cache.update(data)
164
+ self.last_hash_saved = converted_hash
165
+ except json.JSONDecodeError:
166
+ return DataOperationResult.LOAD_FAIL
167
+ return DataOperationResult.LOAD_SUCCESS
168
+
169
+ async with self._save_load_semaphore:
170
+ return await self._central.looper.async_add_executor_job(
171
+ _perform_load, name=f"load-persistent-cache-{self._filename}"
172
+ )
173
+
174
+ async def clear(self) -> None:
175
+ """Remove stored file from disk."""
176
+
177
+ def _perform_clear() -> None:
178
+ delete_file(folder=self._cache_dir, file_name=self._filename)
179
+ self._persistent_cache.clear()
180
+
181
+ async with self._save_load_semaphore:
182
+ await self._central.looper.async_add_executor_job(_perform_clear, name="clear-persistent-cache")
183
+
184
+
185
+ class DeviceDescriptionCache(BasePersistentCache):
186
+ """Cache for device/channel names."""
187
+
188
+ __slots__ = (
189
+ "_addresses",
190
+ "_device_descriptions",
191
+ "_raw_device_descriptions",
192
+ )
193
+
194
+ _file_postfix = FILE_DEVICES
195
+
196
+ def __init__(self, central: hmcu.CentralUnit) -> None:
197
+ """Initialize the device description cache."""
198
+ # {interface_id, [device_descriptions]}
199
+ self._raw_device_descriptions: Final[dict[str, list[DeviceDescription]]] = defaultdict(list)
200
+ super().__init__(
201
+ central=central,
202
+ persistent_cache=self._raw_device_descriptions,
203
+ )
204
+ # {interface_id, {device_address, [channel_address]}}
205
+ self._addresses: Final[dict[str, dict[str, set[str]]]] = defaultdict(lambda: defaultdict(set))
206
+ # {interface_id, {address, device_descriptions}}
207
+ self._device_descriptions: Final[dict[str, dict[str, DeviceDescription]]] = defaultdict(dict)
208
+
209
+ def add_device(self, interface_id: str, device_description: DeviceDescription) -> None:
210
+ """Add a device to the cache."""
211
+ self._remove_device(
212
+ interface_id=interface_id,
213
+ addresses_to_remove=[device_description["ADDRESS"]],
214
+ )
215
+ self._raw_device_descriptions[interface_id].append(device_description)
216
+ self._process_device_description(interface_id=interface_id, device_description=device_description)
217
+
218
+ def get_raw_device_descriptions(self, interface_id: str) -> list[DeviceDescription]:
219
+ """Retrieve raw device descriptions from the cache."""
220
+ return self._raw_device_descriptions[interface_id]
221
+
222
+ def remove_device(self, device: Device) -> None:
223
+ """Remove device from cache."""
224
+ self._remove_device(
225
+ interface_id=device.interface_id,
226
+ addresses_to_remove=[device.address, *device.channels.keys()],
227
+ )
228
+
229
+ def _remove_device(self, interface_id: str, addresses_to_remove: list[str]) -> None:
230
+ """Remove a device from the cache."""
231
+ self._raw_device_descriptions[interface_id] = [
232
+ device
233
+ for device in self._raw_device_descriptions[interface_id]
234
+ if device["ADDRESS"] not in addresses_to_remove
235
+ ]
236
+ for address in addresses_to_remove:
237
+ try:
238
+ if ADDRESS_SEPARATOR not in address and self._addresses[interface_id].get(address):
239
+ del self._addresses[interface_id][address]
240
+ if self._device_descriptions[interface_id].get(address):
241
+ del self._device_descriptions[interface_id][address]
242
+ except KeyError:
243
+ _LOGGER.warning("REMOVE_DEVICE failed: Unable to delete: %s", address)
244
+
245
+ def get_addresses(self, interface_id: str) -> tuple[str, ...]:
246
+ """Return the addresses by interface."""
247
+ return tuple(self._addresses[interface_id].keys())
248
+
249
+ def get_device_descriptions(self, interface_id: str) -> Mapping[str, DeviceDescription]:
250
+ """Return the devices by interface."""
251
+ return self._device_descriptions[interface_id]
252
+
253
+ def find_device_description(self, interface_id: str, device_address: str) -> DeviceDescription | None:
254
+ """Return the device description by interface and device_address."""
255
+ return self._device_descriptions[interface_id].get(device_address)
256
+
257
+ def get_device_description(self, interface_id: str, address: str) -> DeviceDescription:
258
+ """Return the device description by interface and device_address."""
259
+ return self._device_descriptions[interface_id][address]
260
+
261
+ def get_device_with_channels(self, interface_id: str, device_address: str) -> Mapping[str, DeviceDescription]:
262
+ """Return the device dict by interface and device_address."""
263
+ device_descriptions: dict[str, DeviceDescription] = {
264
+ device_address: self.get_device_description(interface_id=interface_id, address=device_address)
265
+ }
266
+ children = device_descriptions[device_address]["CHILDREN"]
267
+ for channel_address in children:
268
+ device_descriptions[channel_address] = self.get_device_description(
269
+ interface_id=interface_id, address=channel_address
270
+ )
271
+ return device_descriptions
272
+
273
+ def get_model(self, device_address: str) -> str | None:
274
+ """Return the device type."""
275
+ for data in self._device_descriptions.values():
276
+ if items := data.get(device_address):
277
+ return items["TYPE"]
278
+ return None
279
+
280
+ def _convert_device_descriptions(self, interface_id: str, device_descriptions: list[DeviceDescription]) -> None:
281
+ """Convert provided list of device descriptions."""
282
+ for device_description in device_descriptions:
283
+ self._process_device_description(interface_id=interface_id, device_description=device_description)
284
+
285
+ def _process_device_description(self, interface_id: str, device_description: DeviceDescription) -> None:
286
+ """Convert provided dict of device descriptions."""
287
+ address = device_description["ADDRESS"]
288
+ device_address = get_device_address(address)
289
+ self._device_descriptions[interface_id][address] = device_description
290
+
291
+ if device_address not in self._addresses[interface_id][device_address]:
292
+ self._addresses[interface_id][device_address].add(device_address)
293
+ self._addresses[interface_id][device_address].add(address)
294
+
295
+ async def load(self) -> DataOperationResult:
296
+ """Load device data from disk into _device_description_cache."""
297
+ if not self._central.config.use_caches:
298
+ _LOGGER.debug("load: not caching paramset descriptions for %s", self._central.name)
299
+ return DataOperationResult.NO_LOAD
300
+ if (result := await super().load()) == DataOperationResult.LOAD_SUCCESS:
301
+ for (
302
+ interface_id,
303
+ device_descriptions,
304
+ ) in self._raw_device_descriptions.items():
305
+ self._convert_device_descriptions(interface_id, device_descriptions)
306
+ return result
307
+
308
+
309
+ class ParamsetDescriptionCache(BasePersistentCache):
310
+ """Cache for paramset descriptions."""
311
+
312
+ __slots__ = (
313
+ "_address_parameter_cache",
314
+ "_raw_paramset_descriptions",
315
+ )
316
+
317
+ _file_postfix = FILE_PARAMSETS
318
+
319
+ def __init__(self, central: hmcu.CentralUnit) -> None:
320
+ """Init the paramset description cache."""
321
+ # {interface_id, {channel_address, paramsets}}
322
+ self._raw_paramset_descriptions: Final[dict[str, dict[str, dict[ParamsetKey, dict[str, ParameterData]]]]] = (
323
+ defaultdict(lambda: defaultdict(lambda: defaultdict(dict)))
324
+ )
325
+ super().__init__(
326
+ central=central,
327
+ persistent_cache=self._raw_paramset_descriptions,
328
+ )
329
+
330
+ # {(device_address, parameter), [channel_no]}
331
+ self._address_parameter_cache: Final[dict[tuple[str, str], set[int | None]]] = {}
332
+
333
+ @property
334
+ def raw_paramset_descriptions(
335
+ self,
336
+ ) -> Mapping[str, Mapping[str, Mapping[ParamsetKey, Mapping[str, ParameterData]]]]:
337
+ """Return the paramset descriptions."""
338
+ return self._raw_paramset_descriptions
339
+
340
+ def add(
341
+ self,
342
+ interface_id: str,
343
+ channel_address: str,
344
+ paramset_key: ParamsetKey,
345
+ paramset_description: dict[str, ParameterData],
346
+ ) -> None:
347
+ """Add paramset description to cache."""
348
+ self._raw_paramset_descriptions[interface_id][channel_address][paramset_key] = paramset_description
349
+ self._add_address_parameter(channel_address=channel_address, paramsets=[paramset_description])
350
+
351
+ def remove_device(self, device: Device) -> None:
352
+ """Remove device paramset descriptions from cache."""
353
+ if interface := self._raw_paramset_descriptions.get(device.interface_id):
354
+ for channel_address in device.channels:
355
+ if channel_address in interface:
356
+ del self._raw_paramset_descriptions[device.interface_id][channel_address]
357
+
358
+ def has_interface_id(self, interface_id: str) -> bool:
359
+ """Return if interface is in paramset_descriptions cache."""
360
+ return interface_id in self._raw_paramset_descriptions
361
+
362
+ def get_paramset_keys(self, interface_id: str, channel_address: str) -> tuple[ParamsetKey, ...]:
363
+ """Get paramset_keys from paramset descriptions cache."""
364
+ return tuple(self._raw_paramset_descriptions[interface_id][channel_address])
365
+
366
+ def get_channel_paramset_descriptions(
367
+ self, interface_id: str, channel_address: str
368
+ ) -> Mapping[ParamsetKey, Mapping[str, ParameterData]]:
369
+ """Get paramset descriptions for a channelfrom cache."""
370
+ return self._raw_paramset_descriptions[interface_id].get(channel_address, {})
371
+
372
+ def get_paramset_descriptions(
373
+ self, interface_id: str, channel_address: str, paramset_key: ParamsetKey
374
+ ) -> Mapping[str, ParameterData]:
375
+ """Get paramset descriptions from cache."""
376
+ return self._raw_paramset_descriptions[interface_id][channel_address][paramset_key]
377
+
378
+ def get_parameter_data(
379
+ self, interface_id: str, channel_address: str, paramset_key: ParamsetKey, parameter: str
380
+ ) -> ParameterData | None:
381
+ """Get parameter_data from cache."""
382
+ return self._raw_paramset_descriptions[interface_id][channel_address][paramset_key].get(parameter)
383
+
384
+ def is_in_multiple_channels(self, channel_address: str, parameter: str) -> bool:
385
+ """Check if parameter is in multiple channels per device."""
386
+ if ADDRESS_SEPARATOR not in channel_address:
387
+ return False
388
+ if channels := self._address_parameter_cache.get((get_device_address(channel_address), parameter)):
389
+ return len(channels) > 1
390
+ return False
391
+
392
+ def get_channel_addresses_by_paramset_key(
393
+ self, interface_id: str, device_address: str
394
+ ) -> Mapping[ParamsetKey, list[str]]:
395
+ """Get device channel addresses."""
396
+ channel_addresses: dict[ParamsetKey, list[str]] = {}
397
+ interface_paramset_descriptions = self._raw_paramset_descriptions[interface_id]
398
+ for (
399
+ channel_address,
400
+ paramset_descriptions,
401
+ ) in interface_paramset_descriptions.items():
402
+ if channel_address.startswith(device_address):
403
+ for p_key in paramset_descriptions:
404
+ if (paramset_key := ParamsetKey(p_key)) not in channel_addresses:
405
+ channel_addresses[paramset_key] = []
406
+ channel_addresses[paramset_key].append(channel_address)
407
+
408
+ return channel_addresses
409
+
410
+ def _init_address_parameter_list(self) -> None:
411
+ """
412
+ Initialize a device_address/parameter list.
413
+
414
+ Used to identify, if a parameter name exists is in multiple channels.
415
+ """
416
+ for channel_paramsets in self._raw_paramset_descriptions.values():
417
+ for channel_address, paramsets in channel_paramsets.items():
418
+ self._add_address_parameter(channel_address=channel_address, paramsets=list(paramsets.values()))
419
+
420
+ def _add_address_parameter(self, channel_address: str, paramsets: list[dict[str, Any]]) -> None:
421
+ """Add address parameter to cache."""
422
+ device_address, channel_no = get_split_channel_address(channel_address)
423
+ for paramset in paramsets:
424
+ if not paramset:
425
+ continue
426
+ for parameter in paramset:
427
+ if (device_address, parameter) not in self._address_parameter_cache:
428
+ self._address_parameter_cache[(device_address, parameter)] = set()
429
+ self._address_parameter_cache[(device_address, parameter)].add(channel_no)
430
+
431
+ async def load(self) -> DataOperationResult:
432
+ """Load paramset descriptions from disk into paramset cache."""
433
+ if not self._central.config.use_caches:
434
+ _LOGGER.debug("load: not caching device descriptions for %s", self._central.name)
435
+ return DataOperationResult.NO_LOAD
436
+ if (result := await super().load()) == DataOperationResult.LOAD_SUCCESS:
437
+ self._init_address_parameter_list()
438
+ return result
439
+
440
+ async def save(self) -> DataOperationResult:
441
+ """Save current paramset descriptions to disk."""
442
+ return await super().save()
443
+
444
+
445
+ def _get_cache_path(storage_folder: str) -> str:
446
+ """Return the cache path."""
447
+ return f"{storage_folder}/{CACHE_PATH}"
448
+
449
+
450
+ def _get_filename(central_name: str, file_name: str) -> str:
451
+ """Return the cache filename."""
452
+ return f"{slugify(central_name)}_{file_name}"
453
+
454
+
455
+ def cleanup_cache_dirs(central_name: str, storage_folder: str) -> None:
456
+ """Clean up the used cached directories."""
457
+ cache_dir = _get_cache_path(storage_folder=storage_folder)
458
+ for file_to_delete in (FILE_DEVICES, FILE_PARAMSETS):
459
+ delete_file(folder=cache_dir, file_name=_get_filename(central_name=central_name, file_name=file_to_delete))