DLMS-SPODES-client 0.19.12__py3-none-any.whl → 0.19.14__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.
@@ -86,19 +86,21 @@ class Base[T: result.Result](Protocol):
86
86
  ):
87
87
  if isinstance(res := await init_type.data_link(c), result.Error):
88
88
  return res.with_msg("PH_connect")
89
- await c.close() # todo: change to DiscRequest, or make not closed or reconnect !!!
89
+ if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest, or make not closed or reconnect !!!
90
+ return res_close
90
91
  return result.OK
91
92
 
92
93
  @staticmethod
93
- async def physical_t(c: Client) -> None:
94
- await c.close()
94
+ async def physical_t(c: Client) -> result.Ok | result.Error:
95
+ return await c.close()
95
96
 
96
97
  async def physical(self, c: Client) -> T | result.Error:
97
98
  if OSI.PHYSICAL not in c.level:
98
99
  if isinstance((res := await self.PH_connect(c)), result.Error):
99
100
  return res
100
101
  ret = await self.data_link(c)
101
- await self.physical_t(c)
102
+ if isinstance(res_terminate := await self.physical_t(c), result.Error):
103
+ return res_terminate
102
104
  return ret
103
105
 
104
106
  async def DL_connect(self, c: Client) -> None:
@@ -113,8 +115,6 @@ class Base[T: result.Result](Protocol):
113
115
  c.SA = frame.Address(upper_address=int(c.SAP))
114
116
  c.log(logL.INFO, F"{c.SA=} {c.DA=}")
115
117
  # initialize connection
116
- c.reply.clear()
117
- # replace this
118
118
  if c.settings.cipher.security != Security.NONE:
119
119
  c.log(logL.DEB, F"Security: {c.settings.cipher.security}/n"
120
120
  F"System title: {c.settings.cipher.systemTitle.hex()}"
@@ -124,9 +124,9 @@ class Base[T: result.Result](Protocol):
124
124
  c.log(logL.DEB, F"Dedicated key: {c.settings.cipher.dedicatedKey.hex()}")
125
125
  # SNRM
126
126
  c.get_SNRM_request()
127
- await c.read_data_block()
127
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
128
+ return res_pdu
128
129
  c.level |= OSI.DATA_LINK
129
- c.reply.clear()
130
130
 
131
131
  async def data_link(self, c: Client) -> T | result.Error:
132
132
  if OSI.DATA_LINK not in c.level:
@@ -134,7 +134,7 @@ class Base[T: result.Result](Protocol):
134
134
  # todo: make tile
135
135
  return await self.application(c)
136
136
 
137
- async def AA(self, c: Client) -> None:
137
+ async def AA(self, c: Client) -> result.Ok | result.Error:
138
138
  """Application Associate"""
139
139
  if c.invocationCounter and c.settings.cipher is not None and c.settings.cipher.security != Security.NONE:
140
140
  # create IC object. TODO: remove it after close connection, maybe???
@@ -149,17 +149,19 @@ class Base[T: result.Result](Protocol):
149
149
  challenge = c.settings.ctoSChallenge
150
150
  try:
151
151
  c.aarqRequest(c.m_id)
152
- await c.read_data_block()
153
- ret = c.parseAareResponse(c.reply.data)
152
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
153
+ return res_pdu
154
+ ret = c.parseAareResponse(res_pdu.value)
154
155
  c.level |= OSI.APPLICATION # todo: it's must be result of <ret> look down
155
- c.reply.clear()
156
- await c.read_attribute(IC, 2)
157
- c.settings.cipher.invocationCounter = 1 + int(IC.value)
156
+ if isinstance(res_ic := await ReadObjAttr(IC, 2).exchange(c), result.Error):
157
+ return res_ic
158
+ c.settings.cipher.invocationCounter = 1 + int(res_ic.value)
158
159
  c.log(logL.DEB, "Invocation counter: " + str(c.settings.cipher.invocationCounter))
159
160
  # disconnect
160
161
  if c.media and c.media.is_open():
161
162
  c.log(logL.DEB, "DisconnectRequest")
162
- await c.disconnect_request()
163
+ if isinstance(res_disc_req := await c.disconnect_request(), result.Error):
164
+ return res_disc_req
163
165
  finally:
164
166
  c.SAP = tmp_client_SAP
165
167
  c.settings.useCustomChallenge = challenge is not None
@@ -206,40 +208,41 @@ class Base[T: result.Result](Protocol):
206
208
  # self.client.ctoSChallenge = challenge
207
209
 
208
210
  c.aarqRequest(c.m_id)
209
- await c.read_data_block()
211
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
212
+ return res_pdu
210
213
  # await c.read_attr(ut.CosemAttributeDescriptor((collection.ClassID.ASSOCIATION_LN, ut.CosemObjectInstanceId("0.0.40.0.0.255"), ut.CosemObjectAttributeId(6)))) # for test only
211
- match c.parseAareResponse(c.reply.data):
214
+ match c.parseAareResponse(res_pdu.value):
212
215
  case AcseServiceUser.NULL:
213
- c.log(logL.INFO, 'Authentication success')
216
+ c.log(logL.INFO, "Authentication success")
214
217
  c.level |= OSI.APPLICATION
215
218
  case AcseServiceUser.AUTHENTICATION_REQUIRED:
216
- c.reply.clear()
217
219
  c.getApplicationAssociationRequest()
218
- await c.read_data_block()
219
- c.parseApplicationAssociationResponse()
220
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
221
+ return res_pdu
222
+ c.parseApplicationAssociationResponse(res_pdu.value)
220
223
  case _ as diagnostic:
221
- raise exc.AssociationResultError(diagnostic)
224
+ return result.Error.from_e(exc.AssociationResultError(diagnostic))
222
225
  if c._objects is not None:
223
- c.get_get_request_normal(c._objects.LDN.get_attr_descriptor(2))
224
- await c.read_data_block()
225
226
  if c.is_universal():
226
227
  await change_ldn.exchange(c)
227
228
  else:
228
229
  await match_ldn.exchange(c)
230
+ return result.OK
229
231
 
230
232
  async def application(self, c: Client) -> T | result.Error:
231
233
  if OSI.APPLICATION not in c.level:
232
- await self.AA(c)
234
+ if isinstance(res := await self.AA(c), result.Error):
235
+ return res
233
236
  # no tile
234
237
  return await self.exchange(c)
235
238
 
236
239
  async def exchange(self, c: Client) -> T | result.Error:
237
240
  """application level exchange"""
238
241
 
239
- async def connect(self, c: Client) -> None:
242
+ async def connect(self, c: Client) -> result.Ok | result.Error:
240
243
  await self.PH_connect(c)
241
244
  await self.DL_connect(c)
242
- await self.AA(c)
245
+ return await self.AA(c)
243
246
 
244
247
 
245
248
  class SimpleCopy:
@@ -291,11 +294,11 @@ class StrictOK(Base[result.StrictOk], Protocol):
291
294
  async def exchange(self, c: Client) -> result.StrictOk | result.Error: ...
292
295
 
293
296
 
294
- @dataclass
297
+ @dataclass(frozen=True)
295
298
  class ClientBlocking(SimpleCopy, OK):
296
299
  """complete by time or abort"""
297
- delay: Final[float] = field(default=99999999.0)
298
- msg: str = ""
300
+ delay: float = field(default=99999999.0)
301
+ msg: str = "client blocking"
299
302
 
300
303
  async def run(self, c: Client) -> result.Ok | result.Error:
301
304
  try:
@@ -312,8 +315,9 @@ class ClientBlocking(SimpleCopy, OK):
312
315
 
313
316
 
314
317
  # todo: make with <data_link>
318
+ @dataclass
315
319
  class TestDataLink(SimpleCopy, OK):
316
- msg: str = ""
320
+ msg: str = "test DLink"
317
321
 
318
322
  async def physical(self, c: Client) -> result.Ok | result.Error:
319
323
  if OSI.PHYSICAL not in c.level:
@@ -327,27 +331,29 @@ class TestDataLink(SimpleCopy, OK):
327
331
  )
