DLMS-SPODES-client 0.19.35__py3-none-any.whl → 0.19.37__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.
Files changed (60) hide show
  1. DLMS_SPODES_client/FCS16.py +39 -39
  2. DLMS_SPODES_client/__init__.py +12 -12
  3. DLMS_SPODES_client/client.py +2093 -2093
  4. DLMS_SPODES_client/gurux_common/enums/TraceLevel.py +21 -21
  5. DLMS_SPODES_client/gurux_dlms/AesGcmParameter.py +37 -37
  6. DLMS_SPODES_client/gurux_dlms/CountType.py +16 -16
  7. DLMS_SPODES_client/gurux_dlms/GXByteBuffer.py +545 -545
  8. DLMS_SPODES_client/gurux_dlms/GXCiphering.py +196 -196
  9. DLMS_SPODES_client/gurux_dlms/GXDLMS.py +426 -426
  10. DLMS_SPODES_client/gurux_dlms/GXDLMSChippering.py +237 -237
  11. DLMS_SPODES_client/gurux_dlms/GXDLMSChipperingStream.py +977 -977
  12. DLMS_SPODES_client/gurux_dlms/GXDLMSConfirmedServiceError.py +90 -90
  13. DLMS_SPODES_client/gurux_dlms/GXDLMSException.py +139 -139
  14. DLMS_SPODES_client/gurux_dlms/GXDLMSLNParameters.py +33 -33
  15. DLMS_SPODES_client/gurux_dlms/GXDLMSSNParameters.py +21 -21
  16. DLMS_SPODES_client/gurux_dlms/GXDLMSSettings.py +254 -254
  17. DLMS_SPODES_client/gurux_dlms/GXReplyData.py +87 -87
  18. DLMS_SPODES_client/gurux_dlms/HdlcControlFrame.py +9 -9
  19. DLMS_SPODES_client/gurux_dlms/MBusCommand.py +8 -8
  20. DLMS_SPODES_client/gurux_dlms/MBusEncryptionMode.py +27 -27
  21. DLMS_SPODES_client/gurux_dlms/ResponseType.py +8 -8
  22. DLMS_SPODES_client/gurux_dlms/SetResponseType.py +29 -29
  23. DLMS_SPODES_client/gurux_dlms/_HDLCInfo.py +9 -9
  24. DLMS_SPODES_client/gurux_dlms/__init__.py +75 -75
  25. DLMS_SPODES_client/gurux_dlms/enums/Access.py +12 -12
  26. DLMS_SPODES_client/gurux_dlms/enums/ApplicationReference.py +14 -14
  27. DLMS_SPODES_client/gurux_dlms/enums/Authentication.py +41 -41
  28. DLMS_SPODES_client/gurux_dlms/enums/BerType.py +35 -35
  29. DLMS_SPODES_client/gurux_dlms/enums/Command.py +285 -285
  30. DLMS_SPODES_client/gurux_dlms/enums/Definition.py +9 -9
  31. DLMS_SPODES_client/gurux_dlms/enums/ErrorCode.py +46 -46
  32. DLMS_SPODES_client/gurux_dlms/enums/ExceptionServiceError.py +12 -12
  33. DLMS_SPODES_client/gurux_dlms/enums/HardwareResource.py +10 -10
  34. DLMS_SPODES_client/gurux_dlms/enums/HdlcFrameType.py +9 -9
  35. DLMS_SPODES_client/gurux_dlms/enums/Initiate.py +10 -10
  36. DLMS_SPODES_client/gurux_dlms/enums/LoadDataSet.py +13 -13
  37. DLMS_SPODES_client/gurux_dlms/enums/ObjectType.py +306 -306
  38. DLMS_SPODES_client/gurux_dlms/enums/Priority.py +7 -7
  39. DLMS_SPODES_client/gurux_dlms/enums/RequestTypes.py +9 -9
  40. DLMS_SPODES_client/gurux_dlms/enums/Security.py +14 -14
  41. DLMS_SPODES_client/gurux_dlms/enums/Service.py +16 -16
  42. DLMS_SPODES_client/gurux_dlms/enums/ServiceClass.py +9 -9
  43. DLMS_SPODES_client/gurux_dlms/enums/ServiceError.py +8 -8
  44. DLMS_SPODES_client/gurux_dlms/enums/Standard.py +18 -18
  45. DLMS_SPODES_client/gurux_dlms/enums/StateError.py +7 -7
  46. DLMS_SPODES_client/gurux_dlms/enums/Task.py +10 -10
  47. DLMS_SPODES_client/gurux_dlms/enums/VdeStateError.py +10 -10
  48. DLMS_SPODES_client/gurux_dlms/enums/__init__.py +33 -33
  49. DLMS_SPODES_client/gurux_dlms/internal/_GXCommon.py +1673 -1673
  50. DLMS_SPODES_client/logger.py +56 -56
  51. DLMS_SPODES_client/services.py +90 -90
  52. DLMS_SPODES_client/session.py +363 -363
  53. DLMS_SPODES_client/settings.py +48 -48
  54. DLMS_SPODES_client/task.py +1884 -1884
  55. {dlms_spodes_client-0.19.35.dist-info → dlms_spodes_client-0.19.37.dist-info}/METADATA +29 -29
  56. dlms_spodes_client-0.19.37.dist-info/RECORD +61 -0
  57. {dlms_spodes_client-0.19.35.dist-info → dlms_spodes_client-0.19.37.dist-info}/WHEEL +1 -1
  58. dlms_spodes_client-0.19.35.dist-info/RECORD +0 -61
  59. {dlms_spodes_client-0.19.35.dist-info → dlms_spodes_client-0.19.37.dist-info}/entry_points.txt +0 -0
  60. {dlms_spodes_client-0.19.35.dist-info → dlms_spodes_client-0.19.37.dist-info}/top_level.txt +0 -0
