DLMS-SPODES 0.87.16__py3-none-any.whl → 0.88.1__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 (103) hide show
  1. DLMS_SPODES/Values/EN/__init__.py +1 -1
  2. DLMS_SPODES/Values/EN/actors.py +8 -8
  3. DLMS_SPODES/Values/EN/relation_to_obis_names.py +387 -387
  4. DLMS_SPODES/Values/RU/__init__.py +1 -1
  5. DLMS_SPODES/Values/RU/actors.py +8 -8
  6. DLMS_SPODES/Values/RU/relation_to_obis_names.py +396 -396
  7. DLMS_SPODES/__init__.py +6 -6
  8. DLMS_SPODES/configEN.ini +126 -126
  9. DLMS_SPODES/config_parser.py +53 -53
  10. DLMS_SPODES/cosem_interface_classes/Overview/__init__.py +0 -0
  11. DLMS_SPODES/cosem_interface_classes/Overview/class_id.py +107 -0
  12. DLMS_SPODES/cosem_interface_classes/__class_init__.py +3 -3
  13. DLMS_SPODES/cosem_interface_classes/__init__.py +3 -2
  14. DLMS_SPODES/cosem_interface_classes/activity_calendar.py +210 -254
  15. DLMS_SPODES/cosem_interface_classes/arbitrator.py +78 -105
  16. DLMS_SPODES/cosem_interface_classes/association_ln/abstract.py +50 -34
  17. DLMS_SPODES/cosem_interface_classes/association_ln/authentication_mechanism_name.py +25 -25
  18. DLMS_SPODES/cosem_interface_classes/association_ln/mechanism_id.py +25 -25
  19. DLMS_SPODES/cosem_interface_classes/association_ln/method.py +5 -5
  20. DLMS_SPODES/cosem_interface_classes/association_ln/ver0.py +440 -485
  21. DLMS_SPODES/cosem_interface_classes/association_ln/ver1.py +126 -133
  22. DLMS_SPODES/cosem_interface_classes/association_ln/ver2.py +30 -36
  23. DLMS_SPODES/cosem_interface_classes/association_ln/ver3.py +3 -4
  24. DLMS_SPODES/cosem_interface_classes/association_sn/ver0.py +14 -12
  25. DLMS_SPODES/cosem_interface_classes/clock.py +81 -131
  26. DLMS_SPODES/cosem_interface_classes/collection.py +2106 -2122
  27. DLMS_SPODES/cosem_interface_classes/cosem_interface_class.py +525 -583
  28. DLMS_SPODES/cosem_interface_classes/data.py +12 -21
  29. DLMS_SPODES/cosem_interface_classes/demand_register/ver0.py +32 -59
  30. DLMS_SPODES/cosem_interface_classes/disconnect_control.py +56 -74
  31. DLMS_SPODES/cosem_interface_classes/extended_register.py +18 -27
  32. DLMS_SPODES/cosem_interface_classes/gprs_modem_setup.py +33 -43
  33. DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver0.py +78 -103
  34. DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver1.py +42 -40
  35. DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver2.py +6 -9
  36. DLMS_SPODES/cosem_interface_classes/iec_hdlc_setup/ver0.py +11 -11
  37. DLMS_SPODES/cosem_interface_classes/iec_hdlc_setup/ver1.py +27 -53
  38. DLMS_SPODES/cosem_interface_classes/iec_local_port_setup.py +9 -11
  39. DLMS_SPODES/cosem_interface_classes/image_transfer/image_transfer_status.py +15 -15
  40. DLMS_SPODES/cosem_interface_classes/image_transfer/ver0.py +54 -126
  41. DLMS_SPODES/cosem_interface_classes/implementations/__init__.py +3 -3
  42. DLMS_SPODES/cosem_interface_classes/implementations/arbitrator.py +19 -19
  43. DLMS_SPODES/cosem_interface_classes/implementations/data.py +491 -487
  44. DLMS_SPODES/cosem_interface_classes/implementations/profile_generic.py +85 -83
  45. DLMS_SPODES/cosem_interface_classes/ipv4_setup.py +42 -72
  46. DLMS_SPODES/cosem_interface_classes/limiter.py +77 -111
  47. DLMS_SPODES/cosem_interface_classes/ln_pattern.py +334 -333
  48. DLMS_SPODES/cosem_interface_classes/modem_configuration/ver0.py +51 -65
  49. DLMS_SPODES/cosem_interface_classes/modem_configuration/ver1.py +27 -39
  50. DLMS_SPODES/cosem_interface_classes/ntp_setup/ver0.py +48 -67
  51. DLMS_SPODES/cosem_interface_classes/obis.py +28 -23
  52. DLMS_SPODES/cosem_interface_classes/overview.py +198 -197
  53. DLMS_SPODES/cosem_interface_classes/parameter.py +548 -547
  54. DLMS_SPODES/cosem_interface_classes/parameters.py +172 -172
  55. DLMS_SPODES/cosem_interface_classes/profile_generic/ver0.py +90 -122
  56. DLMS_SPODES/cosem_interface_classes/profile_generic/ver1.py +268 -277
  57. DLMS_SPODES/cosem_interface_classes/push_setup/ver0.py +13 -12
  58. DLMS_SPODES/cosem_interface_classes/push_setup/ver1.py +9 -10
  59. DLMS_SPODES/cosem_interface_classes/push_setup/ver2.py +124 -166
  60. DLMS_SPODES/cosem_interface_classes/register.py +18 -45
  61. DLMS_SPODES/cosem_interface_classes/register_activation/ver0.py +45 -80
  62. DLMS_SPODES/cosem_interface_classes/register_monitor.py +33 -46
  63. DLMS_SPODES/cosem_interface_classes/reports.py +72 -70
  64. DLMS_SPODES/cosem_interface_classes/schedule.py +88 -176
  65. DLMS_SPODES/cosem_interface_classes/script_table.py +54 -87
  66. DLMS_SPODES/cosem_interface_classes/security_setup/ver0.py +45 -68
  67. DLMS_SPODES/cosem_interface_classes/security_setup/ver1.py +122 -158
  68. DLMS_SPODES/cosem_interface_classes/single_action_schedule.py +34 -50
  69. DLMS_SPODES/cosem_interface_classes/special_days_table.py +54 -84
  70. DLMS_SPODES/cosem_interface_classes/tcp_udp_setup.py +20 -42
  71. DLMS_SPODES/cosem_pdu.py +93 -93
  72. DLMS_SPODES/enums.py +625 -625
  73. DLMS_SPODES/exceptions.py +106 -106
  74. DLMS_SPODES/firmwares.py +99 -99
  75. DLMS_SPODES/hdlc/frame.py +875 -875
  76. DLMS_SPODES/hdlc/sub_layer.py +54 -54
  77. DLMS_SPODES/literals.py +17 -17
  78. DLMS_SPODES/obis/__init__.py +1 -1
  79. DLMS_SPODES/obis/media_id.py +931 -931
  80. DLMS_SPODES/pardata.py +22 -22
  81. DLMS_SPODES/pdu_enums.py +98 -98
  82. DLMS_SPODES/relation_to_OBIS.py +463 -465
  83. DLMS_SPODES/settings.py +551 -551
  84. DLMS_SPODES/types/choices.py +140 -142
  85. DLMS_SPODES/types/common_data_types.py +2379 -2401
  86. DLMS_SPODES/types/cosem_service_types.py +109 -109
  87. DLMS_SPODES/types/implementations/arrays.py +25 -25
  88. DLMS_SPODES/types/implementations/bitstrings.py +97 -97
  89. DLMS_SPODES/types/implementations/double_long_usingneds.py +35 -35
  90. DLMS_SPODES/types/implementations/enums.py +57 -57
  91. DLMS_SPODES/types/implementations/integers.py +12 -11
  92. DLMS_SPODES/types/implementations/long_unsigneds.py +127 -127
  93. DLMS_SPODES/types/implementations/octet_string.py +11 -11
  94. DLMS_SPODES/types/implementations/structs.py +64 -64
  95. DLMS_SPODES/types/type_alias.py +74 -0
  96. DLMS_SPODES/types/useful_types.py +627 -677
  97. {dlms_spodes-0.87.16.dist-info → dlms_spodes-0.88.1.dist-info}/METADATA +30 -30
  98. dlms_spodes-0.88.1.dist-info/RECORD +118 -0
  99. {dlms_spodes-0.87.16.dist-info → dlms_spodes-0.88.1.dist-info}/WHEEL +1 -1
  100. DLMS_SPODES/cosem_interface_classes/a_parameter.py +0 -20
  101. DLMS_SPODES/cosem_interface_classes/attr_indexes.py +0 -12
  102. dlms_spodes-0.87.16.dist-info/RECORD +0 -117
  103. {dlms_spodes-0.87.16.dist-info → dlms_spodes-0.88.1.dist-info}/top_level.txt +0 -0