328
332
  c.SA = frame.Address(upper_address=int(c.SAP))
329
333
  c.get_SNRM_request()
330
- await c.read_data_block()
334
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
335
+ return res_pdu
331
336
  c.level |= OSI.DATA_LINK
332
- await c.close() # todo: change to DiscRequest
337
+ if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest
338
+ return res_close
333
339
  return result.Ok
334
340
 
335
341
  async def exchange(self, c: Client):
336
342
  return result.OK
337
343
 
338
344
 
339
- @dataclass
345
+ @dataclass(frozen=True)
340
346
  class Dummy(SimpleCopy, OK):
341
- msg: str = ""
347
+ msg: str = "dummy"
342
348
 
343
349
  async def exchange(self, c: Client) -> result.Ok | result.Error:
344
350
  """"""
345
351
  return result.OK
346
352
 
347
353
 
348
- @dataclass
354
+ @dataclass(frozen=True)
349
355
  class HardwareDisconnect(SimpleCopy, OK):
350
- msg: str = ""
356
+ msg: str = "hardware disconnect"
351
357
 
352
358
  @override
353
359
  async def exchange(self, c: Client) -> result.Ok | result.Error:
@@ -358,9 +364,9 @@ class HardwareDisconnect(SimpleCopy, OK):
358
364
  return result.OK
359
365
 
360
366
 
361
- @dataclass
367
+ @dataclass(frozen=True)
362
368
  class HardwareReconnect(SimpleCopy, OK):
363
- delay: Final[float] = 0.0
369
+ delay: float = 0.0
364
370
  """delay between disconnect and restore Application"""
365
371
  msg: str = "reconnect media without response"
366
372
 
@@ -370,17 +376,18 @@ class HardwareReconnect(SimpleCopy, OK):
370
376
  if self.delay != 0.0:
371
377
  c.log(logL.INFO, F"delay({self.delay})")
372
378
  await asyncio.sleep(self.delay)
373
- await self.connect(c) # restore Application
379
+ if isinstance(res_connect := await self.connect(c), result.Error): # restore Application
380
+ return res_connect
374
381
  return result.OK
375
382
 
376
383
 
377
- @dataclass
384
+ @dataclass(frozen=True)
378
385
  class Loop(OK):
379
386
  task: Base[result.Result]
