aiohomematic 2025.10.10__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.

@@ -18,7 +18,7 @@ from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
18
18
  from aiohomematic import central as hmcu
19
19
  from aiohomematic.central.decorators import callback_backend_system
20
20
  from aiohomematic.const import IP_ANY_V4, PORT_ANY, BackendSystemEvent
21
- from aiohomematic.support import find_free_port, log_boundary_error
21
+ from aiohomematic.support import log_boundary_error
22
22
 
23
23
  _LOGGER: Final = logging.getLogger(__name__)
24
24
 
@@ -177,23 +177,19 @@ class RpcServer(threading.Thread):
177
177
  _initialized: bool = False
178
178
  _instances: Final[dict[tuple[str, int], RpcServer]] = {}
179
179
 
180
- def __init__(
181
- self,
182
- *,
183
- ip_addr: str,
184
- port: int,
185
- ) -> None:
180
+ def __init__(self, *, server: SimpleXMLRPCServer) -> None:
186
181
  """Init XmlRPC server."""
187
- if self._initialized:
188
- return
182
+ self._server = server
183
+ self._server.register_introspection_functions()
184
+ self._server.register_multicall_functions()
185
+ self._server.register_instance(RPCFunctions(rpc_server=self), allow_dotted_names=True)
189
186
  self._initialized = True
190
- self._listen_ip_addr: Final = ip_addr
191
- self._listen_port: Final[int] = find_free_port() if port == PORT_ANY else port
192
- self._address: Final[tuple[str, int]] = (ip_addr, self._listen_port)
187
+ self._address: Final[tuple[str, int]] = cast(tuple[str, int], server.server_address)
188
+ self._listen_ip_addr: Final = self._address[0]
189
+ self._listen_port: Final = self._address[1]
193
190
  self._centrals: Final[dict[str, hmcu.CentralUnit]] = {}
194
- self._simple_rpc_server: SimpleXMLRPCServer
195
191
  self._instances[self._address] = self
196
- threading.Thread.__init__(self, name=f"RpcServer {ip_addr}:{self._listen_port}")
192
+ threading.Thread.__init__(self, name=f"RpcServer {self._listen_ip_addr}:{self._listen_port}")
197
193
 
198
194
  def run(self) -> None:
199
195
  """Run the RPC-Server thread."""
@@ -202,15 +198,15 @@ class RpcServer(threading.Thread):
202
198
  self._listen_ip_addr,
203
199
  self._listen_port,
204
200
  )
205
- if self._simple_rpc_server:
206
- self._simple_rpc_server.serve_forever()
201
+ if self._server:
202
+ self._server.serve_forever()
207
203
 
208
204
  def stop(self) -> None:
209
205
  """Stop the RPC-Server."""
210
206
  _LOGGER.debug("STOP: Shutting down RPC-Server")
211
- self._simple_rpc_server.shutdown()
207
+ self._server.shutdown()
212
208
  _LOGGER.debug("STOP: Stopping RPC-Server")
213
- self._simple_rpc_server.server_close()
209
+ self._server.server_close()
214
210
  # Ensure the server thread has actually terminated to avoid slow teardown
215
211
  with contextlib.suppress(RuntimeError):
216
212
  self.join(timeout=1.0)
@@ -269,16 +265,14 @@ class XmlRpcServer(RpcServer):
269
265
 
270
266
  if self._initialized:
271
267
  return
