DLMS-SPODES-client 0.19.22__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 -1842
  55. {dlms_spodes_client-0.19.22.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.22.dist-info/RECORD +0 -61
  58. {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.23.dist-info}/WHEEL +0 -0
  59. {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.23.dist-info}/entry_points.txt +0 -0
  60. {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.23.dist-info}/top_level.txt +0 -0
@@ -1,1842 +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
- if isinstance(res_open := await c.media.open(), result.Error):
80
- return res_open
81
- c.level = OSI.PHYSICAL
82
- c.log(logL.INFO, F"Open port communication channel: {c.media}")
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
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