380
387
  func: Callable[[Any], bool]
381
- delay: Final[int] = 0.0
382
- msg: str = ""
383
- attempt_amount: Final[int] = 0
388
+ delay: int = 0.0
389
+ msg: str = "loop"
390
+ attempt_amount: int = 0
384
391
  """0 is never end loop"""
385
392
 
386
393
  def copy(self) -> Self:
@@ -411,7 +418,7 @@ class ConditionalTask[T](Base):
411
418
  comp_value: T
412
419
  predicate: Callable[[T, T], bool]
413
420
  main_task: Base[result.Result]
414
- msg: str
421
+ msg: str = "conditional task"
415
422
 
416
423
  async def exchange(self, c: Client) -> result.Result:
417
424
  res = await self.precondition_task.exchange(c)
@@ -434,7 +441,7 @@ class Scheduler[T: result.Result](Base[T]):
434
441
  repetition_delay_min: Final[int] = 1
435
442
  repetition_delay_exponent: Final[int] = 100
436
443
  repetition_delay_max: Final[int] = 100
437
- msg: str = ""
444
+ msg: str = "sheduler"
438
445
 
439
446
  def copy(self) -> "Scheduler[T]":
440
447
  return Scheduler(
@@ -492,11 +499,11 @@ class Subtasks[U: Base[result.Result]](Protocol):
492
499
  class List[T: result.Result, U: Base[result.Result]](Subtasks[U], _List[T]):
493
500
  """for exchange task sequence"""
494
501
  __is_exchange: bool
495
- msg: str
496
502
  err_ignore: bool
503
+ msg: str
497
504
  __current: Base[T]
498
505
 
499
- def __init__(self, *tasks: Base[T], msg: str = "", err_ignore: bool = True):
506
+ def __init__(self, *tasks: Base[T], msg: str = "", err_ignore: bool = False):
500
507
  self.tasks = list(tasks)
501
508
  self.__current = self
502
509
  self.__is_exchange = False
@@ -527,15 +534,9 @@ class List[T: result.Result, U: Base[result.Result]](Subtasks[U], _List[T]):
527
534
  self.__is_exchange = True
528
535
  for t in self.tasks:
529
536
  self.__current = t
530
- try:
531
- res.append(await t.exchange(c))
532
- except exc.ResultError as e: # todo: make without except
533
- res.append(result.Error.from_e(e, msg=t.msg))
534
- if (
535
- not self.err_ignore
536
- and not res.is_ok()
537
- ):
538
- break
537
+ res.append(await t.exchange(c))
538
+ if not self.err_ignore:
539
+ return result.Error(res.err)
539
540
  return res
540
541
 
541
542
 
@@ -546,7 +547,7 @@ class Sequence[*Ts](Subtasks[Base[result.Result]], _Sequence[*Ts]):
546
547
  __current: "Base[result.Result] | Sequence[*Ts]"
547
548
  tasks: tuple[Base[result.Result], ...]
548
549
 
549
- def __init__(self, *tasks: Base[result.Result], msg: str = "", err_ignore: bool = False):
550
+ def __init__(self, *tasks: Base[result.Result], msg: str = "sequence", err_ignore: bool = False):
550
551
  self.tasks = tasks
551
552
  self.__current = self
552
553
  self.msg = self.__class__.__name__ if msg == "" else msg
@@ -569,21 +570,19 @@ class Sequence[*Ts](Subtasks[Base[result.Result]], _Sequence[*Ts]):
569
570
  res = result.Sequence()
570
571
  for t in self.tasks:
571
572
  self.__current = t
572
- try:
573
- if (
574
- isinstance(res_one := await t.exchange(c), result.Error)
575
- and not self.err_ignore
576
- ):
573
+ if isinstance(res_one := await t.exchange(c), result.Error):
574
+ if self.err_ignore:
575
+ res_one = res_one.with_msg(self.msg)
576
+ else:
577
577
  return res_one
578
- res = res.add(res_one)
579
- except exc.ResultError as e: # todo: make without except
580
- res.add(result.Error.from_e(e).with_msg(t.msg))
578
+ res = res.add(res_one)
581
579
  return cast("result.Sequence[*Ts]", res)
582
580
 
583
581
 
582
+ @dataclass(frozen=True)
584
583
  class SetLocalTime(SimpleCopy, OK):
585
584
  """without decide time transfer"""
586
- msg: str
585
+ msg: str = "set local time"
587
586
 
588
587
  async def exchange(self, c: Client) -> result.Ok | result.Error:
589
588
  clock_obj: Clock = c.objects.get_object("0.0.1.0.0.255")
@@ -603,68 +602,81 @@ class SetLocalTime(SimpleCopy, OK):
603
602
  return result.OK
604
603
 
605
604
 
605
+ @dataclass(frozen=True)
606
606
  class GetFirmwareVersion(SimpleCopy, CDT):
607
- """get firmware version by Client.id"""
608
- msg: str
607
+ msg: str = "get firmware version"
609
608
 
610
609
  async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
611
610
  return await Par2Data(Parameter(c.objects.id.f_ver.par[:6]).get_attr(c.objects.id.f_ver.par[6])).exchange(c)
612
611
 
613
612
 
613
+ @dataclass(frozen=True)
614
+ class ReadByDescriptor(SimpleCopy, Simple[bytes]):
615
+ desc: ut.CosemMethodDescriptor
616
+ msg: str = "get encoding by Cosem-Attribute-Descriptor"
617
+
618
+ async def exchange(self, c: Client) -> result.SimpleOrError[bytes]:
619
+ c.get_get_request_normal(self.desc)
620
+ return await c.read_data_block()
621
+
622
+
623
+ @dataclass(frozen=True)
614
624
  class FindFirmwareVersion(SimpleCopy, Simple[collection.ParameterValue]):
615
- """try find COSEM server version, return: instance(B group), CDT"""
616
- msg: str
625
+ msg: str = "try find COSEM server version, return: instance(B group), CDT"
617
626
 
618
627
  async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
619
628
  err = result.ErrorAccumulator()
620
629
  for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.1.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.2.255", 2))):
621
- try:
630
+ if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
631
+ err.append_err(res_read.err)
632
+ in_e, out_e = res_read.err.split(exc.ResultError)
633
+ if (
634
+ out_e is None
635
+ and in_e.exceptions[0].result == pdu.DataAccessResult(4)
636
+ ):
637
+ continue
638
+ else:
639
+ return res_read
640
+ else:
622
641
  res = result.Simple(collection.ParameterValue(
623
642
  par=desc.instance_id.contents + desc.attribute_id.contents,
624
- value=await c.read_attr(desc)
643
+ value=res_read.value
625
644
  ))
626
645
  res.propagate_err(err)
627
646
  return res
628
- except exc.ResultError as e:
629
- err.append_err(e)
630
- if e.result == pdu.DataAccessResult.OBJECT_UNDEFINED:
631
- """try search in old object and set to new"""
632
- else:
633
- break
634
- if isinstance((res := err.result), result.Error):
635
- return res
636
- raise RuntimeError(f"empty {err}")
647
+ return err.as_error()
637
648
 
638
649
 
650
+ @dataclass(frozen=True)
639
651
  class FindFirmwareId(SimpleCopy, Simple[collection.ParameterValue]):
640
- """try find COSEM server identifier, return: FirmwareID"""
641
652
  msg: str = "find firmaware Identifier"
642
653
 
643
654
  async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
644
655
  err = result.ErrorAccumulator()
645
656
  for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.0.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.1.255", 2))):
646
- try:
657
+ if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
658
+ err.append_err(res_read.err)
659
+ in_e, out_e = res_read.err.split(exc.ResultError)
660
+ if (
661
+ out_e is None
662
+ and in_e.exceptions[0].result == pdu.DataAccessResult(4)
663
+ ):
664
+ continue
665
+ else:
666
+ return res_read
667
+ else:
647
668
  res = result.Simple(collection.ParameterValue(
648
669
  par=desc.instance_id.contents + desc.attribute_id.contents,
649
- value=await c.read_attr(desc)
670
+ value=res_read.value
650
671
  ))
651
672
  res.propagate_err(err)
652
673
  return res
653
- except exc.ResultError as e:
654
- err.append_err(e)
655
- if e.result==pdu.DataAccessResult.OBJECT_UNDEFINED:
656
- """try search in old object and set to new"""
657
- else:
658
- break
659
- if isinstance((res := err.result), result.Error):
660
- return res
661
- raise RuntimeError(f"empty {err}")
674
+ return err.as_error()
662
675
 
663
676
 
664
- @dataclass
677
+ @dataclass(frozen=True)
665
678
  class KeepAlive(SimpleCopy, OK):
666
- """use read LDN.ln"""
667
- msg: str = "keep alive"
679
+ msg: str = "keep alive(read LND.ln)"
668
680
 
669
681
  async def exchange(self, c: Client) -> result.Ok | result.Error:
670
682
  if isinstance(res := await Par2Data(dlms_par.LDN.value).exchange(c), result.Error):
@@ -677,8 +689,9 @@ class GetLDN(SimpleCopy, CDT[octet_string.LDN]):
677
689
  msg: str = "get LDN"
678
690
 
679
691
  async def exchange(self, c: Client) -> result.SimpleOrError[octet_string.LDN]:
680
- data = await c.read_attr(collection.AttrDesc.LDN_VALUE)
681
- return result.Simple(octet_string.LDN(data))
692
+ if isinstance(res := await ReadByDescriptor(collection.AttrDesc.LDN_VALUE).exchange(c), result.Error):
693
+ return res
694
+ return result.Simple(octet_string.LDN(res.value))
682
695
 
683
696
 
684
697
  # todo: possible implementation ConditionalTask
@@ -697,7 +710,7 @@ class GetLDN(SimpleCopy, CDT[octet_string.LDN]):
697
710
  @dataclass
698
711
  class MatchLDN(OK):
699
712
  universal: bool = field(default=False)
700
- msg: str = ""
713
+ msg: str = "matching LDN"
701
714
 
702
715
  async def exchange(self, c: Client) -> result.Ok | result.Error:
703
716
  if isinstance((res := await GetLDN().exchange(c)), result.Error):
@@ -748,11 +761,7 @@ class CreateType(SimpleCopy, Simple[collection.Collection]):
748
761
  col: collection.Collection = field(init=False)
749
762
 
750
763
  def __post_init__(self):
751
- self.col = collection.Collection(id_=collection.ID(
752
- man=self.col_id.man,
753
- f_id=self.col_id.f_id,
754
- f_ver=self.col_id.f_ver
755
- ))
764
+ self.col = collection.Collection(id_=self.col_id)
756
765
  """common collection"""
757
766
  self.wait_list = Lock(name="wait <object list>")
758
767
 
@@ -761,7 +770,9 @@ class CreateType(SimpleCopy, Simple[collection.Collection]):
761
770
  await self.wait_list.acquire(c)
762
771
  try:
763
772
  if self.obj_list is None:
764
- self.obj_list = cdt.Array(await c.read_attr(collection.AttrDesc.OBJECT_LIST), type_=structs.ObjectListElement)
773
+ if isinstance(res_obj_list := await ReadByDescriptor(collection.AttrDesc.OBJECT_LIST).exchange(c), result.Error):
774
+ return res_obj_list
775
+ self.obj_list = cdt.Array(res_obj_list.value, type_=structs.ObjectListElement)
765
776
  if len(self.col) == 0:
766
777
  for country_olel in filter(lambda it: ln_pattern.COUNTRY_SPECIFIC == it.logical_name, self.obj_list):
767
778
  self.col.set_country(collection.CountrySpecificIdentifiers(country_olel.logical_name.d))
@@ -775,9 +786,11 @@ class CreateType(SimpleCopy, Simple[collection.Collection]):
775
786
  country_desc is not None
776
787
  and next(filter(lambda it: country_desc.instance_id.contents == it.logical_name.contents, self.obj_list), False)
777
788
  ):
