DLMS-SPODES 0.87.12__py3-none-any.whl → 0.87.15__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 (100) 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/__class_init__.py +3 -3
  11. DLMS_SPODES/cosem_interface_classes/__init__.py +1 -1
  12. DLMS_SPODES/cosem_interface_classes/a_parameter.py +20 -20
  13. DLMS_SPODES/cosem_interface_classes/activity_calendar.py +254 -254
  14. DLMS_SPODES/cosem_interface_classes/arbitrator.py +105 -105
  15. DLMS_SPODES/cosem_interface_classes/association_ln/abstract.py +34 -34
  16. DLMS_SPODES/cosem_interface_classes/association_ln/authentication_mechanism_name.py +25 -25
  17. DLMS_SPODES/cosem_interface_classes/association_ln/mechanism_id.py +25 -25
  18. DLMS_SPODES/cosem_interface_classes/association_ln/method.py +5 -5
  19. DLMS_SPODES/cosem_interface_classes/association_ln/ver0.py +485 -485
  20. DLMS_SPODES/cosem_interface_classes/association_ln/ver1.py +133 -133
  21. DLMS_SPODES/cosem_interface_classes/association_ln/ver2.py +36 -36
  22. DLMS_SPODES/cosem_interface_classes/association_ln/ver3.py +4 -4
  23. DLMS_SPODES/cosem_interface_classes/association_sn/ver0.py +12 -12
  24. DLMS_SPODES/cosem_interface_classes/attr_indexes.py +12 -12
  25. DLMS_SPODES/cosem_interface_classes/clock.py +131 -131
  26. DLMS_SPODES/cosem_interface_classes/collection.py +2122 -2122
  27. DLMS_SPODES/cosem_interface_classes/cosem_interface_class.py +583 -583
  28. DLMS_SPODES/cosem_interface_classes/data.py +21 -21
  29. DLMS_SPODES/cosem_interface_classes/demand_register/ver0.py +59 -59
  30. DLMS_SPODES/cosem_interface_classes/disconnect_control.py +74 -74
  31. DLMS_SPODES/cosem_interface_classes/extended_register.py +27 -27
  32. DLMS_SPODES/cosem_interface_classes/gprs_modem_setup.py +43 -43
  33. DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver0.py +103 -103
  34. DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver1.py +40 -40
  35. DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver2.py +9 -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 +53 -53
  38. DLMS_SPODES/cosem_interface_classes/iec_local_port_setup.py +11 -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 +126 -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 +487 -487
  44. DLMS_SPODES/cosem_interface_classes/implementations/profile_generic.py +83 -83
  45. DLMS_SPODES/cosem_interface_classes/ipv4_setup.py +72 -72
  46. DLMS_SPODES/cosem_interface_classes/limiter.py +111 -111
  47. DLMS_SPODES/cosem_interface_classes/ln_pattern.py +333 -333
  48. DLMS_SPODES/cosem_interface_classes/modem_configuration/ver0.py +65 -65
  49. DLMS_SPODES/cosem_interface_classes/modem_configuration/ver1.py +39 -39
  50. DLMS_SPODES/cosem_interface_classes/ntp_setup/ver0.py +67 -67
  51. DLMS_SPODES/cosem_interface_classes/obis.py +23 -23
  52. DLMS_SPODES/cosem_interface_classes/overview.py +197 -197
  53. DLMS_SPODES/cosem_interface_classes/parameter.py +547 -547
  54. DLMS_SPODES/cosem_interface_classes/parameters.py +172 -172
  55. DLMS_SPODES/cosem_interface_classes/profile_generic/ver0.py +122 -122
  56. DLMS_SPODES/cosem_interface_classes/profile_generic/ver1.py +277 -277
  57. DLMS_SPODES/cosem_interface_classes/push_setup/ver0.py +12 -12
  58. DLMS_SPODES/cosem_interface_classes/push_setup/ver1.py +10 -10
  59. DLMS_SPODES/cosem_interface_classes/push_setup/ver2.py +166 -166
  60. DLMS_SPODES/cosem_interface_classes/register.py +45 -45
  61. DLMS_SPODES/cosem_interface_classes/register_activation/ver0.py +80 -80
  62. DLMS_SPODES/cosem_interface_classes/register_monitor.py +46 -46
  63. DLMS_SPODES/cosem_interface_classes/reports.py +70 -70
  64. DLMS_SPODES/cosem_interface_classes/schedule.py +176 -176
  65. DLMS_SPODES/cosem_interface_classes/script_table.py +87 -87
  66. DLMS_SPODES/cosem_interface_classes/security_setup/ver0.py +68 -68
  67. DLMS_SPODES/cosem_interface_classes/security_setup/ver1.py +158 -158
  68. DLMS_SPODES/cosem_interface_classes/single_action_schedule.py +50 -50
  69. DLMS_SPODES/cosem_interface_classes/special_days_table.py +84 -84
  70. DLMS_SPODES/cosem_interface_classes/tcp_udp_setup.py +42 -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 +465 -463
  83. DLMS_SPODES/settings.py +551 -551
  84. DLMS_SPODES/types/choices.py +142 -142
  85. DLMS_SPODES/types/common_data_types.py +2401 -2399
  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 +11 -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/useful_types.py +677 -677
  96. {dlms_spodes-0.87.12.dist-info → dlms_spodes-0.87.15.dist-info}/METADATA +30 -30
  97. dlms_spodes-0.87.15.dist-info/RECORD +117 -0
  98. {dlms_spodes-0.87.12.dist-info → dlms_spodes-0.87.15.dist-info}/WHEEL +1 -1
  99. dlms_spodes-0.87.12.dist-info/RECORD +0 -117
  100. {dlms_spodes-0.87.12.dist-info → dlms_spodes-0.87.15.dist-info}/top_level.txt +0 -0
