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