789
+ if isinstance(res_country_ver := await ReadByDescriptor(country_desc).exchange(c), result.Error):
790
+ return res_country_ver
778
791
  self.col.set_country_ver(collection.ParameterValue(
779
792
  par=country_desc.instance_id.contents + country_desc.attribute_id.contents,
780
- value=await c.read_attr(country_desc)
793
+ value=res_country_ver.value
781
794
  ))
782
795
  c.log(logL.INFO, F"set country version: {self.col.country_ver}")
783
796
  else:
@@ -945,7 +958,7 @@ class MapTypeCreator:
945
958
  @dataclass
946
959
  class InitType(SimpleCopy, Simple[collection.Collection]):
947
960
  adapter: Adapter
948
- msg: str = ""
961
+ msg: str = "initiate type"
949
962
 
950
963
  async def exchange(self, c: Client) -> result.SimpleOrError[collection.Collection]:
951
964
  if isinstance((res := await Sequence(
@@ -989,10 +1002,11 @@ get_adapter(gag) # Dummy Adapter
989
1002
 
990
1003
 
991
1004
  @dataclass
1005
+ @deprecated("use <ReadObjAttr>")
992
1006
  class ReadAttribute(SimpleCopy, CDT):
993
1007
  ln: collection.LNContaining
994
1008
  index: int
995
- msg: str = ""
1009
+ msg: str = "read LN attribute"
996
1010
 
997
1011
  async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
998
1012
  obj = c.objects.get_object(self.ln)
@@ -1003,7 +1017,7 @@ class ReadAttribute(SimpleCopy, CDT):
1003
1017
  class ReadObjAttr(SimpleCopy, CDT):
1004
1018
  obj: collection.InterfaceClass
1005
1019
  index: int
1006
- msg: str = ""
1020
+ msg: str = "read object attribute"
1007
1021
 
1008
1022
  async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
1009
1023
  # TODO: check is_readable?
@@ -1012,10 +1026,11 @@ class ReadObjAttr(SimpleCopy, CDT):
1012
1026
  value=self.index,
1013
1027
  with_selection=bool(c.negotiated_conformance.selective_access)))
1014
1028
  start_read_time: float = time.perf_counter()
1015
- await c.read_data_block()
1029
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1030
+ return res_pdu
1016
1031
  c.last_transfer_time = datetime.timedelta(seconds=time.perf_counter()-start_read_time)
1017
1032
  try:
1018
- self.obj.set_attr(self.index, c.reply.data.get_data())
1033
+ self.obj.set_attr(self.index, res_pdu.value)
1019
1034
  return result.Simple(self.obj.get_attr(self.index))
1020
1035
  except ValueError as e:
1021
1036
  return result.Error.from_e(e)
@@ -1025,7 +1040,7 @@ class ReadObjAttr(SimpleCopy, CDT):
1025
1040
  return result.Error.from_e(e)
1026
1041
 
1027
1042
 
1028
- @dataclass
1043
+ @dataclass(frozen=True)
1029
1044
  class Par2Data[T: cdt.CommonDataType](SimpleCopy, CDT[T]):
1030
1045
  """get CommonDataType by Parameter"""
1031
1046
  par: Parameter
@@ -1056,13 +1071,13 @@ def is_empty(value: cdt.CommonDataType | None) -> bool:
1056
1071
  return True if value is None else False
1057
1072
 
1058
1073
 
1059
- @dataclass
1074
+ @dataclass(frozen=True)
1060
1075
  class ReadAttributeIf(SimpleCopy, Base):
1061
1076
  """read if func with arg as value is True"""
1062
1077
  ln: collection.LNContaining
1063
1078
  index: int
1064
1079
  func: AttrValueComp
1065
- msg: str
1080
+ msg: str = "read attribute with condition"
1066
1081
 
1067
1082
  async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
1068
1083
  # TODO: check is_readable
@@ -1075,7 +1090,7 @@ class ReadAttributeIf(SimpleCopy, Base):
1075
1090
  return result.OK
1076
1091
 
1077
1092
 
1078
- @dataclass
1093
+ @dataclass(frozen=True)
1079
1094
  class ReadEmptyAttribute(SimpleCopy, Base):
1080
1095
  ln: collection.LNContaining
1081
1096
  index: int
@@ -1090,7 +1105,7 @@ class ReadEmptyAttribute(SimpleCopy, Base):
1090
1105
  ).exchange(c)