272
- super().__init__(ip_addr=ip_addr, port=port)
273
- self._simple_rpc_server = HomematicXMLRPCServer(
274
- addr=self._address,
275
- requestHandler=RequestHandler,
276
- logRequests=False,
277
- allow_none=True,
268
+ super().__init__(
269
+ server=HomematicXMLRPCServer(
270
+ addr=(ip_addr, port),
271
+ requestHandler=RequestHandler,
272
+ logRequests=False,
273
+ allow_none=True,
274
+ )
278
275
  )
279
- self._simple_rpc_server.register_introspection_functions()
280
- self._simple_rpc_server.register_multicall_functions()
281
- self._simple_rpc_server.register_instance(RPCFunctions(rpc_server=self), allow_dotted_names=True)
282
276
 
283
277
  def __new__(cls, ip_addr: str, port: int) -> XmlRpcServer: # noqa: PYI034 # kwonly: disable
284
278
  """Create new RPC server."""
@@ -368,8 +368,8 @@ class AioJsonRpcAioHttpClient(LogContextMixin):
368
368
  _LOGGER.debug("POST_SCRIPT: method: %s [%s]", method, script_name)
369
369
 
370
370
  try:
371
- if not response[_JsonKey.ERROR]:
372
- response[_JsonKey.RESULT] = orjson.loads(response[_JsonKey.RESULT])
371
+ if not response[_JsonKey.ERROR] and (resp := response[_JsonKey.RESULT]) and isinstance(resp, str):
372
+ response[_JsonKey.RESULT] = orjson.loads(resp)
373
373
  finally:
374
374
  if not keep_session:
375
375
  await self._do_logout(session_id=session_id)
aiohomematic/const.py CHANGED
@@ -19,7 +19,7 @@ import sys
19
19
  from types import MappingProxyType
20
20
  from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
21
21
 
22
- VERSION: Final = "2025.10.10"
22
+ VERSION: Final = "2025.10.11"
23
23
 
24
24
  # Detect test speedup mode via environment
25
25
  _TEST_SPEEDUP: Final = (
@@ -41,7 +41,7 @@ DEFAULT_MULTIPLIER: Final = 1.0
41
41
  DEFAULT_OPTIONAL_SETTINGS: Final[tuple[OptionalSettings | str, ...]] = ()
42
42
  DEFAULT_PERIODIC_REFRESH_INTERVAL: Final = 15
43
43
  DEFAULT_PROGRAM_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
44
- DEFAULT_SESSION_RECORDER_START_FOR_SECONDS: Final = 120
44
+ DEFAULT_SESSION_RECORDER_START_FOR_SECONDS: Final = 180
45
45
  DEFAULT_STORAGE_DIRECTORY: Final = "aiohomematic_storage"
46
46
  DEFAULT_SYSVAR_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
47
47
  DEFAULT_SYS_SCAN_INTERVAL: Final = 30
@@ -43,6 +43,7 @@ import json
43
43
  import logging
44
44
  import os
45
45
  from typing import Any, Final, Self
46
+ import zipfile
46
47
 
47
48
  import orjson
48
49
  from slugify import slugify
@@ -198,7 +199,13 @@ class BasePersistentFile(ABC):
198
199
  )
199
200
 
200
201
  async def load(self, *, file_path: str | None = None) -> DataOperationResult:
201
- """Load data from disk into the dictionary."""
202
+ """
203
+ Load data from disk into the dictionary.
204
+
205
+ Supports plain JSON files and ZIP archives containing a JSON file.
206
+ When a ZIP archive is provided, the first JSON member inside the archive
207
+ will be loaded.
208
+ """
202
209
  if not file_path and not check_or_create_directory(directory=self._directory):
203
210
  return DataOperationResult.NO_LOAD
204
211
 
@@ -206,16 +213,25 @@ class BasePersistentFile(ABC):
206
213
  return DataOperationResult.NO_LOAD
207
214
 
208
215
  def _perform_load() -> DataOperationResult:
209
- with open(file=file_path, encoding=UTF_8) as file_pointer:
210
- try:
211
- data = json.loads(file_pointer.read(), object_hook=regular_to_default_dict_hook)
212
- if (converted_hash := hash_sha256(value=data)) == self.last_hash_saved:
213
- return DataOperationResult.NO_LOAD
214
- self._persistent_content.clear()
215
- self._persistent_content.update(data)
216
- self.last_hash_saved = converted_hash
217
- except json.JSONDecodeError:
218
- return DataOperationResult.LOAD_FAIL
216
+ try:
217
+ if zipfile.is_zipfile(file_path):
218
+ with zipfile.ZipFile(file_path, mode="r") as zf:
219
+ # Prefer json files; pick the first .json entry if available
220
+ if not (json_members := [n for n in zf.namelist() if n.lower().endswith(".json")]):
221
+ return DataOperationResult.LOAD_FAIL
222
+ raw = zf.read(json_members[0]).decode(UTF_8)
223
+ data = json.loads(raw, object_hook=regular_to_default_dict_hook)
224
+ else:
225
+ with open(file=file_path, encoding=UTF_8) as file_pointer:
226
+ data = json.loads(file_pointer.read(), object_hook=regular_to_default_dict_hook)
227
+
228
+ if (converted_hash := hash_sha256(value=data)) == self.last_hash_saved:
229
+ return DataOperationResult.NO_LOAD
230
+ self._persistent_content.clear()
231
+ self._persistent_content.update(data)
232
+ self.last_hash_saved = converted_hash
233
+ except (json.JSONDecodeError, zipfile.BadZipFile, UnicodeDecodeError, OSError):
234
+ return DataOperationResult.LOAD_FAIL
219
235
  return DataOperationResult.LOAD_SUCCESS
220
236
 
221
237
  async with self._save_load_semaphore:
aiohomematic/support.py CHANGED
@@ -582,6 +582,15 @@ def create_random_device_addresses(*, addresses: list[str]) -> dict[str, str]:
582
582
  return {adr: f"VCU{int(random.randint(1000000, 9999999))}" for adr in addresses}
583
583
 
584
584
 
585
+ def shrink_json_file(file_name: str) -> None:
586
+ """Shrink a json file."""
587
+ with open(file_name, "rb") as f:
588
+ data = orjson.loads(f.read())
589
+
590
+ with open(file_name, "wb") as f:
591
+ f.write(orjson.dumps(data))
592
+
593
+
585
594
  # --- Structured error boundary logging helpers ---
586
595
 
587
596
  _BOUNDARY_MSG = "error_boundary"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic
3
- Version: 2025.10.10
3
+ Version: 2025.10.11
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>
@@ -1,6 +1,6 @@
1
1
  aiohomematic/__init__.py,sha256=Uo9CIoil0Arl3GwtgMZAwM8jhcgoBKcZEgj8cXYlswY,2258
2
2
  aiohomematic/async_support.py,sha256=01chvt-Ac_UIAWI39VeGpQV9AmxpSCbNyfPPAwX_Qqc,7865
3
- aiohomematic/const.py,sha256=fF-aNIwaVEwjG9QxsL-oRntUYi-DcnxH8aRrwYAJ560,27427
3
+ aiohomematic/const.py,sha256=FeWi0VLHTN0nP61c4xZjt1TRJfn2azFchkYPHiXCPrQ,27427
4
4
  aiohomematic/context.py,sha256=hGE-iPcPt21dY-1MZar-Hyh9YaKL-VS42xjrulIVyRQ,429
5
5
  aiohomematic/converter.py,sha256=FiHU71M5RZ7N5FXJYh2CN14s63-PM-SHdb0cJ_CLx54,3602
6
6
  aiohomematic/decorators.py,sha256=cSW0aF3PzrW_qW6H0sjRNH9eqO8ysqhXZDgJ2OJTZM4,11038
@@ -8,14 +8,14 @@ aiohomematic/exceptions.py,sha256=RLldRD4XY8iYuNYVdspCbbphGcKsximB7R5OL7cYKw0,50
8
8
  aiohomematic/hmcli.py,sha256=_QZFKcfr_KJrdiyBRbhz0f8LZ95glD7LgJBmQc8cwog,4911
9
9
  aiohomematic/property_decorators.py,sha256=3Id1_rWIYnwyN_oSMgbh7XNKz9HPkGTC1CeS5ei04ZQ,17139
10
10
  aiohomematic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- aiohomematic/support.py,sha256=R275ZIKoufMZORcBa7Tiq0MZ7KtMZiqoUXUJX4LM1qA,23083
11
+ aiohomematic/support.py,sha256=eUbtdnrkq99o2DxJzyai5LHqrkpXC_gWQtrkI4zIgzg,23310
12
12
  aiohomematic/validator.py,sha256=qX5janicu4jLrAVzKoyWgXe1XU4EOjk5-QhNFL4awTQ,3541
13
13
  aiohomematic/central/__init__.py,sha256=xqlWOAKbnLzYTteVHIdxFpLCBcrTzgub8ZztJd43HTw,94122
14
14
  aiohomematic/central/decorators.py,sha256=vrujdw2QMXva-7DGXMQyittujx0q7cPuGD-SCeQlD30,6886
15
- aiohomematic/central/rpc_server.py,sha256=PX6idUEZ5j9fx9y__Q--Zcc2cyFMhLBie-lNvkx1bsI,10949
15
+ aiohomematic/central/rpc_server.py,sha256=EhvBy8oMjBTR8MvH5QXo3lvlsCNJrvu6B85_CAg6sG8,10742
16
16
  aiohomematic/client/__init__.py,sha256=14lx62VvPm9yQgm5nUVdzgAKkhS8GXeAvV8gmGbldl8,73941
17
17
  aiohomematic/client/_rpc_errors.py,sha256=IaYjX60mpBJ43gDCJjuUSVraamy5jXHTRjOnutK4azs,2962
18
- aiohomematic/client/json_rpc.py,sha256=u25nedb3AEK54GN9F4z3oOCfoE-YTYZpL4166OsRPAg,51274
18
+ aiohomematic/client/json_rpc.py,sha256=mrPvRR4hmc2MfMec8tjdQbF2RK1u0W1byOFUsiEP4fs,51319
19
19
  aiohomematic/client/rpc_proxy.py,sha256=T6tmfBAJJSFxzBLrhKJc6_KiHyTs5EVnStQsVJA5YkY,11604
20
20
  aiohomematic/model/__init__.py,sha256=gUYa8ROWSbXjZTWUTmINZ1bbYAxGkVpA-onxaJN2Iso,5436
21
21
  aiohomematic/model/data_point.py,sha256=VdwzjRrBDaYhWyIQL4JVC9wYTFMSwvwymYSEAPxjms8,41573
@@ -67,12 +67,10 @@ aiohomematic/rega_scripts/set_program_state.fn,sha256=0bnv7lUj8FMjDZBz325tDVP61m
67
67
  aiohomematic/rega_scripts/set_system_variable.fn,sha256=sTmr7vkPTPnPkor5cnLKlDvfsYRbGO1iq2z_2pMXq5E,383
68
68
  aiohomematic/store/__init__.py,sha256=PHwF_tw_zL20ODwLywHgpOLWrghQo_BMZzeiQSXN1Fc,1081
69
69
  aiohomematic/store/dynamic.py,sha256=kgZs5gJ4i8bHZKkJ883xuLecSKdjj6UwlLRJAvQcNGI,22528
70
- aiohomematic/store/persistent.py,sha256=RVd8opnPmjM-iXhtZ79B5a6QDNsRUkbA0LF6UKTT2cw,39430
70
+ aiohomematic/store/persistent.py,sha256=SBL8AhqUzpoPtJ50GkLYHwvRJS52fBWqNPjgvykxbY8,40233
71
71
  aiohomematic/store/visibility.py,sha256=0y93kPTugqQsrh6kKamfgwBkbIdBPEZpQVv_1NaLz3A,31662
72
- aiohomematic-2025.10.10.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=gpWkbyt_iCQCwxsvKahYl4knFrzyBku5WwbR83_mgM8,12825
75
- aiohomematic-2025.10.10.dist-info/METADATA,sha256=iobe6GKEwejmW7IGIY47Jwabivu15Kn1cLifFivDh7E,7604
76
- aiohomematic-2025.10.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
- aiohomematic-2025.10.10.dist-info/top_level.txt,sha256=5TDRlUWQPThIUwQjOj--aUo4UA-ow4m0sNhnoCBi5n8,34
78
- aiohomematic-2025.10.10.dist-info/RECORD,,
72
+ aiohomematic-2025.10.11.dist-info/licenses/LICENSE,sha256=q-B0xpREuZuvKsmk3_iyVZqvZ-vJcWmzMZpeAd0RqtQ,1083
73
+ aiohomematic-2025.10.11.dist-info/METADATA,sha256=r7XE6UJklJaiC8x0NM-lKx3BSErN8oj2ajLnb2Y9mVY,7604
74
+ aiohomematic-2025.10.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
75
+ aiohomematic-2025.10.11.dist-info/top_level.txt,sha256=iGUvt1N-E72vKRq7Anpp62HwkQngStrUK0JfL1zj1TE,13
76
+ aiohomematic-2025.10.11.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ aiohomematic
@@ -1,2 +0,0 @@
1
- aiohomematic
2
- aiohomematic_support
@@ -1 +0,0 @@
1
- """Module to support aiohomematic testing with a local client."""
@@ -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
- file_name=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 file_name in os.listdir(resource_path):
333
- if file_name not in include_list or file_name in exclude_list:
334
- continue
335
- if file_content := await self._load_json_file(anchor=anchor, resource=resource, file_name=file_name):
336
- result.append(file_content)
337
- return result
338
-
339
- async def _load_json_file(self, *, anchor: str, resource: str, file_name: 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, file_name),
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"