@@ -1,548 +1,548 @@
1
- from typing_extensions import deprecated
2
- from dataclasses import dataclass, field
3
- import numpy as np
4
- from struct import Struct, pack, unpack_from
5
- from typing import Optional, cast, Iterator, Self
6
- import re
7
- from functools import cached_property
8
- from .. import exceptions as exc
9
- from .obis import OBIS
10
-
11
- _pattern = re.compile("((?:\d{1,3}\.){5}\d{1,3})(?::(m?\d{1,3}))?")
12
- Index = Struct("?B")
13
-
14
-
15
- @dataclass(frozen=True)
16
- class Parameter:
17
- """
18
- Parameter ::= SEQUENCE
19
- {
20
- ln Cosem-Object-Instance-Id
21
- descriptor OPTIONAL
22
- }
23
-
24
- index Unsigned8
25
- nest_index Unsigned16
26
- piece Unsigned8
27
-
28
- descriptor :: = CHOICE
29
- {
30
- attribute [0] IMPLICIT Desc
31
- method [1] IMPLICIT Desc
32
- }
33
-
34
- Desc ::= SEQUENCE
35
- {
36
- index
37
- SEQUENCE (SIZE(255)) OF nest_index OPTIONAL
38
- piece OPTIONAL
39
- }
40
- """
41
- _value: bytes
42
-
43
- def __bytes__(self) -> bytes:
44
- return self._value
45
-
46
- @classmethod
47
- def parse(cls, value: str) -> Self:
48
- """create from string. Only LN, attr/meth type ddd.ddd.ddd.ddd.ddd.ddd:aaa, ex.: 0.0.1.0.0.255 """
49
- if (res := _pattern.fullmatch(value)) is None:
50
- raise ValueError(F"in {cls.__name__}.parse got wrong :{value:}")
51
- else:
52
- groups = iter(res.groups())
53
- ret = bytes(map(int, next(groups).split(".")))
54
- if (a := next(groups)) is not None:
55
- if a.startswith('m'):
56
- a = a[1:]
57
- g1 = 256
58
- else:
59
- g1 = 0
60
- ret += (g1 + int(a)).to_bytes(2)
61
- return cls(ret)
62
-
63
- @cached_property
64
- def logical_name(self) -> "Parameter":
65
- return self.get_attr(1)
66
-
67
- def __eq__(self, other: object) -> bool:
68
- if isinstance(other, Parameter):
69
- return cast("bool", self._value==other._value)
70
- return NotImplemented
71
-
72
- def __lt__(self, other: "Parameter") -> bool:
73
- """comparing for sort method"""
74
- if len(self._value) > len(other._value):
75
- return True
76
- else:
77
- return False
78
-
79
- def __str__(self) -> str:
80
- if (l := len(self._value)) < 6:
81
- return "No valid"
82
- elif l == 7:
83
- return "No valid Index"
84
- else:
85
- res = F"{".".join(map(str, self._value[:6]))}"
86
- if l > 6:
87
- res += F":{"m" if self.is_method() else ""}{self.i}"
88
- if l > 8:
89
- res += F" {"/".join(map(str, self.elements()))}"
90
- if self.has_piece():
91
- res += F"p{self.piece}"
92
- return res
93
-
94
- def validate(self) -> None:
95
- if (length := len(self._value)) < 6:
96
- raise exc.DLMSException(F"Parameter got {length=}, expected at least 6")
97
- if length == 7:
98
- raise exc.DLMSException(F"Parameter got wrong index")
99
-
100
- @property
101
- def has_index(self) -> bool:
102
- return len(self._value) > 6
103
-
104
- @property
105
- @deprecated("use obis")
106
- def ln(self) -> bytes:
107
- """Logical Name"""
108
- return self._value[:6]
109
-
110
- def is_method(self) -> bool:
111
- return self._value[6] == 1
112
-
113
- @property
114
- def i(self) -> int:
115
- """attribute or method index"""
116
- return self._value[7]
117
-
118
- def get_attr(self, i: int) -> Self:
119
- """get attribute"""
120
- val = Index.pack(0, i)
121
- return self.__class__(self._value[:6] + val)
122
-
123
- def get_meth(self, i: int) -> Self:
124
- """get method"""
125
- val = Index.pack(1, i)
126
- return self.__class__(self._value[:6] + val)
127
-
128
- def set_i(self, index: int, is_method: bool = False) -> "Parameter":
129
- val = Index.pack(is_method, index)
130
- if len(self._value) == 6:
131
- tmp = self._value + val
132
- else:
133
- tmp_ = bytearray(self._value)
134
- tmp_[6:8] = val
135
- tmp = bytes(tmp_)
136
- return self.__class__(tmp)
137
-
138
- def append_validate(self) -> None:
139
- if (l := len(self._value)) < 7:
140
- raise exc.DLMSException(F"Parameter must has index before")
141
- elif l % 2 != 0:
142
- raise exc.DLMSException(F"Can't append to Parameter with piece")
143
-
144
- def append(self, index: int) -> "Parameter":
145
- """add new sequence(array or struct) index element"""
146
- self.append_validate()
147
- return self.__class__(self._value + pack(">H", index))
148
-
149
- def extend(self, *indexes: int) -> "Parameter":
150
- self.append_validate()
151
- return self.__class__(self._value + pack(F">{len(indexes)}H", *indexes))
152
-
153
- def pop(self) -> tuple[Optional[int], int, "Parameter"]:
154
- """
155
- :return piece, last index and parent Parameter
156
- ex.: Parameter("0.0.0.0.0.0:2 1/1/1 p3") => (1, Parameter("0.0.0.0.0.0:2 1/1"))
157
- """
158
- if self.has_piece():
159
- return self._value[-1], int.from_bytes(self._value[-3:-1]), self.__class__(self._value[:-3])
160
- else:
161
- return None, int.from_bytes(self._value[-2:]), self.__class__(self._value[:-2])
162
-
163
- def set_piece(self, index: int) -> "Parameter":
164
- """add new sequence(array or struct) index element"""
165
- if len(self._value) >= 7:
166
- return self.__class__(self._value + pack("B", index))
167
- else:
168
- raise exc.DLMSException(F"Parameter must has index before")
169
-
170
- def has_piece(self) -> bool:
171
- if (
172
- (l := len(self._value)) >= 9
173
- and l % 2 != 0
174
- ):
175
- return True
176
- else:
177
- return False
178
-
179
- @property
180
- def piece(self) -> Optional[int]:
181
- if self.has_piece():
182
- return self._value[-1]
183
- return None
184
-
185
- def clear_piece(self) -> "Parameter":
186
- if self.has_piece():
187
- return self.__class__(self._value[:-1])
188
- return self
189
-
190
- def elements(self, start: int = 0) -> Iterator[int]:
191
- """return: index elements nested in attribute, started with"""
192
- for i in range(8 + start, 8 + 2 * self.n_elements, 2):
193
- res = int.from_bytes(self._value[i:i + 2], "big")
194
- yield res
195
-
196
- def __iter__(self) -> Iterator[int]:
197
- for it in self._value[:6]:
198
- yield it
199
- if self._value[6] == 1:
200
- yield -self.i
201
- else:
202
- yield self.i
203
-
204
- @property
205
- def last_element(self) -> int:
206
- """:return last element index"""
207
- if self.n_elements == 0:
208
- raise ValueError("Parameter hasn't elements")
209
- if self.has_piece():
210
- val = self._value[-3: -1]
211
- else:
212
- val = self._value[-2:]
213
- return int.from_bytes(val, "big")
214
-
215
- @property
216
- def n_elements(self) -> int:
217
- """return: amount of elements nested in attribute"""
218
- return max(0, (len(self._value) - 8) // 2)
219
-
220
- def set(self,
221
- a: Optional[int] = None,
222
- b: Optional[int] = None,
223
- c: Optional[int] = None,
224
- d: Optional[int] = None,
225
- e: Optional[int] = None,
226
- f: Optional[int] = None
227
- ) -> "Parameter":
228
- val = bytearray(self._value)
229
- if a is not None:
230
- val[0] = a
231
- if b is not None:
232
- val[1] = b
233
- if c is not None:
234
- val[2] = c
235
- if d is not None:
236
- val[3] = d
237
- if e is not None:
238
- val[4] = e
239
- if f is not None:
240
- val[5] = f
241
- return self.__class__(bytes(val))
242
-
243
- def __contains__(self, item: "Parameter") -> bool:
244
- return item._value in self._value
245
-
246
- def __getitem__(self, item: int) -> Optional[int]:
247
- if self.n_elements > 0:
248
- return cast(int, unpack_from(">H", self._value, item * 2 + 8)[0])
249
- else:
250
- return None
251
-
252
- @property
253
- def a(self) -> int:
254
- return self._value[0]
255
-
256
- @property
257
- def b(self) -> int:
258
- return self._value[1]
259
-
260
- @property
261
- def c(self) -> int:
262
- return self._value[2]
263
-
264
- @property
265
- def d(self) -> int:
266
- return self._value[3]
267
-
268
- @property
269
- def e(self) -> int:
270
- return self._value[4]
271
-
272
- @property
273
- def f(self) -> int:
274
- return self._value[5]
275
-
276
- @property
277
- def attr(self) -> "Parameter":
278
- if self.has_index:
279
- return Parameter(self._value[:8])
280
- else:
281
- raise exc.DLMSException(F"Parameter must has index before")
282
-
283
- @property
284
- def obj(self) -> "Parameter":
285
- return Parameter(self._value[:6])
286
-
287
- @property
288
- def obis(self) -> OBIS:
289
- return OBIS(self._value[:6])
290
-
291
-
292
- RANGE64 = bytes(range(65)) # Предвычисленный диапазон 0-64
293
-
294
-
295
- @dataclass(eq=False, frozen=True)
296
- class ParPattern:
297
- value: bytes
298
- positions: tuple[int, ...]
299
- "7 elements , -1 is SKIP, last element is attribute index if positive and method index else negative"
300
-
301
- def __post_init__(self) -> None:
302
- if 6 > len(self.positions) > 7:
303
- raise ValueError(f"positions must have exactly 6 elements, got {len(self.positions)}")
304
-
305
- @classmethod
306
- def parse(cls, pattern: str) -> "ParPattern":
307
- """Парсинг строк вида:
308
- - "a.0.(1,2,3).(0-64).0.f" (6 элементов)
309
- - "a.0.1.0.0.f:2" (6 элементов + индекс через :)
310
- - "a.0.1.0.0.f:m2" (6 элементов + метод через :)
311
- """
312
- # Разделяем основную часть и индекс
313
- if ':' in pattern:
314
- main_part, index_part = pattern.rsplit(':', 1)
315
- is_method = index_part.startswith('m')
316
- if is_method:
317
- index_part = index_part[1:]
318
- else:
319
- main_part = pattern
320
- index_part = None
321
-
322
- # Парсим основную часть (6 элементов)
323
- parts = main_part.split('.', maxsplit=5)
324
- if len(parts) < 6:
325
- parts.extend(['f'] * (6 - len(parts))) # Дополняем до 6 элементов
326
-
327
- value_parts = []
328
- positions = []
329
- current_pos = 0
330
-
331
- for i, part in enumerate(parts[:6]): # Ровно 6 элементов
332
- if len(part)==1 and ord(part)==97 + i: # a-f
333
- if part=='b':
334
- value_parts.append(RANGE64)
335
- positions.append(current_pos)
336
- current_pos += len(RANGE64)
337
- else:
338
- positions.append(-1) # SKIP
339
- continue
340
-
341
- try:
342
- if part.isdigit(): # Простое число
343
- num = int(part)
344
- if not 0 <= num <= 255:
345
- raise ValueError
346
- value_parts.append(bytes([num]))
347
- positions.append(current_pos)
348
- current_pos += 1
349
- elif part.startswith(('(', '!(')) and part.endswith(')'):
350
- elements = cls._parse_group(part[2 if part.startswith('!(') else 1:-1])
351
- if part.startswith('!('):
352
- elements = set(range(256)) - elements
353
- value_parts.append(bytes(elements))
354
- positions.append(current_pos)
355
- current_pos += len(value_parts[-1])
356
- else:
357
- raise ValueError
358
- except ValueError:
359
- raise ValueError(f"Invalid pattern part: {part} at position {i}")
360
-
361
- # Добавляем индекс (7-й элемент)
362
- if index_part is not None:
363
- try:
364
- index_val = int(index_part)
365
- if not 0 <= index_val <= 255:
366
- raise ValueError
367
- # Упаковываем: старший бит = is_method, остальные 7 бит = значение
368
- packed = bytes([(0x80 if is_method else 0) | (index_val & 0x7F)])
369
- value_parts.append(packed)
370
- positions.append(current_pos)
371
- except ValueError:
372
- raise ValueError(f"Invalid index value: {index_part}")
373
- else:
374
- positions.append(-1) # SKIP для 7-го элемента
375
-
376
- return cls(
377
- value=b''.join(value_parts),
378
- positions=tuple(positions)
379
- )
380
-
381
- @staticmethod
382
- def _parse_group(group_str: str) -> set[int]:
383
- """Парсинг групп вида '1,2,3' или '10-20'"""
384
- elements: set[int] = set()
385
- for item in group_str.split(','):
386
- item = item.strip()
387
- if '-' in item:
388
- start, end = map(int, item.split('-'))
389
- elements.update(range(start, end + 1))
390
- else:
391
- elements.add(int(item))
392
- return elements
393
-
394
- # @functools.cache
395
- def __hash__(self) -> int:
396
- print("hash")
397
- hash_parts: list[Optional[bytes]] = []
398
- for pos in self.positions:
399
- if pos == -1:
400
- hash_parts.append(None)
401
- else:
402
- hash_parts.append(self.value[pos:pos + 1])
403
- return hash(tuple(hash_parts))
404
-
405
- def __eq__(self, other: object) -> bool:
406
- print("eq")
407
- if not isinstance(other, ParPattern):
408
- return False
409
-
410
- for i in range(6):
411
- pos_self = self.positions[i]
412
- pos_other = other.positions[i]
413
-
414
- # Проверка соответствия схемы позиций
415
- if (pos_self == -1) != (pos_other == -1):
416
- return False
417
-
418
- if pos_self != -1:
419
- # Сравнение значений
420
- val_self = self.value[pos_self]
421
- val_other = other.value[pos_other]
422
- if val_self != val_other:
423
- return False
424
- return True
425
-
426
- def __str__(self) -> str:
427
- parts = []
428
- group_chars = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f'}
429
- for i in range(6):
430
- pos = self.positions[i]
431
- if pos == -1:
432
- parts.append(group_chars[i])
433
- continue
434
- if i < 5:
435
- next_pos = self.positions[i + 1] if self.positions[i + 1] != -1 else len(self.value)
436
- else:
437
- next_pos = len(self.value)
438
- length = next_pos - pos
439
- val = self.value[pos]
440
- if (
441
- i == 1
442
- and length == 65
443
- and self.value[pos:pos + 65] == RANGE64
444
- ):
445
- parts.append('(0-64)')
446
- continue
447
- if length > 1:
448
- values = list(self.value[pos:next_pos])
449
- ranges = []
450
- start = values[0]
451
- prev = start
452
- for v in values[1:]:
453
- if v != prev + 1:
454
- if start == prev:
455
- ranges.append(str(start))
456
- else:
457
- ranges.append(f"{start}-{prev}")
458
- start = v
459
- prev = v
460
- if start == prev:
461
- ranges.append(str(start))
462
- else:
463
- ranges.append(f"{start}-{prev}")
464
- if (
465
- len(ranges) == 1
466
- and '-' in ranges[0]
467
- ):
468
- parts.append(f"({ranges[0]})")
469
- else:
470
- parts.append(f"({','.join(ranges)})")
471
- else:
472
- parts.append(str(val))
473
- return '.'.join(parts)
474
-
475
-
476
- @dataclass(frozen=True)
477
- class PatternMatcher:
478
- """Класс для быстрого сопоставления Parameter с ParPattern"""
479
- patterns: dict['ParPattern', str]
480
- _masks: list[tuple[tuple[np.ndarray, ...], str]] = field(init=False)
481
-
482
- def __post_init__(self) -> None:
483
- # Предварительная обработка шаблонов
484
- masks = []
485
- for pattern, value in self.patterns.items():
486
- pattern_masks = []
487
- for i in range(7): # Для всех 7 элементов
488
- if pattern.positions[i]==-1: # SKIP
489
- mask = np.ones(256, dtype=bool) # Все значения подходят
490
- else:
491
- start = pattern.positions[i]
492
- if i < 6: # Первые 6 элементов (a-f)
493
- end = start + 1
494
- values = pattern.value[start:end]
495
- else: # 7-й элемент (индекс)
496
- end = start + 1
497
- values = pattern.value[start:end]
498
- # Для методов учитываем старший бит
499
- if values[0] & 0x80:
500
- values = bytes([values[0] & 0x7F])
501
-
502
- mask = np.zeros(256, dtype=bool)
503
- for v in values:
504
- mask[v] = True
505
- pattern_masks.append(mask)
506
- masks.append((tuple(pattern_masks), value))
507
- object.__setattr__(self, '_masks', masks)
508
-
509
- def find_match(self, param: 'Parameter') -> str:
510
- """Находит первое совпадение параметра с шаблоном"""
511
- param_values = (
512
- param.a, param.b, param.c, param.d, param.e, param.f,
513
- param.i if param.has_index else -1
514
- )
515
-
516
- for masks, value in self._masks:
517
- match = True
518
- for i in range(7):
519
- val = param_values[i]
520
- if val==-1: # Нет значения (для 7-го элемента)
521
- if masks[i].any(): # Если в шаблоне требуется конкретное значение
522
- match = False
523
- break
524
- elif not masks[i][val]:
525
- match = False
526
- break
527
- if match:
528
- return value
529
- return None
530
-
531
-
532
- if __name__ == "__main__":
533
- # Создаем тестовые шаблоны
534
- patterns = {
535
- ParPattern.parse("a.0.(1,2,3).(0-64).0.f:2"): "Pattern1",
536
- ParPattern.parse("a.0.1.0.0.f:m2"): "Pattern2",
537
- # ... другие шаблоны
538
- }
539
-
540
- # Создаем матчер
541
- matcher = PatternMatcher(patterns)
542
-
543
- # Тестовый параметр
544
- param = Parameter.parse("0.0.1.0.0.255:2")
545
-
546
- # Поиск совпадения
547
- result = matcher.find_match(param)
1
+ from typing_extensions import deprecated
2
+ from dataclasses import dataclass, field
3
+ import numpy as np
4
+ from struct import Struct, pack, unpack_from
5
+ from typing import Optional, cast, Iterator, Self
6
+ import re
7
+ from functools import cached_property
8
+ from .. import exceptions as exc
9
+ from .obis import OBIS
10
+
11
+ _pattern = re.compile("((?:\d{1,3}\.){5}\d{1,3})(?::(m?\d{1,3}))?")
12
+ Index = Struct("?B")
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Parameter:
17
+ """
18
+ Parameter ::= SEQUENCE
19
+ {
20
+ ln Cosem-Object-Instance-Id
21
+ descriptor OPTIONAL
22
+ }
23
+
24
+ index Unsigned8
25
+ nest_index Unsigned16
26
+ piece Unsigned8
27
+
28
+ descriptor :: = CHOICE
29
+ {
30
+ attribute [0] IMPLICIT Desc
31
+ method [1] IMPLICIT Desc
32
+ }
33
+
34
+ Desc ::= SEQUENCE
35
+ {
36
+ index
37
+ SEQUENCE (SIZE(255)) OF nest_index OPTIONAL
38
+ piece OPTIONAL
39
+ }
40
+ """
41
+ _value: bytes
42
+
43
+ def __bytes__(self) -> bytes:
44
+ return self._value
45
+
46
+ @classmethod
47
+ def parse(cls, value: str) -> Self:
48
+ """create from string. Only LN, attr/meth type ddd.ddd.ddd.ddd.ddd.ddd:aaa, ex.: 0.0.1.0.0.255 """
49
+ if (res := _pattern.fullmatch(value)) is None:
50
+ raise ValueError(F"in {cls.__name__}.parse got wrong :{value:}")
51
+ else:
52
+ groups = iter(res.groups())
53
+ ret = bytes(map(int, next(groups).split(".")))
54
+ if (a := next(groups)) is not None:
55
+ if a.startswith('m'):
56
+ a = a[1:]
57
+ g1 = 256
58
+ else:
59
+ g1 = 0
60
+ ret += (g1 + int(a)).to_bytes(2)
61
+ return cls(ret)
62
+
63
+ @cached_property
64
+ def logical_name(self) -> "Parameter":
65
+ return self.get_attr(1)
66
+
67
+ def __eq__(self, other: object) -> bool:
68
+ if isinstance(other, Parameter):
69
+ return cast("bool", self._value==other._value)
70
+ return NotImplemented
71
+
72
+ def __lt__(self, other: "Parameter") -> bool:
73
+ """comparing for sort method"""
74
+ if len(self._value) > len(other._value):
75
+ return True
76
+ else:
77
+ return False
78
+
79
+ def __str__(self) -> str:
80
+ if (l := len(self._value)) < 6:
81
+ return "No valid"
82
+ elif l == 7:
83
+ return "No valid Index"
84
+ else:
85
+ res = F"{".".join(map(str, self._value[:6]))}"
86
+ if l > 6:
87
+ res += F":{"m" if self.is_method() else ""}{self.i}"
88
+ if l > 8:
89
+ res += F" {"/".join(map(str, self.elements()))}"
90
+ if self.has_piece():
91
+ res += F"p{self.piece}"
92
+ return res
93
+
94
+ def validate(self) -> None:
95
+ if (length := len(self._value)) < 6:
96
+ raise exc.DLMSException(F"Parameter got {length=}, expected at least 6")
97
+ if length == 7:
98
+ raise exc.DLMSException(F"Parameter got wrong index")
99
+
100
+ @property
101
+ def has_index(self) -> bool:
102
+ return len(self._value) > 6
103
+
104
+ @property
105
+ @deprecated("use obis")
106
+ def ln(self) -> bytes:
107
+ """Logical Name"""
108
+ return self._value[:6]
109
+
110
+ def is_method(self) -> bool:
111
+ return self._value[6] == 1
112
+
113
+ @property
114
+ def i(self) -> int:
115
+ """attribute or method index"""
116
+ return self._value[7]
117
+
118
+ def get_attr(self, i: int) -> Self:
119
+ """get attribute"""
120
+ val = Index.pack(0, i)
121
+ return self.__class__(self._value[:6] + val)
122
+
123
+ def get_meth(self, i: int) -> Self:
124
+ """get method"""
125
+ val = Index.pack(1, i)
126
+ return self.__class__(self._value[:6] + val)
127
+
128
+ def set_i(self, index: int, is_method: bool = False) -> "Parameter":
129
+ val = Index.pack(is_method, index)
130
+ if len(self._value) == 6:
131
+ tmp = self._value + val
132
+ else:
133
+ tmp_ = bytearray(self._value)
134
+ tmp_[6:8] = val
135
+ tmp = bytes(tmp_)
136
+ return self.__class__(tmp)
137
+
138
+ def append_validate(self) -> None:
139
+ if (l := len(self._value)) < 7:
140
+ raise exc.DLMSException(F"Parameter must has index before")
141
+ elif l % 2 != 0:
142
+ raise exc.DLMSException(F"Can't append to Parameter with piece")
143
+
144
+ def append(self, index: int) -> "Parameter":
145
+ """add new sequence(array or struct) index element"""
146
+ self.append_validate()
147
+ return self.__class__(self._value + pack(">H", index))
148
+
149
+ def extend(self, *indexes: int) -> "Parameter":
150
+ self.append_validate()
151
+ return self.__class__(self._value + pack(F">{len(indexes)}H", *indexes))
152
+
153
+ def pop(self) -> tuple[Optional[int], int, "Parameter"]:
154
+ """
155
+ :return piece, last index and parent Parameter
156
+ ex.: Parameter("0.0.0.0.0.0:2 1/1/1 p3") => (1, Parameter("0.0.0.0.0.0:2 1/1"))
157
+ """
158
+ if self.has_piece():
159
+ return self._value[-1], int.from_bytes(self._value[-3:-1]), self.__class__(self._value[:-3])
160
+ else:
161
+ return None, int.from_bytes(self._value[-2:]), self.__class__(self._value[:-2])
162
+
163
+ def set_piece(self, index: int) -> "Parameter":
164
+ """add new sequence(array or struct) index element"""
165
+ if len(self._value) >= 7:
166
+ return self.__class__(self._value + pack("B", index))
167
+ else:
168
+ raise exc.DLMSException(F"Parameter must has index before")
169
+
170
+ def has_piece(self) -> bool:
171
+ if (
172
+ (l := len(self._value)) >= 9
173
+ and l % 2 != 0
174
+ ):
175
+ return True
176
+ else:
177
+ return False
178
+
179
+ @property
180
+ def piece(self) -> Optional[int]:
181
+ if self.has_piece():
182
+ return self._value[-1]
183
+ return None
184
+
185
+ def clear_piece(self) -> "Parameter":
186
+ if self.has_piece():
187
+ return self.__class__(self._value[:-1])
188
+ return self
189
+
190
+ def elements(self, start: int = 0) -> Iterator[int]:
191
+ """return: index elements nested in attribute, started with"""
192
+ for i in range(8 + start, 8 + 2 * self.n_elements, 2):
193
+ res = int.from_bytes(self._value[i:i + 2], "big")
194
+ yield res
195
+
196
+ def __iter__(self) -> Iterator[int]:
197
+ for it in self._value[:6]:
198
+ yield it
199
+ if self._value[6] == 1:
200
+ yield -self.i
201
+ else:
202
+ yield self.i
203
+
204
+ @property
205
+ def last_element(self) -> int:
206
+ """:return last element index"""
207
+ if self.n_elements == 0:
208
+ raise ValueError("Parameter hasn't elements")
209
+ if self.has_piece():
210
+ val = self._value[-3: -1]
211
+ else:
212
+ val = self._value[-2:]
213
+ return int.from_bytes(val, "big")
214
+
215
+ @property
216
+ def n_elements(self) -> int:
217
+ """return: amount of elements nested in attribute"""
218
+ return max(0, (len(self._value) - 8) // 2)
219
+
220
+ def set(self,
221
+ a: Optional[int] = None,
222
+ b: Optional[int] = None,
223
+ c: Optional[int] = None,
224
+ d: Optional[int] = None,
225
+ e: Optional[int] = None,
226
+ f: Optional[int] = None
227
+ ) -> "Parameter":
228
+ val = bytearray(self._value)
229
+ if a is not None:
230
+ val[0] = a
231
+ if b is not None:
232
+ val[1] = b
233
+ if c is not None:
234
+ val[2] = c
235
+ if d is not None:
236
+ val[3] = d
237
+ if e is not None:
238
+ val[4] = e
239
+ if f is not None:
240
+ val[5] = f
241
+ return self.__class__(bytes(val))
242
+
243
+ def __contains__(self, item: "Parameter") -> bool:
244
+ return item._value in self._value
245
+
246
+ def __getitem__(self, item: int) -> Optional[int]:
247
+ if self.n_elements > 0:
248
+ return cast(int, unpack_from(">H", self._value, item * 2 + 8)[0])
249
+ else:
250
+ return None
251
+
252
+ @property
253
+ def a(self) -> int:
254
+ return self._value[0]
255
+
256
+ @property
257
+ def b(self) -> int:
258
+ return self._value[1]
259
+
260
+ @property
261
+ def c(self) -> int:
262
+ return self._value[2]
263
+
264
+ @property
265
+ def d(self) -> int:
266
+ return self._value[3]
267
+
268
+ @property
269
+ def e(self) -> int:
270
+ return self._value[4]
271
+
272
+ @property
273
+ def f(self) -> int:
274
+ return self._value[5]
275
+
276
+ @property
277
+ def attr(self) -> "Parameter":
278
+ if self.has_index:
279
+ return Parameter(self._value[:8])
280
+ else:
281
+ raise exc.DLMSException(F"Parameter must has index before")
282
+
283
+ @property
284
+ def obj(self) -> "Parameter":
285
+ return Parameter(self._value[:6])
286
+
287
+ @property
288
+ def obis(self) -> OBIS:
289
+ return OBIS(self._value[:6])
290
+
291
+
292
+ RANGE64 = bytes(range(65)) # Предвычисленный диапазон 0-64
293
+
294
+
295
+ @dataclass(eq=False, frozen=True)
296
+ class ParPattern:
297
+ value: bytes
298
+ positions: tuple[int, ...]
299
+ "7 elements , -1 is SKIP, last element is attribute index if positive and method index else negative"
300
+
301
+ def __post_init__(self) -> None:
302
+ if 6 > len(self.positions) > 7:
303
+ raise ValueError(f"positions must have exactly 6 elements, got {len(self.positions)}")
304
+
305
+ @classmethod
306
+ def parse(cls, pattern: str) -> "ParPattern":
307
+ """Парсинг строк вида:
308
+ - "a.0.(1,2,3).(0-64).0.f" (6 элементов)
309
+ - "a.0.1.0.0.f:2" (6 элементов + индекс через :)
310
+ - "a.0.1.0.0.f:m2" (6 элементов + метод через :)
311
+ """
312
+ # Разделяем основную часть и индекс
313
+ if ':' in pattern:
314
+ main_part, index_part = pattern.rsplit(':', 1)
315
+ is_method = index_part.startswith('m')
316
+ if is_method:
317
+ index_part = index_part[1:]
318
+ else:
319
+ main_part = pattern
320
+ index_part = None
321
+
322
+ # Парсим основную часть (6 элементов)
323
+ parts = main_part.split('.', maxsplit=5)
324
+ if len(parts) < 6:
325
+ parts.extend(['f'] * (6 - len(parts))) # Дополняем до 6 элементов
326
+
327
+ value_parts = []
328
+ positions = []
329
+ current_pos = 0
330
+
331
+ for i, part in enumerate(parts[:6]): # Ровно 6 элементов
332
+ if len(part)==1 and ord(part)==97 + i: # a-f
333
+ if part=='b':
334
+ value_parts.append(RANGE64)
335
+ positions.append(current_pos)
336
+ current_pos += len(RANGE64)
337
+ else:
338
+ positions.append(-1) # SKIP
339
+ continue
340
+
341
+ try:
342
+ if part.isdigit(): # Простое число
343
+ num = int(part)
344
+ if not 0 <= num <= 255:
345
+ raise ValueError
346
+ value_parts.append(bytes([num]))
347
+ positions.append(current_pos)
348
+ current_pos += 1
349
+ elif part.startswith(('(', '!(')) and part.endswith(')'):
350
+ elements = cls._parse_group(part[2 if part.startswith('!(') else 1:-1])
351
+ if part.startswith('!('):
352
+ elements = set(range(256)) - elements
353
+ value_parts.append(bytes(elements))
354
+ positions.append(current_pos)
355
+ current_pos += len(value_parts[-1])
356
+ else:
357
+ raise ValueError
358
+ except ValueError:
359
+ raise ValueError(f"Invalid pattern part: {part} at position {i}")
360
+
361
+ # Добавляем индекс (7-й элемент)
362
+ if index_part is not None:
363
+ try:
364
+ index_val = int(index_part)
365
+ if not 0 <= index_val <= 255:
366
+ raise ValueError
367
+ # Упаковываем: старший бит = is_method, остальные 7 бит = значение
368
+ packed = bytes([(0x80 if is_method else 0) | (index_val & 0x7F)])
369
+ value_parts.append(packed)
370
+ positions.append(current_pos)
371
+ except ValueError:
372
+ raise ValueError(f"Invalid index value: {index_part}")
373
+ else:
374
+ positions.append(-1) # SKIP для 7-го элемента
375
+
376
+ return cls(
377
+ value=b''.join(value_parts),
378
+ positions=tuple(positions)
379
+ )
380
+
381
+ @staticmethod
382
+ def _parse_group(group_str: str) -> set[int]:
383
+ """Парсинг групп вида '1,2,3' или '10-20'"""
384
+ elements: set[int] = set()
385
+ for item in group_str.split(','):
386
+ item = item.strip()
387
+ if '-' in item:
388
+ start, end = map(int, item.split('-'))
389
+ elements.update(range(start, end + 1))
390
+ else:
391
+ elements.add(int(item))
392
+ return elements
393
+
394
+ # @functools.cache
395
+ def __hash__(self) -> int:
396
+ print("hash")
397
+ hash_parts: list[Optional[bytes]] = []
398
+ for pos in self.positions:
399
+ if pos == -1:
400
+ hash_parts.append(None)
401
+ else:
402
+ hash_parts.append(self.value[pos:pos + 1])
403
+ return hash(tuple(hash_parts))
404
+
405
+ def __eq__(self, other: object) -> bool:
406
+ print("eq")
407
+ if not isinstance(other, ParPattern):
408
+ return False
409
+
410
+ for i in range(6):
411
+ pos_self = self.positions[i]
412
+ pos_other = other.positions[i]
413
+
414
+ # Проверка соответствия схемы позиций
415
+ if (pos_self == -1) != (pos_other == -1):
416
+ return False
417
+
418
+ if pos_self != -1:
419
+ # Сравнение значений
420
+ val_self = self.value[pos_self]
421
+ val_other = other.value[pos_other]
422
+ if val_self != val_other:
423
+ return False
424
+ return True
425
+
426
+ def __str__(self) -> str:
427
+ parts = []
428
+ group_chars = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f'}
429
+ for i in range(6):
430
+ pos = self.positions[i]
431
+ if pos == -1:
432
+ parts.append(group_chars[i])
433
+ continue
434
+ if i < 5:
435
+ next_pos = self.positions[i + 1] if self.positions[i + 1] != -1 else len(self.value)
436
+ else:
437
+ next_pos = len(self.value)
438
+ length = next_pos - pos
439
+ val = self.value[pos]
440
+ if (
441
+ i == 1
442
+ and length == 65
443
+ and self.value[pos:pos + 65] == RANGE64
444
+ ):
445
+ parts.append('(0-64)')
446
+ continue
447
+ if length > 1:
448
+ values = list(self.value[pos:next_pos])
449
+ ranges = []
450
+ start = values[0]
451
+ prev = start
452
+ for v in values[1:]:
453
+ if v != prev + 1:
454
+ if start == prev:
455
+ ranges.append(str(start))
456
+ else:
457
+ ranges.append(f"{start}-{prev}")
458
+ start = v
459
+ prev = v
460
+ if start == prev:
461
+ ranges.append(str(start))
462
+ else:
463
+ ranges.append(f"{start}-{prev}")
464
+ if (
465
+ len(ranges) == 1
466
+ and '-' in ranges[0]
467
+ ):
468
+ parts.append(f"({ranges[0]})")
469
+ else:
470
+ parts.append(f"({','.join(ranges)})")
471
+ else:
472
+ parts.append(str(val))
473
+ return '.'.join(parts)
474
+
475
+
476
+ @dataclass(frozen=True)
477
+ class PatternMatcher:
478
+ """Класс для быстрого сопоставления Parameter с ParPattern"""
479
+ patterns: dict['ParPattern', str]
480
+ _masks: list[tuple[tuple[np.ndarray, ...], str]] = field(init=False)
481
+
482
+ def __post_init__(self) -> None:
483
+ # Предварительная обработка шаблонов
484
+ masks = []
485
+ for pattern, value in self.patterns.items():
486
+ pattern_masks = []
487
+ for i in range(7): # Для всех 7 элементов
488
+ if pattern.positions[i]==-1: # SKIP
489
+ mask = np.ones(256, dtype=bool) # Все значения подходят
490
+ else:
491
+ start = pattern.positions[i]
492
+ if i < 6: # Первые 6 элементов (a-f)
493
+ end = start + 1
494
+ values = pattern.value[start:end]
495
+ else: # 7-й элемент (индекс)
496
+ end = start + 1
497
+ values = pattern.value[start:end]
498
+ # Для методов учитываем старший бит
499
+ if values[0] & 0x80:
500
+ values = bytes([values[0] & 0x7F])
501
+
502
+ mask = np.zeros(256, dtype=bool)
503
+ for v in values:
504
+ mask[v] = True
505
+ pattern_masks.append(mask)
506
+ masks.append((tuple(pattern_masks), value))
507
+ object.__setattr__(self, '_masks', masks)
508
+
509
+ def find_match(self, param: 'Parameter') -> str:
510
+ """Находит первое совпадение параметра с шаблоном"""
511
+ param_values = (
512
+ param.a, param.b, param.c, param.d, param.e, param.f,
513
+ param.i if param.has_index else -1
514
+ )
515
+
516
+ for masks, value in self._masks:
517
+ match = True
518
+ for i in range(7):
519
+ val = param_values[i]
520
+ if val==-1: # Нет значения (для 7-го элемента)
521
+ if masks[i].any(): # Если в шаблоне требуется конкретное значение
522
+ match = False
523
+ break
524
+ elif not masks[i][val]:
525
+ match = False
526
+ break
527
+ if match:
528
+ return value
529
+ return None
530
+
531
+
532
+ if __name__ == "__main__":
533
+ # Создаем тестовые шаблоны
534
+ patterns = {
535
+ ParPattern.parse("a.0.(1,2,3).(0-64).0.f:2"): "Pattern1",
536
+ ParPattern.parse("a.0.1.0.0.f:m2"): "Pattern2",
537
+ # ... другие шаблоны
538
+ }
539
+
540
+ # Создаем матчер
541
+ matcher = PatternMatcher(patterns)
542
+
543
+ # Тестовый параметр
544
+ param = Parameter.parse("0.0.1.0.0.255:2")
545
+
546
+ # Поиск совпадения
547
+ result = matcher.find_match(param)
548
548
  print(f"Found: {result}") # Выведет "Pattern1"