@@ -1,677 +1,627 @@
1
- from __future__ import annotations
2
- from functools import lru_cache
3
- from abc import ABC, abstractmethod
4
- from typing import Type, Any, Callable
5
- from dataclasses import dataclass
6
- from ..types import common_data_types as cdt
7
- from ..exceptions import DLMSException
8
- from ..settings import settings
9
-
10
-
11
- class UserfulTypesException(DLMSException):
12
- """override DLMSException"""
13
-
14
-
15
- class _String(ABC):
16
- LENGTH: int | None
17
-
18
- def __init__(self, value: bytes | bytearray | str | int | tuple | UsefulType = None):
19
- match value:
20
- case None: self.__dict__["contents"] = bytes(self.LENGTH)
21
- case bytes() if self.LENGTH is None or self.LENGTH <= len(value): self.__dict__["contents"] = value[:self.LENGTH]
22
- # case bytes() if self.LENGTH <= len(value): self.contents = value[:self.LENGTH]
23
- case bytes(): raise ValueError(F'Length of contents for {self.__class__.__name__} must be at least {self.LENGTH}, but got {len(value)}')
24
- case tuple():
25
- if len(value) == self.LENGTH:
26
- self.__dict__["contents"] = bytes(value)
27
- else:
28
- raise ValueError(F"in {self.__class__.__name__} with {value=} got length: {len(value)}, expect {self.LENGTH}")
29
- case bytearray(): self.__dict__["contents"] = bytes(value) # Attention!!! changed method content getting from bytearray
30
- case str(): self.__dict__["contents"] = self.from_str(value)
31
- case int(): self.__dict__["contents"] = self.from_int(value)
32
- case UsefulType(): self.__dict__["contents"] = value.contents # TODO: make right type
33
- case _: raise ValueError(F'Error create {self.__class__.__name__} with value {value}')
34
-
35
- @abstractmethod
36
- def __len__(self):
37
- """ define in subclasses """
38
-
39
-
40
- class OCTET_STRING(_String):
41
- """ An ordered sequence of octets (8 bit bytes) """
42
-
43
- def from_str(self, value: str) -> bytes:
44
- """ input as hex code """
45
- return bytes.fromhex(value)
46
-
47
- def from_int(self, value: int) -> bytes:
48
- """ Convert with recursion. Maximum convert length is 32 """
49
- def to_bytes_with(length_):
50
- try:
51
- return int.to_bytes(value, length_, 'big')
52
- except OverflowError:
53
- if length_ > 31:
54
- raise ValueError(F'Value {value} is big to convert to bytes')
55
- return to_bytes_with(length_+1)
56
- length = 1
57
- return to_bytes_with(length)
58
-
59
- def __str__(self):
60
- return self.contents.hex(' ')
61
-
62
- def __len__(self):
63
- return len(self.contents)
64
-
65
- def __getitem__(self, item):
66
- return self.contents[item]
67
-
68
- # TODO: maybe remove as redundante?
69
- def decode(self) -> bytes:
70
- """ decode to build in bytes type """
71
- return self.contents
72
-
73
- # TODO: maybe remove as redundante?
74
- def to_str(self, encoding: str = 'cp1251') -> str:
75
- """ decode to cp1251 by default, replace to '?' if unsupported """
76
- temp = list()
77
- for i in self.contents:
78
- temp.append(i if i > 32 else 63)
79
- return bytes(temp).decode(encoding)
80
-
81
-
82
- class CHOICE(ABC):
83
- """ TODO: with cdt.CHOICE """
84
- ELEMENTS: dict[int, SequenceElement | dict[int, SequenceElement]]
85
-
86
- @property
87
- @abstractmethod
88
- def TYPE(self) -> Any:
89
- """ return valid types """
90
-
91
- def __init_subclass__(cls, **kwargs):
92
- if hasattr(cls, 'ELEMENTS'):
93
- for el in cls.ELEMENTS.values():
94
- if isinstance(el, dict):
95
- """pass, maybe it is for cst.AnyTime"""
96
- elif issubclass(el.TYPE, cls.TYPE):
97
- """ type in order """
98
- else:
99
- raise ValueError(F'For {cls.__name__} got type {el.TYPE.__name__} with {el.NAME=}, expected {cls.TYPE.__name__}')
100
- else:
101
- """ subclass with type carry initiate """
102
-
103
- def __getitem__(self, item: int) -> SequenceElement:
104
- return self.ELEMENTS[item]
105
-
106
- @property
107
- def NAME(self) -> str:
108
- return F'CHOICE[{len(self)}]'
109
-
110
- def __len__(self):
111
- return len(self.ELEMENTS)
112
-
113
- def is_key(self, value: int) -> bool:
114
- return value in self.ELEMENTS.keys()
115
-
116
- @classmethod
117
- def parse(cls, value: cdt.Transcript) -> cdt.CommonDataType:
118
- """get instance by pattern: <tag>:<value>"""
119
- tag, value = value.split(":", maxsplit=1)
120
- if (d_t := cls.ELEMENTS.get(int(tag))) is None:
121
- raise UserfulTypesException(F"for {cls.__name__} got {cdt.CommonDataType.__name__}: {cdt.TAG(int(tag).to_bytes(1))}; "
122
- F"expected: {', '.join(map(lambda el: el.NAME, cls.ELEMENTS.values()))}")
123
- else:
124
- return d_t.TYPE.parse(value)
125
-
126
- def __call__(self,
127
- value: bytes | int = None,
128
- force: bool = False) -> cdt.CommonDataType:
129
- """ get instance from encoding or tag(with default value). For CommonDataType only """
130
- try:
131
- match value:
132
- case bytes() as encoding:
133
- match self.ELEMENTS[encoding[0]]:
134
- case SequenceElement() as el: return el.TYPE(encoding)
135
- case dict() as ch:
136
- if encoding[1] in ch.keys():
137
- return ch[encoding[1]].TYPE(encoding) # use for choice cst.Time | DateTime | Date as OctetString
138
- else:
139
- raise ValueError(F"got type with tag: {encoding[0]} and length: {encoding[1]}, expected length {tuple(ch.keys())}")
140
- case err: raise ValueError(F"got {err.__name__}, expected {SequenceElement.__name__} or {dict.__name__}")
141
- case int() if force: return cdt.get_common_data_type_from(value.to_bytes(1, "big"))()
142
- case int() as tag: return self.ELEMENTS[tag].TYPE()
143
- case None: return tuple(self.ELEMENTS.values())[0].TYPE()
144
- case error: raise ValueError(F'Unknown value type {error}')
145
- except KeyError as e:
146
- raise UserfulTypesException(F"for {self.__class__.__name__} got {cdt.CommonDataType.__name__}: {cdt.TAG(e.args[0].to_bytes(1))}; expected: {', '.join(map(lambda el: el.NAME, self.ELEMENTS.values()))}")
147
-
148
- def __get_elements(self) -> list[SequenceElement]:
149
- """all elements with nested values"""
150
- elements = list()
151
- for el in self.ELEMENTS.values():
152
- match el:
153
- case SequenceElement():
154
- elements.append(el)
155
- case dict() as dict_el:
156
- elements.extend(dict_el.values())
157
- case err:
158
- raise ValueError(F"unknown CHOICE element type {err}")
159
- return elements
160
-
161
- def get_types(self) -> tuple[Type[cdt.CommonDataType]]:
162
- """ Use in setter attribute.value for validate """
163
- return tuple((seq_el.TYPE for seq_el in self.__get_elements()))
164
-
165
- def __str__(self):
166
- return F'{CHOICE}: {", ".join((el.NAME for el in self.__get_elements()))}'
167
-
168
-
169
- def get_instance_and_context(meta: Type[UsefulType], value: bytes) -> tuple[UsefulType, bytes]:
170
- instance = meta(value)
171
- return instance, value[len(instance.contents):]
172
-
173
-
174
- @dataclass(frozen=True)
175
- class SequenceElement:
176
- NAME: str
177
- TYPE: Type[UsefulType | SEQUENCE | CHOICE | cdt.CommonDataType]
178
-
179
- def __str__(self):
180
- return F'{self.NAME}: {self.TYPE.__name__}'
181
-
182
-
183
- class SEQUENCE(ABC):
184
- """ TODO: """
185
- ELEMENTS: tuple[SequenceElement | SEQUENCE, ...]
186
- values: list[UsefulType]
187
-
188
- def __init__(self, value: bytes | tuple | list | None | SEQUENCE = None):
189
- self.__dict__['values'] = [None] * len(self.ELEMENTS)
190
- match value:
191
- case tuple() | list():
192
- if len(value) != len(self):
193
- raise ValueError(F'Struct {self.__class__.__name__} got length:{len(value)}, expected length:{len(self)}')
194
- self.from_tuple(value)
195
- case bytes(): self.from_bytes(value)
196
- case None: self.from_default()
197
- case self.__class__(): self.from_bytes(value.contents)
198
- case _: raise TypeError(F'Value: "{value}" not supported')
199
-
200
- def from_default(self):
201
- for i in range(len(self)):
202
- self.values[i] = self.ELEMENTS[i].TYPE()
203
-
204
- def from_bytes(self, value: bytes | bytearray):
205
- for i, element in enumerate(self.ELEMENTS):
206
- self.values[i], value = get_instance_and_context(element.TYPE, value)
207
-
208
- def from_tuple(self, value: tuple | list):
209
- for i, val in enumerate(value):
210
- self.values[i] = self.ELEMENTS[i].TYPE(val)
211
-
212
- def __str__(self):
213
- return F'{{{", ".join(map(lambda val: F"{val[0].NAME}: {val[1]}", zip(self.ELEMENTS, self.values)))}}}'
214
-
215
- def __repr__(self):
216
- return F'{self.__class__.__name__}(({(", ".join(map(str, self.values)))}))'
217
-
218
- def __get_index(self, name: str) -> int | None:
219
- """ get index by name. Return None if not found """
220
- for i, element in enumerate(self.ELEMENTS):
221
- if element.NAME == name:
222
- return i
223
- else:
224
- return None
225
-
226
- def __setattr__(self, key, value: UsefulType):
227
- match self.__get_index(key):
228
- case int() as i if isinstance(value, self.ELEMENTS[i]): self.values[i] = value
229
- case int() as i: raise ValueError(F'Try assign {key} Type got {value.__class__.__name__}, expected {self.ELEMENTS[i].NAME}')
230
- case _: raise ValueError(F'Unsupported change: {key}')
231
-
232
- def __getattr__(self, item: str) -> UsefulType:
233
- match self.__get_index(item):
234
- case int() as i: return self.values[i]
235
- case _: return self.__dict__[item]
236
-
237
- def __getitem__(self, item: str | int) -> UsefulType:
238
- """ get element by index or name """
239
- match item:
240
- case str() as name: return self.values[self.__get_index(name)]
241
- case int() as index: return self.values[index]
242
- case _: raise ValueError(F'Unsupported type {item.__class__}')
243
-
244
- def __setitem__(self, key: int, value: UsefulType):
245
- """ set data to element by index """
246
- if isinstance(value, self.ELEMENTS[key].TYPE):
247
- self.values[key] = value
248
- else:
249
- raise ValueError(F'Type got {value.__class__.__name__}, expected {self.ELEMENTS[key].TYPE}')
250
-
251
- def __len__(self):
252
- return len(self.ELEMENTS)
253
-
254
- @property
255
- def contents(self) -> bytes:
256
- return b''.join(el.contents for el in self.values)
257
-
258
- @property
259
- def NAME(self) -> str:
260
- return F'{self.__class__.__name__}[{len(self)}]'
261
-
262
-
263
- class UsefulType(ABC):
264
- """"""
265
- contents: bytes
266
- __match_args__ = ('contents',)
267
- cb_post_set: Callable
268
- cb_preset: Callable
269
-
270
- @abstractmethod
271
- def __init__(self, value):
272
- """ constructor """
273
-
274
- def __eq__(self, other: UsefulType):
275
- match other:
276
- case self.__class__(self.contents): return True
277
- case _: return False
278
-
279
- def set_contents_from(self, value: UsefulType | bytes | bytearray | str | int | bool | None):
280
- new_value = self.__class__(value)
281
- if hasattr(self, 'cb_preset'):
282
- self.cb_preset(new_value)
283
- self.__dict__['contents'] = new_value.contents
284
- if hasattr(self, 'cb_post_set'):
285
- self.cb_post_set()
286
-
287
- def __setattr__(self, key, value):
288
- match key:
289
- case 'contents' as prop if hasattr(self, 'contents'): raise ValueError(F"Don't support set {prop}")
290
- case _: super().__setattr__(key, value)
291
-
292
-
293
- class DigitalMixin(ABC):
294
- """ Default value is 0 """
295
- SIGNED: bool
296
- LENGTH: int
297
- contents: bytes
298
-
299
- def __init__(self, value: bytes | bytearray | str | int | DigitalMixin = None):
300
- match value:
301
- case bytes() if self.LENGTH <= len(value): self.__dict__["contents"] = value[:self.LENGTH]
302
- case bytes(): raise ValueError(F'Length of contents for {self.__class__.__name__} must be at least {self.LENGTH}, but got {len(value)}')
303
- case bytearray(): self.__dict__["contents"] = bytes(value) # Attention!!! changed method content getting from bytearray
304
- case str('-') if self.SIGNED: self.__dict__["contents"] = bytes(self.LENGTH)
305
- case int(): self.__dict__["contents"] = self.from_int(value)
306
- case str(): self.__dict__["contents"] = self.from_str(value)
307
- case None: self.__dict__["contents"] = bytes(self.LENGTH)
308
- case self.__class__(): self.__dict__["contents"] = value.contents
309
- case _: raise ValueError(F'Error create {self.__class__.__name__} with value: {value}')
310
-
311
- def from_int(self, value: int | float) -> bytes:
312
- try:
313
- return int(value).to_bytes(self.LENGTH, 'big', signed=self.SIGNED)
314
- except OverflowError:
315
- raise ValueError(F'value {value} out of range')
316
-
317
- def from_str(self, value: str) -> bytes:
318
- return self.from_int(float(value))
319
-
320
- def __int__(self):
321
- """ return the build in integer type """
322
- return int.from_bytes(self.contents, 'big', signed=self.SIGNED)
323
-
324
- def __str__(self):
325
- return str(int(self))
326
-
327
- def __repr__(self):
328
- return F'{self.__class__.__name__}({self})'
329
-
330
- def __gt__(self, other: DigitalMixin):
331
- match other:
332
- case DigitalMixin(): return int(self) > int(other)
333
- case _: raise TypeError(F'Compare type is {other.__class__}, expected Digital')
334
-
335
- def __len__(self) -> int:
336
- return self.LENGTH
337
-
338
- def __hash__(self):
339
- return int(self)
340
-
341
- def validate_from(self, value: str, cursor_position: int) -> tuple[str, int]:
342
- """ return validated value and cursor position. Use in Entry. TODO: remove it, make better """
343
- type(self)(value=value)
344
- return value, cursor_position
345
-
346
-
347
- class Integer8(DigitalMixin, UsefulType):
348
- """ INTEGER(-127…128) """
349
- LENGTH = 1
350
- SIGNED = True
351
-
352
-
353
- class Integer16(DigitalMixin, UsefulType):
354
- """ INTEGER(-32 768...32 767) """
355
- LENGTH = 2
356
- SIGNED = True
357
-
358
-
359
- class Integer32(DigitalMixin, UsefulType):
360
- """ INTEGER(-2 147 483 648...2 147 483 647) """
361
- LENGTH = 4
362
- SIGNED = True
363
-
364
-
365
- class Integer64(DigitalMixin, UsefulType):
366
- """ INTEGER(-2^63...2^63-1) """
367
- LENGTH = 8
368
- SIGNED = True
369
-
370
-
371
- class Unsigned8(DigitalMixin, UsefulType):
372
- """ INTEGER(0...255) """
373
- LENGTH = 1
374
- SIGNED = False
375
- # __match_args__ = ('contents',)
376
-
377
-
378
- class Unsigned16(DigitalMixin, UsefulType):
379
- """ INTEGER(0...65 535) """
380
- LENGTH = 2
381
- SIGNED = False
382
-
383
-
384
- class Unsigned32(DigitalMixin, UsefulType):
385
- """ INTEGER(0...4 294 967 295) """
386
- LENGTH = 4
387
- SIGNED = False
388
-
389
-
390
- class Unsigned64(DigitalMixin, UsefulType):
391
- """ INTEGER(0...264-1) """
392
- LENGTH = 8
393
- SIGNED = False
394
-
395
-
396
- class CosemClassId(Unsigned16):
397
- """ Identification code of the IC (range 0 to 65 535). The class_id of each object is retrieved together with the logical name by reading the object_list attribute of an
398
- “Association LN” / ”Association SN” object.
399
- - class_id-s from 0 to 8 191 are reserved to be specified by the DLMS UA.
400
- - class_id-s from 8 192 to 32 767 are reserved for manufacturer specific ICs.
401
- - class_id-s from 32 768 to 65 535 are reserved for user group specific ICs.
402
- The DLMS UA reserves the right to assign ranges to individual manufacturers or user groups. """
403
-
404
- def __str__(self) -> str:
405
- if res := settings.class_name.get(int(self)):
406
- return res
407
- else:
408
- return repr(self)
409
-
410
- def __repr__(self):
411
- return F"{self.__class__.__name__}({int(self)})"
412
-
413
-
414
- class CosemObjectInstanceId(OCTET_STRING, UsefulType):
415
- LENGTH = 6
416
-
417
- def __str__(self):
418
- return F"\"{'.'.join(map(str, self.contents))}\""
419
-
420
- def from_str(self, value: str) -> bytes:
421
- """ create logical_name: octet_string from string type ddd.ddd.ddd.ddd.ddd.ddd, ex.: 0.0.1.0.0.255 """
422
- raw_value = bytes()
423
- for typecast, separator in zip((self.__from_group_A, )*5+(self.__from_group_F, ), ('.', '.', '.', '.', '.', ' ')):
424
- try:
425
- element, value = value.split(separator, 1)
426
- except ValueError:
427
- element, value = value, ''
428
- raw_value += typecast(element)
429
- return raw_value
430
-
431
- def __from_group_A(self, value: str) -> bytes:
432
- if isinstance(value, str):
433
- if value == '':
434
- return b'\x00'
435
- try:
436
- return int(value).to_bytes(1, 'big')
437
- except OverflowError:
438
- raise ValueError(F'Int too big to convert {value}')
439
- else:
440
- raise TypeError(F'Unsupported type validation from string, got {value.__class__}')
441
-
442
- def __from_group_F(self, value: str) -> bytes:
443
- if isinstance(value, str):
444
- if value == '':
445
- return b'\xff'
446
- try:
447
- return int(value).to_bytes(1, 'big')
448
- except OverflowError:
449
- raise ValueError(F'Int too big to convert {value}')
450
- else:
451
- raise TypeError(F'Unsupported type validation from string, got {value.__class__}')
452
-
453
-
454
- class CosemObjectAttributeId(Integer8):
455
- """ TODO """
456
-
457
- @lru_cache(14) # for test
458
- def __new__(cls, *args, **kwargs):
459
- return super().__new__(cls)
460
-
461
-
462
- class CosemObjectMethodId(Integer8):
463
- """ TODO """
464
-
465
-
466
- class AccessSelectionParameters(Unsigned8):
467
- """ Unsigned8(0..1) """
468
-
469
- def __init__(self, value: int | str | Unsigned8 = 1):
470
- super(AccessSelectionParameters, self).__init__(value)
471
- if int(self) > 1 or int(self) < 0:
472
- raise ValueError(F'The {self.__class__.__name__} got {self}, expected 0..1')
473
-
474
-
475
- class CosemAttributeDescriptor(SEQUENCE):
476
- class_id: CosemClassId
477
- instance_id: CosemObjectInstanceId
478
- attribute_id: CosemObjectAttributeId
479
- access_selection_parameters: AccessSelectionParameters
480
- ELEMENTS = (SequenceElement('class_id', CosemClassId),
481
- SequenceElement('instance_id', CosemObjectInstanceId),
482
- SequenceElement('attribute_id', CosemObjectAttributeId))
483
-
484
- def __init__(self, value: bytes | tuple | list = None):
485
- super(CosemAttributeDescriptor, self).__init__(value)
486
- self.__dict__['access_selection_parameters'] = AccessSelectionParameters(0)
487
-
488
- @property
489
- def contents(self) -> bytes:
490
- """ Always contain Access_Selection_Parameters. DLMS UA 1000-2 Ed.9 Excerpt 9.3.9.1.3 """
491
- return super(CosemAttributeDescriptor, self).contents + self.access_selection_parameters.contents
492
-
493
-
494
- class CosemMethodDescriptor(SEQUENCE):
495
- class_id: CosemClassId
496
- instance_id: CosemObjectInstanceId
497
- method_id: CosemObjectMethodId
498
- ELEMENTS = (SequenceElement('class_id', CosemClassId),
499
- SequenceElement('instance_id', CosemObjectInstanceId),
500
- SequenceElement('method_id', CosemObjectMethodId))
501
-
502
- def __init__(self, value: tuple[CosemClassId, CosemObjectInstanceId, CosemObjectMethodId]):
503
- super(CosemMethodDescriptor, self).__init__(value)
504
-
505
-
506
- class Data(CHOICE, ABC):
507
- TYPE = cdt.CommonDataType
508
-
509
-
510
- class SelectiveAccessDescriptor(SEQUENCE, ABC):
511
- """ Selective access specification always starts with an access selector, followed by an access-specific access parameter list.
512
- Specified IS/IEC 62056-53 : 2006, 7.4.1.6 Selective access """
513
- access_selector: Unsigned8
514
- access_parameters: cdt.CommonDataType
515
- ELEMENTS: tuple[SequenceElement, SequenceElement]
516
-
517
- def __init__(self, value: tuple | bytes | None = None):
518
- super(SelectiveAccessDescriptor, self).__init__(value)
519
- self.access_selector.cb_post_set = self.__validate_selector
520
-
521
- @property
522
- @abstractmethod
523
- def ELEMENTS(self) -> tuple[SequenceElement, SequenceElement]:
524
- """ return elements """
525
-
526
- def from_default(self):
527
- self.values[0] = self.ELEMENTS[0].TYPE()
528
- self.values[1] = self.ELEMENTS[1].TYPE.ELEMENTS[int(self.access_selector)].TYPE()
529
-
530
- def from_bytes(self, value: bytes | bytearray):
531
- self.values[0], value = get_instance_and_context(self.ELEMENTS[0].TYPE, value)
532
- self.values[1] = self.ELEMENTS[1].TYPE.ELEMENTS[int(self.access_selector)].TYPE(value)
533
-
534
- def from_tuple(self, value: tuple | list):
535
- self.values[0] = self.ELEMENTS[0].TYPE(value[0])
536
- self.values[1] = self.ELEMENTS[1].TYPE.ELEMENTS[int(self.access_selector)].TYPE(value[1])
537
-
538
- @property
539
- def contents(self) -> bytes:
540
- return self.access_selector.contents+self.access_parameters.encoding
541
-
542
- def __validate_selector(self):
543
- self.values[1] = self.ELEMENTS[1].TYPE.ELEMENTS[int(self.access_selector)].TYPE()
544
- print('change sel')
545
-
546
- def set_selector(self, index: int, value=None):
547
- """ set selector value from int type """
548
- self.access_selector.set_contents_from(index)
549
- if value:
550
- self.access_parameters.set(value)
551
-
552
-
553
- class CosemAttributeDescriptorWithSelection(SEQUENCE):
554
- cosem_attribute_descriptor: CosemAttributeDescriptor
555
- access_selection: SelectiveAccessDescriptor
556
- ELEMENTS: tuple[CosemAttributeDescriptor, SequenceElement]
557
-
558
- def __init__(self, value: bytes | tuple | list = None):
559
- super(CosemAttributeDescriptorWithSelection, self).__init__(value)
560
- self.cosem_attribute_descriptor.access_selection_parameters.set_contents_from(1)
561
-
562
- @property
563
- @abstractmethod
564
- def ELEMENTS(self) -> tuple[SequenceElement, SequenceElement]:
565
- """ return elements. Need initiate in subclass """
566
-
567
-
568
- class InvokeIdAndPriority(Unsigned8):
569
-
570
- def __init__(self, value: bytes | bytearray | str | int | DigitalMixin = 0b1100_0000):
571
- super(InvokeIdAndPriority, self).__init__(value)
572
- if self.contents[0] & 0b00110000:
573
- raise ValueError(F'For {self.__class__.__name__} set reserved bits')
574
-
575
- @classmethod
576
- def from_parameters(cls, invoke_id: int = 0,
577
- service_class: int = 0,
578
- priority: int = 0):
579
- instance = cls()
580
- instance.invoke_id = invoke_id
581
- instance.service_class = service_class
582
- instance.priority = priority
583
- return instance
584
-
585
- @property
586
- def invoke_id(self) -> int:
587
- return self.contents[0] & 0b0000_1111
588
-
589
- @invoke_id.setter
590
- def invoke_id(self, value: int):
591
- if value & 0b00001111:
592
- self.__dict__['contents'] = int((self.contents[0] & 0b1111_0000) | value).to_bytes(1, 'big')
593
- else:
594
- raise ValueError(F'Got {value} for invoke-id, expected 0..15')
595
-
596
- @property
597
- def service_class(self) -> int:
598
- """ 0: Unconfirmed or 1: Confirmed bit return """
599
- return (self.contents[0] >> 6) & 0b1
600
-
601
- @service_class.setter
602
- def service_class(self, value: int):
603
- match value:
604
- case 0 | 1: self.__dict__['contents'] = int((self.contents[0] & 0b1011_1111) | (value << 6)).to_bytes(1, 'big')
605
- case _: raise ValueError(F'Got {value} for service_class, expected 0..1')
606
-
607
- @property
608
- def priority(self) -> int:
609
- """ 0: Normal or 1: High bit return """
610
- return (self.contents[0] >> 7) & 0b1
611
-
612
- @priority.setter
613
- def priority(self, value: int):
614
- match value:
615
- case 0 | 1: self.__dict__['contents'] = int((self.contents[0] & 0b0111_1111) | (value << 7)).to_bytes(1, 'big')
616
- case _: raise ValueError(F'Got {value} for service_class, expected 0..1')
617
-
618
- def __str__(self):
619
- return F'priority: {"High" if self.contents[0] & 0b1000_0000 else "Normal"}, ' \
620
- F'service-class: {"Confirmed" if self.contents[0] & 0b0100_0000 else "Unconfirmed"}, ' \
621
- F'invoke-id: {self.invoke_id},'
622
-
623
-
624
- if __name__ == '__main__':
625
- a = CosemObjectAttributeId(1)
626
- b = CosemObjectAttributeId(2)
627
- print(a > b)
628
-
629
- a = CosemClassId(cdt.LongUnsigned(1).contents)
630
- a = InvokeIdAndPriority()
631
- c = a.invoke_id
632
- d = a.service_class
633
- f = a.priority
634
- a.service_class = 1
635
- a.invoke_id = 14
636
- e = InvokeIdAndPriority.from_parameters(1, 1, 1)
637
- class AccessSelector(Unsigned8):
638
- """ Unsigned8 1..4 """
639
- def __init__(self, value: int | str | Unsigned8 = 1):
640
- super(AccessSelector, self).__init__(value)
641
- if int(self) > 4 or int(self) < 1:
642
- raise ValueError(F'The {self.__class__.__name__} got {self}, expected 1..4')
643
-
644
-
645
- class MyData(Data):
646
- ELEMENTS = {1: SequenceElement('0 a', cdt.NullData),
647
- 2: SequenceElement('1 d', cdt.Integer),
648
- 3: SequenceElement('second', cdt.ScalUnitType),
649
- 4: SequenceElement('3', cdt.Integer)}
650
-
651
- class My(SelectiveAccessDescriptor):
652
- access_selector: AccessSelector
653
- access_parameters: MyData
654
- ELEMENTS = (SequenceElement('access_selector', AccessSelector),
655
- SequenceElement('access_parameters', MyData))
656
-
657
- ba = My()
658
- b = My((3, (10, 10)))
659
- b2 = b.access_selector
660
- b3 = b.access_parameters
661
- b4 = b.access_parameters.unit
662
- b_repr = My(b'\x03\x0f"')
663
- b.set_selector(3, 34)
664
- a = CosemAttributeDescriptor((1, '1.1.1.1.1.1', 1))
665
- a_repr = CosemAttributeDescriptor(b'\x00\x01\x01\x01\x01\x01\x01\x01\x01\x00')
666
-
667
- class MyWith(CosemAttributeDescriptorWithSelection):
668
- access_selection: My
669
- ELEMENTS = (SequenceElement('cosem_attribute_descriptor', CosemAttributeDescriptor),
670
- SequenceElement('access_selection', My))
671
-
672
- c = MyWith()
673
- c_from = MyWith((a, b))
674
- c_repr = MyWith(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00')
675
- print(a)
676
- c = CosemAttributeDescriptor(b'\x00\x01\x01\x01\x01\x01\x01\x01\x01\x00')
677
- print(c)
1
+ from typing_extensions import deprecated
2
+ from functools import lru_cache
3
+ from abc import ABC, abstractmethod
4
+ from typing import Type, Any, Callable, Protocol, runtime_checkable
5
+ from dataclasses import dataclass
6
+ from StructResult import result
7
+ from ..types import common_data_types as cdt
8
+ from ..types.type_alias import Encoding
9
+ from ..exceptions import DLMSException
10
+ from ..settings import settings
11
+
12
+
13
+ class UserfulTypesException(DLMSException):
14
+ """override DLMSException"""
15
+
16
+
17
+ @runtime_checkable
18
+ class UsefulType(Protocol):
19
+ """"""
20
+ contents: bytes
21
+ __match_args__ = ('contents',)
22
+ cb_post_set: Callable
23
+ cb_preset: Callable
24
+
25
+ def __init__(self, value):
26
+ """ constructor """
27
+
28
+ def __eq__(self, other: "UsefulType"):
29
+ match other:
30
+ case self.__class__(self.contents): return True
31
+ case _: return False
32
+
33
+ def set_contents_from(self, value: bytes | bytearray | str | int | bool | None):
34
+ new_value = self.__class__(value)
35
+ if hasattr(self, 'cb_preset'):
36
+ self.cb_preset(new_value)
37
+ self.__dict__['contents'] = new_value.contents
38
+ if hasattr(self, 'cb_post_set'):
39
+ self.cb_post_set()
40
+
41
+ def __setattr__(self, key, value):
42
+ match key:
43
+ case 'contents' as prop if hasattr(self, 'contents'): raise ValueError(F"Don't support set {prop}")
44
+ case _: super().__setattr__(key, value)
45
+
46
+
47
+ class _String(Protocol):
48
+ LENGTH: int | None
49
+
50
+ def __init__(self, value: bytes | bytearray | str | int | tuple | UsefulType = None):
51
+ match value:
52
+ case None: self.__dict__["contents"] = bytes(self.LENGTH)
53
+ case bytes() if self.LENGTH is None or self.LENGTH <= len(value): self.__dict__["contents"] = value[:self.LENGTH]
54
+ # case bytes() if self.LENGTH <= len(value): self.contents = value[:self.LENGTH]
55
+ case bytes(): raise ValueError(F'Length of contents for {self.__class__.__name__} must be at least {self.LENGTH}, but got {len(value)}')
56
+ case tuple():
57
+ if len(value) == self.LENGTH:
58
+ self.__dict__["contents"] = bytes(value)
59
+ else:
60
+ raise ValueError(F"in {self.__class__.__name__} with {value=} got length: {len(value)}, expect {self.LENGTH}")
61
+ case bytearray(): self.__dict__["contents"] = bytes(value) # Attention!!! changed method content getting from bytearray
62
+ case str(): self.__dict__["contents"] = self.from_str(value)
63
+ case int(): self.__dict__["contents"] = self.from_int(value)
64
+ case UsefulType(): self.__dict__["contents"] = value.contents # TODO: make right type
65
+ case _: raise ValueError(F'Error create {self.__class__.__name__} with value {value}')
66
+
67
+ def __len__(self):
68
+ """ define in subclasses """
69
+
70
+
71
+ class OCTET_STRING(_String):
72
+ """ An ordered sequence of octets (8 bit bytes) """
73
+
74
+ def from_str(self, value: str) -> bytes:
75
+ """ input as hex code """
76
+ return bytes.fromhex(value)
77
+
78
+ def from_int(self, value: int) -> bytes:
79
+ """ Convert with recursion. Maximum convert length is 32 """
80
+ def to_bytes_with(length_):
81
+ try:
82
+ return int.to_bytes(value, length_, 'big')
83
+ except OverflowError:
84
+ if length_ > 31:
85
+ raise ValueError(F'Value {value} is big to convert to bytes')
86
+ return to_bytes_with(length_+1)
87
+ length = 1
88
+ return to_bytes_with(length)
89
+
90
+ def __str__(self):
91
+ return self.contents.hex(' ')
92
+
93
+ def __len__(self):
94
+ return len(self.contents)
95
+
96
+ def __getitem__(self, item):
97
+ return self.contents[item]
98
+
99
+ # TODO: maybe remove as redundante?
100
+ def decode(self) -> bytes:
101
+ """ decode to build in bytes type """
102
+ return self.contents
103
+
104
+ # TODO: maybe remove as redundante?
105
+ def to_str(self, encoding: str = 'cp1251') -> str:
106
+ """ decode to cp1251 by default, replace to '?' if unsupported """
107
+ temp = list()
108
+ for i in self.contents:
109
+ temp.append(i if i > 32 else 63)
110
+ return bytes(temp).decode(encoding)
111
+
112
+
113
+ @runtime_checkable
114
+ class CHOICE(Protocol):
115
+ """ TODO: with cdt.CHOICE """
116
+ ELEMENTS: "dict[int, SequenceElement | dict[int, SequenceElement]]"
117
+
118
+ @property
119
+ def TYPE(self) -> Any:
120
+ """ return valid types """
121
+
122
+ # def __init_subclass__(cls, **kwargs):
123
+ # if hasattr(cls, 'ELEMENTS'):
124
+ # for el in cls.ELEMENTS.values():
125
+ # if isinstance(el, dict):
126
+ # """pass, maybe it is for cst.AnyTime"""
127
+ # elif issubclass(el.TYPE, cls.TYPE):
128
+ # """ type in order """
129
+ # else:
130
+ # raise ValueError(F'For {cls.__name__} got type {el.TYPE.__name__} with {el.NAME=}, expected {cls.TYPE.__name__}')
131
+ # else:
132
+ # """ subclass with type carry initiate """
133
+
134
+ def __getitem__(self, item: int) -> "SequenceElement":
135
+ return self.ELEMENTS[item]
136
+
137
+ @property
138
+ def NAME(self) -> str:
139
+ return F'CHOICE[{len(self)}]'
140
+
141
+ def __len__(self):
142
+ return len(self.ELEMENTS)
143
+
144
+ def is_key(self, value: int) -> bool:
145
+ return value in self.ELEMENTS.keys()
146
+
147
+ @classmethod
148
+ def parse(cls, value: cdt.Transcript) -> cdt.CommonDataType:
149
+ """get instance by pattern: <tag>:<value>"""
150
+ tag, value = value.split(":", maxsplit=1)
151
+ if (d_t := cls.ELEMENTS.get(int(tag))) is None:
152
+ raise UserfulTypesException(F"for {cls.__name__} got {cdt.CommonDataType.__name__}: {cdt.TAG(int(tag).to_bytes(1))}; "
153
+ F"expected: {', '.join(map(lambda el: el.NAME, cls.ELEMENTS.values()))}")
154
+ else:
155
+ return d_t.TYPE.parse(value)
156
+
157
+ @classmethod
158
+ def from_encoding(cls, encoding: Encoding) -> result.SimpleOrError[cdt.CommonDataType]:
159
+ match cls.ELEMENTS[encoding[0]]:
160
+ case SequenceElement() as el:
161
+ return el.TYPE.from_encoding(encoding)
162
+ case dict() as ch:
163
+ if encoding[1] in ch.keys():
164
+ return ch[encoding[1]].TYPE.from_encoding(encoding) # use for choice cst.Time | DateTime | Date as OctetString
165
+ else:
166
+ return result.Error.from_e(ValueError(F"got type with tag: {encoding[0]} and length: {encoding[1]}, expected length {tuple(ch.keys())}"))
167
+ case err:
168
+ raise RuntimeError(F"got {err.__name__}, expected {SequenceElement.__name__} or {dict.__name__}")
169
+
170
+ @deprecated("use <from_encoding> and more")
171
+ def __call__(self,
172
+ value: bytes | int = None,
173
+ force: bool = False) -> cdt.CommonDataType:
174
+ """ get instance from encoding or tag(with default value). For CommonDataType only """
175
+ try:
176
+ match value:
177
+ case bytes() as encoding: return self.__class__.from_encoding(encoding).unwrap()
178
+ case int() if force: return cdt.get_common_data_type_from(value.to_bytes(1, "big"))()
179
+ case int() as tag: return self.ELEMENTS[tag].TYPE()
180
+ case None: return tuple(self.ELEMENTS.values())[0].TYPE()
181
+ case error: raise ValueError(F'Unknown value type {error}')
182
+ except KeyError as e:
183
+ raise UserfulTypesException(F"for {self.__class__.__name__} got {cdt.CommonDataType.__name__}: {cdt.TAG(e.args[0].to_bytes(1))}; expected: {', '.join(map(lambda el: el.NAME, self.ELEMENTS.values()))}")
184
+
185
+ def __get_elements(self) -> "list[SequenceElement]":
186
+ """all elements with nested values"""
187
+ elements = []
188
+ for el in self.ELEMENTS.values():
189
+ match el:
190
+ case SequenceElement():
191
+ elements.append(el)
192
+ case dict() as dict_el:
193
+ elements.extend(dict_el.values())
194
+ case err:
195
+ raise ValueError(F"unknown CHOICE element type {err}")
196
+ return elements
197
+
198
+ def get_types(self) -> tuple[Type[cdt.CommonDataType]]:
199
+ """ Use in setter attribute.value for validate """
200
+ return tuple((seq_el.TYPE for seq_el in self.__get_elements()))
201
+
202
+ def __str__(self):
203
+ return F'{CHOICE}: {", ".join((el.NAME for el in self.__get_elements()))}'
204
+
205
+
206
+ def get_instance_and_context(meta: type[UsefulType], value: bytes) -> tuple[UsefulType, bytes]:
207
+ instance = meta(value)
208
+ return instance, value[len(instance.contents):]
209
+
210
+
211
+ class SEQUENCE(Protocol):
212
+ """ TODO: """
213
+ ELEMENTS: "tuple[SequenceElement | SEQUENCE, ...]"
214
+ values: list[UsefulType]
215
+
216
+ def __init__(self, value: "bytes | tuple | list | None | SEQUENCE" = None):
217
+ self.__dict__['values'] = [None] * len(self.ELEMENTS)
218
+ match value:
219
+ case tuple() | list():
220
+ if len(value) != len(self):
221
+ raise ValueError(F'Struct {self.__class__.__name__} got length:{len(value)}, expected length:{len(self)}')
222
+ self.from_tuple(value)
223
+ case bytes(): self.from_bytes(value)
224
+ case None: self.from_default()
225
+ case self.__class__(): self.from_bytes(value.contents)
226
+ case _: raise TypeError(F'Value: "{value}" not supported')
227
+
228
+ def from_default(self):
229
+ for i in range(len(self)):
230
+ self.values[i] = self.ELEMENTS[i].TYPE()
231
+
232
+ def from_bytes(self, value: bytes | bytearray):
233
+ for i, element in enumerate(self.ELEMENTS):
234
+ self.values[i], value = get_instance_and_context(element.TYPE, value)
235
+
236
+ def from_tuple(self, value: tuple | list):
237
+ for i, val in enumerate(value):
238
+ self.values[i] = self.ELEMENTS[i].TYPE(val)
239
+
240
+ def __str__(self):
241
+ return F'{{{", ".join(map(lambda val: F"{val[0].NAME}: {val[1]}", zip(self.ELEMENTS, self.values)))}}}'
242
+
243
+ def __repr__(self):
244
+ return F'{self.__class__.__name__}(({(", ".join(map(str, self.values)))}))'
245
+
246
+ def __get_index(self, name: str) -> int | None:
247
+ """ get index by name. Return None if not found """
248
+ for i, element in enumerate(self.ELEMENTS):
249
+ if element.NAME == name:
250
+ return i
251
+ else:
252
+ return None
253
+
254
+ def __setattr__(self, key, value: UsefulType):
255
+ match self.__get_index(key):
256
+ case int() as i if isinstance(value, self.ELEMENTS[i]): self.values[i] = value
257
+ case int() as i: raise ValueError(F'Try assign {key} Type got {value.__class__.__name__}, expected {self.ELEMENTS[i].NAME}')
258
+ case _: raise ValueError(F'Unsupported change: {key}')
259
+
260
+ def __getattr__(self, item: str) -> UsefulType:
261
+ match self.__get_index(item):
262
+ case int() as i: return self.values[i]
263
+ case _: return self.__dict__[item]
264
+
265
+ def __getitem__(self, item: str | int) -> UsefulType:
266
+ """ get element by index or name """
267
+ match item:
268
+ case str() as name: return self.values[self.__get_index(name)]
269
+ case int() as index: return self.values[index]
270
+ case _: raise ValueError(F'Unsupported type {item.__class__}')
271
+
272
+ def __setitem__(self, key: int, value: UsefulType):
273
+ """ set data to element by index """
274
+ if isinstance(value, self.ELEMENTS[key].TYPE):
275
+ self.values[key] = value
276
+ else:
277
+ raise ValueError(F'Type got {value.__class__.__name__}, expected {self.ELEMENTS[key].TYPE}')
278
+
279
+ def __len__(self):
280
+ return len(self.ELEMENTS)
281
+
282
+ @property
283
+ def contents(self) -> bytes:
284
+ return b''.join(el.contents for el in self.values)
285
+
286
+ @property
287
+ def NAME(self) -> str:
288
+ return F'{self.__class__.__name__}[{len(self)}]'
289
+
290
+
291
+ @dataclass(frozen=True)
292
+ class SequenceElement:
293
+ NAME: str
294
+ TYPE: Type[UsefulType | SEQUENCE | CHOICE | cdt.CommonDataType]
295
+
296
+ def __str__(self):
297
+ return F'{self.NAME}: {self.TYPE.__name__}'
298
+
299
+
300
+
301
+ class DigitalMixin:
302
+ """ Default value is 0 """
303
+ SIGNED: bool
304
+ LENGTH: int
305
+ contents: bytes
306
+
307
+ def __init__(self, value: "bytes | bytearray | str | int | DigitalMixin" = None):
308
+ match value:
309
+ case bytes() if self.LENGTH <= len(value): self.__dict__["contents"] = value[:self.LENGTH]
310
+ case bytes(): raise ValueError(F'Length of contents for {self.__class__.__name__} must be at least {self.LENGTH}, but got {len(value)}')
311
+ case bytearray(): self.__dict__["contents"] = bytes(value) # Attention!!! changed method content getting from bytearray
312
+ case str('-') if self.SIGNED: self.__dict__["contents"] = bytes(self.LENGTH)
313
+ case int(): self.__dict__["contents"] = self.from_int(value)
314
+ case str(): self.__dict__["contents"] = self.from_str(value)
315
+ case None: self.__dict__["contents"] = bytes(self.LENGTH)
316
+ case self.__class__(): self.__dict__["contents"] = value.contents
317
+ case _: raise ValueError(F'Error create {self.__class__.__name__} with value: {value}')
318
+
319
+ def from_int(self, value: int | float) -> bytes:
320
+ try:
321
+ return int(value).to_bytes(self.LENGTH, 'big', signed=self.SIGNED)
322
+ except OverflowError:
323
+ raise ValueError(F'value {value} out of range')
324
+
325
+ def from_str(self, value: str) -> bytes:
326
+ return self.from_int(float(value))
327
+
328
+ def __int__(self):
329
+ """ return the build in integer type """
330
+ return int.from_bytes(self.contents, 'big', signed=self.SIGNED)
331
+
332
+ def __str__(self):
333
+ return str(int(self))
334
+
335
+ def __repr__(self):
336
+ return F'{self.__class__.__name__}({self})'
337
+
338
+ def __gt__(self, other: "DigitalMixin"):
339
+ match other:
340
+ case DigitalMixin(): return int(self) > int(other)
341
+ case _: raise TypeError(F'Compare type is {other.__class__}, expected Digital')
342
+
343
+ def __len__(self) -> int:
344
+ return self.LENGTH
345
+
346
+ def __hash__(self):
347
+ return int(self)
348
+
349
+ def validate_from(self, value: str, cursor_position: int) -> tuple[str, int]:
350
+ """ return validated value and cursor position. Use in Entry. TODO: remove it, make better """
351
+ type(self)(value=value)
352
+ return value, cursor_position
353
+
354
+
355
+ class Integer8(DigitalMixin, UsefulType):
356
+ """ INTEGER(-127…128) """
357
+ LENGTH = 1
358
+ SIGNED = True
359
+
360
+
361
+ class Integer16(DigitalMixin, UsefulType):
362
+ """ INTEGER(-32 768...32 767) """
363
+ LENGTH = 2
364
+ SIGNED = True
365
+
366
+
367
+ class Integer32(DigitalMixin, UsefulType):
368
+ """ INTEGER(-2 147 483 648...2 147 483 647) """
369
+ LENGTH = 4
370
+ SIGNED = True
371
+
372
+
373
+ class Integer64(DigitalMixin, UsefulType):
374
+ """ INTEGER(-2^63...2^63-1) """
375
+ LENGTH = 8
376
+ SIGNED = True
377
+
378
+
379
+ class Unsigned8(DigitalMixin, UsefulType):
380
+ """ INTEGER(0...255) """
381
+ LENGTH = 1
382
+ SIGNED = False
383
+ # __match_args__ = ('contents',)
384
+
385
+
386
+ class Unsigned16(DigitalMixin, UsefulType):
387
+ """ INTEGER(0...65 535) """
388
+ LENGTH = 2
389
+ SIGNED = False
390
+
391
+
392
+ class Unsigned32(DigitalMixin, UsefulType):
393
+ """ INTEGER(0...4 294 967 295) """
394
+ LENGTH = 4
395
+ SIGNED = False
396
+
397
+
398
+ class Unsigned64(DigitalMixin, UsefulType):
399
+ """ INTEGER(0...264-1) """
400
+ LENGTH = 8
401
+ SIGNED = False
402
+
403
+
404
+ class CosemClassId(Unsigned16):
405
+ """ Identification code of the IC (range 0 to 65 535). The class_id of each object is retrieved together with the logical name by reading the object_list attribute of an
406
+ “Association LN” / ”Association SN” object.
407
+ - class_id-s from 0 to 8 191 are reserved to be specified by the DLMS UA.
408
+ - class_id-s from 8 192 to 32 767 are reserved for manufacturer specific ICs.
409
+ - class_id-s from 32 768 to 65 535 are reserved for user group specific ICs.
410
+ The DLMS UA reserves the right to assign ranges to individual manufacturers or user groups. """
411
+
412
+ def __str__(self) -> str:
413
+ if res := settings.class_name.get(int(self)):
414
+ return res
415
+ else:
416
+ return repr(self)
417
+
418
+ def __repr__(self):
419
+ return F"{self.__class__.__name__}({int(self)})"
420
+
421
+
422
+ class CosemObjectInstanceId(OCTET_STRING, UsefulType):
423
+ LENGTH = 6
424
+
425
+ def __str__(self):
426
+ return F"\"{'.'.join(map(str, self.contents))}\""
427
+
428
+ def from_str(self, value: str) -> bytes:
429
+ """ create logical_name: octet_string from string type ddd.ddd.ddd.ddd.ddd.ddd, ex.: 0.0.1.0.0.255 """
430
+ raw_value = bytes()
431
+ for typecast, separator in zip((self.__from_group_A, )*5+(self.__from_group_F, ), ('.', '.', '.', '.', '.', ' ')):
432
+ try:
433
+ element, value = value.split(separator, 1)
434
+ except ValueError:
435
+ element, value = value, ''
436
+ raw_value += typecast(element)
437
+ return raw_value
438
+
439
+ def __from_group_A(self, value: str) -> bytes:
440
+ if isinstance(value, str):
441
+ if value == '':
442
+ return b'\x00'
443
+ try:
444
+ return int(value).to_bytes(1, 'big')
445
+ except OverflowError:
446
+ raise ValueError(F'Int too big to convert {value}')
447
+ else:
448
+ raise TypeError(F'Unsupported type validation from string, got {value.__class__}')
449
+
450
+ def __from_group_F(self, value: str) -> bytes:
451
+ if isinstance(value, str):
452
+ if value == '':
453
+ return b'\xff'
454
+ try:
455
+ return int(value).to_bytes(1, 'big')
456
+ except OverflowError:
457
+ raise ValueError(F'Int too big to convert {value}')
458
+ else:
459
+ raise TypeError(F'Unsupported type validation from string, got {value.__class__}')
460
+
461
+
462
+ class CosemObjectAttributeId(Integer8):
463
+ """ TODO """
464
+
465
+ @lru_cache(14) # for test
466
+ def __new__(cls, *args, **kwargs):
467
+ return super().__new__(cls)
468
+
469
+
470
+ class CosemObjectMethodId(Integer8):
471
+ """ TODO """
472
+
473
+
474
+ class AccessSelectionParameters(Unsigned8):
475
+ """ Unsigned8(0..1) """
476
+
477
+ def __init__(self, value: int | str | Unsigned8 = 1):
478
+ super(AccessSelectionParameters, self).__init__(value)
479
+ if int(self) > 1 or int(self) < 0:
480
+ raise ValueError(F'The {self.__class__.__name__} got {self}, expected 0..1')
481
+
482
+
483
+ class CosemAttributeDescriptor(SEQUENCE):
484
+ class_id: CosemClassId
485
+ instance_id: CosemObjectInstanceId
486
+ attribute_id: CosemObjectAttributeId
487
+ access_selection_parameters: AccessSelectionParameters
488
+ ELEMENTS = (SequenceElement('class_id', CosemClassId),
489
+ SequenceElement('instance_id', CosemObjectInstanceId),
490
+ SequenceElement('attribute_id', CosemObjectAttributeId))
491
+
492
+ def __init__(self, value: bytes | tuple | list = None):
493
+ super(CosemAttributeDescriptor, self).__init__(value)
494
+ self.__dict__['access_selection_parameters'] = AccessSelectionParameters(0)
495
+
496
+ @property
497
+ def contents(self) -> bytes:
498
+ """ Always contain Access_Selection_Parameters. DLMS UA 1000-2 Ed.9 Excerpt 9.3.9.1.3 """
499
+ return super(CosemAttributeDescriptor, self).contents + self.access_selection_parameters.contents
500
+
501
+
502
+ class CosemMethodDescriptor(SEQUENCE):
503
+ class_id: CosemClassId
504
+ instance_id: CosemObjectInstanceId
505
+ method_id: CosemObjectMethodId
506
+ ELEMENTS = (SequenceElement('class_id', CosemClassId),
507
+ SequenceElement('instance_id', CosemObjectInstanceId),
508
+ SequenceElement('method_id', CosemObjectMethodId))
509
+
510
+ def __init__(self, value: tuple[CosemClassId, CosemObjectInstanceId, CosemObjectMethodId]):
511
+ super(CosemMethodDescriptor, self).__init__(value)
512
+
513
+
514
+ class Data(CHOICE, Protocol):
515
+ TYPE = cdt.CommonDataType
516
+
517
+
518
+ class SelectiveAccessDescriptor(SEQUENCE):
519
+ """ Selective access specification always starts with an access selector, followed by an access-specific access parameter list.
520
+ Specified IS/IEC 62056-53 : 2006, 7.4.1.6 Selective access """
521
+ access_selector: Unsigned8
522
+ access_parameters: cdt.CommonDataType
523
+ ELEMENTS: tuple[SequenceElement, SequenceElement]
524
+
525
+ def __init__(self, value: tuple | bytes | None = None):
526
+ super(SelectiveAccessDescriptor, self).__init__(value)
527
+ self.access_selector.cb_post_set = self.__validate_selector
528
+
529
+ @property
530
+ def ELEMENTS(self) -> tuple[SequenceElement, SequenceElement]:
531
+ """ return elements """
532
+
533
+ def from_default(self):
534
+ self.values[0] = self.ELEMENTS[0].TYPE()
535
+ self.values[1] = self.ELEMENTS[1].TYPE.ELEMENTS[int(self.access_selector)].TYPE()
536
+
537
+ def from_bytes(self, value: bytes | bytearray):
538
+ self.values[0], value = get_instance_and_context(self.ELEMENTS[0].TYPE, value)
539
+ self.values[1] = self.ELEMENTS[1].TYPE.ELEMENTS[int(self.access_selector)].TYPE(value)
540
+
541
+ def from_tuple(self, value: tuple | list):
542
+ self.values[0] = self.ELEMENTS[0].TYPE(value[0])
543
+ self.values[1] = self.ELEMENTS[1].TYPE.ELEMENTS[int(self.access_selector)].TYPE(value[1])
544
+
545
+ @property
546
+ def contents(self) -> bytes:
547
+ return self.access_selector.contents+self.access_parameters.encoding
548
+
549
+ def __validate_selector(self):
550
+ self.values[1] = self.ELEMENTS[1].TYPE.ELEMENTS[int(self.access_selector)].TYPE()
551
+ print('change sel')
552
+
553
+ def set_selector(self, index: int, value=None):
554
+ """ set selector value from int type """
555
+ self.access_selector.set_contents_from(index)
556
+ if value:
557
+ self.access_parameters.set(value)
558
+
559
+
560
+ class CosemAttributeDescriptorWithSelection(SEQUENCE):
561
+ cosem_attribute_descriptor: CosemAttributeDescriptor
562
+ access_selection: SelectiveAccessDescriptor
563
+ ELEMENTS: tuple[CosemAttributeDescriptor, SequenceElement]
564
+
565
+ def __init__(self, value: bytes | tuple | list = None):
566
+ super(CosemAttributeDescriptorWithSelection, self).__init__(value)
567
+ self.cosem_attribute_descriptor.access_selection_parameters.set_contents_from(1)
568
+
569
+ @property
570
+ def ELEMENTS(self) -> tuple[SequenceElement, SequenceElement]:
571
+ """ return elements. Need initiate in subclass """
572
+
573
+
574
+ class InvokeIdAndPriority(Unsigned8):
575
+
576
+ def __init__(self, value: bytes | bytearray | str | int | DigitalMixin = 0b1100_0000):
577
+ super(InvokeIdAndPriority, self).__init__(value)
578
+ if self.contents[0] & 0b00110000:
579
+ raise ValueError(F'For {self.__class__.__name__} set reserved bits')
580
+
581
+ @classmethod
582
+ def from_parameters(cls, invoke_id: int = 0,
583
+ service_class: int = 0,
584
+ priority: int = 0):
585
+ instance = cls()
586
+ instance.invoke_id = invoke_id
587
+ instance.service_class = service_class
588
+ instance.priority = priority
589
+ return instance
590
+
591
+ @property
592
+ def invoke_id(self) -> int:
593
+ return self.contents[0] & 0b0000_1111
594
+
595
+ @invoke_id.setter
596
+ def invoke_id(self, value: int):
597
+ if value & 0b00001111:
598
+ self.__dict__['contents'] = int((self.contents[0] & 0b1111_0000) | value).to_bytes(1, 'big')
599
+ else:
600
+ raise ValueError(F'Got {value} for invoke-id, expected 0..15')
601
+
602
+ @property
603
+ def service_class(self) -> int:
604
+ """ 0: Unconfirmed or 1: Confirmed bit return """
605
+ return (self.contents[0] >> 6) & 0b1
606
+
607
+ @service_class.setter
608
+ def service_class(self, value: int):
609
+ match value:
610
+ case 0 | 1: self.__dict__['contents'] = int((self.contents[0] & 0b1011_1111) | (value << 6)).to_bytes(1, 'big')
611
+ case _: raise ValueError(F'Got {value} for service_class, expected 0..1')
612
+
613
+ @property
614
+ def priority(self) -> int:
615
+ """ 0: Normal or 1: High bit return """
616
+ return (self.contents[0] >> 7) & 0b1
617
+
618
+ @priority.setter
619
+ def priority(self, value: int):
620
+ match value:
621
+ case 0 | 1: self.__dict__['contents'] = int((self.contents[0] & 0b0111_1111) | (value << 7)).to_bytes(1, 'big')
622
+ case _: raise ValueError(F'Got {value} for service_class, expected 0..1')
623
+
624
+ def __str__(self):
625
+ return F'priority: {"High" if self.contents[0] & 0b1000_0000 else "Normal"}, ' \
626
+ F'service-class: {"Confirmed" if self.contents[0] & 0b0100_0000 else "Unconfirmed"}, ' \
627
+ F'invoke-id: {self.invoke_id},'