aiohomematic 2025.10.9__py3-none-any.whl → 2025.10.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/central/__init__.py +20 -9
- aiohomematic/central/rpc_server.py +21 -27
- aiohomematic/client/__init__.py +4 -2
- aiohomematic/client/json_rpc.py +2 -2
- aiohomematic/const.py +18 -9
- aiohomematic/model/custom/climate.py +4 -4
- aiohomematic/model/data_point.py +3 -4
- aiohomematic/model/device.py +5 -5
- aiohomematic/model/update.py +2 -2
- aiohomematic/store/persistent.py +124 -71
- aiohomematic/support.py +9 -0
- {aiohomematic-2025.10.9.dist-info → aiohomematic-2025.10.11.dist-info}/METADATA +1 -1
- {aiohomematic-2025.10.9.dist-info → aiohomematic-2025.10.11.dist-info}/RECORD +16 -18
- aiohomematic-2025.10.11.dist-info/top_level.txt +1 -0
- aiohomematic-2025.10.9.dist-info/top_level.txt +0 -2
- aiohomematic_support/__init__.py +0 -1
- aiohomematic_support/client_local.py +0 -361
- {aiohomematic-2025.10.9.dist-info → aiohomematic-2025.10.11.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.10.9.dist-info → aiohomematic-2025.10.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
"""The local client-object and its methods."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from _collections import defaultdict
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
import importlib.resources
|
|
9
|
-
import logging
|
|
10
|
-
import os
|
|
11
|
-
from typing import Any, Final, cast
|
|
12
|
-
|
|
13
|
-
import orjson
|
|
14
|
-
|
|
15
|
-
from aiohomematic.client import _LOGGER, Client, ClientConfig
|
|
16
|
-
from aiohomematic.const import (
|
|
17
|
-
ADDRESS_SEPARATOR,
|
|
18
|
-
DP_KEY_VALUE,
|
|
19
|
-
UTF_8,
|
|
20
|
-
WAIT_FOR_CALLBACK,
|
|
21
|
-
CallSource,
|
|
22
|
-
CommandRxMode,
|
|
23
|
-
DescriptionMarker,
|
|
24
|
-
DeviceDescription,
|
|
25
|
-
Interface,
|
|
26
|
-
ParameterData,
|
|
27
|
-
ParamsetKey,
|
|
28
|
-
ProductGroup,
|
|
29
|
-
ProgramData,
|
|
30
|
-
ProxyInitState,
|
|
31
|
-
SystemInformation,
|
|
32
|
-
SystemVariableData,
|
|
33
|
-
)
|
|
34
|
-
from aiohomematic.decorators import inspector
|
|
35
|
-
from aiohomematic.support import is_channel_address
|
|
36
|
-
|
|
37
|
-
LOCAL_SERIAL: Final = "0815_4711"
|
|
38
|
-
BACKEND_LOCAL: Final = "PyDevCCU"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class ClientLocal(Client): # pragma: no cover
|
|
42
|
-
"""Local client object to provide access to locally stored files."""
|
|
43
|
-
|
|
44
|
-
def __init__(self, *, client_config: ClientConfig, local_resources: LocalRessources) -> None:
|
|
45
|
-
"""Initialize the Client."""
|
|
46
|
-
super().__init__(client_config=client_config)
|
|
47
|
-
self._local_resources = local_resources
|
|
48
|
-
self._paramset_descriptions_cache: dict[str, dict[ParamsetKey, dict[str, ParameterData]]] = defaultdict(
|
|
49
|
-
lambda: defaultdict(dict)
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
async def init_client(self) -> None:
|
|
53
|
-
"""Init the client."""
|
|
54
|
-
self._system_information = await self._get_system_information()
|
|
55
|
-
|
|
56
|
-
@property
|
|
57
|
-
def available(self) -> bool:
|
|
58
|
-
"""Return the availability of the client."""
|
|
59
|
-
return True
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def model(self) -> str:
|
|
63
|
-
"""Return the model of the backend."""
|
|
64
|
-
return BACKEND_LOCAL
|
|
65
|
-
|
|
66
|
-
def get_product_group(self, *, model: str) -> ProductGroup:
|
|
67
|
-
"""Return the product group."""
|
|
68
|
-
l_model = model.lower()
|
|
69
|
-
if l_model.startswith("hmipw"):
|
|
70
|
-
return ProductGroup.HMIPW
|
|
71
|
-
if l_model.startswith("hmip"):
|
|
72
|
-
return ProductGroup.HMIP
|
|
73
|
-
if l_model.startswith("hmw"):
|
|
74
|
-
return ProductGroup.HMW
|
|
75
|
-
if l_model.startswith("hm"):
|
|
76
|
-
return ProductGroup.HM
|
|
77
|
-
return ProductGroup.UNKNOWN
|
|
78
|
-
|
|
79
|
-
@property
|
|
80
|
-
def supports_ping_pong(self) -> bool:
|
|
81
|
-
"""Return the supports_ping_pong info of the backend."""
|
|
82
|
-
return True
|
|
83
|
-
|
|
84
|
-
@property
|
|
85
|
-
def supports_push_updates(self) -> bool:
|
|
86
|
-
"""Return the client supports push update."""
|
|
87
|
-
return True
|
|
88
|
-
|
|
89
|
-
async def initialize_proxy(self) -> ProxyInitState:
|
|
90
|
-
"""Init the proxy has to tell the backend where to send the events."""
|
|
91
|
-
return ProxyInitState.INIT_SUCCESS
|
|
92
|
-
|
|
93
|
-
async def deinitialize_proxy(self) -> ProxyInitState:
|
|
94
|
-
"""De-init to stop the backend from sending events for this remote."""
|
|
95
|
-
return ProxyInitState.DE_INIT_SUCCESS
|
|
96
|
-
|
|
97
|
-
async def stop(self) -> None:
|
|
98
|
-
"""Stop depending services."""
|
|
99
|
-
|
|
100
|
-
@inspector(re_raise=False, measure_performance=True)
|
|
101
|
-
async def fetch_all_device_data(self) -> None:
|
|
102
|
-
"""Fetch all device data from the backend."""
|
|
103
|
-
|
|
104
|
-
@inspector(re_raise=False, measure_performance=True)
|
|
105
|
-
async def fetch_device_details(self) -> None:
|
|
106
|
-
"""Fetch names from the backend."""
|
|
107
|
-
|
|
108
|
-
@inspector(re_raise=False, no_raise_return=False)
|
|
109
|
-
async def is_connected(self) -> bool:
|
|
110
|
-
"""
|
|
111
|
-
Perform actions required for connectivity check.
|
|
112
|
-
|
|
113
|
-
Connection is not connected, if three consecutive checks fail.
|
|
114
|
-
Return connectivity state.
|
|
115
|
-
"""
|
|
116
|
-
return True
|
|
117
|
-
|
|
118
|
-
def is_callback_alive(self) -> bool:
|
|
119
|
-
"""Return if XmlRPC-Server is alive based on received events for this client."""
|
|
120
|
-
return True
|
|
121
|
-
|
|
122
|
-
@inspector(re_raise=False, no_raise_return=False)
|
|
123
|
-
async def check_connection_availability(self, *, handle_ping_pong: bool) -> bool:
|
|
124
|
-
"""Send ping to the backend to generate PONG event."""
|
|
125
|
-
if handle_ping_pong and self.supports_ping_pong:
|
|
126
|
-
self._ping_pong_cache.handle_send_ping(ping_ts=datetime.now())
|
|
127
|
-
return True
|
|
128
|
-
|
|
129
|
-
@inspector
|
|
130
|
-
async def execute_program(self, *, pid: str) -> bool:
|
|
131
|
-
"""Execute a program on the backend."""
|
|
132
|
-
return True
|
|
133
|
-
|
|
134
|
-
@inspector
|
|
135
|
-
async def set_program_state(self, *, pid: str, state: bool) -> bool:
|
|
136
|
-
"""Set the program state on the backend."""
|
|
137
|
-
return True
|
|
138
|
-
|
|
139
|
-
@inspector(measure_performance=True)
|
|
140
|
-
async def set_system_variable(self, *, legacy_name: str, value: Any) -> bool:
|
|
141
|
-
"""Set a system variable on the backend."""
|
|
142
|
-
return True
|
|
143
|
-
|
|
144
|
-
@inspector
|
|
145
|
-
async def delete_system_variable(self, *, name: str) -> bool:
|
|
146
|
-
"""Delete a system variable from the backend."""
|
|
147
|
-
return True
|
|
148
|
-
|
|
149
|
-
@inspector
|
|
150
|
-
async def get_system_variable(self, *, name: str) -> str:
|
|
151
|
-
"""Get single system variable from the backend."""
|
|
152
|
-
return "Empty"
|
|
153
|
-
|
|
154
|
-
@inspector(re_raise=False)
|
|
155
|
-
async def get_all_system_variables(
|
|
156
|
-
self, *, markers: tuple[DescriptionMarker | str, ...]
|
|
157
|
-
) -> tuple[SystemVariableData, ...]:
|
|
158
|
-
"""Get all system variables from the backend."""
|
|
159
|
-
return ()
|
|
160
|
-
|
|
161
|
-
@inspector(re_raise=False)
|
|
162
|
-
async def get_all_programs(self, *, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...]:
|
|
163
|
-
"""Get all programs, if available."""
|
|
164
|
-
return ()
|
|
165
|
-
|
|
166
|
-
@inspector(re_raise=False, no_raise_return={})
|
|
167
|
-
async def get_all_rooms(self) -> dict[str, set[str]]:
|
|
168
|
-
"""Get all rooms, if available."""
|
|
169
|
-
return {}
|
|
170
|
-
|
|
171
|
-
@inspector(re_raise=False, no_raise_return={})
|
|
172
|
-
async def get_all_functions(self) -> dict[str, set[str]]:
|
|
173
|
-
"""Get all functions, if available."""
|
|
174
|
-
return {}
|
|
175
|
-
|
|
176
|
-
async def _get_system_information(self) -> SystemInformation:
|
|
177
|
-
"""Get system information of the backend."""
|
|
178
|
-
return SystemInformation(available_interfaces=(Interface.BIDCOS_RF,), serial=LOCAL_SERIAL)
|
|
179
|
-
|
|
180
|
-
@inspector(re_raise=False, measure_performance=True)
|
|
181
|
-
async def list_devices(self) -> tuple[DeviceDescription, ...] | None:
|
|
182
|
-
"""Get device descriptions from the backend."""
|
|
183
|
-
if not self._local_resources:
|
|
184
|
-
_LOGGER.warning(
|
|
185
|
-
"LIST_DEVICES: missing local_resources in config for %s",
|
|
186
|
-
self.central.name,
|
|
187
|
-
)
|
|
188
|
-
return None
|
|
189
|
-
device_descriptions: list[DeviceDescription] = []
|
|
190
|
-
if local_device_descriptions := cast(
|
|
191
|
-
list[Any],
|
|
192
|
-
await self._load_all_json_files(
|
|
193
|
-
anchor=self._local_resources.anchor,
|
|
194
|
-
resource=self._local_resources.device_description_dir,
|
|
195
|
-
include_list=list(self._local_resources.address_device_translation.values()),
|
|
196
|
-
exclude_list=self._local_resources.ignore_devices_on_create,
|
|
197
|
-
),
|
|
198
|
-
):
|
|
199
|
-
for device_description in local_device_descriptions:
|
|
200
|
-
device_descriptions.extend(device_description)
|
|
201
|
-
return tuple(device_descriptions)
|
|
202
|
-
|
|
203
|
-
@inspector(log_level=logging.NOTSET)
|
|
204
|
-
async def get_value(
|
|
205
|
-
self,
|
|
206
|
-
*,
|
|
207
|
-
channel_address: str,
|
|
208
|
-
paramset_key: ParamsetKey,
|
|
209
|
-
parameter: str,
|
|
210
|
-
call_source: CallSource = CallSource.MANUAL_OR_SCHEDULED,
|
|
211
|
-
) -> Any:
|
|
212
|
-
"""Return a value from the backend."""
|
|
213
|
-
return
|
|
214
|
-
|
|
215
|
-
@inspector(re_raise=False, no_raise_return=set())
|
|
216
|
-
async def set_value(
|
|
217
|
-
self,
|
|
218
|
-
*,
|
|
219
|
-
channel_address: str,
|
|
220
|
-
paramset_key: ParamsetKey,
|
|
221
|
-
parameter: str,
|
|
222
|
-
value: Any,
|
|
223
|
-
wait_for_callback: int | None = WAIT_FOR_CALLBACK,
|
|
224
|
-
rx_mode: CommandRxMode | None = None,
|
|
225
|
-
check_against_pd: bool = False,
|
|
226
|
-
) -> set[DP_KEY_VALUE]:
|
|
227
|
-
"""Set single value on paramset VALUES."""
|
|
228
|
-
# store the send value in the last_value_send_cache
|
|
229
|
-
result = self._last_value_send_cache.add_set_value(
|
|
230
|
-
channel_address=channel_address, parameter=parameter, value=value
|
|
231
|
-
)
|
|
232
|
-
# fire an event to fake the state change for a simple parameter
|
|
233
|
-
await self.central.data_point_event(
|
|
234
|
-
interface_id=self.interface_id, channel_address=channel_address, parameter=parameter, value=value
|
|
235
|
-
)
|
|
236
|
-
return result
|
|
237
|
-
|
|
238
|
-
@inspector
|
|
239
|
-
async def get_paramset(
|
|
240
|
-
self,
|
|
241
|
-
*,
|
|
242
|
-
address: str,
|
|
243
|
-
paramset_key: ParamsetKey | str,
|
|
244
|
-
call_source: CallSource = CallSource.MANUAL_OR_SCHEDULED,
|
|
245
|
-
) -> Any:
|
|
246
|
-
"""
|
|
247
|
-
Return a paramset from the backend.
|
|
248
|
-
|
|
249
|
-
Address is usually the channel_address,
|
|
250
|
-
but for bidcos devices there is a master paramset at the device.
|
|
251
|
-
"""
|
|
252
|
-
return {}
|
|
253
|
-
|
|
254
|
-
async def _get_paramset_description(
|
|
255
|
-
self, *, address: str, paramset_key: ParamsetKey
|
|
256
|
-
) -> dict[str, ParameterData] | None:
|
|
257
|
-
"""Get paramset description from the backend."""
|
|
258
|
-
if not self._local_resources:
|
|
259
|
-
_LOGGER.warning(
|
|
260
|
-
"GET_PARAMSET_DESCRIPTION: missing local_resources in config for %s",
|
|
261
|
-
self.central.name,
|
|
262
|
-
)
|
|
263
|
-
return None
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
address not in self._paramset_descriptions_cache
|
|
267
|
-
and (file_name := self._local_resources.address_device_translation.get(address.split(ADDRESS_SEPARATOR)[0]))
|
|
268
|
-
and (
|
|
269
|
-
data := await self._load_json_file(
|
|
270
|
-
anchor=self._local_resources.anchor,
|
|
271
|
-
resource=self._local_resources.paramset_description_dir,
|
|
272
|
-
filename=file_name,
|
|
273
|
-
)
|
|
274
|
-
)
|
|
275
|
-
):
|
|
276
|
-
self._paramset_descriptions_cache.update(data)
|
|
277
|
-
|
|
278
|
-
return self._paramset_descriptions_cache[address].get(paramset_key)
|
|
279
|
-
|
|
280
|
-
@inspector(measure_performance=True)
|
|
281
|
-
async def put_paramset(
|
|
282
|
-
self,
|
|
283
|
-
*,
|
|
284
|
-
channel_address: str,
|
|
285
|
-
paramset_key_or_link_address: ParamsetKey | str,
|
|
286
|
-
values: Any,
|
|
287
|
-
wait_for_callback: int | None = WAIT_FOR_CALLBACK,
|
|
288
|
-
rx_mode: CommandRxMode | None = None,
|
|
289
|
-
check_against_pd: bool = False,
|
|
290
|
-
) -> set[DP_KEY_VALUE]:
|
|
291
|
-
"""
|
|
292
|
-
Set paramsets manually.
|
|
293
|
-
|
|
294
|
-
Address is usually the channel_address,
|
|
295
|
-
but for bidcos devices there is a master paramset at the device.
|
|
296
|
-
"""
|
|
297
|
-
# store the send value in the last_value_send_cache
|
|
298
|
-
if isinstance(paramset_key_or_link_address, str) and is_channel_address(address=paramset_key_or_link_address):
|
|
299
|
-
result = set()
|
|
300
|
-
else:
|
|
301
|
-
result = self._last_value_send_cache.add_put_paramset(
|
|
302
|
-
channel_address=channel_address,
|
|
303
|
-
paramset_key=ParamsetKey(paramset_key_or_link_address),
|
|
304
|
-
values=values,
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
# fire an event to fake the state change for the content of a paramset
|
|
308
|
-
for parameter in values:
|
|
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
|
-
)
|
|
315
|
-
return result
|
|
316
|
-
|
|
317
|
-
async def _load_all_json_files(
|
|
318
|
-
self,
|
|
319
|
-
*,
|
|
320
|
-
anchor: str,
|
|
321
|
-
resource: str,
|
|
322
|
-
include_list: list[str] | None = None,
|
|
323
|
-
exclude_list: list[str] | None = None,
|
|
324
|
-
) -> list[Any] | None:
|
|
325
|
-
"""Load all json files from disk into dict."""
|
|
326
|
-
if not include_list:
|
|
327
|
-
return []
|
|
328
|
-
if not exclude_list:
|
|
329
|
-
exclude_list = []
|
|
330
|
-
result: list[Any] = []
|
|
331
|
-
resource_path = os.path.join(str(importlib.resources.files(anchor)), resource)
|
|
332
|
-
for filename in os.listdir(resource_path):
|
|
333
|
-
if filename not in include_list or filename in exclude_list:
|
|
334
|
-
continue
|
|
335
|
-
if file_content := await self._load_json_file(anchor=anchor, resource=resource, filename=filename):
|
|
336
|
-
result.append(file_content)
|
|
337
|
-
return result
|
|
338
|
-
|
|
339
|
-
async def _load_json_file(self, *, anchor: str, resource: str, filename: str) -> Any | None:
|
|
340
|
-
"""Load json file from disk into dict."""
|
|
341
|
-
package_path = str(importlib.resources.files(anchor))
|
|
342
|
-
|
|
343
|
-
def _perform_load() -> Any | None:
|
|
344
|
-
with open(
|
|
345
|
-
file=os.path.join(package_path, resource, filename),
|
|
346
|
-
encoding=UTF_8,
|
|
347
|
-
) as fptr:
|
|
348
|
-
return orjson.loads(fptr.read())
|
|
349
|
-
|
|
350
|
-
return await self.central.looper.async_add_executor_job(_perform_load, name="load-json-file")
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
@dataclass(frozen=True, kw_only=True, slots=True)
|
|
354
|
-
class LocalRessources:
|
|
355
|
-
"""Dataclass with information for local client."""
|
|
356
|
-
|
|
357
|
-
address_device_translation: dict[str, str]
|
|
358
|
-
ignore_devices_on_create: list[str]
|
|
359
|
-
anchor: str = "pydevccu"
|
|
360
|
-
device_description_dir: str = "device_descriptions"
|
|
361
|
-
paramset_description_dir: str = "paramset_descriptions"
|
|
File without changes
|
|
File without changes
|