DLMS-SPODES 0.87.13__py3-none-any.whl → 0.87.16__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 -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 +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.13.dist-info → dlms_spodes-0.87.16.dist-info}/METADATA +30 -30
  97. dlms_spodes-0.87.16.dist-info/RECORD +117 -0
  98. {dlms_spodes-0.87.13.dist-info → dlms_spodes-0.87.16.dist-info}/WHEEL +1 -1
  99. dlms_spodes-0.87.13.dist-info/RECORD +0 -117
  100. {dlms_spodes-0.87.13.dist-info → dlms_spodes-0.87.16.dist-info}/top_level.txt +0 -0
@@ -1,333 +1,333 @@
1
- from dataclasses import dataclass
2
- from typing import Self, Literal
3
- from ..types import cst
4
- from copy import copy
5
-
6
- SKIP: int = 0
7
- RANGE256 = set(range(256))
8
- RANGE64 = bytes(range(64))
9
- RANGE64_WITH_LENGTH = b'\x40' + RANGE64
10
-
11
-
12
- @dataclass(frozen=True, slots=True)
13
- class LNPattern:
14
- """
15
- LNPattern ::= SEQUENCE (SIZE (6)) OF GroupPattern
16
-
17
- GroupPattern ::= CHOICE {
18
- skip [0] NULL, -- SKIP marker
19
- single [1] INTEGER (0..255), -- single value
20
- multiple [2] SEQUENCE OF INTEGER (0..255) -- set of values
21
- }
22
- LNPattern Binary Encoding Specification:
23
- ---------------------------------------
24
- A compact tag-less binary format for storing 6 OBIS-like groups.
25
-
26
- Structure:
27
- [L1][V1][L2][V2]...[L6][V6]
28
- where:
29
- - Ln: 1-byte length prefix for group n (0-255)
30
- - Vn: Value bytes (interpretation depends on Ln)
31
-
32
- Length Semantics:
33
- - L=0 : SKIP group (no value bytes follow)
34
- - L=1 : Single value (V is 1-byte integer 0-255)
35
- - L=2..255 : Value set (V contains L bytes as possible values)
36
-
37
- Special Cases:
38
- - Group 'b' (index 1) when marked SKIP uses predefined RANGE64 (0-64)
39
- - Empty exclusion sets "!()" are prohibited
40
-
41
- Example:
42
- Pattern "a.1.(2-5).!().0.f" encodes as:
43
- [00][01][01][04][02][03][04][05][00][01][00][00]
44
- (SKIP|1|{2,3,4,5}|SKIP|0|SKIP)
45
-
46
- Properties:
47
- - Fixed overhead: 6 bytes (1 length byte per group)
48
- - Max size: 6 + 255*6 = 1536 bytes
49
- - Order-preserving
50
- - Comparison-friendly memory layout
51
- """
52
- buffer: bytes
53
-
54
- @classmethod
55
- def parse(cls, value: str) -> Self:
56
- buffer = bytearray()
57
- parts = value.split('.', maxsplit=5)
58
- if len(parts) != 6:
59
- raise ValueError(f"got {len(parts)} elements, expected 6")
60
- for i, val in enumerate(parts):
61
- if val.isdigit():
62
- num = int(val)
63
- if 0 <= num <= 255:
64
- buffer.extend((1, num))
65
- continue
66
- raise ValueError(f"Value {val} out of range 0-255")
67
- if len(val) == 1:
68
- if val == "x":
69
- buffer.append(SKIP)
70
- continue
71
- elif (
72
- i == 1
73
- and val == "b"
74
- ):
75
- buffer.extend(RANGE64_WITH_LENGTH)
76
- continue
77
- elif ord(val) == i + 97:
78
- buffer.append(SKIP)
79
- continue
80
- if val == "":
81
- buffer.append(SKIP)
82
- continue
83
- if val[0]=='(' and val[-1]==')':
84
- el = set()
85
- val = val.replace('(', "").replace(')', "")
86
- for j in val.split(","):
87
- j = j.replace(" ", '')
88
- match j.count('-'):
89
- case 0:
90
- el.add(cls.__simple_validate(j))
91
- case 1:
92
- start, end = j.split("-")
93
- el.update(range(
94
- cls.__simple_validate(start),
95
- cls.__simple_validate(end) + 1))
96
- case err:
97
- raise ValueError(F"got a lot of <-> in pattern: {value}, expected one")
98
- # values = bytes(el)
99
- # buffer.extend([len(values)] + list(values))
100
- buffer.append(len(el))
101
- buffer.extend(el)
102
- continue
103
- if val.startswith('!(') and val.endswith(')'):
104
- el = copy(RANGE256)
105
- val = val.replace('!(', "").replace(')', "")
106
- for j in val.split(","):
107
- j = j.replace(" ", '')
108
- match j.count('-'):
109
- case 0:
110
- el.discard(cls.__simple_validate(j))
111
- case 1:
112
- start, end = j.split("-")
113
- el.difference_update(range(
114
- cls.__simple_validate(start),
115
- cls.__simple_validate(end) + 1))
116
- case err:
117
- raise ValueError(F"got a lot of <-> in pattern: {value}, expected one")
118
- if len(el)==0:
119
- raise ValueError(F"no one element in group: {chr(97 + i)}")
120
- # values = bytes(el)
121
- # buffer.extend([len(values)] + list(values))
122
- buffer.append(len(el))
123
- buffer.extend(el)
124
- continue
125
- raise ValueError(f"Invalid pattern: {val}")
126
- return cls(bytes(buffer))
127
-
128
- @staticmethod
129
- def __simple_validate(value: str) -> int:
130
- if value.isdigit() and (0 <= (new := int(value)) <= 255):
131
- return new
132
- else:
133
- raise ValueError(F"got not valid element: {value} in pattern, expected 0..255")
134
-
135
- def __eq__(self, other: "LNPattern") -> bool:
136
- ptr = 0
137
- for i in range(6):
138
- length = self.buffer[ptr]
139
- ptr += 1
140
- if length == 0: # SKIP
141
- continue
142
- other_byte = other.contents[i]
143
- if length == 1: # Single byte
144
- if self.buffer[ptr]!=other_byte:
145
- return False
146
- else: # Multiple bytes
147
- if other_byte not in self.buffer[ptr:ptr + length]:
148
- return False
149
- ptr += length
150
- return True
151
-
152
- @staticmethod
153
- def _format_ranges(values: list[int]) -> str:
154
- if not values:
155
- return "!()"
156
- ranges = []
157
- start = end = values[0]
158
- for num in values[1:]:
159
- if num==end + 1:
160
- end = num
161
- else:
162
- ranges.append((start, end))
163
- start = end = num
164
- ranges.append((start, end))
165
- parts = []
166
- for start, end in ranges:
167
- if start==end:
168
- parts.append(str(start))
169
- elif end==start + 1: # Диапазон из 2 чисел
170
- parts.extend([str(start), str(end)])
171
- else:
172
- parts.append(f"{start}-{end}")
173
- if len(parts) > 3 and len(values) > 128: # Эмпирический порог
174
- all_values = set(range(256))
175
- excluded = sorted(all_values - set(values))
176
- if len(excluded) < len(values):
177
- return f"!({','.join(self._format_ranges(excluded))})"
178
- return f"({','.join(parts)})"
179
-
180
- def __str__(self) -> str:
181
- parts = []
182
- ptr = 0
183
- for _ in range(6):
184
- length = self.buffer[ptr]
185
- ptr += 1
186
- if length == 0:
187
- parts.append("x")
188
- elif length == 1:
189
- parts.append(str(self.buffer[ptr]))
190
- else:
191
- values = sorted(set(self.buffer[ptr:ptr + length]))
192
- parts.append(self._format_ranges(values))
193
- ptr += length
194
- return ".".join(parts)
195
-
196
-
197
- @dataclass
198
- class LNPatterns:
199
- value: tuple[LNPattern, ...]
200
-
201
- def __iter__(self):
202
- return iter(self.value)
203
-
204
- def __str__(self) -> str:
205
- return f"[{" | ".join(map(str, self.value))}]"
206
-
207
-
208
- ABSTRACT = LNPattern.parse("0.....")
209
- ELECTRICITY = LNPattern.parse("1.....")
210
- HCA = LNPattern.parse("4.....")
211
- THERMAL = LNPattern.parse("(5,6).....")
212
- GAS = LNPattern.parse("7.....")
213
- WATER = LNPattern.parse("(8,9).....")
214
- OTHER_MEDIA = LNPattern.parse("15.....")
215
-
216
-
217
- BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES = LNPatterns((
218
- LNPattern.parse("0.b.0.1.(0,2,3,5).f"),
219
- LNPattern.parse("0.b.0.1.(1,4).255")))
220
- PROGRAM_ENTRIES = LNPatterns((
221
- ACTIVE_FIRMWARE_IDENTIFIER := LNPattern.parse("0.b.0.2.0.255"),
222
- ACTIVE_FIRMWARE_VERSION := LNPattern.parse("0.b.0.2.1.255"),
223
- ACTIVE_FIRMWARE_SIGNATURE := LNPattern.parse("0.b.0.2.8.255")
224
- ))
225
- TIME_ENTRIES = LNPattern.parse("0.b.0.9.(1,2).255")
226
- CLOCK_OBJECTS = LNPatterns((
227
- CLOCK := LNPattern.parse("0.b.1.0.e.255"),
228
- UNIX_CLOCK := LNPattern.parse("0.b.1.1.e.255"),
229
- MICROSECONDS_CLOCK := LNPattern.parse("0.b.1.2.e.255"),
230
- MINUTES_CLOCK := LNPattern.parse("0.b.1.3.e.255"),
231
- HOURS_CLOCK := LNPattern.parse("0.b.1.4.e.255"),
232
- DAYS_CLOCK := LNPattern.parse("0.b.1.5.e.255"),
233
- WEEKS_CLOCK := LNPattern.parse("0.b.1.6.e.255")
234
- ))
235
- TARIFFICATION_SCRIPT_TABLE = LNPattern.parse("0.b.10.0.100.255")
236
- PUSH_SCRIPT_TABLE = LNPattern.parse("0.b.10.0.108.255")
237
- SINGLE_ACTION_SCHEDULE = LNPatterns((
238
- END_OF_BILLING_PERIOD_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.0.255"),
239
- DISCONNECT_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.1.255"),
240
- IMAGE_ACTIVATION_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.2.255"),
241
- OUTPUT_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.3.255"),
242
- PUSH_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.4.255"),
243
- LOAD_PROFILE_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.5.255"),
244
- M_BUS_PROFILE_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.6.255"),
245
- FUNCTION_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.7.255")
246
- ))
247
- ACTIVITY_CALENDAR = LNPattern.parse("0.b.13.0.e.255")
248
- ASSOCIATION = LNPattern.parse("0.0.40.0.e.255") # 6_2_33
249
- NON_CURRENT_ASSOCIATION = LNPattern.parse("0.0.40.0.(1-255).255") # MY
250
- SAP_ASSIGNMENT = LNPattern.parse("0.0.41.0.0.255") # 6.2.34
251
- COSEM_logical_device_name = LNPattern.parse("0.0.42.0.0.255") # 6.2.35
252
- INFORMATION_SECURITY_RELATED = LNPatterns((
253
- LNPattern.parse("0.0.43.(0,2).e.255"),
254
- INVOCATION_COUNTER := LNPattern.parse("0.b.43.1.e.255") # 6.2.36
255
- ))
256
- IMAGE_TRANSFER = LNPattern.parse("0.b.44.0.e.255") # 6.2.37
257
- FUNCTION_CONTROL = LNPattern.parse("0.b.44.1.e.255") # 6.2.38
258
- COMMUNICATION_PORT_PROTECTION = LNPattern.parse("0.b.44.2.e.255") # 6.2.39
259
- UTILITY_TABLE = LNPattern.parse("0.b.65.(0-63).e.255") # 6.2.40
260
- COMPACT_DATA = LNPattern.parse("0.b.66.0.e.255") # 6.2.41
261
- DEVICE_ID = LNPattern.parse("0.b.96.1.(0,1,2,3,4,5,6,7,8,9,255).255") # 6.2.42
262
- METERING_POINT_ID = LNPattern.parse("0.b.96.1.10.255") # 6.2.43
263
- PARAMETER_CHANGES_CALIBRATION_AND_ACCESS = LNPattern.parse("0.b.96.2.e.f") # 6.2.44
264
- INPUT_OUTPUT_CONTROL_SIGNALS = LNPattern.parse("0.b.96.3.(0-4).f") # 6.2.45
265
- DISCONNECT_CONTROL = LNPattern.parse("0.b.96.3.10.f") # 6.2.46
266
- ARBITRATOR = LNPattern.parse("0.b.96.3.(20-29).f") # 6.2.47
267
- INTERNAL_CONTROL_SIGNALS = LNPattern.parse("0.b.96.4.(0-4).f") # 6.2.48
268
- INTERNAL_OPERATING_STATUS = LNPattern.parse("0.b.96.5.(0-4).f") # 6.2.49
269
- BATTERY_ENTRIES = LNPattern.parse("0.b.96.6.(0,1,2,3,4,5,6,10,11).f") # 6.2.50
270
- POWER_FAILURE_MONITORING = LNPattern.parse("0.b.96.7.(0-21).f") # 6.2.51
271
- OPERATING_TIME = LNPattern.parse("0.b.96.8.(0-63).f")
272
- ENVIRONMENT_RELATED_PARAMETERS = LNPattern.parse("0.b.96.9.(0-2).f")
273
- STATUS_REGISTER = LNPattern.parse("0.b.96.10.(1-10).f")
274
- EVENT_CODE = LNPattern.parse("0.b.96.11.(0-99).f")
275
- COMMUNICATION_PORT_LOG_PARAMETERS = LNPattern.parse("0.b.96.12.(0-6).f")
276
- CONSUMER_MESSAGES = LNPattern.parse("0.b.96.13.(0,1).f")
277
- CURRENTLY_ACTIVE_TARIFF = LNPattern.parse("0.b.96.14.(0-15).f")
278
- EVENT_COUNTER = LNPattern.parse("0.b.96.15.(0-99).f")
279
- PROFILE_ENTRY_DIGITAL_SIGNATURE = LNPattern.parse("0.b.96.16.(0-9).f")
280
- PROFILE_ENTRY_COUNTER = LNPattern.parse("0.b.96.17.(0-127).f")
281
- METER_TAMPER_EVENT_RELATED = LNPattern.parse("0.b.96.20.(0-34).f")
282
- MANUFACTURER_SPECIFIC_ABSTRACT = LNPattern.parse("0.b.96.(50-99).e.f")
283
-
284
- GENERAL_AND_SERVICE_ENTRY = LNPatterns((
285
- *BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES,
286
- *PROGRAM_ENTRIES,
287
- TIME_ENTRIES,
288
- DEVICE_ID,
289
- PARAMETER_CHANGES_CALIBRATION_AND_ACCESS,
290
- INPUT_OUTPUT_CONTROL_SIGNALS,
291
- INTERNAL_CONTROL_SIGNALS,
292
- INTERNAL_OPERATING_STATUS,
293
- BATTERY_ENTRIES,
294
- POWER_FAILURE_MONITORING,
295
- OPERATING_TIME,
296
- ENVIRONMENT_RELATED_PARAMETERS,
297
- STATUS_REGISTER,
298
- EVENT_CODE,
299
- COMMUNICATION_PORT_LOG_PARAMETERS,
300
- CONSUMER_MESSAGES,
301
- CURRENTLY_ACTIVE_TARIFF,
302
- EVENT_COUNTER,
303
- PROFILE_ENTRY_DIGITAL_SIGNATURE,
304
- PROFILE_ENTRY_COUNTER,
305
- METER_TAMPER_EVENT_RELATED,
306
- MANUFACTURER_SPECIFIC_ABSTRACT))
307
- """DLMS UA 1000-1 Ed. 14 7.4.1"""
308
-
309
- LIMITER = LNPattern.parse("0.b.17.0.e.255") # 6.2.15
310
- ALARM_REGISTER = LNPattern.parse("0.b.97.98.(0-9).255") # 6.2.64
311
- COUNTRY_SPECIFIC_IDENTIFIERS = LNPattern.parse("0.b.94.d.e.f") # 7.3.4.3
312
- ALARM_REGISTER_FILTER = LNPattern.parse("0.b.97.98.(10-19).255") # 6.2.64
313
- ALARM_REGISTER_DESCRIPTOR = LNPattern.parse("0.b.97.98.(20-29).255") # 6.2.64
314
- ALARM_REGISTER_PROFILE = LNPattern.parse("0.b.97.98.255.255") # 6.2.64
315
- ALARM_REGISTER_TABLE = LNPattern.parse("0.b.97.98.255.255") # 6.2.64
316
- ALARM_REGISTER_FILTER_DESCRIPTOR = LNPatterns((ALARM_REGISTER, ALARM_REGISTER_FILTER, ALARM_REGISTER_DESCRIPTOR, ALARM_REGISTER_PROFILE))
317
- # electricity
318
- ID_NUMBERS_ELECTRICITY = LNPattern.parse("1.b.0.0.(0-9).255")
319
- ELECTRIC_PROGRAM_ENTRIES = LNPattern.parse("1.b.0.2.e.255")
320
- OUTPUT_PULSE_VALUES_OR_CONSTANTS = LNPattern.parse("1.0.0.3.(0-9).255")
321
- RATIOS = LNPattern.parse("1.0.0.4.(0-7).255")
322
- RECORDING_INTERVAL = LNPattern.parse("1.0.0.8.(4,5).255")
323
- OTHER_ELECTRICITY_RELATED_GENERAL_PURPOSE = LNPattern.parse("1.b.0.(2,3,4,6,7,8,9,10).e.255")
324
- # my special
325
- COUNTRY_SPECIFIC = LNPattern.parse("a.b.94.d.e.f") # 7.2.4 Table 54
326
-
327
- # 7.5.2.1
328
- INSTANTANEOUS_VALUE_EL_REL = LNPattern.parse("1.b.!(0,93,94,96,97,98,99).7.e.f")
329
- ELECTRICITY_RELATED = LNPatterns((
330
- INSTANTANEOUS_VALUE_EL_REL,
331
- # todo: more other LNPatterns
332
- ))
333
- """7.5.2.1 Processing of measurement values Table 66"""
1
+ from dataclasses import dataclass
2
+ from typing import Self, Literal
3
+ from ..types import cst
4
+ from copy import copy
5
+
6
+ SKIP: int = 0
7
+ RANGE256 = set(range(256))
8
+ RANGE64 = bytes(range(64))
9
+ RANGE64_WITH_LENGTH = b'\x40' + RANGE64
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class LNPattern:
14
+ """
15
+ LNPattern ::= SEQUENCE (SIZE (6)) OF GroupPattern
16
+
17
+ GroupPattern ::= CHOICE {
18
+ skip [0] NULL, -- SKIP marker
19
+ single [1] INTEGER (0..255), -- single value
20
+ multiple [2] SEQUENCE OF INTEGER (0..255) -- set of values
21
+ }
22
+ LNPattern Binary Encoding Specification:
23
+ ---------------------------------------
24
+ A compact tag-less binary format for storing 6 OBIS-like groups.
25
+
26
+ Structure:
27
+ [L1][V1][L2][V2]...[L6][V6]
28
+ where:
29
+ - Ln: 1-byte length prefix for group n (0-255)
30
+ - Vn: Value bytes (interpretation depends on Ln)
31
+
32
+ Length Semantics:
33
+ - L=0 : SKIP group (no value bytes follow)
34
+ - L=1 : Single value (V is 1-byte integer 0-255)
35
+ - L=2..255 : Value set (V contains L bytes as possible values)
36
+
37
+ Special Cases:
38
+ - Group 'b' (index 1) when marked SKIP uses predefined RANGE64 (0-64)
39
+ - Empty exclusion sets "!()" are prohibited
40
+
41
+ Example:
42
+ Pattern "a.1.(2-5).!().0.f" encodes as:
43
+ [00][01][01][04][02][03][04][05][00][01][00][00]
44
+ (SKIP|1|{2,3,4,5}|SKIP|0|SKIP)
45
+
46
+ Properties:
47
+ - Fixed overhead: 6 bytes (1 length byte per group)
48
+ - Max size: 6 + 255*6 = 1536 bytes
49
+ - Order-preserving
50
+ - Comparison-friendly memory layout
51
+ """
52
+ buffer: bytes
53
+
54
+ @classmethod
55
+ def parse(cls, value: str) -> Self:
56
+ buffer = bytearray()
57
+ parts = value.split('.', maxsplit=5)
58
+ if len(parts) != 6:
59
+ raise ValueError(f"got {len(parts)} elements, expected 6")
60
+ for i, val in enumerate(parts):
61
+ if val.isdigit():
62
+ num = int(val)
63
+ if 0 <= num <= 255:
64
+ buffer.extend((1, num))
65
+ continue
66
+ raise ValueError(f"Value {val} out of range 0-255")
67
+ if len(val) == 1:
68
+ if val == "x":
69
+ buffer.append(SKIP)
70
+ continue
71
+ elif (
72
+ i == 1
73
+ and val == "b"
74
+ ):
75
+ buffer.extend(RANGE64_WITH_LENGTH)
76
+ continue
77
+ elif ord(val) == i + 97:
78
+ buffer.append(SKIP)
79
+ continue
80
+ if val == "":
81
+ buffer.append(SKIP)
82
+ continue
83
+ if val[0]=='(' and val[-1]==')':
84
+ el = set()
85
+ val = val.replace('(', "").replace(')', "")
86
+ for j in val.split(","):
87
+ j = j.replace(" ", '')
88
+ match j.count('-'):
89
+ case 0:
90
+ el.add(cls.__simple_validate(j))
91
+ case 1:
92
+ start, end = j.split("-")
93
+ el.update(range(
94
+ cls.__simple_validate(start),
95
+ cls.__simple_validate(end) + 1))
96
+ case err:
97
+ raise ValueError(F"got a lot of <-> in pattern: {value}, expected one")
98
+ # values = bytes(el)
99
+ # buffer.extend([len(values)] + list(values))
100
+ buffer.append(len(el))
101
+ buffer.extend(el)
102
+ continue
103
+ if val.startswith('!(') and val.endswith(')'):
104
+ el = copy(RANGE256)
105
+ val = val.replace('!(', "").replace(')', "")
106
+ for j in val.split(","):
107
+ j = j.replace(" ", '')
108
+ match j.count('-'):
109
+ case 0:
110
+ el.discard(cls.__simple_validate(j))
111
+ case 1:
112
+ start, end = j.split("-")
113
+ el.difference_update(range(
114
+ cls.__simple_validate(start),
115
+ cls.__simple_validate(end) + 1))
116
+ case err:
117
+ raise ValueError(F"got a lot of <-> in pattern: {value}, expected one")
118
+ if len(el)==0:
119
+ raise ValueError(F"no one element in group: {chr(97 + i)}")
120
+ # values = bytes(el)
121
+ # buffer.extend([len(values)] + list(values))
122
+ buffer.append(len(el))
123
+ buffer.extend(el)
124
+ continue
125
+ raise ValueError(f"Invalid pattern: {val}")
126
+ return cls(bytes(buffer))
127
+
128
+ @staticmethod
129
+ def __simple_validate(value: str) -> int:
130
+ if value.isdigit() and (0 <= (new := int(value)) <= 255):
131
+ return new
132
+ else:
133
+ raise ValueError(F"got not valid element: {value} in pattern, expected 0..255")
134
+
135
+ def __eq__(self, other: "LNPattern") -> bool:
136
+ ptr = 0
137
+ for i in range(6):
138
+ length = self.buffer[ptr]
139
+ ptr += 1
140
+ if length == 0: # SKIP
141
+ continue
142
+ other_byte = other.contents[i]
143
+ if length == 1: # Single byte
144
+ if self.buffer[ptr]!=other_byte:
145
+ return False
146
+ else: # Multiple bytes
147
+ if other_byte not in self.buffer[ptr:ptr + length]:
148
+ return False
149
+ ptr += length
150
+ return True
151
+
152
+ @staticmethod
153
+ def _format_ranges(values: list[int]) -> str:
154
+ if not values:
155
+ return "!()"
156
+ ranges = []
157
+ start = end = values[0]
158
+ for num in values[1:]:
159
+ if num==end + 1:
160
+ end = num
161
+ else:
162
+ ranges.append((start, end))
163
+ start = end = num
164
+ ranges.append((start, end))
165
+ parts = []
166
+ for start, end in ranges:
167
+ if start==end:
168
+ parts.append(str(start))
169
+ elif end==start + 1: # Диапазон из 2 чисел
170
+ parts.extend([str(start), str(end)])
171
+ else:
172
+ parts.append(f"{start}-{end}")
173
+ if len(parts) > 3 and len(values) > 128: # Эмпирический порог
174
+ all_values = set(range(256))
175
+ excluded = sorted(all_values - set(values))
176
+ if len(excluded) < len(values):
177
+ return f"!({','.join(self._format_ranges(excluded))})"
178
+ return f"({','.join(parts)})"
179
+
180
+ def __str__(self) -> str:
181
+ parts = []
182
+ ptr = 0
183
+ for _ in range(6):
184
+ length = self.buffer[ptr]
185
+ ptr += 1
186
+ if length == 0:
187
+ parts.append("x")
188
+ elif length == 1:
189
+ parts.append(str(self.buffer[ptr]))
190
+ else:
191
+ values = sorted(set(self.buffer[ptr:ptr + length]))
192
+ parts.append(self._format_ranges(values))
193
+ ptr += length
194
+ return ".".join(parts)
195
+
196
+
197
+ @dataclass
198
+ class LNPatterns:
199
+ value: tuple[LNPattern, ...]
200
+
201
+ def __iter__(self):
202
+ return iter(self.value)
203
+
204
+ def __str__(self) -> str:
205
+ return f"[{" | ".join(map(str, self.value))}]"
206
+
207
+
208
+ ABSTRACT = LNPattern.parse("0.....")
209
+ ELECTRICITY = LNPattern.parse("1.....")
210
+ HCA = LNPattern.parse("4.....")
211
+ THERMAL = LNPattern.parse("(5,6).....")
212
+ GAS = LNPattern.parse("7.....")
213
+ WATER = LNPattern.parse("(8,9).....")
214
+ OTHER_MEDIA = LNPattern.parse("15.....")
215
+
216
+
217
+ BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES = LNPatterns((
218
+ LNPattern.parse("0.b.0.1.(0,2,3,5).f"),
219
+ LNPattern.parse("0.b.0.1.(1,4).255")))
220
+ PROGRAM_ENTRIES = LNPatterns((
221
+ ACTIVE_FIRMWARE_IDENTIFIER := LNPattern.parse("0.b.0.2.0.255"),
222
+ ACTIVE_FIRMWARE_VERSION := LNPattern.parse("0.b.0.2.1.255"),
223
+ ACTIVE_FIRMWARE_SIGNATURE := LNPattern.parse("0.b.0.2.8.255")
224
+ ))
225
+ TIME_ENTRIES = LNPattern.parse("0.b.0.9.(1,2).255")
226
+ CLOCK_OBJECTS = LNPatterns((
227
+ CLOCK := LNPattern.parse("0.b.1.0.e.255"),
228
+ UNIX_CLOCK := LNPattern.parse("0.b.1.1.e.255"),
229
+ MICROSECONDS_CLOCK := LNPattern.parse("0.b.1.2.e.255"),
230
+ MINUTES_CLOCK := LNPattern.parse("0.b.1.3.e.255"),
231
+ HOURS_CLOCK := LNPattern.parse("0.b.1.4.e.255"),
232
+ DAYS_CLOCK := LNPattern.parse("0.b.1.5.e.255"),
233
+ WEEKS_CLOCK := LNPattern.parse("0.b.1.6.e.255")
234
+ ))
235
+ TARIFFICATION_SCRIPT_TABLE = LNPattern.parse("0.b.10.0.100.255")
236
+ PUSH_SCRIPT_TABLE = LNPattern.parse("0.b.10.0.108.255")
237
+ SINGLE_ACTION_SCHEDULE = LNPatterns((
238
+ END_OF_BILLING_PERIOD_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.0.255"),
239
+ DISCONNECT_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.1.255"),
240
+ IMAGE_ACTIVATION_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.2.255"),
241
+ OUTPUT_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.3.255"),
242
+ PUSH_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.4.255"),
243
+ LOAD_PROFILE_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.5.255"),
244
+ M_BUS_PROFILE_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.6.255"),
245
+ FUNCTION_CONTROL_SINGLE_ACTION_SCHEDULE := LNPattern.parse("0.b.15.0.7.255")
246
+ ))
247
+ ACTIVITY_CALENDAR = LNPattern.parse("0.b.13.0.e.255")
248
+ ASSOCIATION = LNPattern.parse("0.0.40.0.e.255") # 6_2_33
249
+ NON_CURRENT_ASSOCIATION = LNPattern.parse("0.0.40.0.(1-255).255") # MY
250
+ SAP_ASSIGNMENT = LNPattern.parse("0.0.41.0.0.255") # 6.2.34
251
+ COSEM_logical_device_name = LNPattern.parse("0.0.42.0.0.255") # 6.2.35
252
+ INFORMATION_SECURITY_RELATED = LNPatterns((
253
+ LNPattern.parse("0.0.43.(0,2).e.255"),
254
+ INVOCATION_COUNTER := LNPattern.parse("0.b.43.1.e.255") # 6.2.36
255
+ ))
256
+ IMAGE_TRANSFER = LNPattern.parse("0.b.44.0.e.255") # 6.2.37
257
+ FUNCTION_CONTROL = LNPattern.parse("0.b.44.1.e.255") # 6.2.38
258
+ COMMUNICATION_PORT_PROTECTION = LNPattern.parse("0.b.44.2.e.255") # 6.2.39
259
+ UTILITY_TABLE = LNPattern.parse("0.b.65.(0-63).e.255") # 6.2.40
260
+ COMPACT_DATA = LNPattern.parse("0.b.66.0.e.255") # 6.2.41
261
+ DEVICE_ID = LNPattern.parse("0.b.96.1.(0,1,2,3,4,5,6,7,8,9,255).255") # 6.2.42
262
+ METERING_POINT_ID = LNPattern.parse("0.b.96.1.10.255") # 6.2.43
263
+ PARAMETER_CHANGES_CALIBRATION_AND_ACCESS = LNPattern.parse("0.b.96.2.e.f") # 6.2.44
264
+ INPUT_OUTPUT_CONTROL_SIGNALS = LNPattern.parse("0.b.96.3.(0-4).f") # 6.2.45
265
+ DISCONNECT_CONTROL = LNPattern.parse("0.b.96.3.10.f") # 6.2.46
266
+ ARBITRATOR = LNPattern.parse("0.b.96.3.(20-29).f") # 6.2.47
267
+ INTERNAL_CONTROL_SIGNALS = LNPattern.parse("0.b.96.4.(0-4).f") # 6.2.48
268
+ INTERNAL_OPERATING_STATUS = LNPattern.parse("0.b.96.5.(0-4).f") # 6.2.49
269
+ BATTERY_ENTRIES = LNPattern.parse("0.b.96.6.(0,1,2,3,4,5,6,10,11).f") # 6.2.50
270
+ POWER_FAILURE_MONITORING = LNPattern.parse("0.b.96.7.(0-21).f") # 6.2.51
271
+ OPERATING_TIME = LNPattern.parse("0.b.96.8.(0-63).f")
272
+ ENVIRONMENT_RELATED_PARAMETERS = LNPattern.parse("0.b.96.9.(0-2).f")
273
+ STATUS_REGISTER = LNPattern.parse("0.b.96.10.(1-10).f")
274
+ EVENT_CODE = LNPattern.parse("0.b.96.11.(0-99).f")
275
+ COMMUNICATION_PORT_LOG_PARAMETERS = LNPattern.parse("0.b.96.12.(0-6).f")
276
+ CONSUMER_MESSAGES = LNPattern.parse("0.b.96.13.(0,1).f")
277
+ CURRENTLY_ACTIVE_TARIFF = LNPattern.parse("0.b.96.14.(0-15).f")
278
+ EVENT_COUNTER = LNPattern.parse("0.b.96.15.(0-99).f")
279
+ PROFILE_ENTRY_DIGITAL_SIGNATURE = LNPattern.parse("0.b.96.16.(0-9).f")
280
+ PROFILE_ENTRY_COUNTER = LNPattern.parse("0.b.96.17.(0-127).f")
281
+ METER_TAMPER_EVENT_RELATED = LNPattern.parse("0.b.96.20.(0-34).f")
282
+ MANUFACTURER_SPECIFIC_ABSTRACT = LNPattern.parse("0.b.96.(50-99).e.f")
283
+
284
+ GENERAL_AND_SERVICE_ENTRY = LNPatterns((
285
+ *BILLING_PERIOD_VALUES_RESET_COUNTER_ENTRIES,
286
+ *PROGRAM_ENTRIES,
287
+ TIME_ENTRIES,
288
+ DEVICE_ID,
289
+ PARAMETER_CHANGES_CALIBRATION_AND_ACCESS,
290
+ INPUT_OUTPUT_CONTROL_SIGNALS,
291
+ INTERNAL_CONTROL_SIGNALS,
292
+ INTERNAL_OPERATING_STATUS,
293
+ BATTERY_ENTRIES,
294
+ POWER_FAILURE_MONITORING,
295
+ OPERATING_TIME,
296
+ ENVIRONMENT_RELATED_PARAMETERS,
297
+ STATUS_REGISTER,
298
+ EVENT_CODE,
299
+ COMMUNICATION_PORT_LOG_PARAMETERS,
300
+ CONSUMER_MESSAGES,
301
+ CURRENTLY_ACTIVE_TARIFF,
302
+ EVENT_COUNTER,
303
+ PROFILE_ENTRY_DIGITAL_SIGNATURE,
304
+ PROFILE_ENTRY_COUNTER,
305
+ METER_TAMPER_EVENT_RELATED,
306
+ MANUFACTURER_SPECIFIC_ABSTRACT))
307
+ """DLMS UA 1000-1 Ed. 14 7.4.1"""
308
+
309
+ LIMITER = LNPattern.parse("0.b.17.0.e.255") # 6.2.15
310
+ ALARM_REGISTER = LNPattern.parse("0.b.97.98.(0-9).255") # 6.2.64
311
+ COUNTRY_SPECIFIC_IDENTIFIERS = LNPattern.parse("0.b.94.d.e.f") # 7.3.4.3
312
+ ALARM_REGISTER_FILTER = LNPattern.parse("0.b.97.98.(10-19).255") # 6.2.64
313
+ ALARM_REGISTER_DESCRIPTOR = LNPattern.parse("0.b.97.98.(20-29).255") # 6.2.64
314
+ ALARM_REGISTER_PROFILE = LNPattern.parse("0.b.97.98.255.255") # 6.2.64
315
+ ALARM_REGISTER_TABLE = LNPattern.parse("0.b.97.98.255.255") # 6.2.64
316
+ ALARM_REGISTER_FILTER_DESCRIPTOR = LNPatterns((ALARM_REGISTER, ALARM_REGISTER_FILTER, ALARM_REGISTER_DESCRIPTOR, ALARM_REGISTER_PROFILE))
317
+ # electricity
318
+ ID_NUMBERS_ELECTRICITY = LNPattern.parse("1.b.0.0.(0-9).255")
319
+ ELECTRIC_PROGRAM_ENTRIES = LNPattern.parse("1.b.0.2.e.255")
320
+ OUTPUT_PULSE_VALUES_OR_CONSTANTS = LNPattern.parse("1.0.0.3.(0-9).255")
321
+ RATIOS = LNPattern.parse("1.0.0.4.(0-7).255")
322
+ RECORDING_INTERVAL = LNPattern.parse("1.0.0.8.(4,5).255")
323
+ OTHER_ELECTRICITY_RELATED_GENERAL_PURPOSE = LNPattern.parse("1.b.0.(2,3,4,6,7,8,9,10).e.255")
324
+ # my special
325
+ COUNTRY_SPECIFIC = LNPattern.parse("a.b.94.d.e.f") # 7.2.4 Table 54
326
+
327
+ # 7.5.2.1
328
+ INSTANTANEOUS_VALUE_EL_REL = LNPattern.parse("1.b.!(0,93,94,96,97,98,99).7.e.f")
329
+ ELECTRICITY_RELATED = LNPatterns((
330
+ INSTANTANEOUS_VALUE_EL_REL,
331
+ # todo: more other LNPatterns
332
+ ))
333
+ """7.5.2.1 Processing of measurement values Table 66"""