1091
1106
 
1092
1107
 
1093
- @dataclass
1108
+ @dataclass(frozen=True)
1094
1109
  class ReadWritableAttributes(SimpleCopy, Base):
1095
1110
  ln: collection.LNContaining
1096
1111
  indexes: tuple[int, ...]
@@ -1116,7 +1131,7 @@ class ReadWritableAttributes(SimpleCopy, Base):
1116
1131
 
1117
1132
 
1118
1133
  # copy past from ReadWritableAttributes
1119
- @dataclass
1134
+ @dataclass(frozen=True)
1120
1135
  class ActualizeAttributes(SimpleCopy, OK):
1121
1136
  ln: collection.LNContaining
1122
1137
  indexes: tuple[int, ...]
@@ -1142,7 +1157,7 @@ class ActualizeAttributes(SimpleCopy, OK):
1142
1157
  return result.OK
1143
1158
 
1144
1159
 
1145
- @dataclass
1160
+ @dataclass(frozen=True)
1146
1161
  class ReadAttributes(SimpleCopy, _List[cdt.CommonDataType]):
1147
1162
  ln: collection.LNContaining
1148
1163
  indexes: tuple[int, ...]
@@ -1181,7 +1196,8 @@ class Write(SimpleCopy, OK):
1181
1196
  obj=res_obj.value,
