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