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