1182
1197
  attr_index=self.par_data.par.i,
1183
1198
  value=enc)
1184
- ret = await c.read_data_block()
1199
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1200
+ return res_pdu
1185
1201
  return result.OK
1186
1202
 
1187
1203
 
@@ -1209,7 +1225,8 @@ class Write2(SimpleCopy, OK):
1209
1225
  obj=res_obj.value,
1210
1226
  attr_index=self.par.i,
1211
1227
  value=enc)
1212
- ret = await c.read_data_block()
1228
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1229
+ return res_pdu
1213
1230
  return result.OK
1214
1231
 
1215
1232
 
@@ -1249,7 +1266,8 @@ class WriteParValue(SimpleCopy, OK):
1249
1266
  obj=obj,
1250
1267
  attr_index=self.par_value.par.i,
1251
1268
  value=set_data.encoding)
1252
- ret = await c.read_data_block()
1269
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1270
+ return res_pdu
1253
1271
  return result.OK
1254
1272
 
1255
1273
 
@@ -1275,7 +1293,8 @@ class WriteAttribute(SimpleCopy, OK):
1275
1293
  obj=obj,
1276
1294
  attr_index=self.index,
1277
1295
  value=enc)
1278
- ret = await c.read_data_block()
1296
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1297
+ return res_pdu
1279
1298
  return result.OK # todo: return Data-Access-Result
1280
1299
 
1281
1300
 
@@ -1327,7 +1346,8 @@ class Execute2(SimpleCopy, OK):
1327
1346
  meth_desc=res.value.get_meth_descriptor(self.par.i),
1328
1347
  method=self.data
1329
1348
  )
1330
- await c.read_data_block()
1349
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1350
+ return res_pdu
1331
1351
  return result.OK
1332
1352
  except Exception as e:
1333
1353
  return result.Error.from_e(e)
@@ -1340,8 +1360,9 @@ class WriteTime(SimpleCopy, Base):
1340
1360
  try:
1341
1361
  obj = c._objects.clock
1342
1362
  c.get_get_request_normal(obj.get_attr_descriptor(3))