@@ -1,1884 +1,1884 @@
1
- import asyncio
2
- import random
3
- from copy import copy
4
- import re
5
- from typing_extensions import deprecated
6
- import hashlib
7
- from dataclasses import dataclass, field
8
- from typing import Callable, Any, Optional, Protocol, cast, override, Self, Final, Iterable, TypeVarTuple
9
- from itertools import count
10
- import datetime
11
- import time
12
- from semver import Version as SemVer
13
- from StructResult import result
14
- from DLMS_SPODES.pardata import ParValues
15
- from DLMS_SPODES.types.implementations import enums, long_unsigneds, bitstrings, octet_string, structs, arrays, integers
16
- from DLMS_SPODES.pardata import ParData
17
- from DLMS_SPODES.cosem_interface_classes import parameters as dlms_par
18
- from DLMS_SPODES.cosem_interface_classes.parameter import Parameter
19
- from DLMS_SPODES import exceptions as exc, pdu_enums as pdu
20
- from DLMS_SPODES.cosem_interface_classes import (
21
- cosem_interface_class as ic,
22
- collection,
23
- overview,
24
- ln_pattern
25
- )
26
- from DLMS_SPODES.cosem_interface_classes.clock import Clock
27
- from DLMS_SPODES.cosem_interface_classes.image_transfer.image_transfer_status import ImageTransferStatus
28
- from DLMS_SPODES.cosem_interface_classes.image_transfer.ver0 import ImageTransferInitiate, ImageBlockTransfer, ImageToActivateInfo
29
- from DLMS_SPODES.cosem_interface_classes.association_ln.ver0 import ObjectListType, ObjectListElement
30
- from DLMS_SPODES.cosem_interface_classes import association_ln
31
- from DLMS_SPODES.types import cdt, ut, cst
32
- from DLMS_SPODES.hdlc import frame
33
- from DLMS_SPODES.enums import Transmit, Application, Conformance
34
- from DLMS_SPODES.firmwares import get_firmware
35
- from DLMS_SPODES.cosem_interface_classes.image_transfer import image_transfer_status as i_t_status
36
- from DLMSAdapter.main import AdapterException, Adapter, gag
37
- from DLMSCommunicationProfile.osi import OSI
38
- from .logger import LogLevel as logL
39
- from .client import Client, Security, Data, mechanism_id, AcseServiceUser, State
40
-
41
-
42
- firm_id_pat = re.compile(b".*(?P<fid>PWRM_M2M_[^_]{1,10}_[^_]{1,10}).+")
43
- boot_ver_pat = re.compile(b"(?P<boot_ver>\\d{1,4}).+")
44
-
45
-
46
- type Errors = list[Exception]
47
-
48
-
49
- class Base[T: result.Result](Protocol):
50
- """Exchange task for DLMS client"""
51
- msg: str
52
-
53
- def copy(self) -> Self: ...
54
-
55
- @property
56
- def current(self) -> 'Base[T] | Self':
57
- return self
58
-
59
- async def run(self, c: Client) -> T | result.Error:
60
- """exception handling block"""
61
- try:
62
- return await self.physical(c)
63
- except (ConnectionRefusedError, TimeoutError) as e:
64
- return result.Error.from_e(e)
65
- except exc.DLMSException as e:
66
- return result.Error.from_e(e)
67
- except Exception as e:
68
- return result.Error.from_e(e)
69
- # except asyncio.CancelledError as e:
70
- # await c.close() # todo: change to DiscRequest
71
- # return result.Error.from_e(exc.Abort("manual stop")) # for handle BaseException
72
- finally:
73
- c.received_frames.clear() # for next exchange need clear all received frames. todo: this bag, remove in future
74
-
75
- async def PH_connect(self, c: Client) -> result.Ok | result.Error:
76
- if c.media is None:
77
- return result.Error.from_e(exc.NoPort("no media"), "PH_connect")
78
- if not c.media.is_open():
79
- if isinstance(res_open := await c.media.open(), result.Error):
80
- return res_open
81
- c.log(logL.INFO, F"Open port communication channel: {c.media} {res_open.value}sec")
82
- c.level = OSI.PHYSICAL
83
- # todo: replace to <data_link>
84
- if (
85
- c._objects is None
86
- and not isinstance(self, InitType)
87
- ):
88
- if isinstance(res := await init_type.data_link(c), result.Error):
89
- return res.with_msg("PH_connect")
90
- if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest, or make not closed or reconnect !!!
91
- return res_close
92
- return result.OK
93
-
94
- @staticmethod
95
- async def physical_t(c: Client) -> result.Ok | result.Error:
96
- return await c.close()
97
-
98
- async def physical(self, c: Client) -> T | result.Error:
99
- if OSI.PHYSICAL not in c.level:
100
- if isinstance((res := await self.PH_connect(c)), result.Error):
101
- return res
102
- ret = await self.data_link(c)
103
- if isinstance(res_terminate := await self.physical_t(c), result.Error):
104
- return res_terminate
105
- return ret
106
-
107
- async def DL_connect(self, c: Client) -> result.Ok | result.Error:
108
- """Data link Layer connect"""
109
- c.send_frames.clear()
110
- # calculate addresses todo: move to c.com_profile(HDLC)
111
- c.DA = frame.Address(
112
- upper_address=int(c.server_SAP),
113
- lower_address=c.com_profile.parameters.device_address,
114
- length=c.addr_size
115
- )
116
- c.SA = frame.Address(upper_address=int(c.SAP))
117
- c.log(logL.INFO, F"{c.SA=} {c.DA=}")
118
- # initialize connection
119
- if c.settings.cipher.security != Security.NONE:
120
- c.log(logL.DEB, F"Security: {c.settings.cipher.security}/n"
121
- F"System title: {c.settings.cipher.systemTitle.hex()}"
122
- F"Authentication key: {c.settings.cipher.authenticationKey.hex()}"
123
- F"Block cipher key: {c.settings.cipher.blockCipherKey.hex()}")
124
- if c.settings.cipher.dedicatedKey:
125
- c.log(logL.DEB, F"Dedicated key: {c.settings.cipher.dedicatedKey.hex()}")
126
- # SNRM
127
- c.get_SNRM_request()
128
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
129
- return res_pdu
130
- c.level |= OSI.DATA_LINK
131
- return result.OK
132
-
133
- async def data_link(self, c: Client) -> T | result.Error:
134
- if OSI.DATA_LINK not in c.level:
135
- if isinstance(res_conn := await self.DL_connect(c), result.Error):
136
- return res_conn
137
- # todo: make tile
138
- return await self.application(c)
139
-
140
- async def AA(self, c: Client) -> result.Ok | result.Error:
141
- """Application Associate"""
142
- if c.invocationCounter and c.settings.cipher is not None and c.settings.cipher.security != Security.NONE:
143
- # create IC object. TODO: remove it after close connection, maybe???
144
- c.settings.proposedConformance |= Conformance.GENERAL_PROTECTION
145
-
146
- # my block
147
- IC: Data = c.objects.add_if_missing(ut.CosemClassId(1),
148
- logical_name=cst.LogicalName(bytearray((0, c.get_channel_index(), 43, 1,
149
- c.current_association.security_setup_reference.e, 255))),
150
- version=cdt.Unsigned(0))
151
- tmp_client_SAP = c.current_association.associated_partners_id.client_SAP
152
- challenge = c.settings.ctoSChallenge
153
- try:
154
- c.aarqRequest(c.m_id)
155
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
156
- return res_pdu
157
- ret = c.parseAareResponse(res_pdu.value)
158
- c.level |= OSI.APPLICATION # todo: it's must be result of <ret> look down
159
- if isinstance(res_ic := await ReadObjAttr(IC, 2).exchange(c), result.Error):
160
- return res_ic
161
- c.settings.cipher.invocationCounter = 1 + int(res_ic.value)
162
- c.log(logL.DEB, "Invocation counter: " + str(c.settings.cipher.invocationCounter))
163
- # disconnect
164
- if c.media and c.media.is_open():
165
- c.log(logL.DEB, "DisconnectRequest")
166
- if isinstance(res_disc_req := await c.disconnect_request(), result.Error):
167
- return res_disc_req
168
- finally:
169
- c.SAP = tmp_client_SAP
170
- c.settings.useCustomChallenge = challenge is not None
171
- c.settings.ctoSChallenge = challenge
172
-
173
- # gurux with several removed methods
174
- # add = self.settings.clientAddress
175
- # auth = self.settings.authentication
176
- # security = self.client.ciphering.security
177
- # challenge = self.client.ctoSChallenge
178
- # try:
179
- # self.client.clientAddress = 16
180
- # self.settings.authentication = Authentication.NONE
181
- # self.client.ciphering.security = Security.NONE
182
- # reply = GXReplyData()
183
- # self.get_SNRM_request()
184
- # self.status = Status.READ
185
- # self.read_data_block2()
186
- # self.objects.IEC_HDLS_setup.set_from_info(self.reply.data.get_data())
187
- # self.connection_state = ConnectionState.HDLC
188
- # self.reply.clear()
189
- # self.aarqRequest()
190
- # self.read_data_block2()
191
- # self.parseAareResponse(reply.data)
192
- # reply.clear()
193
- # item = GXDLMSData(self.invocationCounter)
194
- # data = self.client.read(item, 2)[0]
195
- # reply = GXReplyData()
196
- # self.read_data_block(data, reply)
197
- # item.encodings[2] = reply.data.get_data()
198
- # Update data type on read.
199
- # if item.getDataType(2) == cdt.NullData.TAG:
200
- # item.setDataType(2, reply.valueType)
201
- # self.client.updateValue(item, 2, reply.value)
202
- # self.client.ciphering.invocationCounter = 1 + item.value
203
- # print("Invocation counter: " + str(self.client.ciphering.invocationCounter))
204
- # if self.media and self.media.isOpen():
205
- # self.log(logL.INFO, "DisconnectRequest")
206
- # self.disconnect_request()
207
- # finally:
208
- # self.settings.clientAddress = add
209
- # self.settings.authentication = auth
210
- # self.client.ciphering.security = security
211
- # self.client.ctoSChallenge = challenge
212
-
213
- c.aarqRequest(c.m_id)
214
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
215
- return res_pdu
216
- # await c.read_attr(ut.CosemAttributeDescriptor((collection.ClassID.ASSOCIATION_LN, ut.CosemObjectInstanceId("0.0.40.0.0.255"), ut.CosemObjectAttributeId(6)))) # for test only
217
- try:
218
- parse = c.parseAareResponse(res_pdu.value)
219
- except IndexError as e:
220
- print(e)
221
- match parse:
222
- case AcseServiceUser.NULL:
223
- c.log(logL.INFO, "Authentication success")
224
- c.level |= OSI.APPLICATION
225
- case AcseServiceUser.AUTHENTICATION_REQUIRED:
226
- c.getApplicationAssociationRequest()
227
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
228
- return res_pdu
229
- c.parseApplicationAssociationResponse(res_pdu.value)
230
- case _ as diagnostic:
231
- return result.Error.from_e(exc.AssociationResultError(diagnostic))
232
- if c._objects is not None:
233
- matchLDN = change_ldn if c.is_universal() else match_ldn
234
- if isinstance(res_match_ldn := await matchLDN.exchange(c), result.Error):
235
- return res_match_ldn
236
- return result.OK
237
-
238
- async def application(self, c: Client) -> T | result.Error:
239
- if OSI.APPLICATION not in c.level:
240
- if isinstance(res := await self.AA(c), result.Error):
241
- return res
242
- # no tile
243
- return await self.exchange(c)
244
-
245
- async def exchange(self, c: Client) -> T | result.Error:
246
- """application level exchange"""
247
-
248
- async def connect(self, c: Client) -> result.Ok | result.Error:
249
- await self.PH_connect(c)
250
- await self.DL_connect(c)
251
- return await self.AA(c)
252
-
253
-
254
- class SimpleCopy:
255
- def copy(self) -> Self:
256
- return self
257
-
258
-
259
- class Simple[T](Base[result.Simple[T]], Protocol):
260
- """Simple result"""
261
- @override
262
- async def exchange(self, c: Client) -> result.SimpleOrError[T]: ...
263
-
264
-
265
- class Boolean(Simple[bool], Protocol):
266
- """Simple[bool] result"""
267
- @override
268
- async def exchange(self, c: Client) -> result.SimpleOrError[bool]: ...
269
-
270
-
271
- class CDT[T: cdt.CommonDataType](Simple[T], Protocol):
272
- """Simple[CDT] result"""
273
- @override
274
- async def exchange(self, c: Client) -> result.SimpleOrError[T]: ...
275
-
276
-
277
- class _List[T](Base[result.List[T]], Protocol):
278
- """With List result"""
279
- @override
280
- async def exchange(self, c: Client) -> result.List[T] | result.Error: ...
281
-
282
-
283
- class _Sequence[*Ts](Base[result.Sequence[*Ts]], Protocol):
284
- """With List result"""
285
- @override
286
- async def exchange(self, c: Client) -> result.Sequence[*Ts] | result.Error: ...
287
-
288
-
289
- class OK(Base[result.Ok], Protocol):
290
- """Always result OK"""
291
-
292
- @override
293
- async def exchange(self, c: Client) -> result.Ok | result.Error: ...
294
-
295
-
296
- class StrictOK(Base[result.StrictOk], Protocol):
297
- """Always result OK"""
298
-
299
- @override
300
- async def exchange(self, c: Client) -> result.StrictOk | result.Error: ...
301
-
302
-
303
- @dataclass(frozen=True)
304
- class ClientBlocking(SimpleCopy, OK):
305
- """complete by time or abort"""
306
- delay: float = field(default=99999999.0)
307
- msg: str = "client blocking"
308
-
309
- async def run(self, c: Client) -> result.Ok | result.Error:
310
- try:
311
- c.level = OSI.APPLICATION
312
- c.log(logL.WARN, F"blocked for {self.delay} second")
313
- await asyncio.sleep(self.delay)
314
- return result.OK
315
- finally:
316
- c.level = OSI.NONE
317
- return result.OK
318
-
319
- async def exchange(self, c: Client) -> result.Ok | result.Error:
320
- raise RuntimeError(f"not support for {self.__class__.__name__}")
321
-
322
-
323
- # todo: make with <data_link>
324
- @dataclass
325
- class TestDataLink(SimpleCopy, OK):
326
- msg: str = "test DLink"
327
-
328
- async def physical(self, c: Client) -> result.Ok | result.Error:
329
- if OSI.PHYSICAL not in c.level:
330
- if not c.media.is_open():
331
- if isinstance(res_open := await c.media.open(), result.Error):
332
- return res_open
333
- c.level = OSI.PHYSICAL
334
- c.DA = frame.Address(
335
- upper_address=int(c.server_SAP),
336
- lower_address=c.com_profile.parameters.device_address,
337
- length=c.addr_size
338
- )
339
- c.SA = frame.Address(upper_address=int(c.SAP))
340
- c.get_SNRM_request()
341
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
342
- return res_pdu
343
- c.level |= OSI.DATA_LINK
344
- if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest
345
- return res_close
346
- return result.Ok
347
-
348
- async def exchange(self, c: Client):
349
- return result.OK
350
-
351
-
352
- @dataclass(frozen=True)
353
- class Dummy(SimpleCopy, OK):
354
- msg: str = "dummy"
355
-
356
- async def exchange(self, c: Client) -> result.Ok | result.Error:
357
- """"""
358
- return result.OK
359
-
360
-
361
- @dataclass(frozen=True)
362
- class HardwareDisconnect(SimpleCopy, OK):
363
- msg: str = "hardware disconnect"
364
-
365
- @override
366
- async def exchange(self, c: Client) -> result.Ok | result.Error:
367
- await c.media.close()
368
- c.level = OSI.NONE
369
- msg = '' if self.msg is None else F": {self.msg}"
370
- c.log(logL.WARN, F"HARDWARE DISCONNECT{msg}")
371
- return result.OK
372
-
373
-
374
- @dataclass(frozen=True)
375
- class HardwareReconnect(SimpleCopy, OK):
376
- delay: float = 0.0
377
- """delay between disconnect and restore Application"""
378
- msg: str = "reconnect media without response"
379
-
380
- async def exchange(self, c: Client) -> result.Ok | result.Error:
381
- if isinstance((res := await HardwareDisconnect().exchange(c)), result.Error):
382
- return res
383
- if self.delay != 0.0:
384
- c.log(logL.INFO, F"delay({self.delay})")
385
- await asyncio.sleep(self.delay)
386
- if isinstance(res_connect := await self.connect(c), result.Error): # restore Application
387
- return res_connect
388
- return result.OK
389
-
390
-
391
- @dataclass(frozen=True)
392
- class Loop(OK):
393
- task: Base[result.Result]
394
- func: Callable[[Any], bool]
395
- delay: int = 0.0
396
- msg: str = "loop"
397
- attempt_amount: int = 0
398
- """0 is never end loop"""
399
-
400
- def copy(self) -> Self:
401
- return Loop(
402
- task=self.task.copy(),
403
- func=self.func,
404
- delay=self.delay,
405
- msg=self.msg,
406
- attempt_amount=self.attempt_amount
407
- )
408
-
409
- async def run(self, c: Client) -> result.Result:
410
- attempt = count()
411
- while not self.func(await super(Loop, self).run(c)):
412
- if next(attempt) == self.attempt_amount:
413
- return result.Error.from_e(ValueError("end of attempts"))
414
- await asyncio.sleep(self.delay)
415
- return result.OK
416
-
417
- async def exchange(self, c: Client) -> result.Result:
418
- return await self.task.exchange(c)
419
-
420
-
421
- @dataclass
422
- class ConditionalTask[T](Base):
423
- """Warning: experimental"""
424
- precondition_task: Base[result.Result]
425
- comp_value: T
426
- predicate: Callable[[T, T], bool]
427
- main_task: Base[result.Result]
428
- msg: str = "conditional task"
429
-
430
- async def exchange(self, c: Client) -> result.Result:
431
- res = await self.precondition_task.exchange(c)
432
- if self.predicate(self.comp_value, res.value):
433
- return await self.main_task.exchange(c)
434
- return result.Error.from_e(ValueError("Condition not satisfied"))
435
-
436
-
437
- DEFAULT_DATETIME_SCHEDULER = cdt.DateTime.parse("1.1.0001")
438
-
439
-
440
- @dataclass
441
- class Scheduler[T: result.Result](Base[T]):
442
- """𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦 = 𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦_𝑚𝑖𝑛 × (𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦_𝑒𝑥𝑝𝑜𝑛𝑒𝑛𝑡 × 0.01) ** 𝑛"""
443
- task: Base[T]
444
- execution_datetime: Final[cdt.DateTime] = DEFAULT_DATETIME_SCHEDULER
445
- start_interval: Final[int] = 0
446
- number_of_retries: Final[int] = 3
447
- total_of_retries: Final[int] = 100
448
- repetition_delay_min: Final[int] = 1
449
- repetition_delay_exponent: Final[int] = 100
450
- repetition_delay_max: Final[int] = 100
451
- msg: str = "sheduler"
452
-
453
- def copy(self) -> "Scheduler[T]":
454
- return Scheduler(
455
- task=self.task.copy(),
456
- execution_datetime=self.execution_datetime,
457
- start_interval=self.start_interval,
458
- number_of_retries=self.number_of_retries,
459
- total_of_retries=self.total_of_retries,
460
- repetition_delay_min=self.repetition_delay_min,
461
- repetition_delay_exponent=self.repetition_delay_exponent,
462
- repetition_delay_max=self.repetition_delay_max,
463
- msg=self.msg
464
- )
465
-
466
- async def run(self, c: Client) -> T | result.Error:
467
- if self.start_interval != 0:
468
- await asyncio.sleep(random.uniform(0, self.start_interval))
469
- c.log(logL.INFO, f"start {self.__class__.__name__}")
470
- total_of_retries = count()
471
- is_start: bool = True
472
- acc = result.ErrorAccumulator()
473
- while True:
474
- dt = self.execution_datetime.get_right_nearest_datetime(now := datetime.datetime.now())
475
- if dt is None:
476
- if is_start:
477
- is_start = False
478
- else:
479
- return acc.as_error(exc.Timeout("start time is out"), msg=self.msg)
480
- else:
481
- delay = (dt - now).total_seconds()
482
- c.log(logL.WARN, f"wait for {delay=}")
483
- await asyncio.sleep(delay)
484
- for n in range(self.number_of_retries):
485
- if next(total_of_retries) > self.total_of_retries:
486
- return acc.as_error(exc.Timeout("out of total retries"), msg=self.msg)
487
- await asyncio.sleep(min(self.repetition_delay_max, self.repetition_delay_min*(self.repetition_delay_exponent * 0.01)**n))
488
- if isinstance(res := await super(Scheduler, self).run(c), result.Error):
489
- acc.append_err(res.err)
490
- else:
491
- return res
492
-
493
- async def exchange(self, c: Client) -> T | result.Error:
494
- return await self.task.exchange(c)
495
-
496
-
497
- @dataclass
498
- class Subtasks[U: Base[result.Result]](Protocol):
499
- """for register longer other tasks into task"""
500
- tasks: Iterable[U]
501
-
502
- @property
503
- def current(self) -> U | Self: ...
504
-
505
-
506
- class List[T: result.Result, U: Base[result.Result]](Subtasks[U], _List[T]):
507
- """for exchange task sequence"""
508
- __is_exchange: bool
509
- err_ignore: bool
510
- msg: str
511
- __current: Base[T]
512
-
513
- def __init__(self, *tasks: Base[T], msg: str = "", err_ignore: bool = False):
514
- self.tasks = list(tasks)
515
- self.__current = self
516
- self.__is_exchange = False
517
- self.msg = self.__class__.__name__ if msg == "" else msg
518
- self.err_ignore = err_ignore
519
-
520
- def copy(self) -> Self:
521
- if all((isinstance(t, SimpleCopy) for t in self.tasks)):
522
- return self
523
- return List(
524
- *(t.copy() for t in self.tasks),
525
- msg=self.msg,
526
- err_ignore=self.err_ignore
527
- )
528
-
529
- @property
530
- def current(self) -> 'Base[T] | Self':
531
- return self.__current
532
-
533
- def append(self, task: Base[T]):
534
- if not self.__is_exchange:
535
- self.tasks.append(task)
536
- else:
537
- raise RuntimeError(F"append to {self.__class__.__name__} not allowed, already exchange started")
538
-
539
- async def exchange(self, c: Client) -> result.List[T] | result.Error:
540
- res = result.List()
541
- self.__is_exchange = True
542
- for t in self.tasks:
543
- self.__current = t
544
- if (
545
- isinstance(res_one := await t.exchange(c), result.Error)
546
- and not self.err_ignore
547
- ):
548
- return res_one
549
- res.append(res_one)
550
- return res
551
-
552
-
553
- class Sequence[*Ts](Subtasks[Base[result.Result]], _Sequence[*Ts]):
554
- """for exchange task sequence"""
555
- msg: str
556
- err_ignore: bool
557
- __current: "Base[result.Result] | Sequence[*Ts]"
558
- tasks: tuple[Base[result.Result], ...]
559
-
560
- def __init__(self, *tasks: Base[result.Result], msg: str = "sequence", err_ignore: bool = False):
561
- self.tasks = tasks
562
- self.__current = self
563
- self.msg = self.__class__.__name__ if msg == "" else msg
564
- self.err_ignore = err_ignore
565
-
566
- def copy(self) -> "Sequence[*Ts]":
567
- if all((isinstance(t, SimpleCopy) for t in self.tasks)):
568
- return self
569
- return Sequence(
570
- *(t.copy() for t in self.tasks),
571
- msg=self.msg,
572
- err_ignore=self.err_ignore
573
- )
574
-
575
- @property
576
- def current(self) -> Base[result.Result] | Self:
577
- return self.__current
578
-
579
- async def exchange(self, c: Client) -> result.Sequence[*Ts] | result.Error:
580
- res = result.Sequence()
581
- for t in self.tasks:
582
- self.__current = t
583
- if isinstance(res_one := await t.exchange(c), result.Error):
584
- if self.err_ignore:
585
- res_one = res_one.with_msg(self.msg)
586
- else:
587
- return res_one
588
- res = res.add(res_one)
589
- return cast("result.Sequence[*Ts]", res)
590
-
591
-
592
- @dataclass(frozen=True)
593
- class SetLocalTime(SimpleCopy, OK):
594
- """without decide time transfer"""
595
- msg: str = "set local time"
596
-
597
- async def exchange(self, c: Client) -> result.Ok | result.Error:
598
- clock_obj: Clock = c.objects.get_object("0.0.1.0.0.255")
599
- if isinstance(res := await ReadAttribute(
600
- ln=clock_obj.logical_name,
601
- index=3
602
- ).exchange(c), result.Error):
603
- return ret
604
- delta = datetime.timedelta(minutes=int(res.value))
605
- dt = cst.OctetStringDateTime(datetime.datetime.now(datetime.UTC)+delta)
606
- if isinstance(res := await WriteAttribute(
607
- ln=clock_obj.logical_name,
608
- index=2,
609
- value=dt.encoding
610
- ).exchange(c), result.Error):
611
- return res
612
- return result.OK
613
-
614
-
615
- @dataclass(frozen=True)
616
- class GetFirmwareVersion(SimpleCopy, CDT):
617
- msg: str = "get firmware version"
618
-
619
- async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
620
- return await Par2Data(Parameter(c.objects.id.f_ver.par[:6]).get_attr(c.objects.id.f_ver.par[6])).exchange(c)
621
-
622
-
623
- @dataclass(frozen=True)
624
- class ReadByDescriptor(SimpleCopy, Simple[bytes]):
625
- desc: ut.CosemMethodDescriptor
626
- msg: str = "get encoding by Cosem-Attribute-Descriptor"
627
-
628
- async def exchange(self, c: Client) -> result.SimpleOrError[bytes]:
629
- c.get_get_request_normal(self.desc)
630
- return await c.read_data_block()
631
-
632
-
633
- @dataclass(frozen=True)
634
- class FindFirmwareVersion(SimpleCopy, Simple[collection.ParameterValue]):
635
- msg: str = "try find COSEM server version, return: instance(B group), CDT"
636
-
637
- async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
638
- err = result.ErrorAccumulator()
639
- for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.1.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.2.255", 2))):
640
- if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
641
- err.append_err(res_read.err)
642
- in_e, out_e = res_read.err.split(exc.ResultError)
643
- if (
644
- out_e is None
645
- and in_e.exceptions[0].result == pdu.DataAccessResult(4)
646
- ):
647
- continue
648
- else:
649
- return res_read
650
- else:
651
- res = result.Simple(collection.ParameterValue(
652
- par=desc.instance_id.contents + desc.attribute_id.contents,
653
- value=res_read.value
654
- ))
655
- res.propagate_err(err)
656
- return res
657
- return err.as_error()
658
-
659
-
660
- @dataclass(frozen=True)
661
- class FindFirmwareId(SimpleCopy, Simple[collection.ParameterValue]):
662
- msg: str = "find firmaware Identifier"
663
-
664
- async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
665
- err = result.ErrorAccumulator()
666
- for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.0.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.1.255", 2))):
667
- if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
668
- err.append_err(res_read.err)
669
- in_e, out_e = res_read.err.split(exc.ResultError)
670
- if (
671
- out_e is None
672
- and in_e.exceptions[0].result == pdu.DataAccessResult(4)
673
- ):
674
- continue
675
- else:
676
- return res_read
677
- else:
678
- res = result.Simple(collection.ParameterValue(
679
- par=desc.instance_id.contents + desc.attribute_id.contents,
680
- value=res_read.value
681
- ))
682
- res.propagate_err(err)
683
- return res
684
- return err.as_error()
685
-
686
-
687
- @dataclass(frozen=True)
688
- class KeepAlive(SimpleCopy, OK):
689
- msg: str = "keep alive(read LND.ln)"
690
-
691
- async def exchange(self, c: Client) -> result.Ok | result.Error:
692
- if isinstance(res := await Par2Data(dlms_par.LDN.value).exchange(c), result.Error):
693
- return res
694
- return result.OK
695
-
696
-
697
- class GetLDN(SimpleCopy, CDT[octet_string.LDN]):
698
- """:return LDN value"""
699
- msg: str = "get LDN"
700
-
701
- async def exchange(self, c: Client) -> result.SimpleOrError[octet_string.LDN]:
702
- if isinstance(res := await ReadByDescriptor(collection.AttrDesc.LDN_VALUE).exchange(c), result.Error):
703
- return res
704
- return result.Simple(octet_string.LDN(res.value))
705
-
706
-
707
- # todo: possible implementation ConditionalTask
708
- # def compare_ldn(man: bytes, ldn: octet_string.LDN) -> bool:
709
- # return man == ldn.get_manufacturer()
710
- #
711
- #
712
- # check_LDN = ConditionalTask(
713
- # precondition_task=GetLDN,
714
- # comp_value=b"KPZ",
715
- # predicate=compare_ldn,
716
- # main_task=None
717
- # )
718
-
719
-
720
- @dataclass
721
- class MatchLDN(OK):
722
- universal: bool = field(default=False)
723
- msg: str = "matching LDN"
724
-
725
- async def exchange(self, c: Client) -> result.Ok | result.Error:
726
- if isinstance((res := await GetLDN().exchange(c)), result.Error):
727
- return res.with_msg("match LDN")
728
- if c.objects.LDN.value is None:
729
- c._objects.LDN.set_attr(2, res.value)
730
- elif c._objects.LDN.value == res.value:
731
- """secret matching"""
732
- elif self.universal:
733
- c.log(logL.WARN, F"connected to other server, change LDN")
734
- await init_type.exchange(c) # todo: maybe set spec?
735
- else:
736
- return result.Error.from_e(ValueError(F"got LDN: {res.value}, expected {c._objects.LDN.value}"))
737
- return result.OK
738
-
739
-
740
- match_ldn = MatchLDN()
741
- change_ldn = MatchLDN(universal=True)
742
-
743
-
744
- @dataclass
745
- class Lock:
746
- __lock: asyncio.Lock = field(init=False, default_factory=asyncio.Lock)
747
- piece: float = field(default=0.8)
748
- name: str = ""
749
-
750
- async def acquire(self, c: Client):
751
- keep_alive = KeepAlive(self.name)
752
- while True:
753
- try:
754
- await asyncio.wait_for(self.__lock.acquire(), c.com_profile.parameters.inactivity_time_out * self.piece) # todo: make not custom <inactivity_time_out>
755
- return
756
- except TimeoutError as e:
757
- await keep_alive.exchange(c)
758
-
759
- def release(self):
760
- self.__lock.release()
761
-
762
-
763
- @dataclass
764
- class CreateType(SimpleCopy, Simple[collection.Collection]):
765
- col_id: collection.ID
766
- msg: str = "CreateType".__class__.__name__
767
- obj_list: Optional[cdt.Array] = None
768
- wait_list: asyncio.Lock = field(init=False)
769
- """wait <object list>"""
770
- queue: asyncio.Queue = field(default_factory=asyncio.Queue)
771
- col: collection.Collection = field(init=False)
772
-
773
- def __post_init__(self):
774
- self.col = collection.Collection(id_=self.col_id)
775
- """common collection"""
776
- self.wait_list = Lock(name="wait <object list>")
777
-
778
- async def exchange(self, c: Client) -> result.Simple[collection.Collection]:
779
- res = result.Simple(self.col)
780
- await self.wait_list.acquire(c)
781
- try:
782
- if self.obj_list is None:
783
- if isinstance(res_obj_list := await ReadByDescriptor(collection.AttrDesc.OBJECT_LIST).exchange(c), result.Error):
784
- return res_obj_list
785
- self.obj_list = cdt.Array(res_obj_list.value, type_=structs.ObjectListElement)
786
- if len(self.col) == 0:
787
- for country_olel in filter(lambda it: ln_pattern.COUNTRY_SPECIFIC == it.logical_name, self.obj_list):
788
- self.col.set_country(collection.CountrySpecificIdentifiers(country_olel.logical_name.d))
789
- c.log(logL.INFO, F"set country: {self.col.country}")
790
- match self.col.country:
791
- case collection.CountrySpecificIdentifiers.RUSSIA:
792
- country_desc = collection.AttrDesc.SPODES_VERSION
793
- case _:
794
- country_desc = None
795
- if (
796
- country_desc is not None
797
- and next(filter(lambda it: country_desc.instance_id.contents == it.logical_name.contents, self.obj_list), False)
798
- ):
799
- if isinstance(res_country_ver := await ReadByDescriptor(country_desc).exchange(c), result.Error):
800
- return res_country_ver
801
- self.col.set_country_ver(collection.ParameterValue(
802
- par=country_desc.instance_id.contents + country_desc.attribute_id.contents,
803
- value=res_country_ver.value
804
- ))
805
- c.log(logL.INFO, F"set country version: {self.col.country_ver}")
806
- else:
807
- c.log(logL.WARN, "was not find <country specific code version> in object_list")
808
- break
809
- else:
810
- c.log(logL.WARN, "was not find <country specific code> in object_list")
811
- self.col.spec_map = self.col.get_spec()
812
- for o_l_el in self.obj_list:
813
- o_l_el: structs.ObjectListElement
814
- try:
815
- self.col.add_if_missing( # todo: remove add_if?
816
- class_id=ut.CosemClassId(int(o_l_el.class_id)),
817
- version=o_l_el.version,
818
- logical_name=o_l_el.logical_name)
819
- except collection.CollectionMapError as e:
820
- res.append_err(e)
821
- self.col.add_if_missing(
822
- class_id=overview.ClassID.DATA,
823
- version=None, # todo: check version else set 0
824
- logical_name=cst.LogicalName.from_obis("0.0.42.0.0.255")) # todo: make better
825
- for ass in self.col.iter_classID_objects(overview.ClassID.ASSOCIATION_LN):
826
- ass: collection.AssociationLN
827
- if ass.logical_name.e != 0:
828
- await ReadObjAttr(ass, 3).exchange(c) # todo: remove from self.queue
829
- if ass.associated_partners_id.client_SAP == c.SAP:
830
- cur_ass = ass
831
- break
832
- else: # use current association if no variant
833
- cur_ass = self.col.add_if_missing(
834
- class_id=overview.ClassID.ASSOCIATION_LN,
835
- version=None,
836
- logical_name=cst.LogicalName.from_obis("0.0.40.0.0.255")
837
- )
838
- await ReadObjAttr(cur_ass, 3).exchange(c)
839
- cur_ass.set_attr(2, self.obj_list)
840
- if cur_ass.associated_partners_id.client_SAP != c.SAP:
841
- c.log(logL.ERR, F"Wrong current server SAP: {c.SAP} use {cur_ass.associated_partners_id.client_SAP}")
842
- self.queue.put_nowait((cur_ass, 3)) # read forcibly <associated_partners_id>: use in MapTypeCreator(by <has_sap> method)
843
- reduce_ln = ln_pattern.LNPattern.parse("0.0.(40,42).0.0.255")
844
- """reduced objects for read"""
845
- for o_l_el in cur_ass.object_list: # todo: read necessary data for create_type
846
- if reduce_ln == o_l_el.logical_name:
847
- """nothing do it"""
848
- else:
849
- if (obj := self.col.get(o_l_el.logical_name.contents)) is None:
850
- continue
851
- for access in o_l_el.access_rights.attribute_access:
852
- i = int(access.attribute_id)
853
- if (
854
- i == 1 # skip LN
855
- or not access.access_mode.is_readable() # skip not readable
856
- or ( # skip early gotten object_list
857
- cur_ass.logical_name == o_l_el.logical_name
858
- and i == 2
859
- ) or (
860
- access.access_mode.is_writable() # skip unknown type writable element
861
- and not ( # except for:
862
- isinstance(obj.get_attr_element(i).DATA_TYPE, ut.CHOICE)
863
- or (
864
- isinstance(obj, collection.ProfileGeneric)
865
- and i == 3)
866
- )
867
- ) or obj.get_attr_element(i).classifier == collection.ic.Classifier.DYNAMIC # skip DYNAMIC
868
- ):
869
- continue
870
- self.queue.put_nowait((obj, i))
871
- for d_id in collection.get_filtered(
872
- objects=self.col,
873
- keys=(
874
- ln_pattern.DEVICE_ID,
875
- ln_pattern.PROGRAM_ENTRIES)):
876
- self.queue.put_nowait((d_id, 2)) # todo: make second queue2 for ReadEmptyAttribute(d_id.logical_name, 2).exchange(c)
877
- except TimeoutError as e:
878
- c.log(logL.ERR, F"can't got <object list>: {e}")
879
- finally:
880
- self.wait_list.release()
881
- while True:
882
- try:
883
- obj, i = self.queue.get_nowait()
884
- except asyncio.QueueEmpty as e:
885
- c.log(logL.INFO, "QueueEmpty")
886
- try:
887
- await asyncio.wait_for(self.queue.join(), c.com_profile.parameters.inactivity_time_out) # todo: why whis timeout?? # todo: make not custom <inactivity_time_out>
888
- break
889
- except TimeoutError as e:
890
- c.log(logL.INFO, "wait returned tasks in queue")
891
- continue # wait returned tasks in queue
892
- try:
893
- await c.read_attribute(obj, i)
894
- except TimeoutError as e:
895
- c.log(logL.ERR, F"return {obj}:{i} in queue: {e}")
896
- await self.queue.put((obj, i))
897
- except exc.Timeout as e:
898
- c.log(logL.ERR, F"break create type {self.col} by {e}")
899
- await self.queue.put((obj, i))
900
- raise e
901
- except AttributeError as e:
902
- c.log(logL.ERR, F"skip value wrong value for {obj}:{i}: {e}")
903
- except Exception as e: # todo: make better!!!
904
- c.log(logL.ERR, F"skip value wrong value for {obj}:{i}: {e}")
905
- finally:
906
- self.queue.task_done()
907
- print("stop create")
908
- return res
909
-
910
-
911
- type ID_SAP = tuple[collection.ID, enums.ClientSAP]
912
-
913
-
914
- @dataclass(frozen=True)
915
- class IDSAP:
916
- id: collection.ID
917
- sap: enums.ClientSAP
918
-
919
-
920
- @dataclass
921
- class NonInit:
922
- msg: str
923
-
924
- def __getattr__(self, item):
925
- raise RuntimeError(self.msg)
926
-
927
-
928
- class MapTypeCreator:
929
- adapter: Adapter
930
- lock: asyncio.Lock
931
- con: dict[IDSAP, CreateType]
932
-
933
- def __init__(self, adapter: Adapter):
934
- self.adapter = adapter
935
- self.con = dict()
936
- self.lock = asyncio.Lock()
937
-
938
- async def get_collection(
939
- self,
940
- c: Client,
941
- col_id: collection.ID
942
- ) -> result.Simple[collection.Collection]:
943
- new_col: collection.Collection
944
- err: Errors
945
- id_sap = IDSAP(col_id, c.SAP)
946
- async with self.lock:
947
- if id_sap in self.con.keys():
948
- c.log(logL.INFO, F"{self.__class__.__name__} {col_id} already in container")
949
- else:
950
- c.log(logL.INFO, F"{self.__class__.__name__} register new collection: {col_id}")
951
- self.con[id_sap] = CreateType(col_id)
952
- res = await self.con[id_sap].exchange(c)
953
- async with self.lock:
954
- try:
955
- gotten, _ = self.adapter.get_collection(col_id).unpack() # check for first update
956
- if gotten.has_sap(id_sap.sap):
957
- """not need keep"""
958
- else:
959
- self.adapter.set_collection(res.value) # todo: make as ADAPTER.merge_collection(ret)
960
- except AdapterException as e:
961
- self.adapter.set_collection(res.value)
962
- res.value, err = res.value.copy().unpack()
963
- if err is not None:
964
- res.append_err(err)
965
- return res
966
-
967
-
968
- @dataclass
969
- class InitType(SimpleCopy, Simple[collection.Collection]):
970
- adapter: Adapter
971
- msg: str = "initiate type"
972
-
973
- async def exchange(self, c: Client) -> result.SimpleOrError[collection.Collection]:
974
- if isinstance((res := await Sequence(
975
- GetLDN(),
976
- FindFirmwareId(),
977
- FindFirmwareVersion(),
978
- msg="get collection.ID",
979
- ).exchange(c)), result.Error):
980
- return res.with_msg("init type")
981
- ldn, f_id, f_ver = res.value
982
- col_id = collection.ID(ldn.get_manufacturer(), f_id, f_ver)
983
- try:
984
- if (res := self.adapter.get_collection(col_id)).value.has_sap(c.SAP):
985
- c.log(logL.INFO, F"find collection in {self.adapter}")
986
- else:
987
- raise AdapterException(F"was found collection from adapter with absent current {c.SAP}")
988
- except AdapterException as e:
989
- c.log(logL.WARN, F"not find into adapter: {e}")
990
- res = await map_type_creator.get_collection(
991
- c=c,
992
- col_id=col_id
993
- )
994
- c._objects = res.value
995
- c._objects.LDN.set_attr(2, ldn)
996
- return res
997
-
998
-
999
- init_type: InitType
1000
- """init after get_adapter"""
1001
- map_type_creator: MapTypeCreator
1002
- """init after get_adapter"""
1003
-
1004
-
1005
- def get_adapter(value: Adapter):
1006
- global map_type_creator, init_type
1007
- map_type_creator = MapTypeCreator(value)
1008
- init_type = InitType(value)
1009
-
1010
-
1011
- get_adapter(gag) # Dummy Adapter
1012
-
1013
-
1014
- @dataclass
1015
- @deprecated("use <ReadObjAttr>")
1016
- class ReadAttribute(SimpleCopy, CDT):
1017
- ln: collection.LNContaining
1018
- index: int
1019
- msg: str = "read LN attribute"
1020
-
1021
- async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
1022
- obj = c.objects.get_object(self.ln)
1023
- return await ReadObjAttr(obj, self.index).exchange(c)
1024
-
1025
-
1026
- @dataclass
1027
- class ReadObjAttr(SimpleCopy, CDT):
1028
- obj: collection.InterfaceClass
1029
- index: int
1030
- msg: str = "read object attribute"
1031
-
1032
- async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
1033
- # TODO: check is_readable?
1034
- c.get_get_request_normal(
1035
- attr_desc=self.obj.get_attr_descriptor(
1036
- value=self.index,
1037
- with_selection=bool(c.negotiated_conformance.selective_access)))
1038
- start_read_time: float = time.perf_counter()
1039
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
1040
- return res_pdu
1041
- c.last_transfer_time = datetime.timedelta(seconds=time.perf_counter()-start_read_time)
1042
- try:
1043
- self.obj.set_attr(self.index, res_pdu.value)
1044
- return result.Simple(self.obj.get_attr(self.index))
1045
- except ValueError as e:
1046
- return result.Error.from_e(e)
1047
- except ut.UserfulTypesException as e:
1048
- return result.Error.from_e(e)
1049
- except exc.DLMSException as e:
1050
- return result.Error.from_e(e)
1051
-
1052
-
1053
- @dataclass(frozen=True)
1054
- class Par2Data[T: cdt.CommonDataType](SimpleCopy, CDT[T]):
1055
- """get CommonDataType by Parameter"""
1056
- par: Parameter
1057
- msg: str = "read data by Parameter"
1058
-
1059
- async def exchange(self, c: Client) -> result.SimpleOrError[T]:
1060
- if isinstance((res_obj := c.objects.par2obj(self.par)), result.Error):
1061
- return res_obj
1062
- if isinstance((res := await ReadObjAttr(res_obj.value, self.par.i).exchange(c)), result.Error):
1063
- return res
1064
- for el in self.par.elements():
1065
- res.value = res.value[el]
1066
- return res
1067
-
1068
-
1069
- class ReadSequence(List):
1070
- tasks: list[ReadAttribute]
1071
-
1072
- def __post_init__(self):
1073
- assert all((isinstance(t, ReadAttribute) for t in self.tasks))
1074
-
1075
-
1076
- type AttrValueComp = Callable[[cdt.CommonDataType | None], bool]
1077
-
1078
-
1079
- def is_empty(value: cdt.CommonDataType | None) -> bool:
1080
- """is empty attribute value"""
1081
- return True if value is None else False
1082
-
1083
-
1084
- @dataclass(frozen=True)
1085
- class ReadAttributeIf(SimpleCopy, Base):
1086
- """read if func with arg as value is True"""
1087
- ln: collection.LNContaining
1088
- index: int
1089
- func: AttrValueComp
1090
- msg: str = "read attribute with condition"
1091
-
1092
- async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
1093
- # TODO: check is_readable
1094
- obj = c.objects.get_object(self.ln)
1095
- if self.func(obj.get_attr(self.index)):
1096
- return await ReadAttribute(
1097
- ln=self.ln,
1098
- index=self.index).exchange(c)
1099
- else:
1100
- return result.OK
1101
-
1102
-
1103
- @dataclass(frozen=True)
1104
- class ReadEmptyAttribute(SimpleCopy, Base):
1105
- ln: collection.LNContaining
1106
- index: int
1107
- msg: str = "read if attribute is empty"
1108
-
1109
- async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
1110
- # TODO: check is_readable
1111
- return await ReadAttributeIf(
1112
- ln=self.ln,
1113
- index=self.index,
1114
- func=is_empty
1115
- ).exchange(c)
1116
-
1117
-
1118
- @dataclass(frozen=True)
1119
- class ReadWritableAttributes(SimpleCopy, Base):
1120
- ln: collection.LNContaining
1121
- indexes: tuple[int, ...]
1122
- msg: str = "read only writable attribute"
1123
-
1124
- async def exchange(self, c: Client) -> result.List[cdt.CommonDataType]:
1125
- # TODO: check is_readable
1126
- res = result.List()
1127
- indexes: list[int] = []
1128
- ass: collection.AssociationLN = c._objects.sap2association(c.SAP)
1129
- for i in self.indexes:
1130
- if ass.is_writable(self.ln, i):
1131
- indexes.append(i)
1132
- if len(indexes) != 0:
1133
- res.append(await ReadAttributes(
1134
- ln=self.ln,
1135
- indexes=tuple(indexes)
1136
- ).exchange(c))
1137
- else:
1138
- res.append(result.OK)
1139
- c.log(logL.INFO, F"skip {self.__class__.__name__} operation, all is actually")
1140
- return res
1141
-
1142
-
1143
- # copy past from ReadWritableAttributes
1144
- @dataclass(frozen=True)
1145
- class ActualizeAttributes(SimpleCopy, OK):
1146
- ln: collection.LNContaining
1147
- indexes: tuple[int, ...]
1148
- msg: str = "read if attribute is empty or writable"
1149
-
1150
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1151
- # TODO: check is_readable
1152
- ass: collection.AssociationLN = c.objects.sap2association(c.SAP)
1153
- obj = c.objects.get_object(self.ln)
1154
- indexes = [
1155
- i for i in self.indexes if (
1156
- obj.get_attr(i) is None
1157
- or obj.get_attr_element(i).classifier == ic.Classifier.DYNAMIC
1158
- or ass.is_writable(self.ln, i)
1159
- )
1160
- ]
1161
- if len(indexes) != 0:
1162
- if isinstance((res := await ReadAttributes(
1163
- ln=self.ln,
1164
- indexes=tuple(indexes)
1165
- ).exchange(c)), result.Error):
1166
- return res
1167
- return result.OK
1168
-
1169
-
1170
- @dataclass(frozen=True)
1171
- class ReadAttributes(SimpleCopy, _List[cdt.CommonDataType]):
1172
- ln: collection.LNContaining
1173
- indexes: tuple[int, ...]
1174
- msg: str = ""
1175
-
1176
- async def exchange(self, c: Client) -> result.List[cdt.CommonDataType] | result.Error:
1177
- res = result.List()
1178
- obj = c.objects.get_object(self.ln)
1179
- # TODO: check for Get-Request-With-List
1180
- for i in self.indexes:
1181
- res.append(await ReadObjAttr(obj, i).exchange(c))
1182
- return res
1183
-
1184
-
1185
- @dataclass
1186
- @deprecated("use <Write2>")
1187
- class Write(SimpleCopy, OK):
1188
- """write with ParameterData struct"""
1189
- par_data: ParData
1190
- msg: str = "write attribute"
1191
-
1192
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1193
- if isinstance(res_obj := c.objects.par2obj(self.par_data.par), result.Error):
1194
- return res_obj
1195
- if self.par_data.par.n_elements == 0:
1196
- enc = self.par_data.data.encoding
1197
- elif isinstance(res_read := await ReadObjAttr(res_obj.value, self.par_data.par.i).exchange(c), result.Error):
1198
- return res_read
1199
- else:
1200
- data = a_data
1201
- for el in self.par_data.par.elements():
1202
- data = data[el]
1203
- data.set(self.par_data.data)
1204
- enc = data.encoding
1205
- data = c.get_set_request_normal(
1206
- obj=res_obj.value,
1207
- attr_index=self.par_data.par.i,
1208
- value=enc)
1209
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
1210
- return res_pdu
1211
- return result.OK
1212
-
1213
-
1214
- @dataclass(frozen=True)
1215
- class Write2(SimpleCopy, OK):
1216
- """write with ParameterData struct"""
1217
- par: Parameter
1218
- data: cdt.CommonDataType
1219
- msg: str = "write Data"
1220
-
1221
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1222
- if isinstance(res_obj := c.objects.par2obj(self.par), result.Error):
1223
- return res_obj
1224
- if self.par.n_elements == 0:
1225
- enc = self.data.encoding
1226
- elif isinstance(res_read := await Par2Data[cdt.CommonDataType](self.par.attr).exchange(c), result.Error):
1227
- return res_read
1228
- else:
1229
- data = res_read.value
1230
- for el in self.par.elements():
1231
- data = data[el]
1232
- data.set(self.data)
1233
- enc = data.encoding
1234
- data = c.get_set_request_normal(
1235
- obj=res_obj.value,
1236
- attr_index=self.par.i,
1237
- value=enc)
1238
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
1239
- return res_pdu
1240
- return result.OK
1241
-
1242
-
1243
- @dataclass(frozen=True)
1244
- class SetBits(SimpleCopy, OK):
1245
- """set bits by pattern"""
1246
- par: Parameter
1247
- pattern: dict[int, int]
1248
- msg: str = "set bits"
1249
-
1250
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1251
- if isinstance(res_obj := c.objects.par2obj(self.par), result.Error):
1252
- return res_obj
1253
- if isinstance(res_read := await Par2Data[cdt.Digital | cdt.BitString](self.par.attr).exchange(c), result.Error):
1254
- return res_read
1255
- data = res_read.value
1256
- for el in self.par.elements():
1257
- data = data[el]
1258
- if isinstance(data, cdt.Digital):
1259
- data_int = int(data)
1260
- for pos, val in self.pattern.items():
1261
- data_int = (data_int | (1 << pos)) if val else (data_int & ~(1 << pos))
1262
- data.set(data_int)
1263
- elif isinstance(data, cdt.BitString):
1264
- for pos, val in self.pattern.items():
1265
- try:
1266
- data[pos] = val
1267
- except IndexError as e:
1268
- return result.Error.from_e(ValueError(f"can't set bit {pos} in {self.par}"))
1269
- else:
1270
- return result.Error.from_e(TypeError(f"{self.par} not available SetBits"))
1271
- c.get_set_request_normal(
1272
- obj=res_obj.value,
1273
- attr_index=self.par.i,
1274
- value=res_read.value.encoding)
1275
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
1276
- return res_pdu
1277
- return result.OK
1278
-
1279
-
1280
- @dataclass
1281
- @deprecated("use <WriteTranscript>")
1282
- class WriteParValue(SimpleCopy, OK):
1283
- """write with ParameterValues struct"""
1284
- par_value: ParValues[cdt.Transcript]
1285
- msg: str = "write attribute by Transcript"
1286
-
1287
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1288
- if isinstance(res1 := c.objects.par2obj(self.par_value.par), result.Error):
1289
- return res1
1290
- obj = res1.value
1291
- if (a_data := res1.value.get_attr(self.par_value.par.i)) is None:
1292
- if isinstance((res := await Par2Data(self.par_value.par).exchange(c)), result.Error):
1293
- return res
1294
- else:
1295
- a_data = res.value
1296
- s_u = c._objects.par2su(self.par_value.par)
1297
- if isinstance(s_u, cdt.ScalUnitType):
1298
- value: cdt.Transcript = str(float(self.par_value.data) * 10 ** -int(s_u.scaler))
1299
- else:
1300
- value = self.par_value.data
1301
- if self.par_value.par.n_elements == 0:
1302
- set_data = a_data.parse(value)
1303
- elif isinstance((res := await Par2Data(par).exchange(c)), result.Error):
1304
- return res
1305
- else:
1306
- data = a_data
1307
- for el in self.par_value.par.elements():
1308
- data = data[el]
1309
- new_data = data.parse(value) # todo: can't use with CHOICE
1310
- data.set(new_data)
1311
- set_data = a_data
1312
- data = c.get_set_request_normal(
1313
- obj=obj,
1314
- attr_index=self.par_value.par.i,
1315
- value=set_data.encoding)
1316
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
1317
- return res_pdu
1318
- return result.OK
1319
-
1320
-
1321
- @dataclass
1322
- @deprecated("use <Write>")
1323
- class WriteAttribute(SimpleCopy, OK):
1324
- ln: collection.LNContaining
1325
- index: int
1326
- value: bytes | str | int | list | tuple | datetime.datetime
1327
- msg: str = ""
1328
-
1329
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1330
- obj = c._objects.get_object(self.ln)
1331
- if isinstance(self.value, (str, int, list, tuple, datetime.datetime)):
1332
- value2 = await c.encode(
1333
- obj=obj,
1334
- index=self.index,
1335
- value=self.value)
1336
- enc = value2.encoding
1337
- else:
1338
- enc = self.value
1339
- data = c.get_set_request_normal(
1340
- obj=obj,
1341
- attr_index=self.index,
1342
- value=enc)
1343
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
1344
- return res_pdu
1345
- return result.OK # todo: return Data-Access-Result
1346
-
1347
-
1348
- @deprecated("use Execute")
1349
- @dataclass
1350
- class ExecuteByDesc(SimpleCopy, Base):
1351
- """execute method by method descriptor # TODO: rewrite this"""
1352
- desc: ut.CosemMethodDescriptor
1353
- msg: str = "old execute"
1354
-
1355
- async def exchange(self, c: Client) -> result.Result:
1356
- try:
1357
- await c.execute_method(self.desc)
1358
- return result.Simple(pdu.ActionResult.SUCCESS)
1359
- except Exception as e:
1360
- return result.Error.from_e(exc.DLMSException(F'Исполнение {self.desc}'))
1361
-
1362
-
1363
- @deprecated("use Execute2")
1364
- @dataclass
1365
- class Execute(SimpleCopy, OK):
1366
- """execute method"""
1367
- ln: collection.LNContaining
1368
- index: int
1369
- value: cdt.CommonDataType = None # todo: maybe use simple bytes
1370
- msg: str = "execute method"
1371
-
1372
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1373
- obj = c._objects.get_object(self.ln)
1374
- try:
1375
- await c.execute_method2(obj, self.index, self.value)
1376
- return result.OK
1377
- except Exception as e:
1378
- return result.Error.from_e(exc.DLMSException(F'Исполнение {self.desc}'))
1379
-
1380
-
1381
- @dataclass(frozen=True)
1382
- class Execute2(SimpleCopy, OK):
1383
- """execute method"""
1384
- par: Parameter
1385
- data: cdt.CommonDataType
1386
- msg: str = "Execute method"
1387
-
1388
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1389
- if isinstance(res := c.objects.par2obj(self.par), result.Error):
1390
- return res
1391
- try:
1392
- data = c.get_action_request_normal(
1393
- meth_desc=res.value.get_meth_descriptor(self.par.i),
1394
- method=self.data
1395
- )
1396
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
1397
- return res_pdu
1398
- return result.OK
1399
- except Exception as e:
1400
- return result.Error.from_e(e)
1401
-
1402
-
1403
- @dataclass
1404
- class GetTimeDelta(SimpleCopy, Simple[float]):
1405
- """Read and return <time delta> in second: """
1406
- msg: str = "Read Clock.time"
1407
-
1408
- async def exchange(self, c: Client) -> result.SimpleOrError[float]:
1409
- acc = result.ErrorAccumulator()
1410
- obj = c._objects.clock
1411
- if isinstance(
1412
- res_read_tz := await ReadObjAttr(
1413
- obj=obj,
1414
- index=3
1415
- ).exchange(c),
1416
- result.Error):
1417
- return res_read_tz
1418
- tz = datetime.timezone(datetime.timedelta(minutes=int(res_read_tz.value)))
1419
- if isinstance(
1420
- res_read := await ReadObjAttr(
1421
- obj=obj,
1422
- index=2
1423
- ).exchange(c),
1424
- result.Error):
1425
- return res_read
1426
- value = datetime.datetime.now(tz=tz)
1427
- value2 = res_read.value.to_datetime().replace(tzinfo=tz)
1428
- return result.Simple((value2 - value).total_seconds()).append_err(acc.err)
1429
-
1430
-
1431
-
1432
- @dataclass
1433
- class WriteTime(SimpleCopy, Simple[float]):
1434
- """Write and return <record time> in second: """
1435
- limit: float = 5.0
1436
- number_of_retries: int = 10
1437
- msg: str = "write Clock.time"
1438
-
1439
- async def exchange(self, c: Client) -> result.SimpleOrError[float]:
1440
- acc = result.ErrorAccumulator()
1441
- obj = c._objects.clock
1442
- c.get_get_request_normal(obj.get_attr_descriptor(3))
1443
- if isinstance(res_pdu := await c.read_data_block(), result.Error):
1444
- return res_pdu
1445
- tz = obj.get_attr_element(3).DATA_TYPE(res_pdu.value)
1446
- for i in range(self.number_of_retries):
1447
- pre_time = time.time()
1448
- if isinstance(
1449
- res_write := await WriteAttribute(
1450
- ln=obj.logical_name,
1451
- index=2,
1452
- value=(datetime.datetime.utcnow() + datetime.timedelta(minutes=int(tz)))).exchange(c),
1453
- result.Error):
1454
- return res_write
1455
- rec_time = time.time() - pre_time
1456
- if rec_time < self.limit:
1457
- break
1458
- acc.append_e(TimeoutError(f"can't write in {i} attemp in time"))
1459
- else:
1460
- return result.Error.from_e(TimeoutError(f"can't write time for limit: {self.limit} second"))
1461
- return result.Simple(rec_time).append_err(acc.err)
1462
-
1463
-
1464
- @dataclass
1465
- class ImageTransfer(SimpleCopy, StrictOK):
1466
- par: dlms_par.ImageTransfer
1467
- image: bytes
1468
- waiting_for_activation: float = 10.0
1469
- n_t_b: int = field(init=False, default=0)
1470
- """not transferred block"""
1471
- n_blocks: int = field(init=False)
1472
- msg: str = "image transfer"
1473
-
1474
- def __post_init__(self) -> None:
1475
- self.ITI = ImageTransferInitiate((
1476
- bytearray(hashlib.md5(self.image).digest()), # todo: make custom this
1477
- cdt.DoubleLongUnsigned(len(self.image))
1478
- ))
1479
-
1480
- async def exchange(self, c: Client) -> result.StrictOk | result.Error:
1481
- """ update image if blocks is fulls ver 3"""
1482
- offset: int
1483
- res_block_size: result.SimpleOrError[cdt.DoubleLongUnsigned]
1484
- res_status: result.SimpleOrError[i_t_status.ImageTransferStatus]
1485
- res_activate_info: result.SimpleOrError[ImageToActivateInfo]
1486
- res_ntb: result.SimpleOrError[cdt.DoubleLongUnsigned]
1487
- res_tbs: result.SimpleOrError[cdt.BitString]
1488
- previous_status: Optional[i_t_status.ImageTransferStatus] = None
1489
- res = result.StrictOk()
1490
- if isinstance(res_block_size := await Par2Data(self.par.image_block_size).exchange(c), result.Error):
1491
- return res_block_size
1492
- block_size = int(res_block_size.value)
1493
- self.n_blocks, mod = divmod(len(self.image), block_size)
1494
- if mod != 0:
1495
- self.n_blocks += 1
1496
- # TODO: need common counter for exit from infinity loop
1497
- if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
1498
- return res_status
1499
- if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
1500
- return res_activate_info
1501
- if (
1502
- res_status.value in (i_t_status.TRANSFER_NOT_INITIATED, i_t_status.VERIFICATION_FAILED, i_t_status.ACTIVATION_FAILED)
1503
- or len(res_activate_info.value) == 0
1504
- or res_activate_info.value[0].image_to_activate_identification != self.ITI.image_identifier
1505
- ):
1506
- if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1507
- return res_initiate
1508
- c.log(logL.INFO, "Start initiate Image Transfer")
1509
- if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
1510
- return res_status
1511
- elif res_status.value == i_t_status.ACTIVATION_SUCCESSFUL:
1512
- # image in outside memory and already activated early, but erased by hard programming. Need again go to activation
1513
- res_status.value = i_t_status.VERIFICATION_SUCCESSFUL
1514
- else:
1515
- c.log(logL.INFO, "already INITIATED")
1516
- if isinstance(res_ntb := await Par2Data(self.par.image_first_not_transferred_block_number).exchange(c), result.Error):
1517
- return res_ntb
1518
- self.n_t_b = int(res_ntb.value)
1519
- if self.n_t_b > (len(self.image) / block_size): # all blocks were send
1520
- if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
1521
- return res_verify
1522
- c.log(logL.INFO, "Start Verify Transfer")
1523
- while True:
1524
- c.log(logL.STATE, F"{res_status.value=}")
1525
- match res_status.value:
1526
- case i_t_status.VERIFICATION_FAILED if res_status.value == previous_status:
1527
- return result.Error.from_e(exc.DLMSException(), "Verification Error")
1528
- case i_t_status.TRANSFER_INITIATED if res_status.value == previous_status:
1529
- res.append_e(exc.DLMSException("Expected Switch to Verification Initiated status, got Initiated"))
1530
- case i_t_status.TRANSFER_NOT_INITIATED:
1531
- res.append_e(exc.DLMSException("Got Not initiated status after call Initiation"))
1532
- case i_t_status.TRANSFER_INITIATED:
1533
- while self.n_t_b < self.n_blocks:
1534
- offset = self.n_t_b * block_size
1535
- if isinstance(res_tr_block := await TransferBlock(
1536
- par=self.par,
1537
- number=cdt.DoubleLongUnsigned(self.n_t_b),
1538
- value=cdt.OctetString(bytearray(self.image[offset: offset + block_size]))
1539
- ).exchange(c), result.Error):
1540
- return res_tr_block
1541
- self.n_t_b += 1 # todo: maybe get from SERVER - await get_not_transferred_block.exchange(c)
1542
- c.log(logL.INFO, "All blocks transferred")
1543
- if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
1544
- return res_verify
1545
- case i_t_status.VERIFICATION_INITIATED:
1546
- c.log(logL.INFO, "read bitstring. It must grow")
1547
- # TODO: calculate time for waiting or read growing bitstring ?
1548
- if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
1549
- return res_tbs
1550
- if len(res_tbs.value) < self.n_t_b:
1551
- c.log(logL.INFO, F"Got blocks[{len(res_tbs.value)}]") # todo: remove
1552
- else:
1553
- c.log(logL.INFO, "All Bits solved")
1554
- case i_t_status.VERIFICATION_FAILED:
1555
- if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
1556
- return res_tbs
1557
- valid = tuple(res_tbs.value)
1558
- c.log(logL.INFO, F"Got blocks[{len(res_tbs.value)}]") # todo: remove
1559
- for i in filter(lambda it: valid[it] == b'\x00', range(len(valid))):
1560
- offset = i * block_size
1561
- if isinstance(res_tr_block := await TransferBlock(
1562
- par=self.par,
1563
- number=cdt.DoubleLongUnsigned(i),
1564
- value=cdt.OctetString(bytearray(self.image[offset: offset + block_size]))
1565
- ).exchange(c), result.Error):
1566
- return res_tr_block
1567
- if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
1568
- return res_verify
1569
- c.log(logL.INFO, "Start Verify Transfer")
1570
- case i_t_status.VERIFICATION_SUCCESSFUL:
1571
- if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
1572
- return res_tbs
1573
- valid = tuple(res_tbs.value)
1574
- if isinstance(res_ntb := await Par2Data(self.par.image_first_not_transferred_block_number).exchange(c), result.Error):
1575
- return res_ntb
1576
- self.n_t_b = int(res_ntb.value)
1577
- if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
1578
- return res_activate_info
1579
- c.log(logL.INFO, F"md5:{res_activate_info.value[0].image_to_activate_signature}")
1580
- if any(map(lambda it: it == b'\x00', valid)):
1581
- if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1582
- return res_initiate
1583
- c.log(logL.INFO, "Start initiate Image Transfer, after wrong verify. Exist 0 blocks")
1584
- return res.as_error(exc.DLMSException("Exist 0 blocks"))
1585
- elif self.n_t_b < len(valid):
1586
- if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1587
- return res_initiate
1588
- c.log(logL.INFO, "Start initiate Image Transfer, after wrong verify. Got not transferred block")
1589
- return res.as_error(exc.DLMSException(F"Got {res_ntb.value} not transferred block"))
1590
- elif res_activate_info.value[0].image_to_activate_signature != res_activate_info.value[0].image_to_activate_identification:
1591
- if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1592
- return res_initiate
1593
- return res.as_error(exc.DLMSException(
1594
- F"Signature not match to Identification: got {res_activate_info.value[0].image_to_activate_signature}, "
1595
- F"expected {res_activate_info.value[0].image_to_activate_identification}"),
1596
- "Start initiate Image Transfer, after wrong verify")
1597
- else:
1598
- if isinstance(res_activate := await ActivateImage(self.par).exchange(c), result.Error):
1599
- return res_activate
1600
- c.log(logL.INFO, "Start Activate Transfer")
1601
- case i_t_status.ACTIVATION_INITIATED:
1602
- try:
1603
- await c.disconnect_request()
1604
- except TimeoutError as e:
1605
- c.log(logL.ERR, F"can't use <disconnect request>: {e}")
1606
- if isinstance(res_reconnect := await HardwareReconnect(
1607
- delay=self.waiting_for_activation,
1608
- msg="expected reboot server after upgrade"
1609
- ).exchange(c), result.Error):
1610
- return res_reconnect
1611
- case i_t_status.ACTIVATION_SUCCESSFUL:
1612
- if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
1613
- return res_activate_info
1614
- if res_activate_info.value[0].image_to_activate_identification == self.ITI.image_identifier:
1615
- c.log(logL.INFO, "already activated this image")
1616
- return res
1617
- else:
1618
- if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1619
- return res_initiate
1620
- c.log(logL.INFO, "Start initiate Image Transfer")
1621
- # TODO: need wait clearing memory in device ~5 sec
1622
- case i_t_status.ACTIVATION_FAILED:
1623
- return res.as_error(exc.DLMSException(), "Ошибка активации...")
1624
- case err:
1625
- return res.as_error(exc.DLMSException(), f"Unknown image transfer status: {err}")
1626
- previous_status = res_status.value
1627
- await asyncio.sleep(1) # TODO: tune it
1628
- if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
1629
- return res_status
1630
- c.log(logL.INFO, f"{res_status.value=}")
1631
-
1632
-
1633
- @dataclass(frozen=True)
1634
- class TransferBlock(SimpleCopy, OK):
1635
- par: dlms_par.ImageTransfer
1636
- number: cdt.DoubleLongUnsigned
1637
- value: cdt.OctetString
1638
- msg: str = "transfer block"
1639
-
1640
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1641
- if isinstance(res := await Execute2(
1642
- par=self.par.image_block_transfer,
1643
- data=ImageBlockTransfer((self.number, self.value))
1644
- ).exchange(c), result.Error):
1645
- return res
1646
- return result.OK
1647
-
1648
-
1649
- @dataclass(frozen=True)
1650
- class VerifyImage(SimpleCopy, OK):
1651
- par: dlms_par.ImageTransfer
1652
- msg: str = "Verify image"
1653
-
1654
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1655
- if isinstance(res := await Execute2(self.par.image_verify, integers.INTEGER_0).exchange(c), result.Error):
1656
- return res
1657
- return result.OK
1658
-
1659
-
1660
- @dataclass(frozen=True)
1661
- class ActivateImage(SimpleCopy, OK):
1662
- par: dlms_par.ImageTransfer
1663
- msg: str = "Activate image"
1664
-
1665
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1666
- if isinstance(res := await Execute2(self.par.image_activate, integers.INTEGER_0).exchange(c), result.Error):
1667
- return res
1668
- return result.OK
1669
-
1670
-
1671
- # todo: don't work with new API, remake
1672
- class TestAll(OK):
1673
- """read all attributes with check access""" # todo: add Write with access
1674
- msg: str = "test all"
1675
-
1676
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1677
- # todo: refactoring with <append_err>
1678
- res = result.ErrorAccumulator()
1679
- if isinstance(res_objects := c.objects.sap2objects(c.SAP), result.Error):
1680
- return res_objects
1681
- ass: collection.AssociationLN = c.objects.sap2association(c.SAP)
1682
- for obj in res_objects.value:
1683
- indexes: list[int] = [i for i, _ in obj.get_index_with_attributes()]
1684
- c.log(logL.INFO, F"start read {obj} attr: {', '.join(map(str, indexes))}")
1685
- for i in indexes:
1686
- is_readable = ass.is_readable(
1687
- ln=obj.logical_name,
1688
- index=i)
1689
- if isinstance(res_read := await ReadObjAttr(obj, i).exchange(c), result.Error):
1690
- if (
1691
- res_read.has(pdu.DataAccessResult.READ_WRITE_DENIED, exc.ResultError)
1692
- and not is_readable
1693
- ):
1694
- c.log(logL.INFO, F"success ReadAccess TEST")
1695
- else:
1696
- res.append_err(res_read.err)
1697
- elif not is_readable:
1698
- res.append_e(PermissionError(f"{obj} with attr={i} must be unreadable"))
1699
- indexes.remove(i)
1700
- return res.result
1701
-
1702
-
1703
- @dataclass
1704
- class ApplyTemplate(SimpleCopy, Base):
1705
- template: collection.Template
1706
- msg: str = "apply template"
1707
-
1708
- async def exchange(self, c: Client) -> result.Result:
1709
- # todo: search col
1710
- attr: cdt.CommonDataType
1711
- res = result.StrictOk()
1712
- for col in self.template.collections:
1713
- if col == c.objects:
1714
- use_col = col
1715
- break
1716
- else:
1717
- c.log(logL.ERR, F"not find collection for {c}")
1718
- raise asyncio.CancelledError()
1719
- for ln, indexes in self.template.used.items():
1720
- if (obj := res.propagate_err(use_col.logicalName2obj(ln))) is not None:
1721
- for i in indexes:
1722
- if (attr := obj.get_attr(i)) is not None:
1723
- res.propagate_err(await WriteAttribute(
1724
- ln=ln,
1725
- index=i,
1726
- value=attr.encoding
1727
- ).exchange(c))
1728
- else:
1729
- res.append_e(exc.EmptyObj(F"skip apply {self.template} {ln}:{i}: no value"))
1730
- return res
1731
-
1732
-
1733
- @dataclass
1734
- class ReadTemplate(OK):
1735
- template: collection.Template
1736
- msg: str = "read template"
1737
-
1738
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1739
- # todo: copypast from <ApplyTemplate>
1740
- attr: cdt.CommonDataType
1741
- res = result.ErrorAccumulator()
1742
- for col in self.template.collections:
1743
- if col == c._objects:
1744
- use_col = col
1745
- break
1746
- else:
1747
- c.log(logL.ERR, F"not find collection for {c}")
1748
- raise asyncio.CancelledError()
1749
- for ln, indexes in self.template.used.items():
1750
- try:
1751
- obj = use_col.get_object(ln) # todo: maybe not need
1752
- except exc.NoObject as e:
1753
- c.log(logL.WARN, F"skip apply {self.template}: {e}")
1754
- continue
1755
- res.propagate_err(
1756
- await ReadAttributes(
1757
- ln=ln,
1758
- indexes=tuple(indexes)
1759
- ).exchange(c))
1760
- return res.result
1761
-
1762
-
1763
- @dataclass
1764
- class AccessValidate(OK):
1765
- """check all access rights for current SAP"""
1766
- with_correct: bool = False
1767
- msg: str = "all access validate"
1768
-
1769
- # todo: make with result.Error
1770
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1771
- res = result.ErrorAccumulator()
1772
- obj_l: ObjectListType
1773
- el: ObjectListElement
1774
- a_a_i: association_ln.abstract.AttributeAccessItem
1775
- if (obj_l := c.objects.sap2association(c.SAP).object_list) is None:
1776
- return result.Error.from_e(exc.EmptyObj(F"empty object_list for {c._objects.sap2association(c.SAP)}"))
1777
- for el in obj_l:
1778
- for a_a_i in el.access_rights.attribute_access:
1779
- if a_a_i.access_mode.is_readable():
1780
- i = int(a_a_i.attribute_id)
1781
- if isinstance(res_read :=await ReadByDescriptor(ut.CosemAttributeDescriptor((
1782
- int(el.class_id),
1783
- el.logical_name.contents,
1784
- i
1785
- ))).exchange(c), result.Error):
1786
- res.append_err(res_read.err)
1787
- if self.with_correct:
1788
- a_a_i.access_mode.set(1) # todo: make better in future
1789
- elif a_a_i.access_mode.is_writable():
1790
- if isinstance(res_write :=await WriteAttribute(
1791
- ln=el.logical_name,
1792
- index=i,
1793
- value=res_read.value
1794
- ).exchange(c), result.Error):
1795
- res.append_err(res_write.err)
1796
- return res.result
1797
-
1798
-
1799
- @dataclass
1800
- @deprecated("use <WriteList>")
1801
- class WriteParDatas(SimpleCopy, _List[result.Ok]):
1802
- """write by ParData list"""
1803
- par_datas: list[ParData]
1804
- msg: str = ""
1805
-
1806
- async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
1807
- res = result.List()
1808
- for pardata in self.par_datas:
1809
- res.append(await WriteAttribute(
1810
- ln=pardata.par.ln,
1811
- index=pardata.par.i,
1812
- value=pardata.data.encoding
1813
- ).exchange(c))
1814
- return res
1815
-
1816
-
1817
- class WriteList(SimpleCopy, _List[result.Ok]):
1818
- """write by list"""
1819
- par_datas: tuple[tuple[Parameter, cdt.CommonDataType], ...]
1820
- err_ignore: bool
1821
-
1822
- def __init__(self, *par_datas: tuple[Parameter, cdt.CommonDataType], err_ignore: bool = False, msg: str = "write list") -> None:
1823
- self.par_datas = par_datas
1824
- self.err_ignore = err_ignore
1825
- self.msg = msg
1826
-
1827
- async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
1828
- res = result.List[result.Ok]()
1829
- for par, data in self.par_datas:
1830
- if (
1831
- isinstance(res_one := await Write2(par, data).exchange(c), result.Error)
1832
- and not self.err_ignore
1833
- ):
1834
- return res_one
1835
- res.append(res_one)
1836
- return res
1837
-
1838
-
1839
- @dataclass(frozen=True)
1840
- class WriteTranscript(SimpleCopy, OK):
1841
- """write by ParValues[Transcript]"""
1842
- par: Parameter
1843
- value: cdt.Transcript
1844
- msg: str = "write with transcript"
1845
-
1846
- async def exchange(self, c: Client) -> result.Ok | result.Error:
1847
- if isinstance((res := await Par2Data[cdt.CommonDataType](self.par).exchange(c)), result.Error):
1848
- return res
1849
- if isinstance(res.value, cdt.Digital):
1850
- s_u = c.objects.par2su(self.par)
1851
- if isinstance(s_u, cdt.ScalUnitType):
1852
- if not isinstance(self.value, str):
1853
- return result.Error.from_e(TypeError(), f"for {self.par} got type: {self.value}, expected String")
1854
- try:
1855
- data = res.value.parse(value := str(float(self.value) * 10 ** -int(s_u.scaler)))
1856
- except ValueError as e:
1857
- return result.Error.from_e(e, f"for {self.par} got value: {self.value}, expected Float or Digital")
1858
- else:
1859
- data = res.value.parse(self.value)
1860
- else:
1861
- data = res.value.parse(self.value)
1862
- return await Write2(self.par, data).exchange(c)
1863
-
1864
-
1865
- class WriteTranscripts(SimpleCopy, _List[result.Ok]):
1866
- """write by ParValues[Transcript] list"""
1867
- par_values: tuple[tuple[Parameter, cdt.Transcript], ...]
1868
- err_ignore: bool
1869
-
1870
- def __init__(self, *par_values: tuple[Parameter, cdt.Transcript], err_ignore: bool = False, msg: str ="write transcripts"):
1871
- self.par_values = par_values
1872
- self.err_ignore = err_ignore
1873
- self.msg = msg
1874
-
1875
- async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
1876
- res = result.List[result.Ok]()
1877
- for par, value in self.par_values:
1878
- if (
1879
- isinstance(res_one := await WriteTranscript(par, value).exchange(c), result.Error)
1880
- and not self.err_ignore
1881
- ):
1882
- return res_one
1883
- res.append(res_one)
1884
- return res
1
+ import asyncio
2
+ import random
3
+ from copy import copy
4
+ import re
5
+ from typing_extensions import deprecated
6
+ import hashlib
7
+ from dataclasses import dataclass, field
8
+ from typing import Callable, Any, Optional, Protocol, cast, override, Self, Final, Iterable, TypeVarTuple
9
+ from itertools import count
10
+ import datetime
11
+ import time
12
+ from semver import Version as SemVer
13
+ from StructResult import result
14
+ from DLMS_SPODES.pardata import ParValues
15
+ from DLMS_SPODES.types.implementations import enums, long_unsigneds, bitstrings, octet_string, structs, arrays, integers
16
+ from DLMS_SPODES.pardata import ParData
17
+ from DLMS_SPODES.cosem_interface_classes import parameters as dlms_par
18
+ from DLMS_SPODES.cosem_interface_classes.parameter import Parameter
19
+ from DLMS_SPODES import exceptions as exc, pdu_enums as pdu
20
+ from DLMS_SPODES.cosem_interface_classes import (
21
+ cosem_interface_class as ic,
22
+ collection,
23
+ overview,
24
+ ln_pattern
25
+ )
26
+ from DLMS_SPODES.cosem_interface_classes.clock import Clock
27
+ from DLMS_SPODES.cosem_interface_classes.image_transfer.image_transfer_status import ImageTransferStatus
28
+ from DLMS_SPODES.cosem_interface_classes.image_transfer.ver0 import ImageTransferInitiate, ImageBlockTransfer, ImageToActivateInfo
29
+ from DLMS_SPODES.cosem_interface_classes.association_ln.ver0 import ObjectListType, ObjectListElement
30
+ from DLMS_SPODES.cosem_interface_classes import association_ln
31
+ from DLMS_SPODES.types import cdt, ut, cst
32
+ from DLMS_SPODES.hdlc import frame
33
+ from DLMS_SPODES.enums import Transmit, Application, Conformance
34
+ from DLMS_SPODES.firmwares import get_firmware
35
+ from DLMS_SPODES.cosem_interface_classes.image_transfer import image_transfer_status as i_t_status
36
+ from DLMSAdapter.main import AdapterException, Adapter, gag
37
+ from DLMSCommunicationProfile.osi import OSI
38
+ from .logger import LogLevel as logL
39
+ from .client import Client, Security, Data, mechanism_id, AcseServiceUser, State
40
+
41
+
42
+ firm_id_pat = re.compile(b".*(?P<fid>PWRM_M2M_[^_]{1,10}_[^_]{1,10}).+")
43
+ boot_ver_pat = re.compile(b"(?P<boot_ver>\\d{1,4}).+")
44
+
45
+
46
+ type Errors = list[Exception]
47
+
48
+
49
+ class Base[T: result.Result](Protocol):
50
+ """Exchange task for DLMS client"""
51
+ msg: str
52
+
53
+ def copy(self) -> Self: ...
54
+
55
+ @property
56
+ def current(self) -> 'Base[T] | Self':
57
+ return self
58
+
59
+ async def run(self, c: Client) -> T | result.Error:
60
+ """exception handling block"""
61
+ try:
62
+ return await self.physical(c)
63
+ except (ConnectionRefusedError, TimeoutError) as e:
64
+ return result.Error.from_e(e)
65
+ except exc.DLMSException as e:
66
+ return result.Error.from_e(e)
67
+ except Exception as e:
68
+ return result.Error.from_e(e)
69
+ # except asyncio.CancelledError as e:
70
+ # await c.close() # todo: change to DiscRequest
71
+ # return result.Error.from_e(exc.Abort("manual stop")) # for handle BaseException
72
+ finally:
73
+ c.received_frames.clear() # for next exchange need clear all received frames. todo: this bag, remove in future
74
+
75
+ async def PH_connect(self, c: Client) -> result.Ok | result.Error:
76
+ if c.media is None:
77
+ return result.Error.from_e(exc.NoPort("no media"), "PH_connect")
78
+ if not c.media.is_open():
79
+ if isinstance(res_open := await c.media.open(), result.Error):
80
+ return res_open
81
+ c.log(logL.INFO, F"Open port communication channel: {c.media} {res_open.value}sec")
82
+ c.level = OSI.PHYSICAL
83
+ # todo: replace to <data_link>
84
+ if (
85
+ c._objects is None
86
+ and not isinstance(self, InitType)
87
+ ):
88
+ if isinstance(res := await init_type.data_link(c), result.Error):
89
+ return res.with_msg("PH_connect")
90
+ if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest, or make not closed or reconnect !!!
91
+ return res_close
92
+ return result.OK
93
+
94
+ @staticmethod
95
+ async def physical_t(c: Client) -> result.Ok | result.Error:
96
+ return await c.close()
97
+
98
+ async def physical(self, c: Client) -> T | result.Error:
99
+ if OSI.PHYSICAL not in c.level:
100
+ if isinstance((res := await self.PH_connect(c)), result.Error):
101
+ return res
102
+ ret = await self.data_link(c)
103
+ if isinstance(res_terminate := await self.physical_t(c), result.Error):
104
+ return res_terminate
105
+ return ret
106
+
107
+ async def DL_connect(self, c: Client) -> result.Ok | result.Error:
108
+ """Data link Layer connect"""
109
+ c.send_frames.clear()
110
+ # calculate addresses todo: move to c.com_profile(HDLC)
111
+ c.DA = frame.Address(
112
+ upper_address=int(c.server_SAP),
113
+ lower_address=c.com_profile.parameters.device_address,
114
+ length=c.addr_size
115
+ )
116
+ c.SA = frame.Address(upper_address=int(c.SAP))
117
+ c.log(logL.INFO, F"{c.SA=} {c.DA=}")
118
+ # initialize connection
119
+ if c.settings.cipher.security != Security.NONE:
120
+ c.log(logL.DEB, F"Security: {c.settings.cipher.security}/n"
121
+ F"System title: {c.settings.cipher.systemTitle.hex()}"
122
+ F"Authentication key: {c.settings.cipher.authenticationKey.hex()}"
123
+ F"Block cipher key: {c.settings.cipher.blockCipherKey.hex()}")
124
+ if c.settings.cipher.dedicatedKey:
125
+ c.log(logL.DEB, F"Dedicated key: {c.settings.cipher.dedicatedKey.hex()}")
126
+ # SNRM
127
+ c.get_SNRM_request()
128
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
129
+ return res_pdu
130
+ c.level |= OSI.DATA_LINK
131
+ return result.OK
132
+
133
+ async def data_link(self, c: Client) -> T | result.Error:
134
+ if OSI.DATA_LINK not in c.level:
135
+ if isinstance(res_conn := await self.DL_connect(c), result.Error):
136
+ return res_conn
137
+ # todo: make tile
138
+ return await self.application(c)
139
+
140
+ async def AA(self, c: Client) -> result.Ok | result.Error:
141
+ """Application Associate"""
142
+ if c.invocationCounter and c.settings.cipher is not None and c.settings.cipher.security != Security.NONE:
143
+ # create IC object. TODO: remove it after close connection, maybe???
144
+ c.settings.proposedConformance |= Conformance.GENERAL_PROTECTION
145
+
146
+ # my block
147
+ IC: Data = c.objects.add_if_missing(ut.CosemClassId(1),
148
+ logical_name=cst.LogicalName(bytearray((0, c.get_channel_index(), 43, 1,
149
+ c.current_association.security_setup_reference.e, 255))),
150
+ version=cdt.Unsigned(0))
151
+ tmp_client_SAP = c.current_association.associated_partners_id.client_SAP
152
+ challenge = c.settings.ctoSChallenge
153
+ try:
154
+ c.aarqRequest(c.m_id)
155
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
156
+ return res_pdu
157
+ ret = c.parseAareResponse(res_pdu.value)
158
+ c.level |= OSI.APPLICATION # todo: it's must be result of <ret> look down
159
+ if isinstance(res_ic := await ReadObjAttr(IC, 2).exchange(c), result.Error):
160
+ return res_ic
161
+ c.settings.cipher.invocationCounter = 1 + int(res_ic.value)
162
+ c.log(logL.DEB, "Invocation counter: " + str(c.settings.cipher.invocationCounter))
163
+ # disconnect
164
+ if c.media and c.media.is_open():
165
+ c.log(logL.DEB, "DisconnectRequest")
166
+ if isinstance(res_disc_req := await c.disconnect_request(), result.Error):
167
+ return res_disc_req
168
+ finally:
169
+ c.SAP = tmp_client_SAP
170
+ c.settings.useCustomChallenge = challenge is not None
171
+ c.settings.ctoSChallenge = challenge
172
+
173
+ # gurux with several removed methods
174
+ # add = self.settings.clientAddress
175
+ # auth = self.settings.authentication
176
+ # security = self.client.ciphering.security
177
+ # challenge = self.client.ctoSChallenge
178
+ # try:
179
+ # self.client.clientAddress = 16
180
+ # self.settings.authentication = Authentication.NONE
181
+ # self.client.ciphering.security = Security.NONE
182
+ # reply = GXReplyData()
183
+ # self.get_SNRM_request()
184
+ # self.status = Status.READ
185
+ # self.read_data_block2()
186
+ # self.objects.IEC_HDLS_setup.set_from_info(self.reply.data.get_data())
187
+ # self.connection_state = ConnectionState.HDLC
188
+ # self.reply.clear()
189
+ # self.aarqRequest()
190
+ # self.read_data_block2()
191
+ # self.parseAareResponse(reply.data)
192
+ # reply.clear()
193
+ # item = GXDLMSData(self.invocationCounter)
194
+ # data = self.client.read(item, 2)[0]
195
+ # reply = GXReplyData()
196
+ # self.read_data_block(data, reply)
197
+ # item.encodings[2] = reply.data.get_data()
198
+ # Update data type on read.
199
+ # if item.getDataType(2) == cdt.NullData.TAG:
200
+ # item.setDataType(2, reply.valueType)
201
+ # self.client.updateValue(item, 2, reply.value)
202
+ # self.client.ciphering.invocationCounter = 1 + item.value
203
+ # print("Invocation counter: " + str(self.client.ciphering.invocationCounter))
204
+ # if self.media and self.media.isOpen():
205
+ # self.log(logL.INFO, "DisconnectRequest")
206
+ # self.disconnect_request()
207
+ # finally:
208
+ # self.settings.clientAddress = add
209
+ # self.settings.authentication = auth
210
+ # self.client.ciphering.security = security
211
+ # self.client.ctoSChallenge = challenge
212
+
213
+ c.aarqRequest(c.m_id)
214
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
215
+ return res_pdu
216
+ # await c.read_attr(ut.CosemAttributeDescriptor((collection.ClassID.ASSOCIATION_LN, ut.CosemObjectInstanceId("0.0.40.0.0.255"), ut.CosemObjectAttributeId(6)))) # for test only
217
+ try:
218
+ parse = c.parseAareResponse(res_pdu.value)
219
+ except IndexError as e:
220
+ print(e)
221
+ match parse:
222
+ case AcseServiceUser.NULL:
223
+ c.log(logL.INFO, "Authentication success")
224
+ c.level |= OSI.APPLICATION
225
+ case AcseServiceUser.AUTHENTICATION_REQUIRED:
226
+ c.getApplicationAssociationRequest()
227
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
228
+ return res_pdu
229
+ c.parseApplicationAssociationResponse(res_pdu.value)
230
+ case _ as diagnostic:
231
+ return result.Error.from_e(exc.AssociationResultError(diagnostic))
232
+ if c._objects is not None:
233
+ matchLDN = change_ldn if c.is_universal() else match_ldn
234
+ if isinstance(res_match_ldn := await matchLDN.exchange(c), result.Error):
235
+ return res_match_ldn
236
+ return result.OK
237
+
238
+ async def application(self, c: Client) -> T | result.Error:
239
+ if OSI.APPLICATION not in c.level:
240
+ if isinstance(res := await self.AA(c), result.Error):
241
+ return res
242
+ # no tile
243
+ return await self.exchange(c)
244
+
245
+ async def exchange(self, c: Client) -> T | result.Error:
246
+ """application level exchange"""
247
+
248
+ async def connect(self, c: Client) -> result.Ok | result.Error:
249
+ await self.PH_connect(c)
250
+ await self.DL_connect(c)
251
+ return await self.AA(c)
252
+
253
+
254
+ class SimpleCopy:
255
+ def copy(self) -> Self:
256
+ return self
257
+
258
+
259
+ class Simple[T](Base[result.Simple[T]], Protocol):
260
+ """Simple result"""
261
+ @override
262
+ async def exchange(self, c: Client) -> result.SimpleOrError[T]: ...
263
+
264
+
265
+ class Boolean(Simple[bool], Protocol):
266
+ """Simple[bool] result"""
267
+ @override
268
+ async def exchange(self, c: Client) -> result.SimpleOrError[bool]: ...
269
+
270
+
271
+ class CDT[T: cdt.CommonDataType](Simple[T], Protocol):
272
+ """Simple[CDT] result"""
273
+ @override
274
+ async def exchange(self, c: Client) -> result.SimpleOrError[T]: ...
275
+
276
+
277
+ class _List[T](Base[result.List[T]], Protocol):
278
+ """With List result"""
279
+ @override
280
+ async def exchange(self, c: Client) -> result.List[T] | result.Error: ...
281
+
282
+
283
+ class _Sequence[*Ts](Base[result.Sequence[*Ts]], Protocol):
284
+ """With List result"""
285
+ @override
286
+ async def exchange(self, c: Client) -> result.Sequence[*Ts] | result.Error: ...
287
+
288
+
289
+ class OK(Base[result.Ok], Protocol):
290
+ """Always result OK"""
291
+
292
+ @override
293
+ async def exchange(self, c: Client) -> result.Ok | result.Error: ...
294
+
295
+
296
+ class StrictOK(Base[result.StrictOk], Protocol):
297
+ """Always result OK"""
298
+
299
+ @override
300
+ async def exchange(self, c: Client) -> result.StrictOk | result.Error: ...
301
+
302
+
303
+ @dataclass(frozen=True)
304
+ class ClientBlocking(SimpleCopy, OK):
305
+ """complete by time or abort"""
306
+ delay: float = field(default=99999999.0)
307
+ msg: str = "client blocking"
308
+
309
+ async def run(self, c: Client) -> result.Ok | result.Error:
310
+ try:
311
+ c.level = OSI.APPLICATION
312
+ c.log(logL.WARN, F"blocked for {self.delay} second")
313
+ await asyncio.sleep(self.delay)
314
+ return result.OK
315
+ finally:
316
+ c.level = OSI.NONE
317
+ return result.OK
318
+
319
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
320
+ raise RuntimeError(f"not support for {self.__class__.__name__}")
321
+
322
+
323
+ # todo: make with <data_link>
324
+ @dataclass
325
+ class TestDataLink(SimpleCopy, OK):
326
+ msg: str = "test DLink"
327
+
328
+ async def physical(self, c: Client) -> result.Ok | result.Error:
329
+ if OSI.PHYSICAL not in c.level:
330
+ if not c.media.is_open():
331
+ if isinstance(res_open := await c.media.open(), result.Error):
332
+ return res_open
333
+ c.level = OSI.PHYSICAL
334
+ c.DA = frame.Address(
335
+ upper_address=int(c.server_SAP),
336
+ lower_address=c.com_profile.parameters.device_address,
337
+ length=c.addr_size
338
+ )
339
+ c.SA = frame.Address(upper_address=int(c.SAP))
340
+ c.get_SNRM_request()
341
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
342
+ return res_pdu
343
+ c.level |= OSI.DATA_LINK
344
+ if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest
345
+ return res_close
346
+ return result.Ok
347
+
348
+ async def exchange(self, c: Client):
349
+ return result.OK
350
+
351
+
352
+ @dataclass(frozen=True)
353
+ class Dummy(SimpleCopy, OK):
354
+ msg: str = "dummy"
355
+
356
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
357
+ """"""
358
+ return result.OK
359
+
360
+
361
+ @dataclass(frozen=True)
362
+ class HardwareDisconnect(SimpleCopy, OK):
363
+ msg: str = "hardware disconnect"
364
+
365
+ @override
366
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
367
+ await c.media.close()
368
+ c.level = OSI.NONE
369
+ msg = '' if self.msg is None else F": {self.msg}"
370
+ c.log(logL.WARN, F"HARDWARE DISCONNECT{msg}")
371
+ return result.OK
372
+
373
+
374
+ @dataclass(frozen=True)
375
+ class HardwareReconnect(SimpleCopy, OK):
376
+ delay: float = 0.0
377
+ """delay between disconnect and restore Application"""
378
+ msg: str = "reconnect media without response"
379
+
380
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
381
+ if isinstance((res := await HardwareDisconnect().exchange(c)), result.Error):
382
+ return res
383
+ if self.delay != 0.0:
384
+ c.log(logL.INFO, F"delay({self.delay})")
385
+ await asyncio.sleep(self.delay)
386
+ if isinstance(res_connect := await self.connect(c), result.Error): # restore Application
387
+ return res_connect
388
+ return result.OK
389
+
390
+
391
+ @dataclass(frozen=True)
392
+ class Loop(OK):
393
+ task: Base[result.Result]
394
+ func: Callable[[Any], bool]
395
+ delay: int = 0.0
396
+ msg: str = "loop"
397
+ attempt_amount: int = 0
398
+ """0 is never end loop"""
399
+
400
+ def copy(self) -> Self:
401
+ return Loop(
402
+ task=self.task.copy(),
403
+ func=self.func,
404
+ delay=self.delay,
405
+ msg=self.msg,
406
+ attempt_amount=self.attempt_amount
407
+ )
408
+
409
+ async def run(self, c: Client) -> result.Result:
410
+ attempt = count()
411
+ while not self.func(await super(Loop, self).run(c)):
412
+ if next(attempt) == self.attempt_amount:
413
+ return result.Error.from_e(ValueError("end of attempts"))
414
+ await asyncio.sleep(self.delay)
415
+ return result.OK
416
+
417
+ async def exchange(self, c: Client) -> result.Result:
418
+ return await self.task.exchange(c)
419
+
420
+
421
+ @dataclass
422
+ class ConditionalTask[T](Base):
423
+ """Warning: experimental"""
424
+ precondition_task: Base[result.Result]
425
+ comp_value: T
426
+ predicate: Callable[[T, T], bool]
427
+ main_task: Base[result.Result]
428
+ msg: str = "conditional task"
429
+
430
+ async def exchange(self, c: Client) -> result.Result:
431
+ res = await self.precondition_task.exchange(c)
432
+ if self.predicate(self.comp_value, res.value):
433
+ return await self.main_task.exchange(c)
434
+ return result.Error.from_e(ValueError("Condition not satisfied"))
435
+
436
+
437
+ DEFAULT_DATETIME_SCHEDULER = cdt.DateTime.parse("1.1.0001")
438
+
439
+
440
+ @dataclass
441
+ class Scheduler[T: result.Result](Base[T]):
442
+ """𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦 = 𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦_𝑚𝑖𝑛 × (𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦_𝑒𝑥𝑝𝑜𝑛𝑒𝑛𝑡 × 0.01) ** 𝑛"""
443
+ task: Base[T]
444
+ execution_datetime: Final[cdt.DateTime] = DEFAULT_DATETIME_SCHEDULER
445
+ start_interval: Final[int] = 0
446
+ number_of_retries: Final[int] = 3
447
+ total_of_retries: Final[int] = 100
448
+ repetition_delay_min: Final[int] = 1
449
+ repetition_delay_exponent: Final[int] = 100
450
+ repetition_delay_max: Final[int] = 100
451
+ msg: str = "sheduler"
452
+
453
+ def copy(self) -> "Scheduler[T]":
454
+ return Scheduler(
455
+ task=self.task.copy(),
456
+ execution_datetime=self.execution_datetime,
457
+ start_interval=self.start_interval,
458
+ number_of_retries=self.number_of_retries,
459
+ total_of_retries=self.total_of_retries,
460
+ repetition_delay_min=self.repetition_delay_min,
461
+ repetition_delay_exponent=self.repetition_delay_exponent,
462
+ repetition_delay_max=self.repetition_delay_max,
463
+ msg=self.msg
464
+ )
465
+
466
+ async def run(self, c: Client) -> T | result.Error:
467
+ if self.start_interval != 0:
468
+ await asyncio.sleep(random.uniform(0, self.start_interval))
469
+ c.log(logL.INFO, f"start {self.__class__.__name__}")
470
+ total_of_retries = count()
471
+ is_start: bool = True
472
+ acc = result.ErrorAccumulator()
473
+ while True:
474
+ dt = self.execution_datetime.get_right_nearest_datetime(now := datetime.datetime.now())
475
+ if dt is None:
476
+ if is_start:
477
+ is_start = False
478
+ else:
479
+ return acc.as_error(exc.Timeout("start time is out"), msg=self.msg)
480
+ else:
481
+ delay = (dt - now).total_seconds()
482
+ c.log(logL.WARN, f"wait for {delay=}")
483
+ await asyncio.sleep(delay)
484
+ for n in range(self.number_of_retries):
485
+ if next(total_of_retries) > self.total_of_retries:
486
+ return acc.as_error(exc.Timeout("out of total retries"), msg=self.msg)
487
+ await asyncio.sleep(min(self.repetition_delay_max, self.repetition_delay_min*(self.repetition_delay_exponent * 0.01)**n))
488
+ if isinstance(res := await super(Scheduler, self).run(c), result.Error):
489
+ acc.append_err(res.err)
490
+ else:
491
+ return res
492
+
493
+ async def exchange(self, c: Client) -> T | result.Error:
494
+ return await self.task.exchange(c)
495
+
496
+
497
+ @dataclass
498
+ class Subtasks[U: Base[result.Result]](Protocol):
499
+ """for register longer other tasks into task"""
500
+ tasks: Iterable[U]
501
+
502
+ @property
503
+ def current(self) -> U | Self: ...
504
+
505
+
506
+ class List[T: result.Result, U: Base[result.Result]](Subtasks[U], _List[T]):
507
+ """for exchange task sequence"""
508
+ __is_exchange: bool
509
+ err_ignore: bool
510
+ msg: str
511
+ __current: Base[T]
512
+
513
+ def __init__(self, *tasks: Base[T], msg: str = "", err_ignore: bool = False):
514
+ self.tasks = list(tasks)
515
+ self.__current = self
516
+ self.__is_exchange = False
517
+ self.msg = self.__class__.__name__ if msg == "" else msg
518
+ self.err_ignore = err_ignore
519
+
520
+ def copy(self) -> Self:
521
+ if all((isinstance(t, SimpleCopy) for t in self.tasks)):
522
+ return self
523
+ return List(
524
+ *(t.copy() for t in self.tasks),
525
+ msg=self.msg,
526
+ err_ignore=self.err_ignore
527
+ )
528
+
529
+ @property
530
+ def current(self) -> 'Base[T] | Self':
531
+ return self.__current
532
+
533
+ def append(self, task: Base[T]):
534
+ if not self.__is_exchange:
535
+ self.tasks.append(task)
536
+ else:
537
+ raise RuntimeError(F"append to {self.__class__.__name__} not allowed, already exchange started")
538
+
539
+ async def exchange(self, c: Client) -> result.List[T] | result.Error:
540
+ res = result.List()
541
+ self.__is_exchange = True
542
+ for t in self.tasks:
543
+ self.__current = t
544
+ if (
545
+ isinstance(res_one := await t.exchange(c), result.Error)
546
+ and not self.err_ignore
547
+ ):
548
+ return res_one
549
+ res.append(res_one)
550
+ return res
551
+
552
+
553
+ class Sequence[*Ts](Subtasks[Base[result.Result]], _Sequence[*Ts]):
554
+ """for exchange task sequence"""
555
+ msg: str
556
+ err_ignore: bool
557
+ __current: "Base[result.Result] | Sequence[*Ts]"
558
+ tasks: tuple[Base[result.Result], ...]
559
+
560
+ def __init__(self, *tasks: Base[result.Result], msg: str = "sequence", err_ignore: bool = False):
561
+ self.tasks = tasks
562
+ self.__current = self
563
+ self.msg = self.__class__.__name__ if msg == "" else msg
564
+ self.err_ignore = err_ignore
565
+
566
+ def copy(self) -> "Sequence[*Ts]":
567
+ if all((isinstance(t, SimpleCopy) for t in self.tasks)):
568
+ return self
569
+ return Sequence(
570
+ *(t.copy() for t in self.tasks),
571
+ msg=self.msg,
572
+ err_ignore=self.err_ignore
573
+ )
574
+
575
+ @property
576
+ def current(self) -> Base[result.Result] | Self:
577
+ return self.__current
578
+
579
+ async def exchange(self, c: Client) -> result.Sequence[*Ts] | result.Error:
580
+ res = result.Sequence()
581
+ for t in self.tasks:
582
+ self.__current = t
583
+ if isinstance(res_one := await t.exchange(c), result.Error):
584
+ if self.err_ignore:
585
+ res_one = res_one.with_msg(self.msg)
586
+ else:
587
+ return res_one
588
+ res = res.add(res_one)
589
+ return cast("result.Sequence[*Ts]", res)
590
+
591
+
592
+ @dataclass(frozen=True)
593
+ class SetLocalTime(SimpleCopy, OK):
594
+ """without decide time transfer"""
595
+ msg: str = "set local time"
596
+
597
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
598
+ clock_obj: Clock = c.objects.get_object("0.0.1.0.0.255")
599
+ if isinstance(res := await ReadAttribute(
600
+ ln=clock_obj.logical_name,
601
+ index=3
602
+ ).exchange(c), result.Error):
603
+ return ret
604
+ delta = datetime.timedelta(minutes=int(res.value))
605
+ dt = cst.OctetStringDateTime(datetime.datetime.now(datetime.UTC)+delta)
606
+ if isinstance(res := await WriteAttribute(
607
+ ln=clock_obj.logical_name,
608
+ index=2,
609
+ value=dt.encoding
610
+ ).exchange(c), result.Error):
611
+ return res
612
+ return result.OK
613
+
614
+
615
+ @dataclass(frozen=True)
616
+ class GetFirmwareVersion(SimpleCopy, CDT):
617
+ msg: str = "get firmware version"
618
+
619
+ async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
620
+ return await Par2Data(Parameter(c.objects.id.f_ver.par[:6]).get_attr(c.objects.id.f_ver.par[6])).exchange(c)
621
+
622
+
623
+ @dataclass(frozen=True)
624
+ class ReadByDescriptor(SimpleCopy, Simple[bytes]):
625
+ desc: ut.CosemMethodDescriptor
626
+ msg: str = "get encoding by Cosem-Attribute-Descriptor"
627
+
628
+ async def exchange(self, c: Client) -> result.SimpleOrError[bytes]:
629
+ c.get_get_request_normal(self.desc)
630
+ return await c.read_data_block()
631
+
632
+
633
+ @dataclass(frozen=True)
634
+ class FindFirmwareVersion(SimpleCopy, Simple[collection.ParameterValue]):
635
+ msg: str = "try find COSEM server version, return: instance(B group), CDT"
636
+
637
+ async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
638
+ err = result.ErrorAccumulator()
639
+ for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.1.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.2.255", 2))):
640
+ if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
641
+ err.append_err(res_read.err)
642
+ in_e, out_e = res_read.err.split(exc.ResultError)
643
+ if (
644
+ out_e is None
645
+ and in_e.exceptions[0].args[0] == pdu.DataAccessResult(4)
646
+ ):
647
+ continue
648
+ else:
649
+ return res_read
650
+ else:
651
+ res = result.Simple(collection.ParameterValue(
652
+ par=desc.instance_id.contents + desc.attribute_id.contents,
653
+ value=res_read.value
654
+ ))
655
+ res.propagate_err(err)
656
+ return res
657
+ return err.as_error()
658
+
659
+
660
+ @dataclass(frozen=True)
661
+ class FindFirmwareId(SimpleCopy, Simple[collection.ParameterValue]):
662
+ msg: str = "find firmaware Identifier"
663
+
664
+ async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
665
+ err = result.ErrorAccumulator()
666
+ for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.0.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.1.255", 2))):
667
+ if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
668
+ err.append_err(res_read.err)
669
+ in_e, out_e = res_read.err.split(exc.ResultError)
670
+ if (
671
+ out_e is None
672
+ and in_e.exceptions[0].args[0] == pdu.DataAccessResult(4)
673
+ ):
674
+ continue
675
+ else:
676
+ return res_read
677
+ else:
678
+ res = result.Simple(collection.ParameterValue(
679
+ par=desc.instance_id.contents + desc.attribute_id.contents,
680
+ value=res_read.value
681
+ ))
682
+ res.propagate_err(err)
683
+ return res
684
+ return err.as_error()
685
+
686
+
687
+ @dataclass(frozen=True)
688
+ class KeepAlive(SimpleCopy, OK):
689
+ msg: str = "keep alive(read LND.ln)"
690
+
691
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
692
+ if isinstance(res := await Par2Data(dlms_par.LDN.value).exchange(c), result.Error):
693
+ return res
694
+ return result.OK
695
+
696
+
697
+ class GetLDN(SimpleCopy, CDT[octet_string.LDN]):
698
+ """:return LDN value"""
699
+ msg: str = "get LDN"
700
+
701
+ async def exchange(self, c: Client) -> result.SimpleOrError[octet_string.LDN]:
702
+ if isinstance(res := await ReadByDescriptor(collection.AttrDesc.LDN_VALUE).exchange(c), result.Error):
703
+ return res
704
+ return result.Simple(octet_string.LDN(res.value))
705
+
706
+
707
+ # todo: possible implementation ConditionalTask
708
+ # def compare_ldn(man: bytes, ldn: octet_string.LDN) -> bool:
709
+ # return man == ldn.get_manufacturer()
710
+ #
711
+ #
712
+ # check_LDN = ConditionalTask(
713
+ # precondition_task=GetLDN,
714
+ # comp_value=b"KPZ",
715
+ # predicate=compare_ldn,
716
+ # main_task=None
717
+ # )
718
+
719
+
720
+ @dataclass
721
+ class MatchLDN(OK):
722
+ universal: bool = field(default=False)
723
+ msg: str = "matching LDN"
724
+
725
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
726
+ if isinstance((res := await GetLDN().exchange(c)), result.Error):
727
+ return res.with_msg("match LDN")
728
+ if c.objects.LDN.value is None:
729
+ c._objects.LDN.set_attr(2, res.value)
730
+ elif c._objects.LDN.value == res.value:
731
+ """secret matching"""
732
+ elif self.universal:
733
+ c.log(logL.WARN, F"connected to other server, change LDN")
734
+ await init_type.exchange(c) # todo: maybe set spec?
735
+ else:
736
+ return result.Error.from_e(ValueError(F"got LDN: {res.value}, expected {c._objects.LDN.value}"))
737
+ return result.OK
738
+
739
+
740
+ match_ldn = MatchLDN()
741
+ change_ldn = MatchLDN(universal=True)
742
+
743
+
744
+ @dataclass
745
+ class Lock:
746
+ __lock: asyncio.Lock = field(init=False, default_factory=asyncio.Lock)
747
+ piece: float = field(default=0.8)
748
+ name: str = ""
749
+
750
+ async def acquire(self, c: Client):
751
+ keep_alive = KeepAlive(self.name)
752
+ while True:
753
+ try:
754
+ await asyncio.wait_for(self.__lock.acquire(), c.com_profile.parameters.inactivity_time_out * self.piece) # todo: make not custom <inactivity_time_out>
755
+ return
756
+ except TimeoutError as e:
757
+ await keep_alive.exchange(c)
758
+
759
+ def release(self):
760
+ self.__lock.release()
761
+
762
+
763
+ @dataclass
764
+ class CreateType(SimpleCopy, Simple[collection.Collection]):
765
+ col_id: collection.ID
766
+ msg: str = "CreateType".__class__.__name__
767
+ obj_list: Optional[cdt.Array] = None
768
+ wait_list: asyncio.Lock = field(init=False)
769
+ """wait <object list>"""
770
+ queue: asyncio.Queue = field(default_factory=asyncio.Queue)
771
+ col: collection.Collection = field(init=False)
772
+
773
+ def __post_init__(self):
774
+ self.col = collection.Collection(id_=self.col_id)
775
+ """common collection"""
776
+ self.wait_list = Lock(name="wait <object list>")
777
+
778
+ async def exchange(self, c: Client) -> result.Simple[collection.Collection]:
779
+ res = result.Simple(self.col)
780
+ await self.wait_list.acquire(c)
781
+ try:
782
+ if self.obj_list is None:
783
+ if isinstance(res_obj_list := await ReadByDescriptor(collection.AttrDesc.OBJECT_LIST).exchange(c), result.Error):
784
+ return res_obj_list
785
+ self.obj_list = cdt.Array(res_obj_list.value, type_=structs.ObjectListElement)
786
+ if len(self.col) == 0:
787
+ for country_olel in filter(lambda it: ln_pattern.COUNTRY_SPECIFIC == it.logical_name, self.obj_list):
788
+ self.col.set_country(collection.CountrySpecificIdentifiers(country_olel.logical_name.d))
789
+ c.log(logL.INFO, F"set country: {self.col.country}")
790
+ match self.col.country:
791
+ case collection.CountrySpecificIdentifiers.RUSSIA:
792
+ country_desc = collection.AttrDesc.SPODES_VERSION
793
+ case _:
794
+ country_desc = None
795
+ if (
796
+ country_desc is not None
797
+ and next(filter(lambda it: country_desc.instance_id.contents == it.logical_name.contents, self.obj_list), False)
798
+ ):
799
+ if isinstance(res_country_ver := await ReadByDescriptor(country_desc).exchange(c), result.Error):
800
+ return res_country_ver
801
+ self.col.set_country_ver(collection.ParameterValue(
802
+ par=country_desc.instance_id.contents + country_desc.attribute_id.contents,
803
+ value=res_country_ver.value
804
+ ))
805
+ c.log(logL.INFO, F"set country version: {self.col.country_ver}")
806
+ else:
807
+ c.log(logL.WARN, "was not find <country specific code version> in object_list")
808
+ break
809
+ else:
810
+ c.log(logL.WARN, "was not find <country specific code> in object_list")
811
+ self.col.spec_map = self.col.get_spec()
812
+ for o_l_el in self.obj_list:
813
+ o_l_el: structs.ObjectListElement
814
+ try:
815
+ self.col.add_if_missing( # todo: remove add_if?
816
+ class_id=ut.CosemClassId(int(o_l_el.class_id)),
817
+ version=o_l_el.version,
818
+ logical_name=o_l_el.logical_name)
819
+ except collection.CollectionMapError as e:
820
+ res.append_err(e)
821
+ self.col.add_if_missing(
822
+ class_id=overview.ClassID.DATA,
823
+ version=None, # todo: check version else set 0
824
+ logical_name=cst.LogicalName.from_obis("0.0.42.0.0.255")) # todo: make better
825
+ for ass in self.col.iter_classID_objects(overview.ClassID.ASSOCIATION_LN):
826
+ ass: collection.AssociationLN
827
+ if ass.logical_name.e != 0:
828
+ await ReadObjAttr(ass, 3).exchange(c) # todo: remove from self.queue
829
+ if ass.associated_partners_id.client_SAP == c.SAP:
830
+ cur_ass = ass
831
+ break
832
+ else: # use current association if no variant
833
+ cur_ass = self.col.add_if_missing(
834
+ class_id=overview.ClassID.ASSOCIATION_LN,
835
+ version=None,
836
+ logical_name=cst.LogicalName.from_obis("0.0.40.0.0.255")
837
+ )
838
+ await ReadObjAttr(cur_ass, 3).exchange(c)
839
+ cur_ass.set_attr(2, self.obj_list)
840
+ if cur_ass.associated_partners_id.client_SAP != c.SAP:
841
+ c.log(logL.ERR, F"Wrong current server SAP: {c.SAP} use {cur_ass.associated_partners_id.client_SAP}")
842
+ self.queue.put_nowait((cur_ass, 3)) # read forcibly <associated_partners_id>: use in MapTypeCreator(by <has_sap> method)
843
+ reduce_ln = ln_pattern.LNPattern.parse("0.0.(40,42).0.0.255")
844
+ """reduced objects for read"""
845
+ for o_l_el in cur_ass.object_list: # todo: read necessary data for create_type
846
+ if reduce_ln == o_l_el.logical_name:
847
+ """nothing do it"""
848
+ else:
849
+ if (obj := self.col.get(o_l_el.logical_name.contents)) is None:
850
+ continue
851
+ for access in o_l_el.access_rights.attribute_access:
852
+ i = int(access.attribute_id)
853
+ if (
854
+ i == 1 # skip LN
855
+ or not access.access_mode.is_readable() # skip not readable
856
+ or ( # skip early gotten object_list
857
+ cur_ass.logical_name == o_l_el.logical_name
858
+ and i == 2
859
+ ) or (
860
+ access.access_mode.is_writable() # skip unknown type writable element
861
+ and not ( # except for:
862
+ isinstance(obj.get_attr_element(i).DATA_TYPE, ut.CHOICE)
863
+ or (
864
+ isinstance(obj, collection.ProfileGeneric)
865
+ and i == 3)
866
+ )
867
+ ) or obj.get_attr_element(i).classifier == collection.ic.Classifier.DYNAMIC # skip DYNAMIC
868
+ ):
869
+ continue
870
+ self.queue.put_nowait((obj, i))
871
+ for d_id in collection.get_filtered(
872
+ objects=self.col,
873
+ keys=(
874
+ ln_pattern.DEVICE_ID,
875
+ ln_pattern.PROGRAM_ENTRIES)):
876
+ self.queue.put_nowait((d_id, 2)) # todo: make second queue2 for ReadEmptyAttribute(d_id.logical_name, 2).exchange(c)
877
+ except TimeoutError as e:
878
+ c.log(logL.ERR, F"can't got <object list>: {e}")
879
+ finally:
880
+ self.wait_list.release()
881
+ while True:
882
+ try:
883
+ obj, i = self.queue.get_nowait()
884
+ except asyncio.QueueEmpty as e:
885
+ c.log(logL.INFO, "QueueEmpty")
886
+ try:
887
+ await asyncio.wait_for(self.queue.join(), c.com_profile.parameters.inactivity_time_out) # todo: why whis timeout?? # todo: make not custom <inactivity_time_out>
888
+ break
889
+ except TimeoutError as e:
890
+ c.log(logL.INFO, "wait returned tasks in queue")
891
+ continue # wait returned tasks in queue
892
+ try:
893
+ await c.read_attribute(obj, i)
894
+ except TimeoutError as e:
895
+ c.log(logL.ERR, F"return {obj}:{i} in queue: {e}")
896
+ await self.queue.put((obj, i))
897
+ except exc.Timeout as e:
898
+ c.log(logL.ERR, F"break create type {self.col} by {e}")
899
+ await self.queue.put((obj, i))
900
+ raise e
901
+ except AttributeError as e:
902
+ c.log(logL.ERR, F"skip value wrong value for {obj}:{i}: {e}")
903
+ except Exception as e: # todo: make better!!!
904
+ c.log(logL.ERR, F"skip value wrong value for {obj}:{i}: {e}")
905
+ finally:
906
+ self.queue.task_done()
907
+ print("stop create")
908
+ return res
909
+
910
+
911
+ type ID_SAP = tuple[collection.ID, enums.ClientSAP]
912
+
913
+
914
+ @dataclass(frozen=True)
915
+ class IDSAP:
916
+ id: collection.ID
917
+ sap: enums.ClientSAP
918
+
919
+
920
+ @dataclass
921
+ class NonInit:
922
+ msg: str
923
+
924
+ def __getattr__(self, item):
925
+ raise RuntimeError(self.msg)
926
+
927
+
928
+ class MapTypeCreator:
929
+ adapter: Adapter
930
+ lock: asyncio.Lock
931
+ con: dict[IDSAP, CreateType]
932
+
933
+ def __init__(self, adapter: Adapter):
934
+ self.adapter = adapter
935
+ self.con = dict()
936
+ self.lock = asyncio.Lock()
937
+
938
+ async def get_collection(
939
+ self,
940
+ c: Client,
941
+ col_id: collection.ID
942
+ ) -> result.Simple[collection.Collection]:
943
+ new_col: collection.Collection
944
+ err: Errors
945
+ id_sap = IDSAP(col_id, c.SAP)
946
+ async with self.lock:
947
+ if id_sap in self.con.keys():
948
+ c.log(logL.INFO, F"{self.__class__.__name__} {col_id} already in container")
949
+ else:
950
+ c.log(logL.INFO, F"{self.__class__.__name__} register new collection: {col_id}")
951
+ self.con[id_sap] = CreateType(col_id)
952
+ res = await self.con[id_sap].exchange(c)
953
+ async with self.lock:
954
+ try:
955
+ gotten, _ = self.adapter.get_collection(col_id).unpack() # check for first update
956
+ if gotten.has_sap(id_sap.sap):
957
+ """not need keep"""
958
+ else:
959
+ self.adapter.set_collection(res.value) # todo: make as ADAPTER.merge_collection(ret)
960
+ except AdapterException as e:
961
+ self.adapter.set_collection(res.value)
962
+ res.value, err = res.value.copy().unpack()
963
+ if err is not None:
964
+ res.append_err(err)
965
+ return res
966
+
967
+
968
+ @dataclass
969
+ class InitType(SimpleCopy, Simple[collection.Collection]):
970
+ adapter: Adapter
971
+ msg: str = "initiate type"
972
+
973
+ async def exchange(self, c: Client) -> result.SimpleOrError[collection.Collection]:
974
+ if isinstance((res := await Sequence(
975
+ GetLDN(),
976
+ FindFirmwareId(),
977
+ FindFirmwareVersion(),
978
+ msg="get collection.ID",
979
+ ).exchange(c)), result.Error):
980
+ return res.with_msg("init type")
981
+ ldn, f_id, f_ver = res.value
982
+ col_id = collection.ID(ldn.get_manufacturer(), f_id, f_ver)
983
+ try:
984
+ if (res := self.adapter.get_collection(col_id)).value.has_sap(c.SAP):
985
+ c.log(logL.INFO, F"find collection in {self.adapter}")
986
+ else:
987
+ raise AdapterException(F"was found collection from adapter with absent current {c.SAP}")
988
+ except AdapterException as e:
989
+ c.log(logL.WARN, F"not find into adapter: {e}")
990
+ res = await map_type_creator.get_collection(
991
+ c=c,
992
+ col_id=col_id
993
+ )
994
+ c._objects = res.value
995
+ c._objects.LDN.set_attr(2, ldn)
996
+ return res
997
+
998
+
999
+ init_type: InitType
1000
+ """init after get_adapter"""
1001
+ map_type_creator: MapTypeCreator
1002
+ """init after get_adapter"""
1003
+
1004
+
1005
+ def get_adapter(value: Adapter):
1006
+ global map_type_creator, init_type
1007
+ map_type_creator = MapTypeCreator(value)
1008
+ init_type = InitType(value)
1009
+
1010
+
1011
+ get_adapter(gag) # Dummy Adapter
1012
+
1013
+
1014
+ @dataclass
1015
+ @deprecated("use <ReadObjAttr>")
1016
+ class ReadAttribute(SimpleCopy, CDT):
1017
+ ln: collection.LNContaining
1018
+ index: int
1019
+ msg: str = "read LN attribute"
1020
+
1021
+ async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
1022
+ obj = c.objects.get_object(self.ln)
1023
+ return await ReadObjAttr(obj, self.index).exchange(c)
1024
+
1025
+
1026
+ @dataclass
1027
+ class ReadObjAttr(SimpleCopy, CDT):
1028
+ obj: collection.InterfaceClass
1029
+ index: int
1030
+ msg: str = "read object attribute"
1031
+
1032
+ async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
1033
+ # TODO: check is_readable?
1034
+ c.get_get_request_normal(
1035
+ attr_desc=self.obj.get_attr_descriptor(
1036
+ value=self.index,
1037
+ with_selection=bool(c.negotiated_conformance.selective_access)))
1038
+ start_read_time: float = time.perf_counter()
1039
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1040
+ return res_pdu
1041
+ c.last_transfer_time = datetime.timedelta(seconds=time.perf_counter()-start_read_time)
1042
+ try:
1043
+ self.obj.set_attr(self.index, res_pdu.value)
1044
+ return result.Simple(self.obj.get_attr(self.index))
1045
+ except ValueError as e:
1046
+ return result.Error.from_e(e)
1047
+ except ut.UserfulTypesException as e:
1048
+ return result.Error.from_e(e)
1049
+ except exc.DLMSException as e:
1050
+ return result.Error.from_e(e)
1051
+
1052
+
1053
+ @dataclass(frozen=True)
1054
+ class Par2Data[T: cdt.CommonDataType](SimpleCopy, CDT[T]):
1055
+ """get CommonDataType by Parameter"""
1056
+ par: Parameter
1057
+ msg: str = "read data by Parameter"
1058
+
1059
+ async def exchange(self, c: Client) -> result.SimpleOrError[T]:
1060
+ if isinstance((res_obj := c.objects.par2obj(self.par)), result.Error):
1061
+ return res_obj
1062
+ if isinstance((res := await ReadObjAttr(res_obj.value, self.par.i).exchange(c)), result.Error):
1063
+ return res
1064
+ for el in self.par.elements():
1065
+ res.value = res.value[el]
1066
+ return res
1067
+
1068
+
1069
+ class ReadSequence(List):
1070
+ tasks: list[ReadAttribute]
1071
+
1072
+ def __post_init__(self):
1073
+ assert all((isinstance(t, ReadAttribute) for t in self.tasks))
1074
+
1075
+
1076
+ type AttrValueComp = Callable[[cdt.CommonDataType | None], bool]
1077
+
1078
+
1079
+ def is_empty(value: cdt.CommonDataType | None) -> bool:
1080
+ """is empty attribute value"""
1081
+ return True if value is None else False
1082
+
1083
+
1084
+ @dataclass(frozen=True)
1085
+ class ReadAttributeIf(SimpleCopy, Base):
1086
+ """read if func with arg as value is True"""
1087
+ ln: collection.LNContaining
1088
+ index: int
1089
+ func: AttrValueComp
1090
+ msg: str = "read attribute with condition"
1091
+
1092
+ async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
1093
+ # TODO: check is_readable
1094
+ obj = c.objects.get_object(self.ln)
1095
+ if self.func(obj.get_attr(self.index)):
1096
+ return await ReadAttribute(
1097
+ ln=self.ln,
1098
+ index=self.index).exchange(c)
1099
+ else:
1100
+ return result.OK
1101
+
1102
+
1103
+ @dataclass(frozen=True)
1104
+ class ReadEmptyAttribute(SimpleCopy, Base):
1105
+ ln: collection.LNContaining
1106
+ index: int
1107
+ msg: str = "read if attribute is empty"
1108
+
1109
+ async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
1110
+ # TODO: check is_readable
1111
+ return await ReadAttributeIf(
1112
+ ln=self.ln,
1113
+ index=self.index,
1114
+ func=is_empty
1115
+ ).exchange(c)
1116
+
1117
+
1118
+ @dataclass(frozen=True)
1119
+ class ReadWritableAttributes(SimpleCopy, Base):
1120
+ ln: collection.LNContaining
1121
+ indexes: tuple[int, ...]
1122
+ msg: str = "read only writable attribute"
1123
+
1124
+ async def exchange(self, c: Client) -> result.List[cdt.CommonDataType]:
1125
+ # TODO: check is_readable
1126
+ res = result.List()
1127
+ indexes: list[int] = []
1128
+ ass: collection.AssociationLN = c._objects.sap2association(c.SAP)
1129
+ for i in self.indexes:
1130
+ if ass.is_writable(self.ln, i):
1131
+ indexes.append(i)
1132
+ if len(indexes) != 0:
1133
+ res.append(await ReadAttributes(
1134
+ ln=self.ln,
1135
+ indexes=tuple(indexes)
1136
+ ).exchange(c))
1137
+ else:
1138
+ res.append(result.OK)
1139
+ c.log(logL.INFO, F"skip {self.__class__.__name__} operation, all is actually")
1140
+ return res
1141
+
1142
+
1143
+ # copy past from ReadWritableAttributes
1144
+ @dataclass(frozen=True)
1145
+ class ActualizeAttributes(SimpleCopy, OK):
1146
+ ln: collection.LNContaining
1147
+ indexes: tuple[int, ...]
1148
+ msg: str = "read if attribute is empty or writable"
1149
+
1150
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1151
+ # TODO: check is_readable
1152
+ ass: collection.AssociationLN = c.objects.sap2association(c.SAP)
1153
+ obj = c.objects.get_object(self.ln)
1154
+ indexes = [
1155
+ i for i in self.indexes if (
1156
+ obj.get_attr(i) is None
1157
+ or obj.get_attr_element(i).classifier == ic.Classifier.DYNAMIC
1158
+ or ass.is_writable(self.ln, i)
1159
+ )
1160
+ ]
1161
+ if len(indexes) != 0:
1162
+ if isinstance((res := await ReadAttributes(
1163
+ ln=self.ln,
1164
+ indexes=tuple(indexes)
1165
+ ).exchange(c)), result.Error):
1166
+ return res
1167
+ return result.OK
1168
+
1169
+
1170
+ @dataclass(frozen=True)
1171
+ class ReadAttributes(SimpleCopy, _List[cdt.CommonDataType]):
1172
+ ln: collection.LNContaining
1173
+ indexes: tuple[int, ...]
1174
+ msg: str = ""
1175
+
1176
+ async def exchange(self, c: Client) -> result.List[cdt.CommonDataType] | result.Error:
1177
+ res = result.List()
1178
+ obj = c.objects.get_object(self.ln)
1179
+ # TODO: check for Get-Request-With-List
1180
+ for i in self.indexes:
1181
+ res.append(await ReadObjAttr(obj, i).exchange(c))
1182
+ return res
1183
+
1184
+
1185
+ @dataclass
1186
+ @deprecated("use <Write2>")
1187
+ class Write(SimpleCopy, OK):
1188
+ """write with ParameterData struct"""
1189
+ par_data: ParData
1190
+ msg: str = "write attribute"
1191
+
1192
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1193
+ if isinstance(res_obj := c.objects.par2obj(self.par_data.par), result.Error):
1194
+ return res_obj
1195
+ if self.par_data.par.n_elements == 0:
1196
+ enc = self.par_data.data.encoding
1197
+ elif isinstance(res_read := await ReadObjAttr(res_obj.value, self.par_data.par.i).exchange(c), result.Error):
1198
+ return res_read
1199
+ else:
1200
+ data = a_data
1201
+ for el in self.par_data.par.elements():
1202
+ data = data[el]
1203
+ data.set(self.par_data.data)
1204
+ enc = data.encoding
1205
+ data = c.get_set_request_normal(
1206
+ obj=res_obj.value,
1207
+ attr_index=self.par_data.par.i,
1208
+ value=enc)
1209
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1210
+ return res_pdu
1211
+ return result.OK
1212
+
1213
+
1214
+ @dataclass(frozen=True)
1215
+ class Write2(SimpleCopy, OK):
1216
+ """write with ParameterData struct"""
1217
+ par: Parameter
1218
+ data: cdt.CommonDataType
1219
+ msg: str = "write Data"
1220
+
1221
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1222
+ if isinstance(res_obj := c.objects.par2obj(self.par), result.Error):
1223
+ return res_obj
1224
+ if self.par.n_elements == 0:
1225
+ enc = self.data.encoding
1226
+ elif isinstance(res_read := await Par2Data[cdt.CommonDataType](self.par.attr).exchange(c), result.Error):
1227
+ return res_read
1228
+ else:
1229
+ data = res_read.value
1230
+ for el in self.par.elements():
1231
+ data = data[el]
1232
+ data.set(self.data)
1233
+ enc = data.encoding
1234
+ data = c.get_set_request_normal(
1235
+ obj=res_obj.value,
1236
+ attr_index=self.par.i,
1237
+ value=enc)
1238
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1239
+ return res_pdu
1240
+ return result.OK
1241
+
1242
+
1243
+ @dataclass(frozen=True)
1244
+ class SetBits(SimpleCopy, OK):
1245
+ """set bits by pattern"""
1246
+ par: Parameter
1247
+ pattern: dict[int, int]
1248
+ msg: str = "set bits"
1249
+
1250
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1251
+ if isinstance(res_obj := c.objects.par2obj(self.par), result.Error):
1252
+ return res_obj
1253
+ if isinstance(res_read := await Par2Data[cdt.Digital | cdt.BitString](self.par.attr).exchange(c), result.Error):
1254
+ return res_read
1255
+ data = res_read.value
1256
+ for el in self.par.elements():
1257
+ data = data[el]
1258
+ if isinstance(data, cdt.Digital):
1259
+ data_int = int(data)
1260
+ for pos, val in self.pattern.items():
1261
+ data_int = (data_int | (1 << pos)) if val else (data_int & ~(1 << pos))
1262
+ data.set(data_int)
1263
+ elif isinstance(data, cdt.BitString):
1264
+ for pos, val in self.pattern.items():
1265
+ try:
1266
+ data[pos] = val
1267
+ except IndexError as e:
1268
+ return result.Error.from_e(ValueError(f"can't set bit {pos} in {self.par}"))
1269
+ else:
1270
+ return result.Error.from_e(TypeError(f"{self.par} not available SetBits"))
1271
+ c.get_set_request_normal(
1272
+ obj=res_obj.value,
1273
+ attr_index=self.par.i,
1274
+ value=res_read.value.encoding)
1275
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1276
+ return res_pdu
1277
+ return result.OK
1278
+
1279
+
1280
+ @dataclass
1281
+ @deprecated("use <WriteTranscript>")
1282
+ class WriteParValue(SimpleCopy, OK):
1283
+ """write with ParameterValues struct"""
1284
+ par_value: ParValues[cdt.Transcript]
1285
+ msg: str = "write attribute by Transcript"
1286
+
1287
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1288
+ if isinstance(res1 := c.objects.par2obj(self.par_value.par), result.Error):
1289
+ return res1
1290
+ obj = res1.value
1291
+ if (a_data := res1.value.get_attr(self.par_value.par.i)) is None:
1292
+ if isinstance((res := await Par2Data(self.par_value.par).exchange(c)), result.Error):
1293
+ return res
1294
+ else:
1295
+ a_data = res.value
1296
+ s_u = c._objects.par2su(self.par_value.par)
1297
+ if isinstance(s_u, cdt.ScalUnitType):
1298
+ value: cdt.Transcript = str(float(self.par_value.data) * 10 ** -int(s_u.scaler))
1299
+ else:
1300
+ value = self.par_value.data
1301
+ if self.par_value.par.n_elements == 0:
1302
+ set_data = a_data.parse(value)
1303
+ elif isinstance((res := await Par2Data(par).exchange(c)), result.Error):
1304
+ return res
1305
+ else:
1306
+ data = a_data
1307
+ for el in self.par_value.par.elements():
1308
+ data = data[el]
1309
+ new_data = data.parse(value) # todo: can't use with CHOICE
1310
+ data.set(new_data)
1311
+ set_data = a_data
1312
+ data = c.get_set_request_normal(
1313
+ obj=obj,
1314
+ attr_index=self.par_value.par.i,
1315
+ value=set_data.encoding)
1316
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1317
+ return res_pdu
1318
+ return result.OK
1319
+
1320
+
1321
+ @dataclass
1322
+ @deprecated("use <Write>")
1323
+ class WriteAttribute(SimpleCopy, OK):
1324
+ ln: collection.LNContaining
1325
+ index: int
1326
+ value: bytes | str | int | list | tuple | datetime.datetime
1327
+ msg: str = ""
1328
+
1329
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1330
+ obj = c._objects.get_object(self.ln)
1331
+ if isinstance(self.value, (str, int, list, tuple, datetime.datetime)):
1332
+ value2 = await c.encode(
1333
+ obj=obj,
1334
+ index=self.index,
1335
+ value=self.value)
1336
+ enc = value2.encoding
1337
+ else:
1338
+ enc = self.value
1339
+ data = c.get_set_request_normal(
1340
+ obj=obj,
1341
+ attr_index=self.index,
1342
+ value=enc)
1343
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1344
+ return res_pdu
1345
+ return result.OK # todo: return Data-Access-Result
1346
+
1347
+
1348
+ @deprecated("use Execute")
1349
+ @dataclass
1350
+ class ExecuteByDesc(SimpleCopy, Base):
1351
+ """execute method by method descriptor # TODO: rewrite this"""
1352
+ desc: ut.CosemMethodDescriptor
1353
+ msg: str = "old execute"
1354
+
1355
+ async def exchange(self, c: Client) -> result.Result:
1356
+ try:
1357
+ await c.execute_method(self.desc)
1358
+ return result.Simple(pdu.ActionResult.SUCCESS)
1359
+ except Exception as e:
1360
+ return result.Error.from_e(exc.DLMSException(F'Исполнение {self.desc}'))
1361
+
1362
+
1363
+ @deprecated("use Execute2")
1364
+ @dataclass
1365
+ class Execute(SimpleCopy, OK):
1366
+ """execute method"""
1367
+ ln: collection.LNContaining
1368
+ index: int
1369
+ value: cdt.CommonDataType = None # todo: maybe use simple bytes
1370
+ msg: str = "execute method"
1371
+
1372
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1373
+ obj = c._objects.get_object(self.ln)
1374
+ try:
1375
+ await c.execute_method2(obj, self.index, self.value)
1376
+ return result.OK
1377
+ except Exception as e:
1378
+ return result.Error.from_e(exc.DLMSException(F'Исполнение {self.desc}'))
1379
+
1380
+
1381
+ @dataclass(frozen=True)
1382
+ class Execute2(SimpleCopy, OK):
1383
+ """execute method"""
1384
+ par: Parameter
1385
+ data: cdt.CommonDataType
1386
+ msg: str = "Execute method"
1387
+
1388
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1389
+ if isinstance(res := c.objects.par2obj(self.par), result.Error):
1390
+ return res
1391
+ try:
1392
+ data = c.get_action_request_normal(
1393
+ meth_desc=res.value.get_meth_descriptor(self.par.i),
1394
+ method=self.data
1395
+ )
1396
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1397
+ return res_pdu
1398
+ return result.OK
1399
+ except Exception as e:
1400
+ return result.Error.from_e(e)
1401
+
1402
+
1403
+ @dataclass
1404
+ class GetTimeDelta(SimpleCopy, Simple[float]):
1405
+ """Read and return <time delta> in second: """
1406
+ msg: str = "Read Clock.time"
1407
+
1408
+ async def exchange(self, c: Client) -> result.SimpleOrError[float]:
1409
+ acc = result.ErrorAccumulator()
1410
+ obj = c._objects.clock
1411
+ if isinstance(
1412
+ res_read_tz := await ReadObjAttr(
1413
+ obj=obj,
1414
+ index=3
1415
+ ).exchange(c),
1416
+ result.Error):
1417
+ return res_read_tz
1418
+ tz = datetime.timezone(datetime.timedelta(minutes=int(res_read_tz.value)))
1419
+ if isinstance(
1420
+ res_read := await ReadObjAttr(
1421
+ obj=obj,
1422
+ index=2
1423
+ ).exchange(c),
1424
+ result.Error):
1425
+ return res_read
1426
+ value = datetime.datetime.now(tz=tz)
1427
+ value2 = res_read.value.to_datetime().replace(tzinfo=tz)
1428
+ return result.Simple((value2 - value).total_seconds()).append_err(acc.err)
1429
+
1430
+
1431
+
1432
+ @dataclass
1433
+ class WriteTime(SimpleCopy, Simple[float]):
1434
+ """Write and return <record time> in second: """
1435
+ limit: float = 5.0
1436
+ number_of_retries: int = 10
1437
+ msg: str = "write Clock.time"
1438
+
1439
+ async def exchange(self, c: Client) -> result.SimpleOrError[float]:
1440
+ acc = result.ErrorAccumulator()
1441
+ obj = c._objects.clock
1442
+ c.get_get_request_normal(obj.get_attr_descriptor(3))
1443
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1444
+ return res_pdu
1445
+ tz = obj.get_attr_element(3).DATA_TYPE(res_pdu.value)
1446
+ for i in range(self.number_of_retries):
1447
+ pre_time = time.time()
1448
+ if isinstance(
1449
+ res_write := await WriteAttribute(
1450
+ ln=obj.logical_name,
1451
+ index=2,
1452
+ value=(datetime.datetime.utcnow() + datetime.timedelta(minutes=int(tz)))).exchange(c),
1453
+ result.Error):
1454
+ return res_write
1455
+ rec_time = time.time() - pre_time
1456
+ if rec_time < self.limit:
1457
+ break
1458
+ acc.append_e(TimeoutError(f"can't write in {i} attemp in time"))
1459
+ else:
1460
+ return result.Error.from_e(TimeoutError(f"can't write time for limit: {self.limit} second"))
1461
+ return result.Simple(rec_time).append_err(acc.err)
1462
+
1463
+
1464
+ @dataclass
1465
+ class ImageTransfer(SimpleCopy, StrictOK):
1466
+ par: dlms_par.ImageTransfer
1467
+ image: bytes
1468
+ waiting_for_activation: float = 10.0
1469
+ n_t_b: int = field(init=False, default=0)
1470
+ """not transferred block"""
1471
+ n_blocks: int = field(init=False)
1472
+ msg: str = "image transfer"
1473
+
1474
+ def __post_init__(self) -> None:
1475
+ self.ITI = ImageTransferInitiate((
1476
+ bytearray(hashlib.md5(self.image).digest()), # todo: make custom this
1477
+ cdt.DoubleLongUnsigned(len(self.image))
1478
+ ))
1479
+
1480
+ async def exchange(self, c: Client) -> result.StrictOk | result.Error:
1481
+ """ update image if blocks is fulls ver 3"""
1482
+ offset: int
1483
+ res_block_size: result.SimpleOrError[cdt.DoubleLongUnsigned]
1484
+ res_status: result.SimpleOrError[i_t_status.ImageTransferStatus]
1485
+ res_activate_info: result.SimpleOrError[ImageToActivateInfo]
1486
+ res_ntb: result.SimpleOrError[cdt.DoubleLongUnsigned]
1487
+ res_tbs: result.SimpleOrError[cdt.BitString]
1488
+ previous_status: Optional[i_t_status.ImageTransferStatus] = None
1489
+ res = result.StrictOk()
1490
+ if isinstance(res_block_size := await Par2Data(self.par.image_block_size).exchange(c), result.Error):
1491
+ return res_block_size
1492
+ block_size = int(res_block_size.value)
1493
+ self.n_blocks, mod = divmod(len(self.image), block_size)
1494
+ if mod != 0:
1495
+ self.n_blocks += 1
1496
+ # TODO: need common counter for exit from infinity loop
1497
+ if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
1498
+ return res_status
1499
+ if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
1500
+ return res_activate_info
1501
+ if (
1502
+ res_status.value in (i_t_status.TRANSFER_NOT_INITIATED, i_t_status.VERIFICATION_FAILED, i_t_status.ACTIVATION_FAILED)
1503
+ or len(res_activate_info.value) == 0
1504
+ or res_activate_info.value[0].image_to_activate_identification != self.ITI.image_identifier
1505
+ ):
1506
+ if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1507
+ return res_initiate
1508
+ c.log(logL.INFO, "Start initiate Image Transfer")
1509
+ if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
1510
+ return res_status
1511
+ elif res_status.value == i_t_status.ACTIVATION_SUCCESSFUL:
1512
+ # image in outside memory and already activated early, but erased by hard programming. Need again go to activation
1513
+ res_status.value = i_t_status.VERIFICATION_SUCCESSFUL
1514
+ else:
1515
+ c.log(logL.INFO, "already INITIATED")
1516
+ if isinstance(res_ntb := await Par2Data(self.par.image_first_not_transferred_block_number).exchange(c), result.Error):
1517
+ return res_ntb
1518
+ self.n_t_b = int(res_ntb.value)
1519
+ if self.n_t_b > (len(self.image) / block_size): # all blocks were send
1520
+ if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
1521
+ return res_verify
1522
+ c.log(logL.INFO, "Start Verify Transfer")
1523
+ while True:
1524
+ c.log(logL.STATE, F"{res_status.value=}")
1525
+ match res_status.value:
1526
+ case i_t_status.VERIFICATION_FAILED if res_status.value == previous_status:
1527
+ return result.Error.from_e(exc.DLMSException(), "Verification Error")
1528
+ case i_t_status.TRANSFER_INITIATED if res_status.value == previous_status:
1529
+ res.append_e(exc.DLMSException("Expected Switch to Verification Initiated status, got Initiated"))
1530
+ case i_t_status.TRANSFER_NOT_INITIATED:
1531
+ res.append_e(exc.DLMSException("Got Not initiated status after call Initiation"))
1532
+ case i_t_status.TRANSFER_INITIATED:
1533
+ while self.n_t_b < self.n_blocks:
1534
+ offset = self.n_t_b * block_size
1535
+ if isinstance(res_tr_block := await TransferBlock(
1536
+ par=self.par,
1537
+ number=cdt.DoubleLongUnsigned(self.n_t_b),
1538
+ value=cdt.OctetString(bytearray(self.image[offset: offset + block_size]))
1539
+ ).exchange(c), result.Error):
1540
+ return res_tr_block
1541
+ self.n_t_b += 1 # todo: maybe get from SERVER - await get_not_transferred_block.exchange(c)
1542
+ c.log(logL.INFO, "All blocks transferred")
1543
+ if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
1544
+ return res_verify
1545
+ case i_t_status.VERIFICATION_INITIATED:
1546
+ c.log(logL.INFO, "read bitstring. It must grow")
1547
+ # TODO: calculate time for waiting or read growing bitstring ?
1548
+ if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
1549
+ return res_tbs
1550
+ if len(res_tbs.value) < self.n_t_b:
1551
+ c.log(logL.INFO, F"Got blocks[{len(res_tbs.value)}]") # todo: remove
1552
+ else:
1553
+ c.log(logL.INFO, "All Bits solved")
1554
+ case i_t_status.VERIFICATION_FAILED:
1555
+ if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
1556
+ return res_tbs
1557
+ valid = tuple(res_tbs.value)
1558
+ c.log(logL.INFO, F"Got blocks[{len(res_tbs.value)}]") # todo: remove
1559
+ for i in filter(lambda it: valid[it] == b'\x00', range(len(valid))):
1560
+ offset = i * block_size
1561
+ if isinstance(res_tr_block := await TransferBlock(
1562
+ par=self.par,
1563
+ number=cdt.DoubleLongUnsigned(i),
1564
+ value=cdt.OctetString(bytearray(self.image[offset: offset + block_size]))
1565
+ ).exchange(c), result.Error):
1566
+ return res_tr_block
1567
+ if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
1568
+ return res_verify
1569
+ c.log(logL.INFO, "Start Verify Transfer")
1570
+ case i_t_status.VERIFICATION_SUCCESSFUL:
1571
+ if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
1572
+ return res_tbs
1573
+ valid = tuple(res_tbs.value)
1574
+ if isinstance(res_ntb := await Par2Data(self.par.image_first_not_transferred_block_number).exchange(c), result.Error):
1575
+ return res_ntb
1576
+ self.n_t_b = int(res_ntb.value)
1577
+ if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
1578
+ return res_activate_info
1579
+ c.log(logL.INFO, F"md5:{res_activate_info.value[0].image_to_activate_signature}")
1580
+ if any(map(lambda it: it == b'\x00', valid)):
1581
+ if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1582
+ return res_initiate
1583
+ c.log(logL.INFO, "Start initiate Image Transfer, after wrong verify. Exist 0 blocks")
1584
+ return res.as_error(exc.DLMSException("Exist 0 blocks"))
1585
+ elif self.n_t_b < len(valid):
1586
+ if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1587
+ return res_initiate
1588
+ c.log(logL.INFO, "Start initiate Image Transfer, after wrong verify. Got not transferred block")
1589
+ return res.as_error(exc.DLMSException(F"Got {res_ntb.value} not transferred block"))
1590
+ elif res_activate_info.value[0].image_to_activate_signature != res_activate_info.value[0].image_to_activate_identification:
1591
+ if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1592
+ return res_initiate
1593
+ return res.as_error(exc.DLMSException(
1594
+ F"Signature not match to Identification: got {res_activate_info.value[0].image_to_activate_signature}, "
1595
+ F"expected {res_activate_info.value[0].image_to_activate_identification}"),
1596
+ "Start initiate Image Transfer, after wrong verify")
1597
+ else:
1598
+ if isinstance(res_activate := await ActivateImage(self.par).exchange(c), result.Error):
1599
+ return res_activate
1600
+ c.log(logL.INFO, "Start Activate Transfer")
1601
+ case i_t_status.ACTIVATION_INITIATED:
1602
+ try:
1603
+ await c.disconnect_request()
1604
+ except TimeoutError as e:
1605
+ c.log(logL.ERR, F"can't use <disconnect request>: {e}")
1606
+ if isinstance(res_reconnect := await HardwareReconnect(
1607
+ delay=self.waiting_for_activation,
1608
+ msg="expected reboot server after upgrade"
1609
+ ).exchange(c), result.Error):
1610
+ return res_reconnect
1611
+ case i_t_status.ACTIVATION_SUCCESSFUL:
1612
+ if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
1613
+ return res_activate_info
1614
+ if res_activate_info.value[0].image_to_activate_identification == self.ITI.image_identifier:
1615
+ c.log(logL.INFO, "already activated this image")
1616
+ return res
1617
+ else:
1618
+ if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
1619
+ return res_initiate
1620
+ c.log(logL.INFO, "Start initiate Image Transfer")
1621
+ # TODO: need wait clearing memory in device ~5 sec
1622
+ case i_t_status.ACTIVATION_FAILED:
1623
+ return res.as_error(exc.DLMSException(), "Ошибка активации...")
1624
+ case err:
1625
+ return res.as_error(exc.DLMSException(), f"Unknown image transfer status: {err}")
1626
+ previous_status = res_status.value
1627
+ await asyncio.sleep(1) # TODO: tune it
1628
+ if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
1629
+ return res_status
1630
+ c.log(logL.INFO, f"{res_status.value=}")
1631
+
1632
+
1633
+ @dataclass(frozen=True)
1634
+ class TransferBlock(SimpleCopy, OK):
1635
+ par: dlms_par.ImageTransfer
1636
+ number: cdt.DoubleLongUnsigned
1637
+ value: cdt.OctetString
1638
+ msg: str = "transfer block"
1639
+
1640
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1641
+ if isinstance(res := await Execute2(
1642
+ par=self.par.image_block_transfer,
1643
+ data=ImageBlockTransfer((self.number, self.value))
1644
+ ).exchange(c), result.Error):
1645
+ return res
1646
+ return result.OK
1647
+
1648
+
1649
+ @dataclass(frozen=True)
1650
+ class VerifyImage(SimpleCopy, OK):
1651
+ par: dlms_par.ImageTransfer
1652
+ msg: str = "Verify image"
1653
+
1654
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1655
+ if isinstance(res := await Execute2(self.par.image_verify, integers.INTEGER_0).exchange(c), result.Error):
1656
+ return res
1657
+ return result.OK
1658
+
1659
+
1660
+ @dataclass(frozen=True)
1661
+ class ActivateImage(SimpleCopy, OK):
1662
+ par: dlms_par.ImageTransfer
1663
+ msg: str = "Activate image"
1664
+
1665
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1666
+ if isinstance(res := await Execute2(self.par.image_activate, integers.INTEGER_0).exchange(c), result.Error):
1667
+ return res
1668
+ return result.OK
1669
+
1670
+
1671
+ # todo: don't work with new API, remake
1672
+ class TestAll(OK):
1673
+ """read all attributes with check access""" # todo: add Write with access
1674
+ msg: str = "test all"
1675
+
1676
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1677
+ # todo: refactoring with <append_err>
1678
+ res = result.ErrorAccumulator()
1679
+ if isinstance(res_objects := c.objects.sap2objects(c.SAP), result.Error):
1680
+ return res_objects
1681
+ ass: collection.AssociationLN = c.objects.sap2association(c.SAP)
1682
+ for obj in res_objects.value:
1683
+ indexes: list[int] = [i for i, _ in obj.get_index_with_attributes()]
1684
+ c.log(logL.INFO, F"start read {obj} attr: {', '.join(map(str, indexes))}")
1685
+ for i in indexes:
1686
+ is_readable = ass.is_readable(
1687
+ ln=obj.logical_name,
1688
+ index=i)
1689
+ if isinstance(res_read := await ReadObjAttr(obj, i).exchange(c), result.Error):
1690
+ if (
1691
+ res_read.has(pdu.DataAccessResult.READ_WRITE_DENIED, exc.ResultError)
1692
+ and not is_readable
1693
+ ):
1694
+ c.log(logL.INFO, F"success ReadAccess TEST")
1695
+ else:
1696
+ res.append_err(res_read.err)
1697
+ elif not is_readable:
1698
+ res.append_e(PermissionError(f"{obj} with attr={i} must be unreadable"))
1699
+ indexes.remove(i)
1700
+ return res.result
1701
+
1702
+
1703
+ @dataclass
1704
+ class ApplyTemplate(SimpleCopy, Base):
1705
+ template: collection.Template
1706
+ msg: str = "apply template"
1707
+
1708
+ async def exchange(self, c: Client) -> result.Result:
1709
+ # todo: search col
1710
+ attr: cdt.CommonDataType
1711
+ res = result.StrictOk()
1712
+ for col in self.template.collections:
1713
+ if col == c.objects:
1714
+ use_col = col
1715
+ break
1716
+ else:
1717
+ c.log(logL.ERR, F"not find collection for {c}")
1718
+ raise asyncio.CancelledError()
1719
+ for ln, indexes in self.template.used.items():
1720
+ if (obj := res.propagate_err(use_col.logicalName2obj(ln))) is not None:
1721
+ for i in indexes:
1722
+ if (attr := obj.get_attr(i)) is not None:
1723
+ res.propagate_err(await WriteAttribute(
1724
+ ln=ln,
1725
+ index=i,
1726
+ value=attr.encoding
1727
+ ).exchange(c))
1728
+ else:
1729
+ res.append_e(exc.EmptyObj(F"skip apply {self.template} {ln}:{i}: no value"))
1730
+ return res
1731
+
1732
+
1733
+ @dataclass
1734
+ class ReadTemplate(OK):
1735
+ template: collection.Template
1736
+ msg: str = "read template"
1737
+
1738
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1739
+ # todo: copypast from <ApplyTemplate>
1740
+ attr: cdt.CommonDataType
1741
+ res = result.ErrorAccumulator()
1742
+ for col in self.template.collections:
1743
+ if col == c._objects:
1744
+ use_col = col
1745
+ break
1746
+ else:
1747
+ c.log(logL.ERR, F"not find collection for {c}")
1748
+ raise asyncio.CancelledError()
1749
+ for ln, indexes in self.template.used.items():
1750
+ try:
1751
+ obj = use_col.get_object(ln) # todo: maybe not need
1752
+ except exc.NoObject as e:
1753
+ c.log(logL.WARN, F"skip apply {self.template}: {e}")
1754
+ continue
1755
+ res.propagate_err(
1756
+ await ReadAttributes(
1757
+ ln=ln,
1758
+ indexes=tuple(indexes)
1759
+ ).exchange(c))
1760
+ return res.result
1761
+
1762
+
1763
+ @dataclass
1764
+ class AccessValidate(OK):
1765
+ """check all access rights for current SAP"""
1766
+ with_correct: bool = False
1767
+ msg: str = "all access validate"
1768
+
1769
+ # todo: make with result.Error
1770
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1771
+ res = result.ErrorAccumulator()
1772
+ obj_l: ObjectListType
1773
+ el: ObjectListElement
1774
+ a_a_i: association_ln.abstract.AttributeAccessItem
1775
+ if (obj_l := c.objects.sap2association(c.SAP).object_list) is None:
1776
+ return result.Error.from_e(exc.EmptyObj(F"empty object_list for {c._objects.sap2association(c.SAP)}"))
1777
+ for el in obj_l:
1778
+ for a_a_i in el.access_rights.attribute_access:
1779
+ if a_a_i.access_mode.is_readable():
1780
+ i = int(a_a_i.attribute_id)
1781
+ if isinstance(res_read :=await ReadByDescriptor(ut.CosemAttributeDescriptor((
1782
+ int(el.class_id),
1783
+ el.logical_name.contents,
1784
+ i
1785
+ ))).exchange(c), result.Error):
1786
+ res.append_err(res_read.err)
1787
+ if self.with_correct:
1788
+ a_a_i.access_mode.set(1) # todo: make better in future
1789
+ elif a_a_i.access_mode.is_writable():
1790
+ if isinstance(res_write :=await WriteAttribute(
1791
+ ln=el.logical_name,
1792
+ index=i,
1793
+ value=res_read.value
1794
+ ).exchange(c), result.Error):
1795
+ res.append_err(res_write.err)
1796
+ return res.result
1797
+
1798
+
1799
+ @dataclass
1800
+ @deprecated("use <WriteList>")
1801
+ class WriteParDatas(SimpleCopy, _List[result.Ok]):
1802
+ """write by ParData list"""
1803
+ par_datas: list[ParData]
1804
+ msg: str = ""
1805
+
1806
+ async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
1807
+ res = result.List()
1808
+ for pardata in self.par_datas:
1809
+ res.append(await WriteAttribute(
1810
+ ln=pardata.par.ln,
1811
+ index=pardata.par.i,
1812
+ value=pardata.data.encoding
1813
+ ).exchange(c))
1814
+ return res
1815
+
1816
+
1817
+ class WriteList(SimpleCopy, _List[result.Ok]):
1818
+ """write by list"""
1819
+ par_datas: tuple[tuple[Parameter, cdt.CommonDataType], ...]
1820
+ err_ignore: bool
1821
+
1822
+ def __init__(self, *par_datas: tuple[Parameter, cdt.CommonDataType], err_ignore: bool = False, msg: str = "write list") -> None:
1823
+ self.par_datas = par_datas
1824
+ self.err_ignore = err_ignore
1825
+ self.msg = msg
1826
+
1827
+ async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
1828
+ res = result.List[result.Ok]()
1829
+ for par, data in self.par_datas:
1830
+ if (
1831
+ isinstance(res_one := await Write2(par, data).exchange(c), result.Error)
1832
+ and not self.err_ignore
1833
+ ):
1834
+ return res_one
1835
+ res.append(res_one)
1836
+ return res
1837
+
1838
+
1839
+ @dataclass(frozen=True)
1840
+ class WriteTranscript(SimpleCopy, OK):
1841
+ """write by ParValues[Transcript]"""
1842
+ par: Parameter
1843
+ value: cdt.Transcript
1844
+ msg: str = "write with transcript"
1845
+
1846
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1847
+ if isinstance((res := await Par2Data[cdt.CommonDataType](self.par).exchange(c)), result.Error):
1848
+ return res
1849
+ if isinstance(res.value, cdt.Digital):
1850
+ s_u = c.objects.par2su(self.par)
1851
+ if isinstance(s_u, cdt.ScalUnitType):
1852
+ if not isinstance(self.value, str):
1853
+ return result.Error.from_e(TypeError(), f"for {self.par} got type: {self.value}, expected String")
1854
+ try:
1855
+ data = res.value.parse(value := str(float(self.value) * 10 ** -int(s_u.scaler)))
1856
+ except ValueError as e:
1857
+ return result.Error.from_e(e, f"for {self.par} got value: {self.value}, expected Float or Digital")
1858
+ else:
1859
+ data = res.value.parse(self.value)
1860
+ else:
1861
+ data = res.value.parse(self.value)
1862
+ return await Write2(self.par, data).exchange(c)
1863
+
1864
+
1865
+ class WriteTranscripts(SimpleCopy, _List[result.Ok]):
1866
+ """write by ParValues[Transcript] list"""
1867
+ par_values: tuple[tuple[Parameter, cdt.Transcript], ...]
1868
+ err_ignore: bool
1869
+
1870
+ def __init__(self, *par_values: tuple[Parameter, cdt.Transcript], err_ignore: bool = False, msg: str ="write transcripts"):
1871
+ self.par_values = par_values
1872
+ self.err_ignore = err_ignore
1873
+ self.msg = msg
1874
+
1875
+ async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
1876
+ res = result.List[result.Ok]()
1877
+ for par, value in self.par_values:
1878
+ if (
1879
+ isinstance(res_one := await WriteTranscript(par, value).exchange(c), result.Error)
1880
+ and not self.err_ignore
1881
+ ):
1882
+ return res_one
1883
+ res.append(res_one)
1884
+ return res