slmp-connect-python 0.1.4__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.
- slmp/__init__.py +119 -0
- slmp/async_client.py +1368 -0
- slmp/cli.py +6193 -0
- slmp/client.py +1943 -0
- slmp/constants.py +167 -0
- slmp/core.py +975 -0
- slmp/errors.py +25 -0
- slmp/py.typed +1 -0
- slmp/utils.py +893 -0
- slmp_connect_python-0.1.4.dist-info/METADATA +247 -0
- slmp_connect_python-0.1.4.dist-info/RECORD +15 -0
- slmp_connect_python-0.1.4.dist-info/WHEEL +5 -0
- slmp_connect_python-0.1.4.dist-info/entry_points.txt +20 -0
- slmp_connect_python-0.1.4.dist-info/licenses/LICENSE +21 -0
- slmp_connect_python-0.1.4.dist-info/top_level.txt +1 -0
slmp/utils.py
ADDED
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
"""High-level utility helpers for the SLMP client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import struct
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import AsyncIterator, Iterator
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
11
|
+
|
|
12
|
+
from .constants import DEVICE_CODES, DeviceUnit
|
|
13
|
+
from .core import DeviceRef, parse_device
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .async_client import AsyncSlmpClient
|
|
17
|
+
from .client import SlmpClient
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_WORD_DTYPES = frozenset({"U", "S"})
|
|
21
|
+
_DWORD_DTYPES = frozenset({"D", "L", "F"})
|
|
22
|
+
_UNBATCHED_DEVICE_CODES = frozenset({"G", "HG"})
|
|
23
|
+
_DEFAULT_DWORD_DEVICE_CODES = frozenset({"LTN", "LSTN", "LCN"})
|
|
24
|
+
_LONG_TIMER_READ_FAMILIES: dict[str, tuple[str, str]] = {
|
|
25
|
+
"LTN": ("LTN", "current"),
|
|
26
|
+
"LTS": ("LTN", "contact"),
|
|
27
|
+
"LTC": ("LTN", "coil"),
|
|
28
|
+
"LSTN": ("LSTN", "current"),
|
|
29
|
+
"LSTS": ("LSTN", "contact"),
|
|
30
|
+
"LSTC": ("LSTN", "coil"),
|
|
31
|
+
"LCN": ("LCN", "current"),
|
|
32
|
+
"LCS": ("LCN", "contact"),
|
|
33
|
+
"LCC": ("LCN", "coil"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class _ReadPlanEntry:
|
|
39
|
+
address: str
|
|
40
|
+
device: DeviceRef
|
|
41
|
+
dtype: str
|
|
42
|
+
bit_index: int | None
|
|
43
|
+
batch_kind: str | None
|
|
44
|
+
long_timer_read: tuple[str, str] | None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class _ReadPlan:
|
|
49
|
+
entries: tuple[_ReadPlanEntry, ...]
|
|
50
|
+
word_devices: tuple[DeviceRef, ...]
|
|
51
|
+
dword_devices: tuple[DeviceRef, ...]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Typed single-device read / write (async)
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def read_typed(
|
|
60
|
+
client: AsyncSlmpClient,
|
|
61
|
+
device: str | DeviceRef,
|
|
62
|
+
dtype: str,
|
|
63
|
+
) -> int | float:
|
|
64
|
+
"""Read one logical value and convert it to a Python scalar.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
client: Connected high-level or raw async SLMP client.
|
|
68
|
+
device: Starting device address as a string such as ``"D100"`` or as
|
|
69
|
+
a parsed :class:`DeviceRef`.
|
|
70
|
+
dtype: Application type code. Supported values are ``"BIT"``,
|
|
71
|
+
``"U"``, ``"S"``, ``"D"``, ``"L"``, and ``"F"``.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
``bool`` for ``BIT``, otherwise ``int`` or ``float``.
|
|
75
|
+
"""
|
|
76
|
+
ref = parse_device(device) if isinstance(device, str) else device
|
|
77
|
+
key = dtype.upper()
|
|
78
|
+
long_read = _get_long_timer_read(ref)
|
|
79
|
+
if long_read is not None:
|
|
80
|
+
return await _read_long_family_value(client, ref, key, long_read)
|
|
81
|
+
if key == "BIT":
|
|
82
|
+
values = await client.read_devices(ref, 1, bit_unit=True)
|
|
83
|
+
return bool(values[0])
|
|
84
|
+
if key in ("D", "L", "F"):
|
|
85
|
+
words = await client.read_devices(ref, 2, bit_unit=False)
|
|
86
|
+
raw = struct.pack("<HH", words[0], words[1])
|
|
87
|
+
if key == "F":
|
|
88
|
+
return cast(float, struct.unpack("<f", raw)[0])
|
|
89
|
+
elif key == "L":
|
|
90
|
+
return cast(int, struct.unpack("<i", raw)[0])
|
|
91
|
+
else:
|
|
92
|
+
return cast(int, struct.unpack("<I", raw)[0])
|
|
93
|
+
else:
|
|
94
|
+
words = await client.read_devices(ref, 1, bit_unit=False)
|
|
95
|
+
if key == "S":
|
|
96
|
+
return cast(int, struct.unpack("<h", struct.pack("<H", words[0]))[0])
|
|
97
|
+
return int(words[0])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def write_typed(
|
|
101
|
+
client: AsyncSlmpClient,
|
|
102
|
+
device: str | DeviceRef,
|
|
103
|
+
dtype: str,
|
|
104
|
+
value: int | float,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Write one logical value using the requested application type.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
client: Connected high-level or raw async SLMP client.
|
|
110
|
+
device: Starting device address.
|
|
111
|
+
dtype: Type code accepted by :func:`read_typed`.
|
|
112
|
+
value: Application value to encode and write.
|
|
113
|
+
"""
|
|
114
|
+
key = dtype.upper()
|
|
115
|
+
if key == "BIT":
|
|
116
|
+
await client.write_devices(device, [bool(value)], bit_unit=True)
|
|
117
|
+
return
|
|
118
|
+
if key == "F":
|
|
119
|
+
raw = struct.pack("<f", float(value))
|
|
120
|
+
elif key == "L":
|
|
121
|
+
raw = struct.pack("<i", int(value))
|
|
122
|
+
elif key == "D":
|
|
123
|
+
raw = struct.pack("<I", int(value))
|
|
124
|
+
else:
|
|
125
|
+
await client.write_devices(device, [int(value) & 0xFFFF], bit_unit=False)
|
|
126
|
+
return
|
|
127
|
+
words = list(struct.unpack("<HH", raw))
|
|
128
|
+
await client.write_devices(device, words, bit_unit=False)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Typed single-device read / write (sync)
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def read_typed_sync(
|
|
137
|
+
client: SlmpClient,
|
|
138
|
+
device: str | DeviceRef,
|
|
139
|
+
dtype: str,
|
|
140
|
+
) -> int | float:
|
|
141
|
+
"""Synchronously read one logical value as a Python scalar."""
|
|
142
|
+
ref = parse_device(device) if isinstance(device, str) else device
|
|
143
|
+
key = dtype.upper()
|
|
144
|
+
long_read = _get_long_timer_read(ref)
|
|
145
|
+
if long_read is not None:
|
|
146
|
+
return _read_long_family_value_sync(client, ref, key, long_read)
|
|
147
|
+
if key == "BIT":
|
|
148
|
+
values = client.read_devices(ref, 1, bit_unit=True)
|
|
149
|
+
return bool(values[0])
|
|
150
|
+
if key in ("D", "L", "F"):
|
|
151
|
+
words = client.read_devices(ref, 2, bit_unit=False)
|
|
152
|
+
raw = struct.pack("<HH", words[0], words[1])
|
|
153
|
+
if key == "F":
|
|
154
|
+
return cast(float, struct.unpack("<f", raw)[0])
|
|
155
|
+
elif key == "L":
|
|
156
|
+
return cast(int, struct.unpack("<i", raw)[0])
|
|
157
|
+
else:
|
|
158
|
+
return cast(int, struct.unpack("<I", raw)[0])
|
|
159
|
+
else:
|
|
160
|
+
words = client.read_devices(ref, 1, bit_unit=False)
|
|
161
|
+
if key == "S":
|
|
162
|
+
return cast(int, struct.unpack("<h", struct.pack("<H", words[0]))[0])
|
|
163
|
+
return int(words[0])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def write_typed_sync(
|
|
167
|
+
client: SlmpClient,
|
|
168
|
+
device: str | DeviceRef,
|
|
169
|
+
dtype: str,
|
|
170
|
+
value: int | float,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Synchronously write one logical value using the requested type."""
|
|
173
|
+
key = dtype.upper()
|
|
174
|
+
if key == "BIT":
|
|
175
|
+
client.write_devices(device, [bool(value)], bit_unit=True)
|
|
176
|
+
return
|
|
177
|
+
if key == "F":
|
|
178
|
+
raw = struct.pack("<f", float(value))
|
|
179
|
+
elif key == "L":
|
|
180
|
+
raw = struct.pack("<i", int(value))
|
|
181
|
+
elif key == "D":
|
|
182
|
+
raw = struct.pack("<I", int(value))
|
|
183
|
+
else:
|
|
184
|
+
client.write_devices(device, [int(value) & 0xFFFF], bit_unit=False)
|
|
185
|
+
return
|
|
186
|
+
words = list(struct.unpack("<HH", raw))
|
|
187
|
+
client.write_devices(device, words, bit_unit=False)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Bit-in-word (async + sync)
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def write_bit_in_word(
|
|
196
|
+
client: AsyncSlmpClient,
|
|
197
|
+
device: str | DeviceRef,
|
|
198
|
+
bit_index: int,
|
|
199
|
+
value: bool,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Set or clear one bit inside one word device.
|
|
202
|
+
|
|
203
|
+
This helper is only for word devices such as ``D50``. Direct bit devices
|
|
204
|
+
such as ``M1000`` should be written with :func:`write_typed` using
|
|
205
|
+
``"BIT"``.
|
|
206
|
+
"""
|
|
207
|
+
if not 0 <= bit_index <= 15:
|
|
208
|
+
raise ValueError(f"bit_index must be 0-15, got {bit_index}")
|
|
209
|
+
words = await client.read_devices(device, 1, bit_unit=False)
|
|
210
|
+
current = int(words[0])
|
|
211
|
+
if value:
|
|
212
|
+
current |= 1 << bit_index
|
|
213
|
+
else:
|
|
214
|
+
current &= ~(1 << bit_index)
|
|
215
|
+
await client.write_devices(device, [current & 0xFFFF], bit_unit=False)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def write_bit_in_word_sync(
|
|
219
|
+
client: SlmpClient,
|
|
220
|
+
device: str | DeviceRef,
|
|
221
|
+
bit_index: int,
|
|
222
|
+
value: bool,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Synchronously set or clear one bit inside one word device."""
|
|
225
|
+
if not 0 <= bit_index <= 15:
|
|
226
|
+
raise ValueError(f"bit_index must be 0-15, got {bit_index}")
|
|
227
|
+
words = client.read_devices(device, 1, bit_unit=False)
|
|
228
|
+
current = int(words[0])
|
|
229
|
+
if value:
|
|
230
|
+
current |= 1 << bit_index
|
|
231
|
+
else:
|
|
232
|
+
current &= ~(1 << bit_index)
|
|
233
|
+
client.write_devices(device, [current & 0xFFFF], bit_unit=False)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async def read_bits(
|
|
237
|
+
client: AsyncSlmpClient,
|
|
238
|
+
device: str | DeviceRef,
|
|
239
|
+
count: int,
|
|
240
|
+
) -> list[bool]:
|
|
241
|
+
"""Read a contiguous bit-device range as booleans."""
|
|
242
|
+
return [bool(v) for v in await client.read_devices(device, count, bit_unit=True)]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def read_bits_sync(
|
|
246
|
+
client: SlmpClient,
|
|
247
|
+
device: str | DeviceRef,
|
|
248
|
+
count: int,
|
|
249
|
+
) -> list[bool]:
|
|
250
|
+
"""Synchronously read a contiguous bit-device range as booleans."""
|
|
251
|
+
return [bool(v) for v in client.read_devices(device, count, bit_unit=True)]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
async def write_bits(
|
|
255
|
+
client: AsyncSlmpClient,
|
|
256
|
+
device: str | DeviceRef,
|
|
257
|
+
values: list[bool],
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Write a contiguous bit-device range from booleans."""
|
|
260
|
+
await client.write_devices(device, [bool(v) for v in values], bit_unit=True)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def write_bits_sync(
|
|
264
|
+
client: SlmpClient,
|
|
265
|
+
device: str | DeviceRef,
|
|
266
|
+
values: list[bool],
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Synchronously write a contiguous bit-device range from booleans."""
|
|
269
|
+
client.write_devices(device, [bool(v) for v in values], bit_unit=True)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
# Named-device read (async + sync)
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
async def read_named(
|
|
278
|
+
client: AsyncSlmpClient,
|
|
279
|
+
addresses: list[str],
|
|
280
|
+
) -> dict[str, int | float | bool]:
|
|
281
|
+
"""Read a mixed logical snapshot by address string.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
client: Connected async SLMP client.
|
|
285
|
+
addresses: Address list such as ``"D100"``, ``"D200:F"``,
|
|
286
|
+
``"D300:L"``, ``"D50.3"``, or direct bit devices like ``"M1000"``.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
A dictionary keyed by the original address strings.
|
|
290
|
+
|
|
291
|
+
Notes:
|
|
292
|
+
The address list is compiled once, then grouped into random reads where
|
|
293
|
+
possible. Use ``.bit`` notation only with word devices.
|
|
294
|
+
"""
|
|
295
|
+
plan = _compile_read_plan(addresses)
|
|
296
|
+
return await _read_named_with_plan(client, plan)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def read_named_sync(
|
|
300
|
+
client: SlmpClient,
|
|
301
|
+
addresses: list[str],
|
|
302
|
+
) -> dict[str, int | float | bool]:
|
|
303
|
+
"""Synchronously read a mixed logical snapshot by address string."""
|
|
304
|
+
plan = _compile_read_plan(addresses)
|
|
305
|
+
return _read_named_with_plan_sync(client, plan)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# Named-device write (async + sync)
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def write_named(
|
|
314
|
+
client: AsyncSlmpClient,
|
|
315
|
+
updates: dict[str, int | float | bool],
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Write a mixed logical snapshot by address string.
|
|
318
|
+
|
|
319
|
+
``D50.3`` updates one bit inside one word. Direct bit devices such as
|
|
320
|
+
``M1000`` are normalized to ``"BIT"`` writes.
|
|
321
|
+
"""
|
|
322
|
+
for address, value in updates.items():
|
|
323
|
+
base, dtype, bit_idx = _parse_address(address)
|
|
324
|
+
if dtype == "BIT_IN_WORD":
|
|
325
|
+
_validate_bit_in_word_target(address, parse_device(base))
|
|
326
|
+
await write_bit_in_word(client, base, bit_idx or 0, bool(value))
|
|
327
|
+
else:
|
|
328
|
+
device = parse_device(base)
|
|
329
|
+
resolved_dtype = _resolve_dtype_for_address(address, device, dtype, bit_idx)
|
|
330
|
+
_validate_long_timer_entry(address, device, resolved_dtype)
|
|
331
|
+
await write_typed(client, base, resolved_dtype, value)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def write_named_sync(
|
|
335
|
+
client: SlmpClient,
|
|
336
|
+
updates: dict[str, int | float | bool],
|
|
337
|
+
) -> None:
|
|
338
|
+
"""Synchronously write a mixed logical snapshot by address string."""
|
|
339
|
+
for address, value in updates.items():
|
|
340
|
+
base, dtype, bit_idx = _parse_address(address)
|
|
341
|
+
if dtype == "BIT_IN_WORD":
|
|
342
|
+
_validate_bit_in_word_target(address, parse_device(base))
|
|
343
|
+
write_bit_in_word_sync(client, base, bit_idx or 0, bool(value))
|
|
344
|
+
else:
|
|
345
|
+
device = parse_device(base)
|
|
346
|
+
resolved_dtype = _resolve_dtype_for_address(address, device, dtype, bit_idx)
|
|
347
|
+
_validate_long_timer_entry(address, device, resolved_dtype)
|
|
348
|
+
write_typed_sync(client, base, resolved_dtype, value)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# Address parser (shared)
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _parse_address(address: str) -> tuple[str, str, int | None]:
|
|
357
|
+
"""Parse extended address notation.
|
|
358
|
+
|
|
359
|
+
Returns (base_device, dtype, bit_index).
|
|
360
|
+
"""
|
|
361
|
+
if ":" in address:
|
|
362
|
+
base, dtype = address.split(":", 1)
|
|
363
|
+
return base.strip(), dtype.strip().upper(), None
|
|
364
|
+
if "." in address:
|
|
365
|
+
base, bit_str = address.split(".", 1)
|
|
366
|
+
try:
|
|
367
|
+
return base.strip(), "BIT_IN_WORD", int(bit_str, 16)
|
|
368
|
+
except ValueError:
|
|
369
|
+
pass
|
|
370
|
+
return address.strip(), "U", None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _is_batchable_word_device(device: DeviceRef) -> bool:
|
|
374
|
+
code = DEVICE_CODES.get(device.code)
|
|
375
|
+
return code is not None and code.unit == DeviceUnit.WORD and device.code not in _UNBATCHED_DEVICE_CODES
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _address_has_explicit_dtype(address: str) -> bool:
|
|
379
|
+
return ":" in address
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _normalize_dtype_for_device(device: DeviceRef, dtype: str) -> str:
|
|
383
|
+
code = DEVICE_CODES.get(device.code)
|
|
384
|
+
if code is not None and code.unit == DeviceUnit.BIT and dtype == "U":
|
|
385
|
+
return "BIT"
|
|
386
|
+
return dtype
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _resolve_dtype_for_address(address: str, device: DeviceRef, dtype: str, bit_index: int | None) -> str:
|
|
390
|
+
normalized = _normalize_dtype_for_device(device, dtype or "U")
|
|
391
|
+
if not _address_has_explicit_dtype(address) and bit_index is None and device.code in _DEFAULT_DWORD_DEVICE_CODES:
|
|
392
|
+
return "D"
|
|
393
|
+
return normalized
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _get_long_timer_read(device: DeviceRef) -> tuple[str, str] | None:
|
|
397
|
+
return _LONG_TIMER_READ_FAMILIES.get(device.code)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _validate_long_timer_entry(address: str, device: DeviceRef, dtype: str) -> None:
|
|
401
|
+
long_read = _get_long_timer_read(device)
|
|
402
|
+
if long_read is None:
|
|
403
|
+
return
|
|
404
|
+
_, role = long_read
|
|
405
|
+
if role == "current":
|
|
406
|
+
if dtype not in {"D", "L"}:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
f"Address '{address}' uses a 32-bit long current value. Use the plain form or ':D' / ':L'."
|
|
409
|
+
)
|
|
410
|
+
return
|
|
411
|
+
if dtype != "BIT":
|
|
412
|
+
raise ValueError(
|
|
413
|
+
f"Address '{address}' is a long timer state device. Use the plain device form without a dtype override."
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _validate_bit_in_word_target(address: str, device: DeviceRef) -> None:
|
|
418
|
+
code = DEVICE_CODES.get(device.code)
|
|
419
|
+
if code is None or code.unit != DeviceUnit.WORD:
|
|
420
|
+
raise ValueError(
|
|
421
|
+
f"Address '{address}' uses '.bit' notation, which is only valid for word devices. "
|
|
422
|
+
"Address bit devices directly, for example 'M1000' instead of 'M1000.0'."
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _coerce_long_current_value(current_value: int, dtype: str) -> int:
|
|
427
|
+
if dtype == "L":
|
|
428
|
+
return cast(int, struct.unpack("<i", struct.pack("<I", int(current_value) & 0xFFFFFFFF))[0])
|
|
429
|
+
return int(current_value)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _decode_long_family_words(words: list[int]) -> tuple[int, bool, bool]:
|
|
433
|
+
current_value = int(words[0]) | (int(words[1]) << 16)
|
|
434
|
+
status_word = int(words[2]) & 0xFFFF
|
|
435
|
+
return current_value, bool(status_word & 0x0002), bool(status_word & 0x0001)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
async def _read_long_family_point(
|
|
439
|
+
client: AsyncSlmpClient,
|
|
440
|
+
prefix: str,
|
|
441
|
+
head_no: int,
|
|
442
|
+
) -> tuple[int, bool, bool]:
|
|
443
|
+
if prefix == "LTN":
|
|
444
|
+
timer = (await client.read_long_timer(head_no=head_no, points=1))[0]
|
|
445
|
+
return int(timer.current_value), bool(timer.contact), bool(timer.coil)
|
|
446
|
+
if prefix == "LSTN":
|
|
447
|
+
timer = (await client.read_long_retentive_timer(head_no=head_no, points=1))[0]
|
|
448
|
+
return int(timer.current_value), bool(timer.contact), bool(timer.coil)
|
|
449
|
+
words = await client.read_devices(DeviceRef("LCN", head_no), 4, bit_unit=False)
|
|
450
|
+
return _decode_long_family_words(list(words))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _read_long_family_point_sync(
|
|
454
|
+
client: SlmpClient,
|
|
455
|
+
prefix: str,
|
|
456
|
+
head_no: int,
|
|
457
|
+
) -> tuple[int, bool, bool]:
|
|
458
|
+
if prefix == "LTN":
|
|
459
|
+
timer = client.read_long_timer(head_no=head_no, points=1)[0]
|
|
460
|
+
return int(timer.current_value), bool(timer.contact), bool(timer.coil)
|
|
461
|
+
if prefix == "LSTN":
|
|
462
|
+
timer = client.read_long_retentive_timer(head_no=head_no, points=1)[0]
|
|
463
|
+
return int(timer.current_value), bool(timer.contact), bool(timer.coil)
|
|
464
|
+
words = client.read_devices(DeviceRef("LCN", head_no), 4, bit_unit=False)
|
|
465
|
+
return _decode_long_family_words(list(words))
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
async def _read_long_family_value(
|
|
469
|
+
client: AsyncSlmpClient,
|
|
470
|
+
device: DeviceRef,
|
|
471
|
+
dtype: str,
|
|
472
|
+
long_read: tuple[str, str],
|
|
473
|
+
) -> int | bool:
|
|
474
|
+
prefix, role = long_read
|
|
475
|
+
current_value, contact, coil = await _read_long_family_point(client, prefix, device.number)
|
|
476
|
+
if role == "current":
|
|
477
|
+
return _coerce_long_current_value(current_value, dtype)
|
|
478
|
+
if role == "contact":
|
|
479
|
+
return contact
|
|
480
|
+
return coil
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _read_long_family_value_sync(
|
|
484
|
+
client: SlmpClient,
|
|
485
|
+
device: DeviceRef,
|
|
486
|
+
dtype: str,
|
|
487
|
+
long_read: tuple[str, str],
|
|
488
|
+
) -> int | bool:
|
|
489
|
+
prefix, role = long_read
|
|
490
|
+
current_value, contact, coil = _read_long_family_point_sync(client, prefix, device.number)
|
|
491
|
+
if role == "current":
|
|
492
|
+
return _coerce_long_current_value(current_value, dtype)
|
|
493
|
+
if role == "contact":
|
|
494
|
+
return contact
|
|
495
|
+
return coil
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _compile_read_plan(addresses: list[str]) -> _ReadPlan:
|
|
499
|
+
entries: list[_ReadPlanEntry] = []
|
|
500
|
+
word_devices: list[DeviceRef] = []
|
|
501
|
+
dword_devices: list[DeviceRef] = []
|
|
502
|
+
seen_words: set[DeviceRef] = set()
|
|
503
|
+
seen_dwords: set[DeviceRef] = set()
|
|
504
|
+
|
|
505
|
+
for address in addresses:
|
|
506
|
+
base, dtype, bit_index = _parse_address(address)
|
|
507
|
+
device = parse_device(base)
|
|
508
|
+
dtype = _resolve_dtype_for_address(address, device, dtype, bit_index)
|
|
509
|
+
_validate_long_timer_entry(address, device, dtype)
|
|
510
|
+
batch_kind: str | None = None
|
|
511
|
+
long_timer_read = _get_long_timer_read(device)
|
|
512
|
+
|
|
513
|
+
if long_timer_read is not None:
|
|
514
|
+
batch_kind = "LONG_TIMER"
|
|
515
|
+
elif dtype == "BIT_IN_WORD":
|
|
516
|
+
_validate_bit_in_word_target(address, device)
|
|
517
|
+
if _is_batchable_word_device(device):
|
|
518
|
+
batch_kind = "WORD"
|
|
519
|
+
if device not in seen_words:
|
|
520
|
+
word_devices.append(device)
|
|
521
|
+
seen_words.add(device)
|
|
522
|
+
elif dtype in _WORD_DTYPES:
|
|
523
|
+
if _is_batchable_word_device(device):
|
|
524
|
+
batch_kind = "WORD"
|
|
525
|
+
if device not in seen_words:
|
|
526
|
+
word_devices.append(device)
|
|
527
|
+
seen_words.add(device)
|
|
528
|
+
elif dtype in _DWORD_DTYPES:
|
|
529
|
+
if _is_batchable_word_device(device):
|
|
530
|
+
batch_kind = "DWORD"
|
|
531
|
+
if device not in seen_dwords:
|
|
532
|
+
dword_devices.append(device)
|
|
533
|
+
seen_dwords.add(device)
|
|
534
|
+
|
|
535
|
+
entries.append(_ReadPlanEntry(address, device, dtype, bit_index, batch_kind, long_timer_read))
|
|
536
|
+
|
|
537
|
+
return _ReadPlan(tuple(entries), tuple(word_devices), tuple(dword_devices))
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _decode_word_value(value: int, dtype: str) -> int:
|
|
541
|
+
if dtype == "S":
|
|
542
|
+
return cast(int, struct.unpack("<h", struct.pack("<H", value & 0xFFFF))[0])
|
|
543
|
+
return int(value)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _decode_dword_value(value: int, dtype: str) -> int | float:
|
|
547
|
+
raw = struct.pack("<I", value & 0xFFFFFFFF)
|
|
548
|
+
if dtype == "F":
|
|
549
|
+
return cast(float, struct.unpack("<f", raw)[0])
|
|
550
|
+
if dtype == "L":
|
|
551
|
+
return cast(int, struct.unpack("<i", raw)[0])
|
|
552
|
+
return int(value)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
async def _read_random_maps(
|
|
556
|
+
client: AsyncSlmpClient,
|
|
557
|
+
plan: _ReadPlan,
|
|
558
|
+
) -> tuple[dict[str, int], dict[str, int]]:
|
|
559
|
+
word_values: dict[str, int] = {}
|
|
560
|
+
dword_values: dict[str, int] = {}
|
|
561
|
+
word_devices = list(plan.word_devices)
|
|
562
|
+
dword_devices = list(plan.dword_devices)
|
|
563
|
+
word_index = 0
|
|
564
|
+
dword_index = 0
|
|
565
|
+
|
|
566
|
+
while word_index < len(word_devices) or dword_index < len(dword_devices):
|
|
567
|
+
word_chunk = word_devices[word_index : word_index + 0xFF]
|
|
568
|
+
dword_chunk = dword_devices[dword_index : dword_index + 0xFF]
|
|
569
|
+
word_index += len(word_chunk)
|
|
570
|
+
dword_index += len(dword_chunk)
|
|
571
|
+
if not word_chunk and not dword_chunk:
|
|
572
|
+
break
|
|
573
|
+
result = await client.read_random(word_devices=word_chunk, dword_devices=dword_chunk)
|
|
574
|
+
word_values.update(result.word)
|
|
575
|
+
dword_values.update(result.dword)
|
|
576
|
+
|
|
577
|
+
return word_values, dword_values
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _read_random_maps_sync(
|
|
581
|
+
client: SlmpClient,
|
|
582
|
+
plan: _ReadPlan,
|
|
583
|
+
) -> tuple[dict[str, int], dict[str, int]]:
|
|
584
|
+
word_values: dict[str, int] = {}
|
|
585
|
+
dword_values: dict[str, int] = {}
|
|
586
|
+
word_devices = list(plan.word_devices)
|
|
587
|
+
dword_devices = list(plan.dword_devices)
|
|
588
|
+
word_index = 0
|
|
589
|
+
dword_index = 0
|
|
590
|
+
|
|
591
|
+
while word_index < len(word_devices) or dword_index < len(dword_devices):
|
|
592
|
+
word_chunk = word_devices[word_index : word_index + 0xFF]
|
|
593
|
+
dword_chunk = dword_devices[dword_index : dword_index + 0xFF]
|
|
594
|
+
word_index += len(word_chunk)
|
|
595
|
+
dword_index += len(dword_chunk)
|
|
596
|
+
if not word_chunk and not dword_chunk:
|
|
597
|
+
break
|
|
598
|
+
result = client.read_random(word_devices=word_chunk, dword_devices=dword_chunk)
|
|
599
|
+
word_values.update(result.word)
|
|
600
|
+
dword_values.update(result.dword)
|
|
601
|
+
|
|
602
|
+
return word_values, dword_values
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
async def _read_named_with_plan(
|
|
606
|
+
client: AsyncSlmpClient,
|
|
607
|
+
plan: _ReadPlan,
|
|
608
|
+
) -> dict[str, int | float | bool]:
|
|
609
|
+
result: dict[str, int | float | bool] = {}
|
|
610
|
+
word_values, dword_values = await _read_random_maps(client, plan)
|
|
611
|
+
long_timer_cache: dict[tuple[str, int], Any] = {}
|
|
612
|
+
|
|
613
|
+
for entry in plan.entries:
|
|
614
|
+
if entry.batch_kind == "LONG_TIMER":
|
|
615
|
+
assert entry.long_timer_read is not None
|
|
616
|
+
prefix, role = entry.long_timer_read
|
|
617
|
+
cache_key = (prefix, entry.device.number)
|
|
618
|
+
if cache_key not in long_timer_cache:
|
|
619
|
+
long_timer_cache[cache_key] = await _read_long_family_point(client, prefix, entry.device.number)
|
|
620
|
+
current_value, contact, coil = long_timer_cache[cache_key]
|
|
621
|
+
if role == "current":
|
|
622
|
+
result[entry.address] = _coerce_long_current_value(current_value, entry.dtype)
|
|
623
|
+
elif role == "contact":
|
|
624
|
+
result[entry.address] = bool(contact)
|
|
625
|
+
else:
|
|
626
|
+
result[entry.address] = bool(coil)
|
|
627
|
+
continue
|
|
628
|
+
if entry.batch_kind == "WORD":
|
|
629
|
+
word = word_values[str(entry.device)]
|
|
630
|
+
if entry.dtype == "BIT_IN_WORD":
|
|
631
|
+
result[entry.address] = bool((word >> (entry.bit_index or 0)) & 1)
|
|
632
|
+
else:
|
|
633
|
+
result[entry.address] = _decode_word_value(word, entry.dtype)
|
|
634
|
+
continue
|
|
635
|
+
if entry.batch_kind == "DWORD":
|
|
636
|
+
result[entry.address] = _decode_dword_value(dword_values[str(entry.device)], entry.dtype)
|
|
637
|
+
continue
|
|
638
|
+
if entry.dtype == "BIT_IN_WORD":
|
|
639
|
+
words = await client.read_devices(entry.device, 1, bit_unit=False)
|
|
640
|
+
result[entry.address] = bool((words[0] >> (entry.bit_index or 0)) & 1)
|
|
641
|
+
else:
|
|
642
|
+
result[entry.address] = await read_typed(client, entry.device, entry.dtype or "U")
|
|
643
|
+
|
|
644
|
+
return result
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _read_named_with_plan_sync(
|
|
648
|
+
client: SlmpClient,
|
|
649
|
+
plan: _ReadPlan,
|
|
650
|
+
) -> dict[str, int | float | bool]:
|
|
651
|
+
result: dict[str, int | float | bool] = {}
|
|
652
|
+
word_values, dword_values = _read_random_maps_sync(client, plan)
|
|
653
|
+
long_timer_cache: dict[tuple[str, int], Any] = {}
|
|
654
|
+
|
|
655
|
+
for entry in plan.entries:
|
|
656
|
+
if entry.batch_kind == "LONG_TIMER":
|
|
657
|
+
assert entry.long_timer_read is not None
|
|
658
|
+
prefix, role = entry.long_timer_read
|
|
659
|
+
cache_key = (prefix, entry.device.number)
|
|
660
|
+
if cache_key not in long_timer_cache:
|
|
661
|
+
long_timer_cache[cache_key] = _read_long_family_point_sync(client, prefix, entry.device.number)
|
|
662
|
+
current_value, contact, coil = long_timer_cache[cache_key]
|
|
663
|
+
if role == "current":
|
|
664
|
+
result[entry.address] = _coerce_long_current_value(current_value, entry.dtype)
|
|
665
|
+
elif role == "contact":
|
|
666
|
+
result[entry.address] = bool(contact)
|
|
667
|
+
else:
|
|
668
|
+
result[entry.address] = bool(coil)
|
|
669
|
+
continue
|
|
670
|
+
if entry.batch_kind == "WORD":
|
|
671
|
+
word = word_values[str(entry.device)]
|
|
672
|
+
if entry.dtype == "BIT_IN_WORD":
|
|
673
|
+
result[entry.address] = bool((word >> (entry.bit_index or 0)) & 1)
|
|
674
|
+
else:
|
|
675
|
+
result[entry.address] = _decode_word_value(word, entry.dtype)
|
|
676
|
+
continue
|
|
677
|
+
if entry.batch_kind == "DWORD":
|
|
678
|
+
result[entry.address] = _decode_dword_value(dword_values[str(entry.device)], entry.dtype)
|
|
679
|
+
continue
|
|
680
|
+
if entry.dtype == "BIT_IN_WORD":
|
|
681
|
+
words = client.read_devices(entry.device, 1, bit_unit=False)
|
|
682
|
+
result[entry.address] = bool((words[0] >> (entry.bit_index or 0)) & 1)
|
|
683
|
+
else:
|
|
684
|
+
result[entry.address] = read_typed_sync(client, entry.device, entry.dtype or "U")
|
|
685
|
+
|
|
686
|
+
return result
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# ---------------------------------------------------------------------------
|
|
690
|
+
# Polling (async + sync)
|
|
691
|
+
# ---------------------------------------------------------------------------
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
async def poll(
|
|
695
|
+
client: AsyncSlmpClient,
|
|
696
|
+
addresses: list[str],
|
|
697
|
+
interval: float,
|
|
698
|
+
) -> AsyncIterator[dict[str, int | float | bool]]:
|
|
699
|
+
"""Continuously yield mixed snapshots at a fixed interval.
|
|
700
|
+
|
|
701
|
+
The address list is compiled once and reused for every cycle.
|
|
702
|
+
"""
|
|
703
|
+
plan = _compile_read_plan(addresses)
|
|
704
|
+
while True:
|
|
705
|
+
yield await _read_named_with_plan(client, plan)
|
|
706
|
+
await asyncio.sleep(interval)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def poll_sync(
|
|
710
|
+
client: SlmpClient,
|
|
711
|
+
addresses: list[str],
|
|
712
|
+
interval: float,
|
|
713
|
+
) -> Iterator[dict[str, int | float | bool]]:
|
|
714
|
+
"""Synchronously yield mixed snapshots at a fixed interval."""
|
|
715
|
+
plan = _compile_read_plan(addresses)
|
|
716
|
+
while True:
|
|
717
|
+
yield _read_named_with_plan_sync(client, plan)
|
|
718
|
+
time.sleep(interval)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# ---------------------------------------------------------------------------
|
|
722
|
+
# Chunked reads (async)
|
|
723
|
+
# ---------------------------------------------------------------------------
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
async def read_words(
|
|
727
|
+
client: AsyncSlmpClient,
|
|
728
|
+
device: str | DeviceRef,
|
|
729
|
+
count: int,
|
|
730
|
+
max_per_request: int = 960,
|
|
731
|
+
*,
|
|
732
|
+
allow_split: bool = False,
|
|
733
|
+
) -> list[int]:
|
|
734
|
+
"""Read a contiguous word-device range with optional chunk splitting.
|
|
735
|
+
|
|
736
|
+
Chunk boundaries stay aligned to 2-word boundaries so 32-bit values are
|
|
737
|
+
not torn across split requests.
|
|
738
|
+
"""
|
|
739
|
+
from .core import DeviceRef, parse_device
|
|
740
|
+
|
|
741
|
+
# Always use an even effective_max to keep DWord boundaries aligned.
|
|
742
|
+
effective_max = (max_per_request // 2) * 2
|
|
743
|
+
if effective_max <= 0:
|
|
744
|
+
raise ValueError("max_per_request must be at least 2")
|
|
745
|
+
|
|
746
|
+
if not allow_split:
|
|
747
|
+
if count > effective_max:
|
|
748
|
+
raise ValueError(
|
|
749
|
+
f"count {count} exceeds max_per_request {effective_max};"
|
|
750
|
+
" pass allow_split=True to split the read across multiple requests"
|
|
751
|
+
)
|
|
752
|
+
ref = parse_device(device) if isinstance(device, str) else device
|
|
753
|
+
return list(await client.read_devices(ref, count, bit_unit=False))
|
|
754
|
+
|
|
755
|
+
ref = parse_device(device) if isinstance(device, str) else device
|
|
756
|
+
result: list[int] = []
|
|
757
|
+
remaining = count
|
|
758
|
+
offset = 0
|
|
759
|
+
while remaining > 0:
|
|
760
|
+
chunk = min(remaining, effective_max)
|
|
761
|
+
chunk_ref = DeviceRef(ref.code, ref.number + offset)
|
|
762
|
+
words = await client.read_devices(chunk_ref, chunk, bit_unit=False)
|
|
763
|
+
result.extend(words)
|
|
764
|
+
offset += chunk
|
|
765
|
+
remaining -= chunk
|
|
766
|
+
return result
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
async def read_dwords(
|
|
770
|
+
client: AsyncSlmpClient,
|
|
771
|
+
device: str | DeviceRef,
|
|
772
|
+
count: int,
|
|
773
|
+
max_dwords_per_request: int = 480,
|
|
774
|
+
*,
|
|
775
|
+
allow_split: bool = False,
|
|
776
|
+
) -> list[int]:
|
|
777
|
+
"""Read a contiguous DWord range as unsigned 32-bit integers."""
|
|
778
|
+
words = await read_words(
|
|
779
|
+
client,
|
|
780
|
+
device,
|
|
781
|
+
count * 2,
|
|
782
|
+
max_per_request=max_dwords_per_request * 2,
|
|
783
|
+
allow_split=allow_split,
|
|
784
|
+
)
|
|
785
|
+
result: list[int] = []
|
|
786
|
+
for i in range(count):
|
|
787
|
+
raw = struct.pack("<HH", words[i * 2], words[i * 2 + 1])
|
|
788
|
+
result.append(struct.unpack("<I", raw)[0])
|
|
789
|
+
return result
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
# ---------------------------------------------------------------------------
|
|
793
|
+
# Chunked reads (sync)
|
|
794
|
+
# ---------------------------------------------------------------------------
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def read_words_sync(
|
|
798
|
+
client: SlmpClient,
|
|
799
|
+
device: str | DeviceRef,
|
|
800
|
+
count: int,
|
|
801
|
+
max_per_request: int = 960,
|
|
802
|
+
*,
|
|
803
|
+
allow_split: bool = False,
|
|
804
|
+
) -> list[int]:
|
|
805
|
+
"""Synchronously read a contiguous word-device range."""
|
|
806
|
+
from .core import DeviceRef, parse_device
|
|
807
|
+
|
|
808
|
+
effective_max = (max_per_request // 2) * 2
|
|
809
|
+
if effective_max <= 0:
|
|
810
|
+
raise ValueError("max_per_request must be at least 2")
|
|
811
|
+
|
|
812
|
+
if not allow_split:
|
|
813
|
+
if count > effective_max:
|
|
814
|
+
raise ValueError(
|
|
815
|
+
f"count {count} exceeds max_per_request {effective_max};"
|
|
816
|
+
" pass allow_split=True to split the read across multiple requests"
|
|
817
|
+
)
|
|
818
|
+
ref = parse_device(device) if isinstance(device, str) else device
|
|
819
|
+
return list(client.read_devices(ref, count, bit_unit=False))
|
|
820
|
+
|
|
821
|
+
ref = parse_device(device) if isinstance(device, str) else device
|
|
822
|
+
result: list[int] = []
|
|
823
|
+
remaining = count
|
|
824
|
+
offset = 0
|
|
825
|
+
while remaining > 0:
|
|
826
|
+
chunk = min(remaining, effective_max)
|
|
827
|
+
chunk_ref = DeviceRef(ref.code, ref.number + offset)
|
|
828
|
+
words = client.read_devices(chunk_ref, chunk, bit_unit=False)
|
|
829
|
+
result.extend(words)
|
|
830
|
+
offset += chunk
|
|
831
|
+
remaining -= chunk
|
|
832
|
+
return result
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def read_dwords_sync(
|
|
836
|
+
client: SlmpClient,
|
|
837
|
+
device: str | DeviceRef,
|
|
838
|
+
count: int,
|
|
839
|
+
max_dwords_per_request: int = 480,
|
|
840
|
+
*,
|
|
841
|
+
allow_split: bool = False,
|
|
842
|
+
) -> list[int]:
|
|
843
|
+
"""Synchronously read a contiguous DWord range."""
|
|
844
|
+
words = read_words_sync(
|
|
845
|
+
client,
|
|
846
|
+
device,
|
|
847
|
+
count * 2,
|
|
848
|
+
max_per_request=max_dwords_per_request * 2,
|
|
849
|
+
allow_split=allow_split,
|
|
850
|
+
)
|
|
851
|
+
result: list[int] = []
|
|
852
|
+
for i in range(count):
|
|
853
|
+
raw = struct.pack("<HH", words[i * 2], words[i * 2 + 1])
|
|
854
|
+
result.append(struct.unpack("<I", raw)[0])
|
|
855
|
+
return result
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
# ---------------------------------------------------------------------------
|
|
859
|
+
# Queued client
|
|
860
|
+
# ---------------------------------------------------------------------------
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
class QueuedAsyncSlmpClient:
|
|
864
|
+
"""Serialize all async calls on one shared SLMP connection.
|
|
865
|
+
|
|
866
|
+
The wrapper exposes the same methods as :class:`AsyncSlmpClient`, but every
|
|
867
|
+
coroutine call is executed under one lock. Use it when one connection is
|
|
868
|
+
shared by polling, snapshot, and write tasks.
|
|
869
|
+
"""
|
|
870
|
+
|
|
871
|
+
def __init__(self, inner: AsyncSlmpClient) -> None:
|
|
872
|
+
self._inner = inner
|
|
873
|
+
self._lock = asyncio.Lock()
|
|
874
|
+
|
|
875
|
+
def __getattr__(self, name: str) -> Any:
|
|
876
|
+
attr = getattr(self._inner, name)
|
|
877
|
+
if asyncio.iscoroutinefunction(attr):
|
|
878
|
+
|
|
879
|
+
async def _locked(*args: Any, **kwargs: Any) -> Any:
|
|
880
|
+
async with self._lock:
|
|
881
|
+
return await attr(*args, **kwargs)
|
|
882
|
+
|
|
883
|
+
return _locked
|
|
884
|
+
return attr
|
|
885
|
+
|
|
886
|
+
async def __aenter__(self) -> QueuedAsyncSlmpClient:
|
|
887
|
+
async with self._lock:
|
|
888
|
+
await self._inner.connect()
|
|
889
|
+
return self
|
|
890
|
+
|
|
891
|
+
async def __aexit__(self, *_: object) -> None:
|
|
892
|
+
async with self._lock:
|
|
893
|
+
await self._inner.close()
|