1343
- await c.read_data_block()
1344
- tz = obj.get_attr_element(3).DATA_TYPE(c.reply.data.get_data())
1363
+ if isinstance(res_pdu := await c.read_data_block(), result.Error):
1364
+ return res_pdu
1365
+ tz = obj.get_attr_element(3).DATA_TYPE(res_pdu.value)
1345
1366
  res = await WriteAttribute(
1346
1367
  ln=obj.logical_name,
1347
1368
  index=2,
@@ -1361,7 +1382,7 @@ class ImageTransfer(SimpleCopy, StrictOK):
1361
1382
  n_t_b: int = field(init=False, default=0)
1362
1383
  """not transferred block"""
1363
1384
  n_blocks: int = field(init=False)
1364
- msg: str = ""
1385
+ msg: str = "image transfer"
1365
1386
 
1366
1387
  def __post_init__(self) -> None:
1367
1388
  self.ITI = ImageTransferInitiate((
@@ -1560,62 +1581,42 @@ class ActivateImage(SimpleCopy, OK):
1560
1581
  return result.OK
1561
1582
 
1562
1583
 
1563
- class TestAll(Base):
1584
+ # todo: don't work with new API, remake
1585
+ class TestAll(OK):
1564
1586
  """read all attributes with check access""" # todo: add Write with access
1565
- msg: str = ""
1587
+ msg: str = "test all"
1566
1588
 
1567
- async def exchange(self, c: Client):
1589
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1568
1590
  # todo: refactoring with <append_err>
1569
- res = result.Simple()
1570
- if await init_type.exchange(c):
1571
- ass: collection.AssociationLN = c._objects.sap2association(c.SAP)
1572
- for obj in tuple(c._objects):
1573
- indexes: list[int] = [i for i, _ in obj.get_index_with_attributes()]
1574
- attempt = count(1)
1575
- while True:
1576
- c.log(logL.INFO, F"start read {obj} attr: {', '.join(map(str, indexes))}. attempt={next(attempt)}")
1577
- len_indexes: int = len(indexes)
1578
- for i in tuple(indexes):
1579
- is_readable = ass.is_readable(
1580
- ln=obj.logical_name,
1581
- index=i)
1582
- try:
1583
- await ReadAttribute(
1584
- ln=obj.logical_name,
1585
- index=i
1586
- ).exchange(c)
1587
- if not is_readable:
1588
- c.log(logL.ERR, F"{obj} with attr={i} must be unreadable")
1589
- indexes.remove(i)
1590
- c.log(logL.INFO, F"Get {obj}:{obj.get_attr_element(i)}")
1591
- if res.value is None:
1592
- c.log(logL.WARN, F"ValueError. For {obj} attr:{i} {res}")
1593
- except exc.ResultError as e:
1594
- if e.result == pdu.DataAccessResult.READ_WRITE_DENIED and not is_readable:
1595
- c.log(logL.INFO, F"success ReadAccess TEST: {e}")
1596
- indexes.remove(i)
1597
- else:
1598
- c.log(logL.ERR, F"result_error {e}")
1599
- except exc.ITEApplication as e:
1600
- c.log(logL.ERR, F"ITEApplication. For {obj} attr:{i} {e}")
1601
- except Exception as e:
1602
- c.log(logL.ERR, F"Unknown. For {obj} attr:{i} {e}")
1603
- if len(indexes) == 0:
1604
- c.log(logL.INFO, "all read success")
1605
- break
1606
- elif len(indexes) == len_indexes:
1607
- c.log(logL.ERR, F"can't read attr={', '.join(map(str, indexes))}")
1608
- break
1591
+ res = result.ErrorAccumulator()
1592
+ if isinstance(res_objects := c.objects.sap2objects(c.SAP), result.Error):
1593
+ return res_objects
1594
+ ass: collection.AssociationLN = c.objects.sap2association(c.SAP)
1595
+ for obj in res_objects.value:
1596
+ indexes: list[int] = [i for i, _ in obj.get_index_with_attributes()]
1597
+ c.log(logL.INFO, F"start read {obj} attr: {', '.join(map(str, indexes))}")
1598
+ for i in indexes:
1599
+ is_readable = ass.is_readable(
1600
+ ln=obj.logical_name,
1601
+ index=i)
1602
+ if isinstance(res_read := await ReadObjAttr(obj, i).exchange(c), result.Error):
1603
+ if (
1604
+ res_read.has(pdu.DataAccessResult.READ_WRITE_DENIED, exc.ResultError)
1605
+ and not is_readable
1606
+ ):
1607
+ c.log(logL.INFO, F"success ReadAccess TEST")
1609
1608
  else:
1610
- c.log(logL.WARN, F"has a errors in attr={', '.join(map(str, indexes))}. Need more attempt")
1611
- # todo: check write problem - if send_frame clear then counter is wrong, need fix in client it
1612
- return res
1609
+ res.append_err(res_read.err)
1610
+ elif not is_readable:
1611
+ res.append_e(PermissionError(f"{obj} with attr={i} must be unreadable"))
1612
+ indexes.remove(i)
1613
+ return res.result
1613
1614
 
1614
1615
 
1615
1616
  @dataclass
1616
1617
  class ApplyTemplate(Base):
1617
1618
  template: collection.Template
1618
- msg: str = ""
1619
+ msg: str = "apply template"
1619
1620
 
1620
1621
  async def exchange(self, c: Client) -> result.Result:
1621
1622
  # todo: search col
@@ -1645,7 +1646,7 @@ class ApplyTemplate(Base):
1645
1646
  @dataclass
1646
1647
  class ReadTemplate(OK):
1647
1648
  template: collection.Template
1648
- msg: str = ""
1649
+ msg: str = "read template"
1649
1650
 
1650
1651
  async def exchange(self, c: Client) -> result.Ok | result.Error:
1651
1652
  # todo: copypast from <ApplyTemplate>
@@ -1673,50 +1674,39 @@ class ReadTemplate(OK):
1673
1674
 
1674
1675
 
1675
1676
  @dataclass
1676
- class AccessValidate(Base[result.Ok | result.Simple[list[Parameter]]]):
1677
+ class AccessValidate(OK):
1677
1678
  """check all access rights for current SAP"""
1678
1679
  with_correct: bool = False
1679
- msg: str = ""
1680
+ msg: str = "all access validate"
1680
1681
 
1681
1682
  # todo: make with result.Error
1682
- async def exchange(self, c: Client) -> result.Ok | result.Simple[list[Parameter]] | result.Error:
1683
+ async def exchange(self, c: Client) -> result.Ok | result.Error:
1684
+ res = result.ErrorAccumulator()
1683
1685
  obj_l: ObjectListType
1684
1686
  el: ObjectListElement
1685
1687
  a_a_i: association_ln.abstract.AttributeAccessItem
1686
- if c._objects is None:
1687
- return result.Error.from_e(exc.DLMSException("not find Collection"))
1688
- elif (obj_l := c._objects.sap2association(c.SAP).object_list) is None:
1688
+ if (obj_l := c.objects.sap2association(c.SAP).object_list) is None:
1689
1689
  return result.Error.from_e(exc.EmptyObj(F"empty object_list for {c._objects.sap2association(c.SAP)}"))
1690
- res = result.Simple(value=[])
1691
1690
  for el in obj_l:
1692
1691
  for a_a_i in el.access_rights.attribute_access:
1693
1692
  if a_a_i.access_mode.is_readable():
1694
1693
  i = int(a_a_i.attribute_id)
1695
- try:
1696
- res_ = await ReadAttribute(
1697
- ln=el.logical_name,
1698
- index=int(a_a_i.attribute_id)
1699
- ).exchange(c)
1700
- if a_a_i.access_mode.is_writable():
1701
- await WriteAttribute(
1702
- ln=el.logical_name,
1703
- index=i,
1704
- value=res_.value.encoding
1705
- ).exchange(c)
1706
- except exc.ResultError as e:
1707
- res.append_e(e)
1708
- res.value.append(Parameter(el.logical_name.contents).set_i(i))
1694
+ if isinstance(res_read :=await ReadByDescriptor(ut.CosemAttributeDescriptor((
1695
+ int(el.class_id),
1696
+ el.logical_name.contents,
1697
+ i
1698
+ ))).exchange(c), result.Error):
1699
+ res.append_err(res_read.err)
1709
1700
  if self.with_correct:
1710
1701
  a_a_i.access_mode.set(1) # todo: make better in future
1711
- except exc.Timeout as e:
1712
- c.log(logL.ERR, F"for {el.logical_name}: {i} - {e}")
1713
- await HardwareReconnect().exchange(c)
1714
- except Exception as e:
1715
- res.append_e(e)
1716
- res.value.append(Parameter(el.logical_name.contents).set_i(i))
1717
- if not res.is_ok():
1718
- return res
1719
- return result.OK
1702
+ elif a_a_i.access_mode.is_writable():
1703
+ if isinstance(res_write :=await WriteAttribute(
1704
+ ln=el.logical_name,
1705
+ index=i,
1706
+ value=res_read.value
1707
+ ).exchange(c), result.Error):
1708
+ res.append_err(res_write.err)
1709
+ return res.result
1720
1710
 
1721
1711
 
1722
1712
  @dataclass
@@ -1742,7 +1732,7 @@ class WriteList(SimpleCopy, _List[result.Ok]):
1742
1732
  par_datas: tuple[tuple[Parameter, cdt.CommonDataType], ...]
1743
1733
  err_ignore: bool
1744
1734
 
1745
- def __init__(self, *par_datas: tuple[Parameter, cdt.CommonDataType], err_ignore: bool = False, msg: str = "") -> None:
1735
+ def __init__(self, *par_datas: tuple[Parameter, cdt.CommonDataType], err_ignore: bool = False, msg: str = "write list") -> None:
1746
1736
  self.par_datas = par_datas
1747
1737
  self.err_ignore = err_ignore
1748
1738
  self.msg = msg
@@ -1769,7 +1759,7 @@ class WriteTranscript(SimpleCopy, OK):
1769
1759
  async def exchange(self, c: Client) -> result.Ok | result.Error:
1770
1760
  if isinstance((res := await Par2Data[cdt.CommonDataType](self.par).exchange(c)), result.Error):
1771
1761
  return res
1772
- if isinstance(data, cdt.Digital):
1762
+ if isinstance(res.value, cdt.Digital):
1773
1763
  s_u = c.objects.par2su(self.par)
1774
1764
  if isinstance(s_u, cdt.ScalUnitType):
1775
1765
  if not isinstance(self.value, str):
@@ -1790,7 +1780,7 @@ class WriteTranscripts(SimpleCopy, _List[result.Ok]):
1790
1780
  par_values: tuple[tuple[Parameter, cdt.Transcript], ...]
1791
1781
  err_ignore: bool
1792
1782
 
1793
- def __init__(self, *par_values: tuple[Parameter, cdt.Transcript], err_ignore: bool = False, msg: str =""):
1783
+ def __init__(self, *par_values: tuple[Parameter, cdt.Transcript], err_ignore: bool = False, msg: str ="write transcripts"):
1794
1784
  self.par_values = par_values
1795
1785
  self.err_ignore = err_ignore
1796
1786
  self.msg = msg