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.
- DLMS_SPODES/Values/EN/__init__.py +1 -1
- DLMS_SPODES/Values/EN/actors.py +8 -8
- DLMS_SPODES/Values/EN/relation_to_obis_names.py +387 -387
- DLMS_SPODES/Values/RU/__init__.py +1 -1
- DLMS_SPODES/Values/RU/actors.py +8 -8
- DLMS_SPODES/Values/RU/relation_to_obis_names.py +396 -396
- DLMS_SPODES/__init__.py +6 -6
- DLMS_SPODES/configEN.ini +126 -126
- DLMS_SPODES/config_parser.py +53 -53
- DLMS_SPODES/cosem_interface_classes/Overview/__init__.py +0 -0
- DLMS_SPODES/cosem_interface_classes/Overview/class_id.py +107 -0
- DLMS_SPODES/cosem_interface_classes/__class_init__.py +3 -3
- DLMS_SPODES/cosem_interface_classes/__init__.py +3 -2
- DLMS_SPODES/cosem_interface_classes/activity_calendar.py +210 -254
- DLMS_SPODES/cosem_interface_classes/arbitrator.py +78 -105
- DLMS_SPODES/cosem_interface_classes/association_ln/abstract.py +50 -34
- DLMS_SPODES/cosem_interface_classes/association_ln/authentication_mechanism_name.py +25 -25
- DLMS_SPODES/cosem_interface_classes/association_ln/mechanism_id.py +25 -25
- DLMS_SPODES/cosem_interface_classes/association_ln/method.py +5 -5
- DLMS_SPODES/cosem_interface_classes/association_ln/ver0.py +440 -485
- DLMS_SPODES/cosem_interface_classes/association_ln/ver1.py +126 -133
- DLMS_SPODES/cosem_interface_classes/association_ln/ver2.py +30 -36
- DLMS_SPODES/cosem_interface_classes/association_ln/ver3.py +3 -4
- DLMS_SPODES/cosem_interface_classes/association_sn/ver0.py +14 -12
- DLMS_SPODES/cosem_interface_classes/clock.py +81 -131
- DLMS_SPODES/cosem_interface_classes/collection.py +2106 -2122
- DLMS_SPODES/cosem_interface_classes/cosem_interface_class.py +525 -583
- DLMS_SPODES/cosem_interface_classes/data.py +12 -21
- DLMS_SPODES/cosem_interface_classes/demand_register/ver0.py +32 -59
- DLMS_SPODES/cosem_interface_classes/disconnect_control.py +56 -74
- DLMS_SPODES/cosem_interface_classes/extended_register.py +18 -27
- DLMS_SPODES/cosem_interface_classes/gprs_modem_setup.py +33 -43
- DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver0.py +78 -103
- DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver1.py +42 -40
- DLMS_SPODES/cosem_interface_classes/gsm_diagnostic/ver2.py +6 -9
- DLMS_SPODES/cosem_interface_classes/iec_hdlc_setup/ver0.py +11 -11
- DLMS_SPODES/cosem_interface_classes/iec_hdlc_setup/ver1.py +27 -53
- DLMS_SPODES/cosem_interface_classes/iec_local_port_setup.py +9 -11
- DLMS_SPODES/cosem_interface_classes/image_transfer/image_transfer_status.py +15 -15
- DLMS_SPODES/cosem_interface_classes/image_transfer/ver0.py +54 -126
- DLMS_SPODES/cosem_interface_classes/implementations/__init__.py +3 -3
- DLMS_SPODES/cosem_interface_classes/implementations/arbitrator.py +19 -19
- DLMS_SPODES/cosem_interface_classes/implementations/data.py +491 -487
- DLMS_SPODES/cosem_interface_classes/implementations/profile_generic.py +85 -83
- DLMS_SPODES/cosem_interface_classes/ipv4_setup.py +42 -72
- DLMS_SPODES/cosem_interface_classes/limiter.py +77 -111
- DLMS_SPODES/cosem_interface_classes/ln_pattern.py +334 -333
- DLMS_SPODES/cosem_interface_classes/modem_configuration/ver0.py +51 -65
- DLMS_SPODES/cosem_interface_classes/modem_configuration/ver1.py +27 -39
- DLMS_SPODES/cosem_interface_classes/ntp_setup/ver0.py +48 -67
- DLMS_SPODES/cosem_interface_classes/obis.py +28 -23
- DLMS_SPODES/cosem_interface_classes/overview.py +198 -197
- DLMS_SPODES/cosem_interface_classes/parameter.py +548 -547
- DLMS_SPODES/cosem_interface_classes/parameters.py +172 -172
- DLMS_SPODES/cosem_interface_classes/profile_generic/ver0.py +90 -122
- DLMS_SPODES/cosem_interface_classes/profile_generic/ver1.py +268 -277
- DLMS_SPODES/cosem_interface_classes/push_setup/ver0.py +13 -12
- DLMS_SPODES/cosem_interface_classes/push_setup/ver1.py +9 -10
- DLMS_SPODES/cosem_interface_classes/push_setup/ver2.py +124 -166
- DLMS_SPODES/cosem_interface_classes/register.py +18 -45
- DLMS_SPODES/cosem_interface_classes/register_activation/ver0.py +45 -80
- DLMS_SPODES/cosem_interface_classes/register_monitor.py +33 -46
- DLMS_SPODES/cosem_interface_classes/reports.py +72 -70
- DLMS_SPODES/cosem_interface_classes/schedule.py +88 -176
- DLMS_SPODES/cosem_interface_classes/script_table.py +54 -87
- DLMS_SPODES/cosem_interface_classes/security_setup/ver0.py +45 -68
- DLMS_SPODES/cosem_interface_classes/security_setup/ver1.py +122 -158
- DLMS_SPODES/cosem_interface_classes/single_action_schedule.py +34 -50
- DLMS_SPODES/cosem_interface_classes/special_days_table.py +54 -84
- DLMS_SPODES/cosem_interface_classes/tcp_udp_setup.py +20 -42
- DLMS_SPODES/cosem_pdu.py +93 -93
- DLMS_SPODES/enums.py +625 -625
- DLMS_SPODES/exceptions.py +106 -106
- DLMS_SPODES/firmwares.py +99 -99
- DLMS_SPODES/hdlc/frame.py +875 -875
- DLMS_SPODES/hdlc/sub_layer.py +54 -54
- DLMS_SPODES/literals.py +17 -17
- DLMS_SPODES/obis/__init__.py +1 -1
- DLMS_SPODES/obis/media_id.py +931 -931
- DLMS_SPODES/pardata.py +22 -22
- DLMS_SPODES/pdu_enums.py +98 -98
- DLMS_SPODES/relation_to_OBIS.py +463 -465
- DLMS_SPODES/settings.py +551 -551
- DLMS_SPODES/types/choices.py +140 -142
- DLMS_SPODES/types/common_data_types.py +2379 -2401
- DLMS_SPODES/types/cosem_service_types.py +109 -109
- DLMS_SPODES/types/implementations/arrays.py +25 -25
- DLMS_SPODES/types/implementations/bitstrings.py +97 -97
- DLMS_SPODES/types/implementations/double_long_usingneds.py +35 -35
- DLMS_SPODES/types/implementations/enums.py +57 -57
- DLMS_SPODES/types/implementations/integers.py +12 -11
- DLMS_SPODES/types/implementations/long_unsigneds.py +127 -127
- DLMS_SPODES/types/implementations/octet_string.py +11 -11
- DLMS_SPODES/types/implementations/structs.py +64 -64
- DLMS_SPODES/types/type_alias.py +74 -0
- DLMS_SPODES/types/useful_types.py +627 -677
- {dlms_spodes-0.87.16.dist-info → dlms_spodes-0.88.1.dist-info}/METADATA +30 -30
- dlms_spodes-0.88.1.dist-info/RECORD +118 -0
- {dlms_spodes-0.87.16.dist-info → dlms_spodes-0.88.1.dist-info}/WHEEL +1 -1
- DLMS_SPODES/cosem_interface_classes/a_parameter.py +0 -20
- DLMS_SPODES/cosem_interface_classes/attr_indexes.py +0 -12
- dlms_spodes-0.87.16.dist-info/RECORD +0 -117
- {dlms_spodes-0.87.16.dist-info → dlms_spodes-0.88.1.dist-info}/top_level.txt +0 -0
|
@@ -1,2402 +1,2380 @@
|
|
|
1
|
-
import struct
|
|
2
|
-
from copy import copy
|
|
3
|
-
from itertools import chain, count
|
|
4
|
-
import inspect
|
|
5
|
-
import re
|
|
6
|
-
from dataclasses import dataclass, field
|
|
7
|
-
from struct import pack, unpack
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
from semver import Version as SemVer
|
|
16
|
-
from ..config_parser import config, get_values
|
|
17
|
-
from .. import config_parser
|
|
18
|
-
from .. import exceptions as exc
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
NAMES
|
|
80
|
-
|
|
81
|
-
cls.NAMES
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return pack(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
def
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def
|
|
237
|
-
"""
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def
|
|
241
|
-
"""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
class
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
def
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
class
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
def
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
def
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
def
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
def
|
|
741
|
-
self.contents
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
):
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
):
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
case
|
|
834
|
-
case
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
case
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
case '' | '
|
|
873
|
-
case
|
|
874
|
-
case '
|
|
875
|
-
case '
|
|
876
|
-
case
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
case
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
return
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
def
|
|
1003
|
-
nonlocal
|
|
1004
|
-
match
|
|
1005
|
-
case '' | '_' | '__':
|
|
1006
|
-
case _ if
|
|
1007
|
-
case _:
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
case
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
def
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
self.
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
def
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
self.
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
@property
|
|
1217
|
-
def
|
|
1218
|
-
return self.values[
|
|
1219
|
-
|
|
1220
|
-
@property
|
|
1221
|
-
def
|
|
1222
|
-
return self.values[
|
|
1223
|
-
|
|
1224
|
-
@property
|
|
1225
|
-
def
|
|
1226
|
-
return self.values[
|
|
1227
|
-
|
|
1228
|
-
@property
|
|
1229
|
-
def
|
|
1230
|
-
return self.values[
|
|
1231
|
-
|
|
1232
|
-
@property
|
|
1233
|
-
def
|
|
1234
|
-
return self.values[
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
def
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
def
|
|
1314
|
-
for
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
self.
|
|
1336
|
-
|
|
1337
|
-
def
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
case
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
def
|
|
1454
|
-
return
|
|
1455
|
-
|
|
1456
|
-
def
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
def
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
def
|
|
1474
|
-
return
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
match value:
|
|
1505
|
-
case
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
def
|
|
1520
|
-
self.__length = value
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
def
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
def
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
def
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
@
|
|
1638
|
-
def
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
return
|
|
1653
|
-
|
|
1654
|
-
def
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
return bytes(
|
|
1673
|
-
|
|
1674
|
-
def
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
def
|
|
1686
|
-
return
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
def
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
def
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
def
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
def
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
def
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
def
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
def
|
|
2157
|
-
return self.
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
def
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
def
|
|
2199
|
-
return
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
"""
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
b'\
|
|
2259
|
-
b'\
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
VisibleString,
|
|
2381
|
-
Utf8String,
|
|
2382
|
-
Bcd,
|
|
2383
|
-
Integer,
|
|
2384
|
-
Long,
|
|
2385
|
-
Unsigned,
|
|
2386
|
-
LongUnsigned,
|
|
2387
|
-
Long64,
|
|
2388
|
-
Long64Unsigned,
|
|
2389
|
-
Enum,
|
|
2390
|
-
Float32,
|
|
2391
|
-
Float64,
|
|
2392
|
-
DateTime,
|
|
2393
|
-
Date,
|
|
2394
|
-
Time,
|
|
2395
|
-
# more
|
|
2396
|
-
)
|
|
2397
|
-
|
|
2398
|
-
ComplexDataTypes: tuple[CommonDataType, ...] = (
|
|
2399
|
-
Array,
|
|
2400
|
-
Structure,
|
|
2401
|
-
CompactArray
|
|
1
|
+
import struct
|
|
2
|
+
from copy import copy
|
|
3
|
+
from itertools import chain, count
|
|
4
|
+
import inspect
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from struct import pack, unpack
|
|
8
|
+
from typing import Any, Callable, TypeAlias, Self, Optional, Iterator, Protocol, Never, runtime_checkable
|
|
9
|
+
from typing_extensions import deprecated
|
|
10
|
+
from collections import deque
|
|
11
|
+
from math import log, ceil
|
|
12
|
+
import datetime
|
|
13
|
+
import logging
|
|
14
|
+
from StructResult import result
|
|
15
|
+
from semver import Version as SemVer
|
|
16
|
+
from ..config_parser import config, get_values
|
|
17
|
+
from .. import config_parser
|
|
18
|
+
from .. import exceptions as exc
|
|
19
|
+
from ..types.type_alias import Encoding
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CDTError(exc.DLMSException):
|
|
23
|
+
"""common error for CDT"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OutOfRange(CDTError):
|
|
27
|
+
"""out of range for CommonDataType"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ValidationError(CDTError):
|
|
31
|
+
"""CommonDataType value not valid"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ParseError(CDTError):
|
|
35
|
+
"""can't parse transcription"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Log:
|
|
40
|
+
lev: int = logging.INFO
|
|
41
|
+
msg: str | Exception = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Report:
|
|
46
|
+
msg: str
|
|
47
|
+
unit: str = None
|
|
48
|
+
log: Log = field(default_factory=Log)
|
|
49
|
+
|
|
50
|
+
def __str__(self):
|
|
51
|
+
return self.msg
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
START_LOG = Log(logging.ERROR, "can't report")
|
|
55
|
+
INFO_LOG = Log(logging.INFO)
|
|
56
|
+
EMPTY_VAL = Log(logging.WARN, "empty value")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ReportMixin:
|
|
60
|
+
"""mixin for cdt"""
|
|
61
|
+
def get_report(self) -> Report:
|
|
62
|
+
"""custom string represent"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
type Message = str
|
|
66
|
+
type Number = int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class IntegerEnum(ReportMixin):
|
|
70
|
+
"""value with represent __int__ to string"""
|
|
71
|
+
NAMES: dict[Number, Message] = None # todo: make with ChainMap or more better
|
|
72
|
+
|
|
73
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
74
|
+
"""initiate NAMES name use config.toml"""
|
|
75
|
+
# todo: copypast from IntegerFlag, make better with no copy(use parent dict), maybe ChainMap
|
|
76
|
+
NAMES = {int(k): v for k, v in class_names.items()} if (class_names := get_values("DLMS", "enum_name", F"{cls.__name__}")) else dict()
|
|
77
|
+
if not cls.NAMES: # todo: make check <is None> in future, after remove defaul <dict()>
|
|
78
|
+
cls.NAMES = NAMES
|
|
79
|
+
elif NAMES: # expand
|
|
80
|
+
cls.NAMES = copy(cls.NAMES)
|
|
81
|
+
cls.NAMES.update(NAMES)
|
|
82
|
+
|
|
83
|
+
def get_report(self) -> Report:
|
|
84
|
+
l = INFO_LOG
|
|
85
|
+
msg = F"({self})"
|
|
86
|
+
if name := self.NAMES.get(int(self)):
|
|
87
|
+
msg += F" {name}"
|
|
88
|
+
else:
|
|
89
|
+
l = Log(logging.WARN, "unknown value")
|
|
90
|
+
return Report(msg, log=l)
|
|
91
|
+
|
|
92
|
+
def get_name(self) -> str:
|
|
93
|
+
return self.NAMES.get(int(self), "??")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# TODO: rewrite with Cython
|
|
97
|
+
def separate(value: str, pattern: str, max_sep: int) -> tuple[str, list[str]]:
|
|
98
|
+
""" separating string to container by pattern. Use in Date and Time """
|
|
99
|
+
paths = []
|
|
100
|
+
separators = path = ''
|
|
101
|
+
while len(value) != 0:
|
|
102
|
+
if value[0] in pattern:
|
|
103
|
+
paths.append(path)
|
|
104
|
+
separators += value[0]
|
|
105
|
+
if len(separators) == max_sep:
|
|
106
|
+
paths.append(value[1:])
|
|
107
|
+
break
|
|
108
|
+
else:
|
|
109
|
+
path = ''
|
|
110
|
+
elif value[0] == ' ':
|
|
111
|
+
paths.append(path)
|
|
112
|
+
separators += value[0]
|
|
113
|
+
paths.append(value[1:])
|
|
114
|
+
break
|
|
115
|
+
else:
|
|
116
|
+
path += value[0]
|
|
117
|
+
value = value[1:]
|
|
118
|
+
else:
|
|
119
|
+
paths.append(path)
|
|
120
|
+
return separators, paths
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def encode_length(length: int) -> bytes:
|
|
124
|
+
""" convert int to ASN.1 format """
|
|
125
|
+
if length < 0x80:
|
|
126
|
+
return length.to_bytes(1, "big")
|
|
127
|
+
elif length < 0x1_00:
|
|
128
|
+
return pack("BB", 0x81, length)
|
|
129
|
+
elif length < 0x1_00_00:
|
|
130
|
+
return pack(">BH", 0x82, length)
|
|
131
|
+
elif length < 0x1_00_00_00_00:
|
|
132
|
+
return pack(">BL", 0x84, length)
|
|
133
|
+
else:
|
|
134
|
+
amount = int(log(length, 256)) + 1
|
|
135
|
+
return pack('B', 0x80 + amount) + length.to_bytes(amount, byteorder='big')
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_length_and_pdu(input_pdu: bytes) -> tuple[int, bytes]:
|
|
139
|
+
""" return Tuple[length, pdu] from value by decoding according to 8.1.3 Length octets ITU-T Rec. X.690 (07/2002) """
|
|
140
|
+
content_start: int = 1
|
|
141
|
+
""" start contents index without length """
|
|
142
|
+
try:
|
|
143
|
+
define_length = input_pdu[0]
|
|
144
|
+
except IndexError:
|
|
145
|
+
raise ValueError('Value is empty')
|
|
146
|
+
if bool(define_length & 0b10000000):
|
|
147
|
+
content_start += define_length - 0x80
|
|
148
|
+
length = int.from_bytes(input_pdu[1:content_start], 'big')
|
|
149
|
+
else:
|
|
150
|
+
length = define_length
|
|
151
|
+
pdu = input_pdu[content_start:]
|
|
152
|
+
return length, pdu
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_type_names = config["DLMS"]["type_name"]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TAG(bytes):
|
|
159
|
+
def __str__(self) -> str:
|
|
160
|
+
name = str(int.from_bytes(self, "big"))
|
|
161
|
+
if _type_names and (t := _type_names.get(name)):
|
|
162
|
+
return t
|
|
163
|
+
else:
|
|
164
|
+
return F"{self.__class__.__name__}({name})"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def call_wrong_tag_in_value(value: bytes, expected: TAG) -> Never:
|
|
168
|
+
raise ValueError(F"can't create {expected} with value {value}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
Transcript: TypeAlias = str | list[Self]
|
|
172
|
+
"""represent of CDT contents by string/list values"""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@runtime_checkable
|
|
176
|
+
class CommonDataType(Protocol):
|
|
177
|
+
""" DLMS BlueBook(IEC 62056-6-2) 13.0 4.1.5 Common data types . X.690: OSI networking and system aspects – Abstract Syntax Notation One (ASN.1) """
|
|
178
|
+
cb_post_set: Callable
|
|
179
|
+
cb_preset: Callable
|
|
180
|
+
contents: bytes
|
|
181
|
+
TAG: TAG = None
|
|
182
|
+
""" 62056-53 8.3 TypeDescription ::= CHOICE. Set at once, no supported change """
|
|
183
|
+
SIZE: int = None
|
|
184
|
+
MIN: int
|
|
185
|
+
MAX: int
|
|
186
|
+
|
|
187
|
+
def __init__(self, value=None) -> None:
|
|
188
|
+
""" constructor """
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def from_encoding(cls, encoding: Encoding) -> result.SimpleOrError["CommonDataType"]:
|
|
192
|
+
try:
|
|
193
|
+
new = cls(encoding)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return result.Error.from_e(e)
|
|
196
|
+
return result.Simple(new)
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def encoding(self) -> bytes:
|
|
200
|
+
""" The complete sequence of octets used to represent the data value. """
|
|
201
|
+
|
|
202
|
+
def __eq__(self, other: "CommonDataType") -> bool:
|
|
203
|
+
if not isinstance(other, CommonDataType):
|
|
204
|
+
return NotImplemented
|
|
205
|
+
return self.encoding == other.encoding
|
|
206
|
+
|
|
207
|
+
def set(self, value: Self | bytes | bytearray | str | int | bool | float | datetime.date | None) -> None:
|
|
208
|
+
""" get new instance from value and set to content with validation """
|
|
209
|
+
|
|
210
|
+
def validate(self) -> None:
|
|
211
|
+
"""override validation if need"""
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def get_types(cls) -> Self:
|
|
215
|
+
""" return DLMS type """
|
|
216
|
+
return cls
|
|
217
|
+
|
|
218
|
+
def __copy__(self) -> Self:
|
|
219
|
+
return self.__class__(self.encoding)
|
|
220
|
+
|
|
221
|
+
@deprecated("use __copy__")
|
|
222
|
+
def copy(self) -> Self:
|
|
223
|
+
""" return copy of object """
|
|
224
|
+
return self.__class__(self.encoding)
|
|
225
|
+
|
|
226
|
+
def get_copy(self, value: Self | bytes | bytearray | str | int | bool | float | datetime.date | None) -> Self:
|
|
227
|
+
"""return copy with value setting"""
|
|
228
|
+
new = self.copy()
|
|
229
|
+
new.set(value)
|
|
230
|
+
return new
|
|
231
|
+
|
|
232
|
+
def to_str(self) -> str:
|
|
233
|
+
""" represent value as string """
|
|
234
|
+
raise ValueError(F'to_str method not support for {self.TAG}')
|
|
235
|
+
|
|
236
|
+
def __int__(self) -> int:
|
|
237
|
+
""" represent value as build-in integer """
|
|
238
|
+
raise ValueError(F'to_int method not support for {self.TAG}')
|
|
239
|
+
|
|
240
|
+
def __bytes__(self) -> bytes:
|
|
241
|
+
""" represent value as string """
|
|
242
|
+
raise ValueError(F'to_bytes method not support for {self.TAG}')
|
|
243
|
+
|
|
244
|
+
# TODO: work not in all types. Solve it
|
|
245
|
+
def __repr__(self) -> str:
|
|
246
|
+
return F'{self.__class__.__name__}({self})'
|
|
247
|
+
|
|
248
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
249
|
+
"""initiate type.NAME use config.toml"""
|
|
250
|
+
if isinstance(tag := kwargs.get("tag"), int):
|
|
251
|
+
cls.TAG = TAG(tag.to_bytes(1, "big"))
|
|
252
|
+
if size := kwargs.get("size"):
|
|
253
|
+
cls.SIZE = size
|
|
254
|
+
|
|
255
|
+
def __hash__(self) -> int:
|
|
256
|
+
return int.from_bytes(self.encoding, "big")
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def parse(cls, value: Transcript) -> Self:
|
|
260
|
+
"""new instance from from Transcript"""
|
|
261
|
+
|
|
262
|
+
def to_transcript(self) -> Transcript:
|
|
263
|
+
"""inverse of parse"""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def get_type_name(value: CommonDataType | type[CommonDataType]) -> str:
|
|
267
|
+
"""type name from type or instance of CDT with length and constant value"""
|
|
268
|
+
if isinstance(value, CommonDataType):
|
|
269
|
+
value = value.__class__
|
|
270
|
+
ret = F"{value.TAG}"
|
|
271
|
+
if value.SIZE is not None:
|
|
272
|
+
ret += F"[{value.SIZE}]"
|
|
273
|
+
elif issubclass(value, Digital) and value.VALUE is not None:
|
|
274
|
+
ret += F"({value.VALUE})"
|
|
275
|
+
elif issubclass(value, Structure):
|
|
276
|
+
ret += F"[{len(value.ELEMENTS)}]"
|
|
277
|
+
return ret
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def get_common_data_type_from(tag: bytes) -> type[CommonDataType]:
|
|
281
|
+
""" search and get class from tag if existed """
|
|
282
|
+
try:
|
|
283
|
+
return __types[tag[:1]]
|
|
284
|
+
except KeyError:
|
|
285
|
+
raise ValueError(F'type with tag:{tag[:1]} is absence in Common Data Type')
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def get_instance_and_pdu(meta: type[CommonDataType], value: Encoding) -> tuple[CommonDataType, Encoding]:
|
|
289
|
+
new = meta(value)
|
|
290
|
+
return new, value[len(new.encoding):]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_instance_and_pdu_from_value(value: bytes | bytearray) -> tuple[CommonDataType, bytes]:
|
|
294
|
+
instance = get_common_data_type_from(value[:1])(value)
|
|
295
|
+
try: # TODO: remove it in future
|
|
296
|
+
return instance, value[len(instance.encoding):]
|
|
297
|
+
except Exception as e:
|
|
298
|
+
print(F'{e.args}')
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@runtime_checkable
|
|
302
|
+
class SimpleDataType(CommonDataType, Protocol):
|
|
303
|
+
|
|
304
|
+
def _new_instance(self, value) -> Self:
|
|
305
|
+
return self.__class__(value)
|
|
306
|
+
|
|
307
|
+
def set(self, value: Self | bytes | bytearray | str | int | bool | float | datetime.date | None) -> None:
|
|
308
|
+
new_value = self._new_instance(value)
|
|
309
|
+
if hasattr(self, 'cb_preset'):
|
|
310
|
+
self.cb_preset(new_value)
|
|
311
|
+
# self.__dict__['contents'] = new_value.contents
|
|
312
|
+
self.contents = new_value.contents
|
|
313
|
+
if hasattr(self, 'cb_post_set'):
|
|
314
|
+
self.cb_post_set()
|
|
315
|
+
|
|
316
|
+
def to_transcript(self) -> str:
|
|
317
|
+
return str(self)
|
|
318
|
+
|
|
319
|
+
def __str__(self) -> str:
|
|
320
|
+
...
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class ConstantMixin:
|
|
324
|
+
"""override set method for SimpleDataType"""
|
|
325
|
+
def set(self, *args, **kwargs) -> None:
|
|
326
|
+
raise AttributeError(F"not support <set> for {self.__class__.__name__} constant")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@runtime_checkable
|
|
330
|
+
class ComplexDataType(CommonDataType, Protocol):
|
|
331
|
+
values: list[CommonDataType, ...]
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def contents(self) -> bytes:
|
|
335
|
+
""" ITU-T Rec. X.690 8.1.1 Structure of an encoding """
|
|
336
|
+
return b"".join(map(lambda el: el.encoding, self.values))
|
|
337
|
+
|
|
338
|
+
def __len__(self) -> int:
|
|
339
|
+
""" elements amount """
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def encoding(self) -> bytes:
|
|
343
|
+
""" The complete sequence of octets used to represent the data value. """
|
|
344
|
+
return self.TAG + encode_length(len(self.values)) + self.contents
|
|
345
|
+
|
|
346
|
+
def to_transcript(self) -> Transcript:
|
|
347
|
+
el: CommonDataType
|
|
348
|
+
return [el.to_transcript() for el in self]
|
|
349
|
+
|
|
350
|
+
def __iter__(self) -> Iterator[Any]: ...
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class _Array(Protocol):
|
|
354
|
+
TYPE: type[CommonDataType]
|
|
355
|
+
values: list[CommonDataType]
|
|
356
|
+
|
|
357
|
+
def remove(self, element: CommonDataType) -> None:
|
|
358
|
+
if isinstance(element, self.TYPE):
|
|
359
|
+
self.values.remove(element)
|
|
360
|
+
|
|
361
|
+
def insert(self, index: int, element: CommonDataType) -> None:
|
|
362
|
+
if isinstance(element, self.TYPE):
|
|
363
|
+
self.values.insert(index, element)
|
|
364
|
+
|
|
365
|
+
def pop(self, index: int | None = None) -> CommonDataType:
|
|
366
|
+
return self.values.pop(index)
|
|
367
|
+
|
|
368
|
+
def __len__(self) -> int:
|
|
369
|
+
return len(self.values)
|
|
370
|
+
|
|
371
|
+
def clear(self) -> None:
|
|
372
|
+
self.values.clear()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class _String(Protocol):
|
|
376
|
+
contents: bytes
|
|
377
|
+
TAG: TAG
|
|
378
|
+
DEFAULT: bytes = b''
|
|
379
|
+
SIZE: Optional[int] = None
|
|
380
|
+
|
|
381
|
+
def __init__(self, value: bytes | bytearray | str | int | SimpleDataType = None) -> None:
|
|
382
|
+
match value:
|
|
383
|
+
case None: self.contents = self.DEFAULT
|
|
384
|
+
case bytes() as encoding:
|
|
385
|
+
length, pdu = get_length_and_pdu(encoding[1:])
|
|
386
|
+
match encoding[:1]:
|
|
387
|
+
case self.TAG if length <= len(pdu):
|
|
388
|
+
self.contents = pdu[:length]
|
|
389
|
+
case self.TAG:
|
|
390
|
+
raise ValueError(F'Length is {length}, but contents got only {len(pdu)}')
|
|
391
|
+
case _:
|
|
392
|
+
raise ValueError(F"init {self.__class__.__name__} got {TAG(encoding[:1])}, expected {self.TAG}")
|
|
393
|
+
case bytearray(): self.contents = bytes(value) # Attention!!! changed method content getting from bytearray
|
|
394
|
+
case str(): self.contents = self.from_str(value)
|
|
395
|
+
case int(): self.contents = self.from_int(value)
|
|
396
|
+
case SimpleDataType(): self.contents = value.contents
|
|
397
|
+
case _: raise ValueError(F'Error create {self.TAG} with value {value}')
|
|
398
|
+
self.validation()
|
|
399
|
+
|
|
400
|
+
def validation(self) -> None:
|
|
401
|
+
""" do any thing """
|
|
402
|
+
if self.SIZE and len(self.contents) != self.SIZE:
|
|
403
|
+
raise ValueError(F'Length of {self.__class__.__name__} must be {self.SIZE}, but got {len(self.contents)}: {self.contents.hex()}')
|
|
404
|
+
|
|
405
|
+
def __len__(self) -> int:
|
|
406
|
+
""" define in subclasses """
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def encoding(self) -> bytes:
|
|
410
|
+
return self.TAG + encode_length(len(self)) + self.contents
|
|
411
|
+
|
|
412
|
+
def clear(self) -> None:
|
|
413
|
+
self.__dict__['contents'] = self.DEFAULT
|
|
414
|
+
|
|
415
|
+
def __bytes__(self) -> bytes:
|
|
416
|
+
return self.contents
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@runtime_checkable
|
|
420
|
+
class Digital(SimpleDataType, Protocol):
|
|
421
|
+
""" Default value is 0 """
|
|
422
|
+
SIGNED: bool
|
|
423
|
+
LENGTH: int
|
|
424
|
+
DEFAULT = None
|
|
425
|
+
VALUE: int | None = None
|
|
426
|
+
"""integer if is it constant value"""
|
|
427
|
+
|
|
428
|
+
def __init__(self, value: bytes | bytearray | str | int | float | Self = None) -> None:
|
|
429
|
+
if value is None:
|
|
430
|
+
value = self.DEFAULT
|
|
431
|
+
match value:
|
|
432
|
+
case bytes():
|
|
433
|
+
length_and_contents = value[1:]
|
|
434
|
+
match value[:1]:
|
|
435
|
+
case self.TAG if self.LENGTH <= len(length_and_contents): self.contents = length_and_contents[:self.LENGTH]
|
|
436
|
+
case self.TAG: raise ValueError(F'Length of contents for {self.TAG} must be at least '
|
|
437
|
+
F'{self.LENGTH}, but got {len(length_and_contents)}')
|
|
438
|
+
case _ as wrong_tag: raise ValueError(F'Expected {self.TAG} type, got {TAG(wrong_tag)}')
|
|
439
|
+
case bytearray(): self.contents = bytes(value) # Attention!!! changed method content getting from bytearray
|
|
440
|
+
case str('-') if self.SIGNED: self.contents = bytes(self.LENGTH)
|
|
441
|
+
case int() | float(): self.contents = self.from_int(value)
|
|
442
|
+
case str(): self.contents = self.from_str(value)
|
|
443
|
+
case None: self.contents = bytes(self.LENGTH)
|
|
444
|
+
case self.__class__(): self.contents = value.contents
|
|
445
|
+
case _: raise ValueError(F'Error create {self.TAG} with value: {value}')
|
|
446
|
+
self.validate()
|
|
447
|
+
|
|
448
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
449
|
+
"""initiate type.VALUE from subclass arg"""
|
|
450
|
+
cls.VALUE = kwargs.get("value")
|
|
451
|
+
if isinstance(cls.VALUE, int):
|
|
452
|
+
"""nothing"""
|
|
453
|
+
else:
|
|
454
|
+
cls.MIN = kwargs.get("min")
|
|
455
|
+
cls.MAX = kwargs.get("max")
|
|
456
|
+
if isinstance(cls.MIN, int) or isinstance(cls.MAX, int):
|
|
457
|
+
if cls.MIN is not None:
|
|
458
|
+
cls.DEFAULT = max(0, cls.MIN)
|
|
459
|
+
else:
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
def validate(self) -> None:
|
|
463
|
+
""" receiving contents validate. override it if need """
|
|
464
|
+
if isinstance(self.VALUE, int) and int(self) != self.VALUE:
|
|
465
|
+
raise ValueError(F"for {self.TAG} got value: {int(self)}, expected {self.VALUE}")
|
|
466
|
+
if isinstance(self.MIN, int) and self.MIN > int(self):
|
|
467
|
+
raise ValueError(F"out of range {self.TAG}, got {int(self)} expected more than {self.MIN}")
|
|
468
|
+
if isinstance(self.MAX, int) and int(self) > self.MAX:
|
|
469
|
+
raise ValueError(F'out of range {self.TAG}, got {int(self)} expected less than {self.MAX}')
|
|
470
|
+
|
|
471
|
+
def _new_instance(self, value) -> Self:
|
|
472
|
+
""" override SimpleDataType for send scaler_unit . use only for check and send contents """
|
|
473
|
+
return self.__class__(value)
|
|
474
|
+
|
|
475
|
+
@classmethod
|
|
476
|
+
def from_int(cls, value: int | float) -> bytes:
|
|
477
|
+
try:
|
|
478
|
+
return int(value).to_bytes(
|
|
479
|
+
length=cls.LENGTH,
|
|
480
|
+
byteorder="big",
|
|
481
|
+
signed=cls.SIGNED)
|
|
482
|
+
except OverflowError:
|
|
483
|
+
raise ValueError(F'value {value} out of range')
|
|
484
|
+
|
|
485
|
+
@classmethod
|
|
486
|
+
def parse(cls, value: str) -> Self:
|
|
487
|
+
return cls(bytearray(cls.from_int(float(value))))
|
|
488
|
+
|
|
489
|
+
def from_str(self, value: str) -> bytes:
|
|
490
|
+
return self.from_int(float(value))
|
|
491
|
+
|
|
492
|
+
def clear(self) -> None:
|
|
493
|
+
if self.DEFAULT:
|
|
494
|
+
self.__dict__['contents'] = self.__class__(self.DEFAULT).contents
|
|
495
|
+
else:
|
|
496
|
+
self.__dict__['contents'] = bytes(self.LENGTH)
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def encoding(self) -> bytes:
|
|
500
|
+
return self.TAG + self.contents
|
|
501
|
+
|
|
502
|
+
def __int__(self) -> int:
|
|
503
|
+
return int.from_bytes(self.contents, 'big', signed=self.SIGNED)
|
|
504
|
+
|
|
505
|
+
def __lshift__(self, other: int) -> None:
|
|
506
|
+
for i in range(other):
|
|
507
|
+
tmp = int.from_bytes(self.contents, "big")
|
|
508
|
+
tmp <<= 1
|
|
509
|
+
tmp &= 0x100**self.LENGTH - 1
|
|
510
|
+
self.__dict__["contents"] = tmp.to_bytes(self.LENGTH, "big")
|
|
511
|
+
|
|
512
|
+
def __rshift__(self, other) -> None:
|
|
513
|
+
for i in range(other):
|
|
514
|
+
tmp = int.from_bytes(self.contents, "big")
|
|
515
|
+
tmp >>= 1
|
|
516
|
+
self.__dict__["contents"] = tmp.to_bytes(self.LENGTH, "big")
|
|
517
|
+
|
|
518
|
+
def __add__(self, other: int) -> Self:
|
|
519
|
+
return self.__class__(int(self) + other)
|
|
520
|
+
|
|
521
|
+
@classmethod
|
|
522
|
+
def max(cls) -> Self:
|
|
523
|
+
if cls.SIGNED:
|
|
524
|
+
return cls(bytearray(b'\x7f'+b'\xff'*(cls.LENGTH-1)))
|
|
525
|
+
else:
|
|
526
|
+
return cls(bytearray(b'\xff'*cls.LENGTH))
|
|
527
|
+
|
|
528
|
+
def __str__(self) -> str:
|
|
529
|
+
return str(int(self))
|
|
530
|
+
|
|
531
|
+
def __gt__(self, other: Self | int) -> None:
|
|
532
|
+
match other:
|
|
533
|
+
case int(): return int(self) > other
|
|
534
|
+
case Digital(): return int(self) > int(other)
|
|
535
|
+
case _: raise ValueError(F'Compare type is {other.__class__}, expected Digital')
|
|
536
|
+
|
|
537
|
+
def __len__(self) -> int:
|
|
538
|
+
return self.LENGTH
|
|
539
|
+
|
|
540
|
+
def __hash__(self) -> int:
|
|
541
|
+
return int(self)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
type BitNumber = int
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class IntegerFlag(ReportMixin, Digital):
|
|
548
|
+
"""value with represent __int__ to string"""
|
|
549
|
+
NAMES: dict[BitNumber, Message] = None
|
|
550
|
+
"""bit number: name"""
|
|
551
|
+
|
|
552
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
553
|
+
"""initiate NAMES name use config.toml"""
|
|
554
|
+
if cls.NAMES is None:
|
|
555
|
+
cls.NAMES = {int(k): v for k, v in class_names.items()} if (class_names := get_values("DLMS", "flag_name", F"{cls.__name__}")) else dict()
|
|
556
|
+
else: # expand
|
|
557
|
+
for k, v in get_values("DLMS", "flag_name", F"{cls.__name__}").items(): # todo: handle None
|
|
558
|
+
cls.NAMES[int(k)] = v
|
|
559
|
+
|
|
560
|
+
def get_report(self) -> Report:
|
|
561
|
+
l = INFO_LOG
|
|
562
|
+
msg = F"({self})"
|
|
563
|
+
mask = 0b1
|
|
564
|
+
val = int(self)
|
|
565
|
+
flags: list[Message] = []
|
|
566
|
+
for i in range(8*self.LENGTH):
|
|
567
|
+
if (mask & val) and (name := self.NAMES.get(i)):
|
|
568
|
+
flags.append(name)
|
|
569
|
+
mask <<= 1
|
|
570
|
+
msg += F" {" | ".join(flags)}"
|
|
571
|
+
return Report(msg, log=l)
|
|
572
|
+
|
|
573
|
+
def __iter__(self) -> Iterator[int]:
|
|
574
|
+
def g():
|
|
575
|
+
value = int(self)
|
|
576
|
+
for _ in range(self.LENGTH * 8):
|
|
577
|
+
yield value & 0b1
|
|
578
|
+
value >>= 1
|
|
579
|
+
|
|
580
|
+
return g()
|
|
581
|
+
|
|
582
|
+
def __getitem__(self, item: int) -> int:
|
|
583
|
+
return tuple(self)[item]
|
|
584
|
+
|
|
585
|
+
def __setitem__(self, key: int, value: int | bool) -> None:
|
|
586
|
+
val = int(self) & ~(1 << key)
|
|
587
|
+
value = (1 << key) if value else 0 # cust to INTEGER and move
|
|
588
|
+
self.__dict__["contents"] = self.__class__(val | value).contents
|
|
589
|
+
|
|
590
|
+
def toggle(self, index: int) -> Self:
|
|
591
|
+
self[index] = not self[index]
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@runtime_checkable
|
|
595
|
+
class Float(SimpleDataType, Protocol):
|
|
596
|
+
FORMAT: str
|
|
597
|
+
|
|
598
|
+
def __init__(self, value: bytes | bytearray | str | int | float | SimpleDataType = None) -> None:
|
|
599
|
+
match value:
|
|
600
|
+
case None: self.clear()
|
|
601
|
+
case bytes() as encoding:
|
|
602
|
+
length_and_contents = encoding[1:]
|
|
603
|
+
match encoding[:1], self.SIZE:
|
|
604
|
+
case self.TAG, int() if self.SIZE <= len(length_and_contents): self.contents = length_and_contents[:self.SIZE]
|
|
605
|
+
case self.TAG, _: raise ValueError(F'Length of contents for {self.TAG} must be at least '
|
|
606
|
+
F'{self.SIZE}, but got {len(length_and_contents)}')
|
|
607
|
+
case _ as wrong_tag, _: raise ValueError(F'Expected {self.TAG} type, got {get_common_data_type_from(wrong_tag).TAG}')
|
|
608
|
+
case bytearray(): self.contents = bytes(value) # Attention!!! changed method content getting from bytearray
|
|
609
|
+
case str(): self.contents = self.from_str(value)
|
|
610
|
+
case int(): self.contents = self.from_float(float(value))
|
|
611
|
+
case float(): self.contents = self.from_float(value)
|
|
612
|
+
case Float(): self.contents = value.contents
|
|
613
|
+
case _: raise ValueError(F'Error create {self.TAG} with value {value}')
|
|
614
|
+
|
|
615
|
+
@classmethod
|
|
616
|
+
def parse(cls, value: str) -> Self:
|
|
617
|
+
try:
|
|
618
|
+
ret = cls.from_float(float(value))
|
|
619
|
+
except ValueError:
|
|
620
|
+
ret = cls.from_float(float.fromhex(value))
|
|
621
|
+
except OverflowError as e:
|
|
622
|
+
raise ParseError(str(e))
|
|
623
|
+
return cls(bytearray(ret))
|
|
624
|
+
|
|
625
|
+
@deprecated("use parse")
|
|
626
|
+
def from_str(self, value: str) -> bytes:
|
|
627
|
+
""" Input 1. float: <sign><integer>.<fraction>[e[-+]power] example: 1.0, -0.003, 1e+12, 4.5e-7
|
|
628
|
+
2. hex_float: <sign>0x<integer>.<fraction>p[+-]<power> example 0x1.e4d00p+15 (62056.0) """
|
|
629
|
+
try:
|
|
630
|
+
return self.from_float(float(value))
|
|
631
|
+
except ValueError:
|
|
632
|
+
return self.from_float(float.fromhex(value))
|
|
633
|
+
except OverflowError:
|
|
634
|
+
raise ValueError
|
|
635
|
+
|
|
636
|
+
@property
|
|
637
|
+
def encoding(self) -> bytes:
|
|
638
|
+
""" The complete sequence of octets used to represent the data value. """
|
|
639
|
+
return self.TAG + self.contents
|
|
640
|
+
|
|
641
|
+
# todo: wrong encode
|
|
642
|
+
@classmethod
|
|
643
|
+
def from_float(cls, value: float) -> bytes:
|
|
644
|
+
""" Input float: <sign><integer>.<fraction>[e[-+]power] example: 1.0, -0.003, 1e+12, 4.5e-7 """
|
|
645
|
+
if 'inf' in str(value):
|
|
646
|
+
raise OverflowError(F'Float overflow error')
|
|
647
|
+
return pack(cls.FORMAT, value)
|
|
648
|
+
|
|
649
|
+
def __float__(self) -> float:
|
|
650
|
+
""" return the build in float type IEEE 60559"""
|
|
651
|
+
return unpack(self.FORMAT, self.contents)[0]
|
|
652
|
+
|
|
653
|
+
def __str__(self) -> str:
|
|
654
|
+
return str(float(self))
|
|
655
|
+
|
|
656
|
+
def clear(self) -> None: # todo: remove this
|
|
657
|
+
self.contents = bytes(self.SIZE)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@runtime_checkable
|
|
661
|
+
class LIST(Protocol):
|
|
662
|
+
""" Special class flag for enumeration any type """
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class __DateTime(Protocol):
|
|
666
|
+
__len__: int
|
|
667
|
+
_separators: tuple[str]
|
|
668
|
+
contents: bytes
|
|
669
|
+
TAG: TAG
|
|
670
|
+
|
|
671
|
+
def __init__(self, value: bytes | bytearray | str | int | bool | float | datetime.datetime | datetime.time | SimpleDataType) -> None:
|
|
672
|
+
match value: # TODO: replace priority case
|
|
673
|
+
case bytes():
|
|
674
|
+
length_and_contents = value[1:]
|
|
675
|
+
match value[:1]:
|
|
676
|
+
case self.TAG if len(self) <= len(length_and_contents):
|
|
677
|
+
self.contents = length_and_contents[:len(self)]
|
|
678
|
+
case self.TAG:
|
|
679
|
+
raise ValueError(F"length of contents for {self.TAG} must be at least {len(self)}, but got {len(length_and_contents)}")
|
|
680
|
+
case _ as wrong_tag:
|
|
681
|
+
raise ValueError(F"got {TAG(wrong_tag)}, expected {self.TAG} type")
|
|
682
|
+
case None: self.clear()
|
|
683
|
+
case bytearray(): self.contents = bytes(value) # Attention!!! changed method content getting from bytearray
|
|
684
|
+
case str(): self.contents = self.from_str(value)
|
|
685
|
+
case datetime.datetime(): self.contents = self.from_datetime(value)
|
|
686
|
+
case datetime.date(): self.contents = self.from_date(value)
|
|
687
|
+
case datetime.time(): self.contents = self.from_time(value)
|
|
688
|
+
case self.__class__(): self.contents = value.contents
|
|
689
|
+
case _: raise ValueError(F"error create {self.TAG} with value {value}")
|
|
690
|
+
|
|
691
|
+
@property
|
|
692
|
+
def encoding(self) -> bytes:
|
|
693
|
+
return self.TAG + self.contents
|
|
694
|
+
|
|
695
|
+
def from_str(self, value: str) -> bytes:
|
|
696
|
+
""" typecast from string to bytes """
|
|
697
|
+
|
|
698
|
+
def from_datetime(self, value: datetime.datetime) -> bytes:
|
|
699
|
+
""" typecast from datetime to bytes """
|
|
700
|
+
raise ValueError('"Date_time" type not supported')
|
|
701
|
+
|
|
702
|
+
def from_date(self, value: datetime.date) -> bytes:
|
|
703
|
+
""" typecast from date to bytes """
|
|
704
|
+
raise ValueError('"Date" type not supported')
|
|
705
|
+
|
|
706
|
+
def from_time(self, value: datetime.time) -> bytes:
|
|
707
|
+
""" typecast from time to bytes """
|
|
708
|
+
raise ValueError('"Time" type not supported')
|
|
709
|
+
|
|
710
|
+
def separator_amount(self, string: str, amount: int = 0) -> int:
|
|
711
|
+
""" returning sum of '.', ':', ' ' in string """
|
|
712
|
+
for separator in set(self._separators):
|
|
713
|
+
amount += string.count(separator)
|
|
714
|
+
return amount
|
|
715
|
+
|
|
716
|
+
def DEFAULT(self) -> bytes:
|
|
717
|
+
""""""
|
|
718
|
+
|
|
719
|
+
def clear(self) -> None:
|
|
720
|
+
self.contents = self.DEFAULT
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
class __Date(Protocol):
|
|
724
|
+
""" years, month, day setters/getters for Date and DateTime """
|
|
725
|
+
TAG: TAG
|
|
726
|
+
|
|
727
|
+
@property
|
|
728
|
+
def year(self) -> int:
|
|
729
|
+
return unpack(">H", self.contents[:2])[0]
|
|
730
|
+
|
|
731
|
+
@property
|
|
732
|
+
def month(self) -> int:
|
|
733
|
+
return self.contents[2]
|
|
734
|
+
|
|
735
|
+
@property
|
|
736
|
+
def day(self) -> int:
|
|
737
|
+
return self.contents[3]
|
|
738
|
+
|
|
739
|
+
@property
|
|
740
|
+
def weekday(self) -> int:
|
|
741
|
+
return self.contents[4]
|
|
742
|
+
|
|
743
|
+
def set_year(self, value: int) -> None:
|
|
744
|
+
""" set day """
|
|
745
|
+
if (
|
|
746
|
+
9999 >= value > 1
|
|
747
|
+
or value == 0xffff
|
|
748
|
+
):
|
|
749
|
+
contents = bytearray(self.contents)
|
|
750
|
+
contents[:2] = value.to_bytes(2, 'big')
|
|
751
|
+
self.__dict__["contents"] = contents
|
|
752
|
+
else:
|
|
753
|
+
raise OutOfRange(F"in year: got {value}, expected 1..9999, 65535")
|
|
754
|
+
|
|
755
|
+
def set_month(self, value: int) -> None:
|
|
756
|
+
""" set month """
|
|
757
|
+
if (
|
|
758
|
+
12 >= value >= 1
|
|
759
|
+
or value in (0xfd, 0xfe, 0xff)
|
|
760
|
+
):
|
|
761
|
+
contents = bytearray(self.contents)
|
|
762
|
+
contents[2] = value
|
|
763
|
+
self.__dict__["contents"] = contents
|
|
764
|
+
else:
|
|
765
|
+
raise OutOfRange(F"in Month: got {value}, expected 1..12, 253, 254, 255")
|
|
766
|
+
|
|
767
|
+
def set_day(self, value: int) -> None:
|
|
768
|
+
""" set day """
|
|
769
|
+
if (
|
|
770
|
+
31 >= value >= 1
|
|
771
|
+
or value in (0xfd, 0xfe, 0xff)
|
|
772
|
+
):
|
|
773
|
+
contents = bytearray(self.contents)
|
|
774
|
+
contents[3] = value
|
|
775
|
+
self.__dict__["contents"] = contents
|
|
776
|
+
else:
|
|
777
|
+
raise OutOfRange(F"in Day: got {value}, expected 1..31, 253, 254, 255")
|
|
778
|
+
|
|
779
|
+
def set_weekday(self, value: int) -> None:
|
|
780
|
+
""" set weekday """
|
|
781
|
+
if (
|
|
782
|
+
7 >= value >= 1
|
|
783
|
+
or value == 0xff
|
|
784
|
+
):
|
|
785
|
+
contents = bytearray(self.contents)
|
|
786
|
+
contents[4] = value
|
|
787
|
+
self.__dict__["contents"] = contents
|
|
788
|
+
else:
|
|
789
|
+
raise OutOfRange(F"got <week day>: {value}, excpected 1..7 or 255")
|
|
790
|
+
|
|
791
|
+
@staticmethod
|
|
792
|
+
def check_date(value: bytes) -> None:
|
|
793
|
+
if len(value) != 5:
|
|
794
|
+
raise ValidationError(F"In the Date type expected length 5, but got {len(value)}")
|
|
795
|
+
year_highbyte, year_lowbyte, month, day_of_month, day_of_week = \
|
|
796
|
+
value[0:2].replace(b'\xff\xff', b'\x01\x00') + \
|
|
797
|
+
value[2:4].replace(b'\xff', b'\x01').replace(b'\xfe', b'\x01').replace(b'\xfd', b'\x01') + \
|
|
798
|
+
value[4:5].replace(b'\xff', b'\x01')
|
|
799
|
+
if (
|
|
800
|
+
datetime.date(year_highbyte * 256 + year_lowbyte, month, day_of_month).weekday() != day_of_week - 1
|
|
801
|
+
and value[4:] != b'\xff'
|
|
802
|
+
and value[0:2] != b'\xff\xff'
|
|
803
|
+
and value[2:3] not in b'\xfd\xfe\xff'
|
|
804
|
+
and value[3:4] not in b'\xfd\xfe\xff'
|
|
805
|
+
):
|
|
806
|
+
raise ValidationError(F"in Date got <week day: {value[4]}, not corresponding with other data")
|
|
807
|
+
|
|
808
|
+
@property
|
|
809
|
+
def strfdate(self) -> str:
|
|
810
|
+
""" get date in format d.m.Y-A or d.m.Y or d.m """
|
|
811
|
+
match self.contents[2]:
|
|
812
|
+
case 0xff: month = '__'
|
|
813
|
+
case 0xfe: month = 'be' # begin
|
|
814
|
+
case 0xfd: month = 'en' # end
|
|
815
|
+
case value: month = str(value).zfill(2)
|
|
816
|
+
match self.contents[3]:
|
|
817
|
+
case 0xff: month_day = '__'
|
|
818
|
+
case 0xfe: month_day = 'la' # last
|
|
819
|
+
case 0xfd: month_day = 'pe' # penult
|
|
820
|
+
case value: month_day = str(value).zfill(2)
|
|
821
|
+
match self.contents[4]:
|
|
822
|
+
case 1: weekday = '-пн'
|
|
823
|
+
case 2: weekday = '-вт'
|
|
824
|
+
case 3: weekday = '-ср'
|
|
825
|
+
case 4: weekday = '-чт'
|
|
826
|
+
case 5: weekday = '-пт'
|
|
827
|
+
case 6: weekday = '-сб'
|
|
828
|
+
case 7: weekday = '-вс'
|
|
829
|
+
case 0xff: weekday = ''
|
|
830
|
+
case value: raise ValueError(F'Got weekday={value}, expected 1..7, ff')
|
|
831
|
+
match unpack('>h', self.contents[:2])[0]:
|
|
832
|
+
case -1 if weekday == '': year = ''
|
|
833
|
+
case -1: year = '.____'
|
|
834
|
+
case value: year = F'.{str(value).zfill(4)}'
|
|
835
|
+
return F'{month_day}.{month}{year}{weekday}'
|
|
836
|
+
|
|
837
|
+
@staticmethod
|
|
838
|
+
def strpdate(value: str) -> bytes | tuple[bytes, str]:
|
|
839
|
+
""" typecasting string to DLMS Date. Where: Y - year, m - month, d - month day, w - weekday """
|
|
840
|
+
def from_year() -> tuple[int, int]:
|
|
841
|
+
nonlocal Y
|
|
842
|
+
match Y:
|
|
843
|
+
case '' | '_' | '__' | '___' | '____': return 0xff, 0xff
|
|
844
|
+
case _ as y if y.isdigit() and len(y) <= 2: return divmod(int(y) + 2000, 0x100)
|
|
845
|
+
case _ if Y.isdigit() and len(Y) <= 4: return divmod(int(Y), 0x100)
|
|
846
|
+
case _: raise ValueError(F'Got wrong year={Y}')
|
|
847
|
+
|
|
848
|
+
def from_month() -> int:
|
|
849
|
+
nonlocal m
|
|
850
|
+
match m:
|
|
851
|
+
case '' | '_' | '__': return 0xff
|
|
852
|
+
case _ if m.isdigit() and 1 <= int(m) <= 12: return int(m)
|
|
853
|
+
case 'begin': return 0xfe
|
|
854
|
+
case 'end': return 0xfd
|
|
855
|
+
case _: raise ValueError(F'Got wrong month={m}')
|
|
856
|
+
|
|
857
|
+
def from_monthday() -> int:
|
|
858
|
+
nonlocal d
|
|
859
|
+
match d:
|
|
860
|
+
case '' | '_' | '__': return 0xff
|
|
861
|
+
case _ if d.isdigit() and 1 <= int(d) <= 31: return int(d)
|
|
862
|
+
case 'last': return 0xfe
|
|
863
|
+
case 'penult': return 0xfd
|
|
864
|
+
case _: raise ValueError(F'Got wrong monthday={d}')
|
|
865
|
+
|
|
866
|
+
def from_weekday() -> int:
|
|
867
|
+
nonlocal w
|
|
868
|
+
match w.lower():
|
|
869
|
+
case '' | '_' | '__': return 0xff
|
|
870
|
+
case _ if w.isdigit() and 1 <= int(w) <= 7: return int(w)
|
|
871
|
+
case '1' | 'по' | 'пон' | 'понедельник' | 'mo' | 'mon' | 'monday': return 1
|
|
872
|
+
case '2' | 'вт' | 'вто' | 'вторник' | 'tu' | 'tue' | 'tuesday': return 2
|
|
873
|
+
case '3' | 'ср' | 'сре' | 'среда' | 'we' | 'wed' | 'wednesday': return 3
|
|
874
|
+
case '4' | 'чт' | 'чет' | 'четверг' | 'th' | 'thu' | 'thursday': return 4
|
|
875
|
+
case '5' | 'пт' | 'пят' | 'пятница' | 'fr' | 'fri' | 'friday': return 5
|
|
876
|
+
case '6' | 'сб' | 'суб' | 'суббота' | 'sa' | 'sat' | 'saturday': return 6
|
|
877
|
+
case '7' | 'вс' | 'вос' | 'воскресенье' | 'su' | 'sun' | 'sunday' | '': return 7
|
|
878
|
+
case _ if any(map(lambda pat: pat.startswith(w),
|
|
879
|
+
('понедельни', 'вторни', 'сред', 'четвер', 'пятниц', 'суббот', 'воскресень',
|
|
880
|
+
'monda', 'tuesda', 'wednesda', 'thursda','frida','saturda', 'sunda'))): return 0xff
|
|
881
|
+
case _: raise ValueError(F'Got wrong weekday={w}')
|
|
882
|
+
|
|
883
|
+
match separate(value, '.-', 3):
|
|
884
|
+
case _, (d,) if d.isdigit(): return bytes((0xff, 0xff, 0xff, from_monthday(), 0xff))
|
|
885
|
+
case _, (w,): return bytes((0xff, 0xff, 0xff, 0xff, from_weekday()))
|
|
886
|
+
case '.', (d, m): return bytes((0xff, 0xff, from_month(), from_monthday(), 0xff))
|
|
887
|
+
case '..', (d, m, Y): return bytes((*from_year(), from_month(), from_monthday(), 0xff))
|
|
888
|
+
case '.-', (d, m, w): return bytes((0xff, 0xff, from_month(), from_monthday(), from_weekday()))
|
|
889
|
+
case '-.', (w, d, m): return bytes((0xff, 0xff, from_month(), from_monthday(), from_weekday()))
|
|
890
|
+
case '..-', (d, m, Y, w): return bytes((*from_year(), from_month(), from_monthday(), from_weekday()))
|
|
891
|
+
case '-..', (w, d, m, Y): return bytes((*from_year(), from_month(), from_monthday(), from_weekday()))
|
|
892
|
+
case _ as separate_result: raise ValueError(F'Unknown date format: separators=<{separate_result[0]}>, values={", ".join(separate_result[1])}')
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class __Time(Protocol):
|
|
896
|
+
""" hour, minute, second, hundredths setters/getters for Time and DateTime """
|
|
897
|
+
contents: bytes
|
|
898
|
+
TAG: TAG
|
|
899
|
+
|
|
900
|
+
@property
|
|
901
|
+
def __contents_offset(self) -> int:
|
|
902
|
+
""" return offset if type is DateTime """
|
|
903
|
+
return 0 if len(self) == 4 else 5
|
|
904
|
+
|
|
905
|
+
def set_hour(self, value: int) -> None:
|
|
906
|
+
""" set hour """
|
|
907
|
+
if (0 <= value <= 23) or value == 0xff:
|
|
908
|
+
contents = bytearray(self.contents)
|
|
909
|
+
contents[0+self.__contents_offset] = value
|
|
910
|
+
self.set(contents)
|
|
911
|
+
else:
|
|
912
|
+
raise OutOfRange(F"in Hour: got {value}, expected 0..23, 255")
|
|
913
|
+
|
|
914
|
+
def set_minute(self, value: int) -> None:
|
|
915
|
+
""" set minute """
|
|
916
|
+
if (0 <= value <= 59) or value == 0xff:
|
|
917
|
+
contents = bytearray(self.contents)
|
|
918
|
+
contents[1+self.__contents_offset] = value
|
|
919
|
+
self.set(contents)
|
|
920
|
+
else:
|
|
921
|
+
raise OutOfRange(F"in Minute: got {value}, expected 0..59, 255")
|
|
922
|
+
|
|
923
|
+
def set_second(self, value: int) -> None:
|
|
924
|
+
""" set minute """
|
|
925
|
+
if (0 <= value <= 59) or value == 0xff:
|
|
926
|
+
contents = bytearray(self.contents)
|
|
927
|
+
contents[2+self.__contents_offset] = value
|
|
928
|
+
self.set(contents)
|
|
929
|
+
else:
|
|
930
|
+
raise OutOfRange(F"in second: got {value}, expected 0..59, 255")
|
|
931
|
+
|
|
932
|
+
def set_hundredths(self, value: int) -> None:
|
|
933
|
+
""" set hun """
|
|
934
|
+
if (0 <= value <= 99) or value == 0xff:
|
|
935
|
+
contents = bytearray(self.contents)
|
|
936
|
+
contents[3+self.__contents_offset] = value
|
|
937
|
+
self.set(contents)
|
|
938
|
+
else:
|
|
939
|
+
raise OutOfRange(F"in Hundredths: got {value}, expected 0..99, 255")
|
|
940
|
+
|
|
941
|
+
@property
|
|
942
|
+
def hour(self) -> int:
|
|
943
|
+
return self.contents[0 + self.__contents_offset]
|
|
944
|
+
|
|
945
|
+
@property
|
|
946
|
+
def minute(self) -> int:
|
|
947
|
+
return self.contents[1 + self.__contents_offset]
|
|
948
|
+
|
|
949
|
+
@property
|
|
950
|
+
def second(self) -> int:
|
|
951
|
+
return self.contents[2 + self.__contents_offset]
|
|
952
|
+
|
|
953
|
+
@property
|
|
954
|
+
def hundredths(self) -> int:
|
|
955
|
+
return self.contents[3 + self.__contents_offset]
|
|
956
|
+
|
|
957
|
+
def check_time(self):
|
|
958
|
+
datetime.time(*tuple(self.contents[0+self.__contents_offset: 4+self.__contents_offset].replace(b'\xff', b'\x00')))
|
|
959
|
+
|
|
960
|
+
@property
|
|
961
|
+
def strftime(self) -> str:
|
|
962
|
+
""" get time in format H:M:S.f or H:M:S or H:M """
|
|
963
|
+
match self.contents[3+self.__contents_offset]:
|
|
964
|
+
case 0xff: hundredths = ''
|
|
965
|
+
case _ as value: hundredths = F'.{str(value).zfill(2)}'
|
|
966
|
+
match self.contents[2+self.__contents_offset]:
|
|
967
|
+
case 0xff if hundredths == '': second = ''
|
|
968
|
+
case 0xff: second = ':__'
|
|
969
|
+
case _ as value: second = F':{str(value).zfill(2)}'
|
|
970
|
+
match self.contents[1+self.__contents_offset]:
|
|
971
|
+
case 0xff: minute = '__'
|
|
972
|
+
case _ as value: minute = str(value).zfill(2)
|
|
973
|
+
match self.contents[0+self.__contents_offset]:
|
|
974
|
+
case 0xff: hour = '__'
|
|
975
|
+
case _ as value: hour = str(value).zfill(2)
|
|
976
|
+
return F'{hour}:{minute}{second}{hundredths}'
|
|
977
|
+
|
|
978
|
+
@staticmethod
|
|
979
|
+
def strptime(value: str) -> bytes:
|
|
980
|
+
""" typecasting string to DLMS Time. Where: H - hour, M - minute, S - second, f - hundredths """
|
|
981
|
+
def from_hour() -> int:
|
|
982
|
+
nonlocal H
|
|
983
|
+
match H:
|
|
984
|
+
case '' | '_' | '__': return 0xff
|
|
985
|
+
case _ if H.isdigit() and 0 <= int(H) <= 23: return int(H)
|
|
986
|
+
case _: raise ValueError(F'Got wrong hour={H}')
|
|
987
|
+
|
|
988
|
+
def from_minute() -> int:
|
|
989
|
+
nonlocal M
|
|
990
|
+
match M:
|
|
991
|
+
case '' | '_' | '__': return 0xff
|
|
992
|
+
case _ if M.isdigit() and 0 <= int(M) <= 59: return int(M)
|
|
993
|
+
case _: raise ValueError(F'Got wrong minute={M}')
|
|
994
|
+
|
|
995
|
+
def from_second() -> int:
|
|
996
|
+
nonlocal S
|
|
997
|
+
match S:
|
|
998
|
+
case '' | '_' | '__': return 0xff
|
|
999
|
+
case _ if S.isdigit() and 0 <= int(S) <= 59: return int(S)
|
|
1000
|
+
case _: raise ValueError(F'Got wrong second={S}')
|
|
1001
|
+
|
|
1002
|
+
def from_hundredths() -> int:
|
|
1003
|
+
nonlocal f
|
|
1004
|
+
match f:
|
|
1005
|
+
case '' | '_' | '__': return 0xff
|
|
1006
|
+
case _ if f.isdigit() and len(f) <= 2: return int(f)
|
|
1007
|
+
case _: raise ValueError(F'Got wrong hundredths={f}')
|
|
1008
|
+
|
|
1009
|
+
match separate(value, ':.', 3):
|
|
1010
|
+
case _, (H,): return bytes((from_hour(), 0xff, 0xff, 0xff))
|
|
1011
|
+
case ':', (H, M): return bytes((from_hour(), from_minute(), 0xff, 0xff))
|
|
1012
|
+
case '.', (S, f): return bytes((0xff, 0xff, from_second(), from_hundredths()))
|
|
1013
|
+
case '::', (H, M, S): return bytes((from_hour(), from_minute(), from_second(), 0xff))
|
|
1014
|
+
case ':.', (M, S, f): return bytes((0xff, from_minute(), from_second(), from_hundredths()))
|
|
1015
|
+
case '::.', (H, M, S, f): return bytes((from_hour(), from_minute(), from_second(), from_hundredths()))
|
|
1016
|
+
case _ as separate_result: raise ValueError(F'Unknown time format: separators={separate_result[0]}, values={", ".join(separate_result[1])}')
|
|
1017
|
+
|
|
1018
|
+
def to_second(self) -> float | int:
|
|
1019
|
+
ret = 0
|
|
1020
|
+
if (hour := self.hour) != 0xff:
|
|
1021
|
+
ret += hour*1440
|
|
1022
|
+
if (minute := self.minute) != 0xff:
|
|
1023
|
+
ret += minute*60
|
|
1024
|
+
if (second := self.second) != 0xff:
|
|
1025
|
+
ret += second
|
|
1026
|
+
if (h := self.hundredths) != 0xff:
|
|
1027
|
+
ret += h//100
|
|
1028
|
+
return ret
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
class NullData(SimpleDataType):
|
|
1032
|
+
""" An ordered sequence of octets (8 bit bytes) """
|
|
1033
|
+
TAG = TAG(b'\x00')
|
|
1034
|
+
|
|
1035
|
+
def __init__(self, value: bytes | str | Self = None) -> None:
|
|
1036
|
+
match value:
|
|
1037
|
+
case bytes() if value[:1] == self.TAG: pass
|
|
1038
|
+
case bytes(): raise ValueError(F"got {TAG(value[:1])}, expected {self.TAG} type, ")
|
|
1039
|
+
case None | str() | NullData(): pass
|
|
1040
|
+
case _: raise ValueError(F"error create {self.TAG} with value {value}")
|
|
1041
|
+
|
|
1042
|
+
@classmethod
|
|
1043
|
+
def parse(cls, value: str = None) -> Self:
|
|
1044
|
+
return cls()
|
|
1045
|
+
|
|
1046
|
+
@property
|
|
1047
|
+
def contents(self) -> bytes:
|
|
1048
|
+
return b''
|
|
1049
|
+
|
|
1050
|
+
def set(self, value) -> None:
|
|
1051
|
+
"""override with no change"""
|
|
1052
|
+
|
|
1053
|
+
def __str__(self) -> str:
|
|
1054
|
+
return 'null-data'
|
|
1055
|
+
|
|
1056
|
+
@property
|
|
1057
|
+
def encoding(self) -> bytes: return b'\x00'
|
|
1058
|
+
|
|
1059
|
+
def clear(self) -> None:
|
|
1060
|
+
""" nothing do it"""
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
class Array(_Array, ComplexDataType):
|
|
1064
|
+
""" The elements of the array are defined in the Attribute or Method description section of a COSEM IC
|
|
1065
|
+
specification """
|
|
1066
|
+
TYPE: type[CommonDataType] = None
|
|
1067
|
+
values: list[CommonDataType]
|
|
1068
|
+
TAG = TAG(b"\x01")
|
|
1069
|
+
|
|
1070
|
+
def __init__(self, value: list[CommonDataType | list] | bytes | None | Self = None, type_: type[CommonDataType] = None) -> None:
|
|
1071
|
+
self.__dict__['values'] = list()
|
|
1072
|
+
if type_:
|
|
1073
|
+
self.__dict__["TYPE"] = type_
|
|
1074
|
+
match value:
|
|
1075
|
+
case list(): # main init data,
|
|
1076
|
+
self.__dict__["values"] = value
|
|
1077
|
+
case bytes():
|
|
1078
|
+
match value[:1], value[1:]:
|
|
1079
|
+
case self.TAG, length_and_contents:
|
|
1080
|
+
length, pdu = get_length_and_pdu(length_and_contents)
|
|
1081
|
+
if length and self.TYPE is None:
|
|
1082
|
+
self.__dict__['TYPE'] = get_common_data_type_from(pdu[:1])
|
|
1083
|
+
for number in range(length):
|
|
1084
|
+
if pdu == b'':
|
|
1085
|
+
raise ValueError(F"{self.TAG} Error of input data length: {number} instead {length}")
|
|
1086
|
+
new_element, pdu = get_instance_and_pdu(self.TYPE, pdu)
|
|
1087
|
+
self.append(new_element)
|
|
1088
|
+
case b'', _: raise ValueError(F'Wrong Value. Value not consist the tag. Empty Value.')
|
|
1089
|
+
case _: raise ValueError(F"Expected {self.TAG} type, got {TAG(value[:1])}")
|
|
1090
|
+
# case list(): deque(map(self.append, value))
|
|
1091
|
+
case None: """create empty array"""
|
|
1092
|
+
case Array(): self.__init__(value.encoding) # TODO: make with bytearray
|
|
1093
|
+
case _: raise ValueError(F'Init {self.__class__} with Value: "{value}" not supported')
|
|
1094
|
+
|
|
1095
|
+
def __str__(self):
|
|
1096
|
+
return F"{self.TAG}[{len(self.values)}]"
|
|
1097
|
+
|
|
1098
|
+
def append(self, element: CommonDataType | None | Any = None) -> None:
|
|
1099
|
+
""" append element to end """
|
|
1100
|
+
if element is None:
|
|
1101
|
+
element = self.new_element()
|
|
1102
|
+
elif hasattr(self.TYPE, "TYPE") and not hasattr(self.TYPE, "TAG"): # for CHOICE
|
|
1103
|
+
self.__dict__['TYPE'] = self.TYPE.ELEMENTS[element.encoding[0]].TYPE
|
|
1104
|
+
else:
|
|
1105
|
+
element = self.TYPE(element)
|
|
1106
|
+
self.values.append(element)
|
|
1107
|
+
|
|
1108
|
+
def new_element(self) -> CommonDataType:
|
|
1109
|
+
"""for override elements validator if it consist ID's. """
|
|
1110
|
+
return self.TYPE()
|
|
1111
|
+
|
|
1112
|
+
@classmethod
|
|
1113
|
+
def parse(cls, value: list) -> Self:
|
|
1114
|
+
return cls([cls.TYPE.parse(val) for val in value])
|
|
1115
|
+
|
|
1116
|
+
def __setattr__(self, key, value: CommonDataType) -> None:
|
|
1117
|
+
match key:
|
|
1118
|
+
case 'TYPE' | 'values' as prop:
|
|
1119
|
+
raise ValueError(F"don't support set {prop}")
|
|
1120
|
+
case _:
|
|
1121
|
+
super().__setattr__(key, value)
|
|
1122
|
+
|
|
1123
|
+
def __getitem__(self, item: int) -> CommonDataType:
|
|
1124
|
+
""" get element by index """
|
|
1125
|
+
return self.values[item]
|
|
1126
|
+
|
|
1127
|
+
def __iter__(self):
|
|
1128
|
+
return iter(self.values)
|
|
1129
|
+
|
|
1130
|
+
def get_type(self) -> type[CommonDataType]:
|
|
1131
|
+
return self.TYPE
|
|
1132
|
+
|
|
1133
|
+
def set_type(self, value: type[CommonDataType]) -> None:
|
|
1134
|
+
""" set new type with clear array"""
|
|
1135
|
+
self.clear()
|
|
1136
|
+
self.__dict__['TYPE'] = value
|
|
1137
|
+
|
|
1138
|
+
def set(self, value: bytes | bytearray | list | None) -> None:
|
|
1139
|
+
self.clear()
|
|
1140
|
+
if hasattr(self, 'cb_preset'):
|
|
1141
|
+
self.cb_preset(value)
|
|
1142
|
+
new_array = Array(value, type_=self.TYPE)
|
|
1143
|
+
if self.TYPE is None and len(new_array) != 0:
|
|
1144
|
+
self.set_type(new_array[0].__class__)
|
|
1145
|
+
else:
|
|
1146
|
+
"""TYPE already initiated"""
|
|
1147
|
+
for el in new_array:
|
|
1148
|
+
self.append(self.TYPE(el))
|
|
1149
|
+
if hasattr(self, 'cb_post_set'):
|
|
1150
|
+
self.cb_post_set()
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
_struct_names = config["DLMS"]["struct_name"]
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
@dataclass(frozen=True)
|
|
1157
|
+
class StructElement:
|
|
1158
|
+
NAME: str
|
|
1159
|
+
TYPE: type[CommonDataType]
|
|
1160
|
+
|
|
1161
|
+
def __str__(self) -> str:
|
|
1162
|
+
if _struct_names and (t := _struct_names.get(self.NAME)):
|
|
1163
|
+
return t
|
|
1164
|
+
else:
|
|
1165
|
+
return self.NAME
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
class Structure(ComplexDataType):
|
|
1169
|
+
""" The elements of the structure are defined in the Attribute or Method description section of a COSEM IC specification """
|
|
1170
|
+
TAG = TAG(b'\x02')
|
|
1171
|
+
ELEMENTS: tuple[StructElement, ...]
|
|
1172
|
+
values: list[CommonDataType]
|
|
1173
|
+
DEFAULT: bytes = None
|
|
1174
|
+
|
|
1175
|
+
def __init__(self, value: list[CommonDataType | list] | bytes | tuple | None | bytearray | Self = None) -> None:
|
|
1176
|
+
if value is None:
|
|
1177
|
+
value = self.DEFAULT
|
|
1178
|
+
self.__dict__['values'] = list()
|
|
1179
|
+
match value:
|
|
1180
|
+
case list(): # main init data,
|
|
1181
|
+
self.__dict__['values'] = value
|
|
1182
|
+
case bytes():
|
|
1183
|
+
self.from_bytes(value)
|
|
1184
|
+
case tuple():
|
|
1185
|
+
self.from_sequence(value)
|
|
1186
|
+
case None:
|
|
1187
|
+
for el in self.ELEMENTS:
|
|
1188
|
+
self.values.append(el.TYPE())
|
|
1189
|
+
case bytearray(): self.from_content(bytes(value))
|
|
1190
|
+
case Structure() if not hasattr(self, "ELEMENTS"):
|
|
1191
|
+
self.from_bytes(value.encoding)
|
|
1192
|
+
case Structure():
|
|
1193
|
+
self.from_content(value.contents)
|
|
1194
|
+
case _: raise ValueError(F'for {self.__class__.__name__} "{value=}" not supported')
|
|
1195
|
+
|
|
1196
|
+
@property
|
|
1197
|
+
def get_el0(self):
|
|
1198
|
+
return self.values[0]
|
|
1199
|
+
|
|
1200
|
+
@property
|
|
1201
|
+
def get_el1(self):
|
|
1202
|
+
return self.values[1]
|
|
1203
|
+
|
|
1204
|
+
@property
|
|
1205
|
+
def get_el2(self):
|
|
1206
|
+
return self.values[2]
|
|
1207
|
+
|
|
1208
|
+
@property
|
|
1209
|
+
def get_el3(self):
|
|
1210
|
+
return self.values[3]
|
|
1211
|
+
|
|
1212
|
+
@property
|
|
1213
|
+
def get_el4(self):
|
|
1214
|
+
return self.values[4]
|
|
1215
|
+
|
|
1216
|
+
@property
|
|
1217
|
+
def get_el5(self):
|
|
1218
|
+
return self.values[5]
|
|
1219
|
+
|
|
1220
|
+
@property
|
|
1221
|
+
def get_el6(self):
|
|
1222
|
+
return self.values[6]
|
|
1223
|
+
|
|
1224
|
+
@property
|
|
1225
|
+
def get_el7(self):
|
|
1226
|
+
return self.values[7]
|
|
1227
|
+
|
|
1228
|
+
@property
|
|
1229
|
+
def get_el8(self):
|
|
1230
|
+
return self.values[8]
|
|
1231
|
+
|
|
1232
|
+
@property
|
|
1233
|
+
def get_el9(self):
|
|
1234
|
+
return self.values[9]
|
|
1235
|
+
|
|
1236
|
+
def __init_subclass__(cls, **kwargs):
|
|
1237
|
+
"""create ELEMENTS from annotations"""
|
|
1238
|
+
if inspect.isabstract(cls):
|
|
1239
|
+
...
|
|
1240
|
+
elif hasattr(cls, "ELEMENTS"):
|
|
1241
|
+
"""init manually, ex: Entry in ProfileGeneric"""
|
|
1242
|
+
if len(kwargs) != 0: # reinit several struct elements
|
|
1243
|
+
elements = list(cls.ELEMENTS)
|
|
1244
|
+
for k in kwargs.keys():
|
|
1245
|
+
for i, el in enumerate(cls.ELEMENTS):
|
|
1246
|
+
if k == el.NAME:
|
|
1247
|
+
elements[i] = StructElement(el.NAME, kwargs[k])
|
|
1248
|
+
cls.ELEMENTS = tuple(elements)
|
|
1249
|
+
else:
|
|
1250
|
+
elements = []
|
|
1251
|
+
for (name, type_), f in zip(cls.__annotations__.items(), (
|
|
1252
|
+
Structure.get_el0, Structure.get_el1, Structure.get_el2, Structure.get_el3, Structure.get_el4, Structure.get_el5, Structure.get_el6, Structure.get_el7,
|
|
1253
|
+
Structure.get_el8, Structure.get_el9)):
|
|
1254
|
+
elements.append((StructElement(
|
|
1255
|
+
NAME=name,
|
|
1256
|
+
TYPE=type_)))
|
|
1257
|
+
setattr(cls, name, f)
|
|
1258
|
+
cls.ELEMENTS = tuple(elements)
|
|
1259
|
+
|
|
1260
|
+
def from_bytes(self, encoding: bytes):
|
|
1261
|
+
tag, length_and_contents = encoding[:1], encoding[1:]
|
|
1262
|
+
if tag != self.TAG:
|
|
1263
|
+
raise ValueError(F'Expected {self.TAG} type, got {TAG(tag)}')
|
|
1264
|
+
length, pdu = get_length_and_pdu(length_and_contents)
|
|
1265
|
+
if not hasattr(self, "ELEMENTS"):
|
|
1266
|
+
el: list[StructElement] = list()
|
|
1267
|
+
for i in range(length):
|
|
1268
|
+
el.append(StructElement(F'#{i}', get_common_data_type_from(pdu[:1])))
|
|
1269
|
+
el_value, pdu = get_instance_and_pdu(el[i].TYPE, pdu)
|
|
1270
|
+
self.values.append(el_value)
|
|
1271
|
+
self.__dict__['ELEMENTS'] = tuple(el)
|
|
1272
|
+
else:
|
|
1273
|
+
if len(self) != length:
|
|
1274
|
+
raise ValueError(F'Struct {self} got length:{length}, expected length:{len(self)}')
|
|
1275
|
+
self.from_content(pdu)
|
|
1276
|
+
|
|
1277
|
+
@deprecated("use parse")
|
|
1278
|
+
def from_sequence(self, sequence: tuple):
|
|
1279
|
+
if len(sequence) != len(self):
|
|
1280
|
+
raise ValueError(F'Struct {self.__class__.__name__} got length:{len(sequence)}, expected length:{len(self)}')
|
|
1281
|
+
for val, el in zip(sequence, self.ELEMENTS):
|
|
1282
|
+
try:
|
|
1283
|
+
self.values.append(el.TYPE(val))
|
|
1284
|
+
except TypeError as e:
|
|
1285
|
+
print(e)
|
|
1286
|
+
|
|
1287
|
+
@classmethod
|
|
1288
|
+
def parse(cls, value: Transcript) -> Self:
|
|
1289
|
+
if len(value) != len(cls.ELEMENTS):
|
|
1290
|
+
raise ValueError(F"in Struct {cls.__name__} got length:{len(value)}, expected length:{len(cls.ELEMENTS)}")
|
|
1291
|
+
return cls([el.TYPE.parse(val) for val, el in zip(value, cls.ELEMENTS)])
|
|
1292
|
+
|
|
1293
|
+
def from_content(self, value: bytes):
|
|
1294
|
+
for el in self.ELEMENTS:
|
|
1295
|
+
el_value, value = get_instance_and_pdu(el.TYPE, value)
|
|
1296
|
+
self.values.append(el_value)
|
|
1297
|
+
|
|
1298
|
+
def __len__(self):
|
|
1299
|
+
return len(self.ELEMENTS)
|
|
1300
|
+
|
|
1301
|
+
def clear(self):
|
|
1302
|
+
for value in self.values:
|
|
1303
|
+
value.clear()
|
|
1304
|
+
|
|
1305
|
+
def __str__(self):
|
|
1306
|
+
""" names with values elements """
|
|
1307
|
+
return F'{{{", ".join(map(str, self.values))}}}'
|
|
1308
|
+
|
|
1309
|
+
def __setattr__(self, key, value: CommonDataType):
|
|
1310
|
+
""" don't support """
|
|
1311
|
+
raise ValueError(F'Unsupported change: {key}')
|
|
1312
|
+
|
|
1313
|
+
def set_name(self, value: str):
|
|
1314
|
+
"""use in ProfileGeneric for new CaptureObject"""
|
|
1315
|
+
self.__dict__["NAME"] = value
|
|
1316
|
+
|
|
1317
|
+
def set(self, value: bytes | bytearray | tuple | list | None):
|
|
1318
|
+
for index, el_value in enumerate(self.get_types()(value)):
|
|
1319
|
+
self[index].set(el_value)
|
|
1320
|
+
|
|
1321
|
+
@property
|
|
1322
|
+
def contents(self) -> bytes:
|
|
1323
|
+
""" ITU-T Rec. X.690 8.1.1 Structure of an encoding """
|
|
1324
|
+
return b''.join((value.encoding for value in self.values))
|
|
1325
|
+
|
|
1326
|
+
@property
|
|
1327
|
+
def complex_data(self) -> bytes:
|
|
1328
|
+
return b''.join((value.contents for value in self.values))
|
|
1329
|
+
|
|
1330
|
+
def __getitem__(self, item: int) -> CommonDataType:
|
|
1331
|
+
""" get element value by index """
|
|
1332
|
+
return self.values[item]
|
|
1333
|
+
|
|
1334
|
+
def __iter__(self) -> Iterator[CommonDataType]:
|
|
1335
|
+
return iter(self.values)
|
|
1336
|
+
|
|
1337
|
+
def __setitem__(self, key: int, value: CommonDataType):
|
|
1338
|
+
""" set data to element by index. """
|
|
1339
|
+
if isinstance(value, t := self.ELEMENTS[key].TYPE):
|
|
1340
|
+
self.values[key] = value
|
|
1341
|
+
else:
|
|
1342
|
+
raise ValueError(F"type got {value.TAG}, expected {t.TAG}")
|
|
1343
|
+
|
|
1344
|
+
def get_a_xdr(self) -> bytes:
|
|
1345
|
+
""" use in AssociationLN """
|
|
1346
|
+
res = bytearray()
|
|
1347
|
+
res.append(40 * int(self.values[0]) + int(self.values[1]))
|
|
1348
|
+
for i in range(2, len(self.ELEMENTS)):
|
|
1349
|
+
value = int(self.values[i])
|
|
1350
|
+
tmp = list()
|
|
1351
|
+
while value != 0 or not tmp:
|
|
1352
|
+
value, tmp1 = divmod(value, 128)
|
|
1353
|
+
tmp.append(tmp1)
|
|
1354
|
+
if len(tmp) != 1:
|
|
1355
|
+
tmp[-1] |= 0b1000_0000
|
|
1356
|
+
while tmp:
|
|
1357
|
+
res.append(tmp.pop())
|
|
1358
|
+
return bytes(res)
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
class AXDR:
|
|
1362
|
+
""" Use in structures for association LN objects """
|
|
1363
|
+
is_xdr: bool
|
|
1364
|
+
# NAME = Structure.NAME + " A-XDR"
|
|
1365
|
+
ELEMENTS: tuple[StructElement, ...]
|
|
1366
|
+
values: tuple[CommonDataType, None]
|
|
1367
|
+
|
|
1368
|
+
def __init__(self, value: bytes = None):
|
|
1369
|
+
match value:
|
|
1370
|
+
case bytes() as encoding:
|
|
1371
|
+
tag, length_and_contents = encoding[:1], encoding[1:]
|
|
1372
|
+
match tag:
|
|
1373
|
+
case b'\x09':
|
|
1374
|
+
values = [None] * len(self.ELEMENTS)
|
|
1375
|
+
self.__dict__['is_xdr'] = True
|
|
1376
|
+
self.__dict__['TAG'] = b'\x09'
|
|
1377
|
+
length, pdu = get_length_and_pdu(length_and_contents)
|
|
1378
|
+
if length <= len(pdu):
|
|
1379
|
+
xdr = pdu[:length]
|
|
1380
|
+
values_in: deque[int] = deque(xdr)
|
|
1381
|
+
values_index = iter(range(len(self.ELEMENTS)))
|
|
1382
|
+
# ger first two values
|
|
1383
|
+
two_values = divmod(values_in.popleft(), 40)
|
|
1384
|
+
# self._set_value(next(values_index), two_values[0])
|
|
1385
|
+
# self._set_value(next(values_index), two_values[1])
|
|
1386
|
+
i = next(values_index)
|
|
1387
|
+
values[i] = self.ELEMENTS[i].TYPE(two_values[0])
|
|
1388
|
+
i = next(values_index)
|
|
1389
|
+
values[i] = self.ELEMENTS[i].TYPE(two_values[1])
|
|
1390
|
+
tmp = 0
|
|
1391
|
+
while values_in:
|
|
1392
|
+
tmp = (tmp & 0b0111_1111) << 7
|
|
1393
|
+
if values_in[0] >= 0b1000_0000:
|
|
1394
|
+
tmp += values_in.popleft() & 0b0111_1111
|
|
1395
|
+
else:
|
|
1396
|
+
tmp += values_in.popleft()
|
|
1397
|
+
# self._set_value(next(values_index), tmp)
|
|
1398
|
+
i = next(values_index)
|
|
1399
|
+
values[i] = self.ELEMENTS[i].TYPE(tmp)
|
|
1400
|
+
tmp = 0
|
|
1401
|
+
self.__dict__['values'] = tuple(values)
|
|
1402
|
+
else:
|
|
1403
|
+
raise ValueError(F"expected {self.TAG} type, got {TAG(encoding[:1])}")
|
|
1404
|
+
case _:
|
|
1405
|
+
self.__dict__['is_xdr'] = False
|
|
1406
|
+
super(AXDR, self).__init__(value)
|
|
1407
|
+
case None: self.__init__(self.DEFAULT)
|
|
1408
|
+
|
|
1409
|
+
@property
|
|
1410
|
+
def contents(self) -> bytes:
|
|
1411
|
+
if self.is_xdr:
|
|
1412
|
+
return self.get_a_xdr()
|
|
1413
|
+
else:
|
|
1414
|
+
return super(AXDR, self).contents
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
class Boolean(SimpleDataType):
|
|
1418
|
+
""" boolean """
|
|
1419
|
+
TAG = TAG(b'\x03')
|
|
1420
|
+
|
|
1421
|
+
def __init__(self, value: bytes | bytearray | str | int | bool | float | datetime.datetime | datetime.time | Self = None):
|
|
1422
|
+
match value:
|
|
1423
|
+
case None: self.clear()
|
|
1424
|
+
case bytes(): self.contents = self.from_bytes(value)
|
|
1425
|
+
case bytearray(): self.contents = bytes(value) # Attention!!! changed method content getting from bytearray
|
|
1426
|
+
case str(): self.contents = self.from_str(value)
|
|
1427
|
+
case int(): self.contents = self.from_int(value)
|
|
1428
|
+
case bool(): self.contents = self.from_bool(value)
|
|
1429
|
+
case Boolean(): self.contents = value.contents
|
|
1430
|
+
case _: call_wrong_tag_in_value(value, self.TAG)
|
|
1431
|
+
|
|
1432
|
+
@property
|
|
1433
|
+
def encoding(self) -> bytes:
|
|
1434
|
+
return self.TAG + self.contents
|
|
1435
|
+
|
|
1436
|
+
def from_bytes(self, encoding: bytes) -> bytes:
|
|
1437
|
+
""" return 0x00 from 0x00, 0x01 from 0x01..0xFF """
|
|
1438
|
+
match len(encoding):
|
|
1439
|
+
case 0: raise ValueError(F"for create {self.TAG} got encoding without data")
|
|
1440
|
+
case 1: raise ValueError(F"for create {self.TAG} got encoding: {encoding.hex()} without contents")
|
|
1441
|
+
case _: """OK"""
|
|
1442
|
+
if (tag := encoding[:1]) != self.TAG:
|
|
1443
|
+
raise ValueError(F"expected {self.TAG} type, got {TAG(tag)}")
|
|
1444
|
+
return self.from_int(encoding[1])
|
|
1445
|
+
|
|
1446
|
+
def __str__(self) -> str:
|
|
1447
|
+
return "false" if self.contents == b'\x00' else "true"
|
|
1448
|
+
|
|
1449
|
+
@classmethod
|
|
1450
|
+
def parse(cls, value: str) -> Self:
|
|
1451
|
+
return cls(bytearray(b'\x00' if value == "false" else b'\x01'))
|
|
1452
|
+
|
|
1453
|
+
def from_int(self, value: int):
|
|
1454
|
+
return b'\x00' if value == 0 else b'\x01'
|
|
1455
|
+
|
|
1456
|
+
def from_str(self, value: str) -> bytes:
|
|
1457
|
+
if value == '0' or 'False'.startswith(value.title()) or 'Ложь'.startswith(value.title()) or \
|
|
1458
|
+
'No'.startswith(value.title()) or 'Нет'.startswith(value.title()):
|
|
1459
|
+
return b'\x00'
|
|
1460
|
+
elif value == '1' or 'True'.startswith(value.title()) or 'Правда'.startswith(value.title()) or \
|
|
1461
|
+
'Yes'.startswith(value.title()) or 'Да'.startswith(value.title()):
|
|
1462
|
+
return b'\x01'
|
|
1463
|
+
|
|
1464
|
+
def from_bool(self, value: bool) -> bytes:
|
|
1465
|
+
return b'\x01' if value else b'\x00'
|
|
1466
|
+
|
|
1467
|
+
def __bool__(self):
|
|
1468
|
+
return False if self.contents == b'\x00' else True
|
|
1469
|
+
|
|
1470
|
+
def clear(self):
|
|
1471
|
+
self.contents = b'\x00'
|
|
1472
|
+
|
|
1473
|
+
def __int__(self):
|
|
1474
|
+
return 0 if self.contents == b'\x00' else 1
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
class BitString(SimpleDataType):
|
|
1478
|
+
""" An ordered sequence of boolean values """
|
|
1479
|
+
TAG = TAG(b'\x04')
|
|
1480
|
+
__length: int
|
|
1481
|
+
default: bytes | bytearray | str | int = b'\x04\x00'
|
|
1482
|
+
|
|
1483
|
+
def __init__(self, value: bytearray | bytes | str | int | Self = None):
|
|
1484
|
+
match value:
|
|
1485
|
+
case None:
|
|
1486
|
+
new_instance = self.__class__(self.default)
|
|
1487
|
+
self.contents = new_instance.contents
|
|
1488
|
+
self.__length = len(new_instance)
|
|
1489
|
+
case bytes(): self.contents = self.from_bytes(value)
|
|
1490
|
+
case bytearray(): self.contents = bytes(value)
|
|
1491
|
+
case str(): self.contents = self.from_str(value)
|
|
1492
|
+
case int(): self.contents = self.from_int(value)
|
|
1493
|
+
case list(): self.contents = self.from_list(value)
|
|
1494
|
+
case BitString():
|
|
1495
|
+
self.contents = value.contents
|
|
1496
|
+
self.__length = len(value)
|
|
1497
|
+
case _: raise ValueError(F"can't create {self.TAG} with value {value}")
|
|
1498
|
+
|
|
1499
|
+
def set_length(self, value: int):
|
|
1500
|
+
self.__length = value
|
|
1501
|
+
|
|
1502
|
+
def from_bytes(self, value: bytes) -> bytes:
|
|
1503
|
+
self.__length, pdu = get_length_and_pdu(value[1:])
|
|
1504
|
+
match value[:1]:
|
|
1505
|
+
case self.TAG if self.__length == 0: return b''
|
|
1506
|
+
case self.TAG if self.__length <= len(pdu) * 8: return pdu[:ceil(self.__length / 8)]
|
|
1507
|
+
case self.TAG: raise ValueError(F'Length is {self.__length}, but contents got only {len(pdu) * 8}')
|
|
1508
|
+
case _ as error: raise ValueError(F"got {TAG(error)}, expected {self.TAG}")
|
|
1509
|
+
|
|
1510
|
+
@classmethod
|
|
1511
|
+
def parse(cls, value: str) -> Self:
|
|
1512
|
+
length = len(value)
|
|
1513
|
+
value = value + '0' * ((8 - length) % 8)
|
|
1514
|
+
new = cls(bytearray((int(value[count:(count + 8)], base=2) for count in range(0, length, 8))))
|
|
1515
|
+
new.set_length(length)
|
|
1516
|
+
return new
|
|
1517
|
+
|
|
1518
|
+
@deprecated("use parse")
|
|
1519
|
+
def from_str(self, value: str) -> bytes:
|
|
1520
|
+
self.__length = len(value)
|
|
1521
|
+
value = value + '0' * ((8 - self.__length) % 8)
|
|
1522
|
+
return bytes((int(value[count:(count + 8)], base=2) for count in range(0, self.__length, 8)))
|
|
1523
|
+
|
|
1524
|
+
def from_list(self, value: list[int]) -> bytes:
|
|
1525
|
+
return self.from_str("".join(map(str, value)))
|
|
1526
|
+
|
|
1527
|
+
def from_int(self, value: int) -> bytes:
|
|
1528
|
+
""" TODO: see like as Conformance """
|
|
1529
|
+
raise ValueError('not supported init from int')
|
|
1530
|
+
|
|
1531
|
+
def set(self, value: Self | bytes | bytearray | str | int | bool | float | datetime.date | None):
|
|
1532
|
+
""" TODO: partly copypast of SimpleDataType"""
|
|
1533
|
+
new_value = self._new_instance(value)
|
|
1534
|
+
if hasattr(self, 'cb_preset'):
|
|
1535
|
+
self.cb_preset(new_value)
|
|
1536
|
+
self.__dict__['contents'] = new_value.contents
|
|
1537
|
+
self.__length = len(new_value)
|
|
1538
|
+
if hasattr(self, 'cb_post_set'):
|
|
1539
|
+
self.cb_post_set()
|
|
1540
|
+
|
|
1541
|
+
def __setitem__(self, key: int, value: int | bool):
|
|
1542
|
+
tmp = list(self)
|
|
1543
|
+
tmp[key] = int(value)
|
|
1544
|
+
self.set(''.join(map(str, tmp)))
|
|
1545
|
+
|
|
1546
|
+
def inverse(self, index: int):
|
|
1547
|
+
""" inverse one bit by index"""
|
|
1548
|
+
self[index] = not list(self)[index]
|
|
1549
|
+
|
|
1550
|
+
def __lshift__(self, other):
|
|
1551
|
+
for i in range(other):
|
|
1552
|
+
tmp: list[int] = list(self)
|
|
1553
|
+
tmp.append(tmp.pop(0))
|
|
1554
|
+
self.set(''.join(map(str, tmp)))
|
|
1555
|
+
|
|
1556
|
+
def __rshift__(self, other):
|
|
1557
|
+
for i in range(other):
|
|
1558
|
+
tmp: list[int] = list(self)
|
|
1559
|
+
tmp.insert(0, tmp.pop())
|
|
1560
|
+
self.set(''.join(map(str, tmp)))
|
|
1561
|
+
|
|
1562
|
+
def __len__(self):
|
|
1563
|
+
return self.__length
|
|
1564
|
+
|
|
1565
|
+
def __setattr__(self, key, value):
|
|
1566
|
+
match key:
|
|
1567
|
+
case 'LENGTH' as prop: raise ValueError(F"Don't support set {prop}")
|
|
1568
|
+
case _: super().__setattr__(key, value)
|
|
1569
|
+
|
|
1570
|
+
def clear(self):
|
|
1571
|
+
"""set all bits as 0"""
|
|
1572
|
+
for i in range(len(self)):
|
|
1573
|
+
self[i] = 0
|
|
1574
|
+
|
|
1575
|
+
@property
|
|
1576
|
+
def encoding(self) -> bytes:
|
|
1577
|
+
return self.TAG + encode_length(len(self)) + self.contents
|
|
1578
|
+
|
|
1579
|
+
def __str__(self):
|
|
1580
|
+
""" TODO: copypast FlagMixin"""
|
|
1581
|
+
return ''.join(map(str, self))
|
|
1582
|
+
|
|
1583
|
+
def __getitem__(self, item) -> bytes:
|
|
1584
|
+
""" get bit from contents by index """
|
|
1585
|
+
return int(str(self)[item]).to_bytes(1, 'big')
|
|
1586
|
+
|
|
1587
|
+
def __iter__(self):
|
|
1588
|
+
def g():
|
|
1589
|
+
l = len(self)
|
|
1590
|
+
c = count()
|
|
1591
|
+
for byte_ in self.contents:
|
|
1592
|
+
for it in range(7, -1, -1):
|
|
1593
|
+
if next(c) < l:
|
|
1594
|
+
yield (byte_ >> it) & 0b00000001
|
|
1595
|
+
|
|
1596
|
+
return g()
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
class DoubleLong(Digital, SimpleDataType):
|
|
1600
|
+
""" Integer32 -2 147 483 648… 2 147 483 647 """
|
|
1601
|
+
TAG = TAG(b'\x05')
|
|
1602
|
+
SIGNED = True
|
|
1603
|
+
LENGTH = 4
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
class DoubleLongUnsigned(Digital, SimpleDataType):
|
|
1607
|
+
""" Unsigned32 0…4 294 967 295 """
|
|
1608
|
+
TAG = TAG(b'\x06')
|
|
1609
|
+
SIGNED = False
|
|
1610
|
+
LENGTH = 4
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
class OctetString(_String, SimpleDataType):
|
|
1614
|
+
""" An ordered sequence of octets (8 bit bytes) """
|
|
1615
|
+
TAG = TAG(b'\x09')
|
|
1616
|
+
|
|
1617
|
+
@deprecated("use parse")
|
|
1618
|
+
def from_str(self, value: str) -> bytes:
|
|
1619
|
+
""" input as hex code """
|
|
1620
|
+
return bytes.fromhex(value)
|
|
1621
|
+
|
|
1622
|
+
def from_int(self, value: int) -> bytes:
|
|
1623
|
+
""" Convert with recursion. Maximum convert length is 32 """
|
|
1624
|
+
def to_bytes_with(length_):
|
|
1625
|
+
try:
|
|
1626
|
+
return int.to_bytes(value, length_, 'big')
|
|
1627
|
+
except OverflowError:
|
|
1628
|
+
if length_ > 31:
|
|
1629
|
+
raise ValueError(F'Value {value} is big to convert to bytes')
|
|
1630
|
+
return to_bytes_with(length_+1)
|
|
1631
|
+
length = 1
|
|
1632
|
+
return to_bytes_with(length)
|
|
1633
|
+
|
|
1634
|
+
def __str__(self):
|
|
1635
|
+
return F"{self.contents.hex(' ')}"
|
|
1636
|
+
|
|
1637
|
+
@classmethod
|
|
1638
|
+
def parse(cls, value: str) -> Self:
|
|
1639
|
+
return cls(bytearray.fromhex(value))
|
|
1640
|
+
|
|
1641
|
+
def __len__(self):
|
|
1642
|
+
return len(self.contents)
|
|
1643
|
+
|
|
1644
|
+
def __getitem__(self, item):
|
|
1645
|
+
return self.contents[item]
|
|
1646
|
+
|
|
1647
|
+
def to_str(self, encoding: str = "utf-8") -> str:
|
|
1648
|
+
""" decode to utf-8 by default, replace to '?' if unsupported """
|
|
1649
|
+
temp = list()
|
|
1650
|
+
for i in self.contents:
|
|
1651
|
+
temp.append(i if i > 32 else 63)
|
|
1652
|
+
return bytes(temp).decode(encoding, errors="ignore")
|
|
1653
|
+
|
|
1654
|
+
def pretty_str(self) -> str:
|
|
1655
|
+
"""decode to utf-8 or hex labal"""
|
|
1656
|
+
try:
|
|
1657
|
+
return self.contents.decode("utf-8")
|
|
1658
|
+
except Exception as e:
|
|
1659
|
+
return F"{self}(HEX)"
|
|
1660
|
+
|
|
1661
|
+
class VisibleString(_String, SimpleDataType):
|
|
1662
|
+
""" An ordered sequence of octets (8 bit bytes) """
|
|
1663
|
+
TAG = TAG(b'\x0A')
|
|
1664
|
+
|
|
1665
|
+
def from_str(self, value: str) -> bytes:
|
|
1666
|
+
return bytes(value, 'cp1251')
|
|
1667
|
+
|
|
1668
|
+
def from_int(self, value: int) -> bytes:
|
|
1669
|
+
return bytes(str(value), 'cp1251')
|
|
1670
|
+
|
|
1671
|
+
def __str__(self):
|
|
1672
|
+
return bytes([char if char >= 0x20 else 63 for char in self.contents]).decode(encoding='cp1251')
|
|
1673
|
+
|
|
1674
|
+
def __len__(self):
|
|
1675
|
+
return len(self.contents)
|
|
1676
|
+
|
|
1677
|
+
@deprecated("use str")
|
|
1678
|
+
def to_str(self) -> str:
|
|
1679
|
+
temp = list()
|
|
1680
|
+
for i in self.contents:
|
|
1681
|
+
temp.append(i if i >= 32 else 63)
|
|
1682
|
+
return bytes(temp).decode(encoding)
|
|
1683
|
+
|
|
1684
|
+
@classmethod
|
|
1685
|
+
def parse(cls, value: str) -> Self:
|
|
1686
|
+
return cls(bytearray(value, encoding="utf-8"))
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
class Utf8String(_String, SimpleDataType):
|
|
1690
|
+
""" An ordered sequence of characters encoded as UTF-8 """
|
|
1691
|
+
TAG = TAG(b'\x0c')
|
|
1692
|
+
|
|
1693
|
+
def from_str(self, value: str) -> bytes:
|
|
1694
|
+
return bytes(value, "utf-8")
|
|
1695
|
+
|
|
1696
|
+
def from_int(self, value: int) -> bytes:
|
|
1697
|
+
return bytes(str(value), "utf-8")
|
|
1698
|
+
|
|
1699
|
+
def __str__(self):
|
|
1700
|
+
return self.contents.decode("utf-8")
|
|
1701
|
+
|
|
1702
|
+
@classmethod
|
|
1703
|
+
def parse(cls, value: str) -> Self:
|
|
1704
|
+
return cls(bytearray(value, "utf-8"))
|
|
1705
|
+
|
|
1706
|
+
def __len__(self):
|
|
1707
|
+
return len(self.contents)
|
|
1708
|
+
|
|
1709
|
+
# TODO: Bcd need more do here, now realisation like as Enum
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
class Bcd(SimpleDataType):
|
|
1713
|
+
""" binary coded decimal """
|
|
1714
|
+
TAG = TAG(TAG(b'\x0d'))
|
|
1715
|
+
|
|
1716
|
+
def __init__(self, value: bytes | bytearray | str | int | Self = None):
|
|
1717
|
+
match value: # TODO: replace priority case
|
|
1718
|
+
case None: bytes(self.contents_length)
|
|
1719
|
+
case bytes(): self.contents = self.from_bytes(value)
|
|
1720
|
+
case bytearray(): self.contents = bytes(value)
|
|
1721
|
+
case str(): self.contents = self.from_str(value)
|
|
1722
|
+
case int(): self.contents = self.from_int(value)
|
|
1723
|
+
case Bcd(): self.contents = value.contents
|
|
1724
|
+
case _: call_wrong_tag_in_value(value, self.TAG)
|
|
1725
|
+
|
|
1726
|
+
def from_bytes(self, encoding: bytes) -> bytes:
|
|
1727
|
+
""" Full encoding receiver: Tag+Length+Content """
|
|
1728
|
+
length_and_contents = encoding[1:]
|
|
1729
|
+
match encoding[:1], self.contents_length:
|
|
1730
|
+
case self.TAG, int() if self.contents_length <= len(length_and_contents): return length_and_contents[:self.contents_length]
|
|
1731
|
+
case self.TAG, _: raise ValueError(F'Length of contents for {self.__class__.__name__} must be at least '
|
|
1732
|
+
F'{self.contents_length}, but got {len(length_and_contents)}')
|
|
1733
|
+
case _ as wrong_tag, _: call_wrong_tag_in_value(wrong_tag, self.TAG)
|
|
1734
|
+
|
|
1735
|
+
@classmethod
|
|
1736
|
+
def parse(cls, value: str) -> Self:
|
|
1737
|
+
try:
|
|
1738
|
+
return cls(bytearray(int(value).to_bytes(1, 'little')))
|
|
1739
|
+
except OverflowError:
|
|
1740
|
+
raise ParseError(F"in {cls.__name__} {value=} out of range")
|
|
1741
|
+
|
|
1742
|
+
@property
|
|
1743
|
+
def encoding(self) -> bytes:
|
|
1744
|
+
return self.TAG + self.contents
|
|
1745
|
+
|
|
1746
|
+
def clear(self):
|
|
1747
|
+
self.contents = b'\x00'
|
|
1748
|
+
|
|
1749
|
+
@property
|
|
1750
|
+
def contents_length(self) -> int: return 1
|
|
1751
|
+
|
|
1752
|
+
@deprecated("use parse")
|
|
1753
|
+
def from_str(self, value: str) -> bytes:
|
|
1754
|
+
try:
|
|
1755
|
+
return int(value).to_bytes(1, 'little')
|
|
1756
|
+
except OverflowError:
|
|
1757
|
+
raise ValueError('Value out of range')
|
|
1758
|
+
|
|
1759
|
+
def from_int(self, value: int) -> bytes:
|
|
1760
|
+
try:
|
|
1761
|
+
return value.to_bytes(1, 'little')
|
|
1762
|
+
except OverflowError:
|
|
1763
|
+
raise ValueError(F'value: {value} not in range')
|
|
1764
|
+
|
|
1765
|
+
def __str__(self):
|
|
1766
|
+
return str(int.from_bytes(self.contents, byteorder='little'))
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
class Integer(Digital, SimpleDataType):
|
|
1770
|
+
""" Integer8 -128…127"""
|
|
1771
|
+
TAG = TAG(b'\x0f')
|
|
1772
|
+
SIGNED = True
|
|
1773
|
+
LENGTH = 1
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
class Long(Digital, SimpleDataType):
|
|
1777
|
+
""" Integer16 -32 768…32 767 """
|
|
1778
|
+
TAG = TAG(b'\x10')
|
|
1779
|
+
SIGNED = True
|
|
1780
|
+
LENGTH = 2
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
class Unsigned(Digital, SimpleDataType):
|
|
1784
|
+
""" Unsigned8 0…255 """
|
|
1785
|
+
TAG = TAG(b'\x11')
|
|
1786
|
+
SIGNED = False
|
|
1787
|
+
LENGTH = 1
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
class LongUnsigned(Digital, SimpleDataType):
|
|
1791
|
+
""" Unsigned16 0…65535"""
|
|
1792
|
+
TAG = TAG(b'\x12')
|
|
1793
|
+
SIGNED = False
|
|
1794
|
+
LENGTH = 2
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
class CompactArray(_Array, ComplexDataType):
|
|
1798
|
+
""" Provides an alternative, compact encoding of complex data. TODO: need test, may be don't work """
|
|
1799
|
+
TAG = TAG(b'\x13')
|
|
1800
|
+
|
|
1801
|
+
def __init__(self, elements_type: type[SimpleDataType | Structure],
|
|
1802
|
+
elements: list[SimpleDataType | Structure] = None,
|
|
1803
|
+
length: int = None):
|
|
1804
|
+
super(CompactArray, self).__init__(elements_type, elements, length)
|
|
1805
|
+
dummy_type_instance = elements_type()
|
|
1806
|
+
self.__element_types = b'' if not len(dummy_type_instance) else \
|
|
1807
|
+
b''.join([dummy_type_instance.length] + [element.TAG for element in dummy_type_instance.ELEMENTS])
|
|
1808
|
+
|
|
1809
|
+
@property
|
|
1810
|
+
def contents(self) -> bytes:
|
|
1811
|
+
return b''.join([element.complex_data for element in self.elements])
|
|
1812
|
+
|
|
1813
|
+
@property
|
|
1814
|
+
def encoding(self) -> bytes:
|
|
1815
|
+
""" self encoding fof compact array """
|
|
1816
|
+
return self.TAG + self.__type.TAG + self.__element_types + encode_length(len(self.elements)) + self.contents
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
class Long64(Digital, SimpleDataType):
|
|
1820
|
+
""" Integer64 - 2**63…2**63-1 """
|
|
1821
|
+
TAG = TAG(b'\x14')
|
|
1822
|
+
SIGNED = True
|
|
1823
|
+
LENGTH = 8
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
class Long64Unsigned(Digital, SimpleDataType):
|
|
1827
|
+
""" Unsigned64 0…2^64-1 """
|
|
1828
|
+
TAG = TAG(b'\x15')
|
|
1829
|
+
SIGNED = False
|
|
1830
|
+
LENGTH = 8
|
|
1831
|
+
|
|
1832
|
+
|
|
1833
|
+
enum_rep = re.compile("\((?P<value>\d{1,3})\).+")
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
class Enum(IntegerEnum, Unsigned):
|
|
1837
|
+
""" The elements of the enumeration type are defined in the “Attribute description” section of a COSEM interface class specification """
|
|
1838
|
+
contents: bytes
|
|
1839
|
+
TAG = TAG(b'\x16')
|
|
1840
|
+
NAMES: dict[int, str] = None
|
|
1841
|
+
__slots__ = ("contents",)
|
|
1842
|
+
__match_args__ = ('value2', )
|
|
1843
|
+
|
|
1844
|
+
def __init__(self, value: bytes | bytearray | str | int | Self = None):
|
|
1845
|
+
match value: # TODO: replace priority case
|
|
1846
|
+
case bytes() as encoding:
|
|
1847
|
+
match encoding[:1]:
|
|
1848
|
+
case self.TAG if len(encoding) >= 2: self.contents = encoding[1:2]
|
|
1849
|
+
case self.TAG: raise ValueError(F'Length of contents for {self.__class__.__name__} must be at least 1, but got {len(encoding[1:])}')
|
|
1850
|
+
case _ as wrong_tag: call_wrong_tag_in_value(wrong_tag, self.TAG)
|
|
1851
|
+
case bytearray(): self.contents = bytes(value)
|
|
1852
|
+
case None: self.contents = self.from_none()
|
|
1853
|
+
case str(): self.contents = self.from_str(value)
|
|
1854
|
+
case int(): self.contents = self.from_int(value)
|
|
1855
|
+
case self.__class__(): self.contents = value.contents
|
|
1856
|
+
case _: raise ValueError(F'Unknown type for {self.__class__.__name__} with value {value}<{value.__class__}>')
|
|
1857
|
+
|
|
1858
|
+
def from_str(self, value: str) -> bytes:
|
|
1859
|
+
if value.isdigit():
|
|
1860
|
+
return self.from_int(int(value))
|
|
1861
|
+
elif res := enum_rep.search(value):
|
|
1862
|
+
return self.from_int(int(res.group("value")))
|
|
1863
|
+
else:
|
|
1864
|
+
raise ValueError(F'Error create {self.__class__.__name__} with value {value}')
|
|
1865
|
+
|
|
1866
|
+
def from_none(self):
|
|
1867
|
+
"""first key value"""
|
|
1868
|
+
if len(self.NAMES) != 0:
|
|
1869
|
+
return next(iter(self.NAMES)).to_bytes(1, "big")
|
|
1870
|
+
else:
|
|
1871
|
+
return b'\x00'
|
|
1872
|
+
|
|
1873
|
+
@classmethod
|
|
1874
|
+
def get_values(cls) -> list[str]:
|
|
1875
|
+
""" TODO: """
|
|
1876
|
+
return [cls(k).get_report().msg for k in cls.NAMES.keys()]
|
|
1877
|
+
|
|
1878
|
+
def __len__(self):
|
|
1879
|
+
return len(self.NAMES)
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
class Float32(Float, SimpleDataType):
|
|
1883
|
+
"""float32. ISO/IEC/IEEE 60559:2011"""
|
|
1884
|
+
TAG = TAG(b'\x17')
|
|
1885
|
+
FORMAT = ">f"
|
|
1886
|
+
SIZE = 4
|
|
1887
|
+
|
|
1888
|
+
class Float64(Float, SimpleDataType):
|
|
1889
|
+
"""float64. ISO/IEC/IEEE 60559:2011"""
|
|
1890
|
+
TAG = TAG(b'\x18')
|
|
1891
|
+
FORMAT = ">d"
|
|
1892
|
+
SIZE = 8
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
_SHORT_MONTHS = (4, 6, 9, 11)
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
class DateTime(__DateTime, __Date, __Time, SimpleDataType):
|
|
1899
|
+
"""date-time"""
|
|
1900
|
+
TAG = TAG(b'\x19')
|
|
1901
|
+
_separators = ('.', '.', '-', ' ', ':', ':', '.', ' ')
|
|
1902
|
+
|
|
1903
|
+
def __init__(self, value: datetime.datetime | datetime.date | bytearray | bytes | str = None):
|
|
1904
|
+
super(DateTime, self).__init__(value)
|
|
1905
|
+
self.check_date(self.contents[0:5])
|
|
1906
|
+
self.check_time()
|
|
1907
|
+
|
|
1908
|
+
def __len__(self) -> int: return 12
|
|
1909
|
+
|
|
1910
|
+
@property
|
|
1911
|
+
def DEFAULT(self): return b'\x07\xe4\x01\x01\xff\x00\x00\x00\x00\x00\xb4\xff'
|
|
1912
|
+
|
|
1913
|
+
#todo: move to parse
|
|
1914
|
+
@classmethod
|
|
1915
|
+
def from_str(cls, value: str) -> bytes:
|
|
1916
|
+
def from_deviation() -> bytes:
|
|
1917
|
+
nonlocal dev
|
|
1918
|
+
match dev:
|
|
1919
|
+
case '':
|
|
1920
|
+
return b'\x80\x00'
|
|
1921
|
+
case '-':
|
|
1922
|
+
return b'\x00\x00'
|
|
1923
|
+
case _ if -720 <= int(dev) <= 720:
|
|
1924
|
+
return pack('>h', int(dev))
|
|
1925
|
+
|
|
1926
|
+
match value.split(sep=' ', maxsplit=2):
|
|
1927
|
+
case date, time, dev: return cls.strpdate(date) + cls.strptime(time) + from_deviation() + b'\xff'
|
|
1928
|
+
case date, time: return cls.strpdate(date) + cls.strptime(time) + b'\x80\x00\xff'
|
|
1929
|
+
case date, : return cls.strpdate(date) + b'\xff\xff\xff\xff\x80\x00\xff'
|
|
1930
|
+
case ['']: return b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xff'
|
|
1931
|
+
case _: raise ValueError(F'a lot of separators')
|
|
1932
|
+
|
|
1933
|
+
@classmethod
|
|
1934
|
+
def parse(cls, value: str) -> Self:
|
|
1935
|
+
return cls(bytearray(cls.from_str(value)))
|
|
1936
|
+
|
|
1937
|
+
def from_datetime(self, value: datetime.datetime) -> bytes:
|
|
1938
|
+
""" convert from build to DLMS datetime, weekday not set for uniquely datetime """
|
|
1939
|
+
match value.utcoffset():
|
|
1940
|
+
case None: deviation = 0x8000
|
|
1941
|
+
case _: deviation = value.utcoffset().seconds // 60
|
|
1942
|
+
return pack('>HBBBBBBBH',
|
|
1943
|
+
value.year,
|
|
1944
|
+
value.month,
|
|
1945
|
+
value.day,
|
|
1946
|
+
255,
|
|
1947
|
+
value.hour,
|
|
1948
|
+
value.minute,
|
|
1949
|
+
value.second,
|
|
1950
|
+
value.microsecond//10_000,
|
|
1951
|
+
deviation)+b'\xFF'
|
|
1952
|
+
|
|
1953
|
+
def from_date(self, value: datetime.date) -> bytes:
|
|
1954
|
+
return bytes(((value.year >> 8) & 0xFF, value.year & 0xFF, value.month, value.day, value.weekday() + 1)) + b'\xFF\xFF\xFF\xFF\x80\x00\xFF'
|
|
1955
|
+
|
|
1956
|
+
def from_time(self, value: datetime.time) -> bytes:
|
|
1957
|
+
return b'\xFF\xFF\xFF\xFF\xFF'+bytes((value.hour, value.minute, value.second, value.microsecond // 10_000)) + \
|
|
1958
|
+
b'\x80\x00\xFF'
|
|
1959
|
+
|
|
1960
|
+
def set_clock_status(self, value: str | int):
|
|
1961
|
+
""" now only set value """
|
|
1962
|
+
self.contents = self.contents[:12] + int(value).to_bytes(1, 'big')
|
|
1963
|
+
|
|
1964
|
+
def __str__(self):
|
|
1965
|
+
match unpack('>h', self.contents[9:11])[0]:
|
|
1966
|
+
case -0x8000: deviation = ''
|
|
1967
|
+
case _ as value: deviation = str(value)
|
|
1968
|
+
return F"{self.strfdate} {self.strftime} {deviation}"
|
|
1969
|
+
|
|
1970
|
+
def to_datetime(self) -> datetime.datetime:
|
|
1971
|
+
return datetime.datetime(
|
|
1972
|
+
year=self.year if self.year != 0xffff else datetime.MINYEAR,
|
|
1973
|
+
month=1 if self.month in (0xff, 0xfe, 0xfd) else self.month,
|
|
1974
|
+
day=1 if self.day in (0xff, 0xfe, 0xfd) else self.day,
|
|
1975
|
+
hour=self.hour if self.hour != 0xff else 0,
|
|
1976
|
+
minute=self.minute if self.minute != 0xff else 0,
|
|
1977
|
+
second=self.second if self.second != 0xff else 0,
|
|
1978
|
+
microsecond=self.hundredths*10000 if self.hundredths != 0xff else 0,
|
|
1979
|
+
tzinfo=datetime.timezone.utc if self.deviation == -0x8000 else datetime.timezone(datetime.timedelta(minutes=self.deviation)))
|
|
1980
|
+
|
|
1981
|
+
@property
|
|
1982
|
+
def deviation(self) -> int:
|
|
1983
|
+
return unpack(">h", self.contents[9:11])[0]
|
|
1984
|
+
|
|
1985
|
+
def set_deviation(self, value: int):
|
|
1986
|
+
if (
|
|
1987
|
+
-720 <= value <= 720
|
|
1988
|
+
or value == -0x8000
|
|
1989
|
+
):
|
|
1990
|
+
contents = bytearray(self.contents)
|
|
1991
|
+
contents[9:11] = pack(">h", value)
|
|
1992
|
+
self.__dict__["contents"] = bytes(contents)
|
|
1993
|
+
else:
|
|
1994
|
+
raise OutOfRange(F"in year: got {value}, expected -720..720, 32768")
|
|
1995
|
+
|
|
1996
|
+
@property
|
|
1997
|
+
def time_zone(self) -> datetime.timezone | None:
|
|
1998
|
+
""":return timezone from deviation """
|
|
1999
|
+
if self.deviation == -0x8000:
|
|
2000
|
+
return None
|
|
2001
|
+
else:
|
|
2002
|
+
return datetime.timezone(datetime.timedelta(minutes=self.deviation))
|
|
2003
|
+
|
|
2004
|
+
def get_left_nearest_date(self, point: datetime.datetime) -> datetime.datetime | None:
|
|
2005
|
+
""" search and return date(datetime format) in left from point """
|
|
2006
|
+
res: datetime.datetime = self.to_datetime()
|
|
2007
|
+
""" time in left from point """
|
|
2008
|
+
months = range(point.month, 0, -1) if self.month == 0xff else (self.month,)
|
|
2009
|
+
""" months sequence from 12 to 1 with start from current month or self month """
|
|
2010
|
+
days = range(point.day, 0, -1) if self.day == 0xff else (self.day,)
|
|
2011
|
+
""" days sequence from 31 to 1 with start from current day or self day """
|
|
2012
|
+
for year in range(point.year, datetime.MINYEAR, -1) if self.year == 0xffff else (self.year, ):
|
|
2013
|
+
res = res.replace(year=year)
|
|
2014
|
+
for month in months:
|
|
2015
|
+
res = res.replace(month=month)
|
|
2016
|
+
for day in days:
|
|
2017
|
+
res = res.replace(day=day)
|
|
2018
|
+
if res > point:
|
|
2019
|
+
continue
|
|
2020
|
+
elif (
|
|
2021
|
+
self.weekday != 0xff
|
|
2022
|
+
and self.weekday != (res.weekday() + 1)
|
|
2023
|
+
):
|
|
2024
|
+
continue
|
|
2025
|
+
else:
|
|
2026
|
+
return res
|
|
2027
|
+
days = range(31, 0, -1) if self.day == 0xff else self.day,
|
|
2028
|
+
months = range(12, 0, -1) if self.month == 0xff else self.month,
|
|
2029
|
+
return None
|
|
2030
|
+
|
|
2031
|
+
def get_right_nearest_date(self, point: datetime.datetime) -> datetime.datetime | None:
|
|
2032
|
+
""" search and return date(datetime format) in rigth from point """
|
|
2033
|
+
res: datetime.datetime = self.to_datetime()
|
|
2034
|
+
""" time in left from point """
|
|
2035
|
+
months = range(point.month, 12) if self.month == 0xff else (self.month,)
|
|
2036
|
+
""" months sequence from 12 to 1 with start from current month or self month """
|
|
2037
|
+
days = range(point.day, 32) if self.day == 0xff else (self.day,)
|
|
2038
|
+
""" days sequence from 31 to 1 with start from current day or self day """
|
|
2039
|
+
for year in range(point.year, datetime.MAXYEAR) if self.year == 0xffff else (self.year, ):
|
|
2040
|
+
res = res.replace(year=year)
|
|
2041
|
+
for month in months:
|
|
2042
|
+
res = res.replace(month=month)
|
|
2043
|
+
for day in days:
|
|
2044
|
+
res = res.replace(day=day)
|
|
2045
|
+
if res < point:
|
|
2046
|
+
continue
|
|
2047
|
+
elif (
|
|
2048
|
+
self.weekday!=0xff
|
|
2049
|
+
and self.weekday != (res.weekday() + 1)
|
|
2050
|
+
):
|
|
2051
|
+
continue
|
|
2052
|
+
else:
|
|
2053
|
+
return res
|
|
2054
|
+
days = range(0, 32) if self.day == 0xff else self.day,
|
|
2055
|
+
months = range(0, 12) if self.month == 0xff else self.month,
|
|
2056
|
+
return None
|
|
2057
|
+
|
|
2058
|
+
def get_right_nearest_datetime(self, point: datetime.datetime) -> datetime.datetime | None:
|
|
2059
|
+
""" search and return datetime in right from point """
|
|
2060
|
+
years = range(point.year, datetime.MAXYEAR + 1) if self.year == 0xFFFF else (self.year,)
|
|
2061
|
+
months = range(point.month, 13) if self.month == 0xFF else (self.month,)
|
|
2062
|
+
days = range(point.day, 32) if self.day == 0xFF else (self.day,)
|
|
2063
|
+
hours = range(point.hour, 24) if self.hour == 0xFF else (self.hour,)
|
|
2064
|
+
minutes = range(point.minute, 60) if self.minute == 0xFF else (self.minute,)
|
|
2065
|
+
seconds = range(point.second, 60) if self.second == 0xFF else (self.second,)
|
|
2066
|
+
if self.time_zone is None:
|
|
2067
|
+
point = point.replace(tzinfo=None)
|
|
2068
|
+
for y in years:
|
|
2069
|
+
for m in months:
|
|
2070
|
+
max_day = 31
|
|
2071
|
+
if m == 2:
|
|
2072
|
+
max_day = 29 if (
|
|
2073
|
+
y % 4 == 0
|
|
2074
|
+
and (
|
|
2075
|
+
y % 100 != 0
|
|
2076
|
+
or y % 400 == 0
|
|
2077
|
+
)
|
|
2078
|
+
) else 28
|
|
2079
|
+
elif m in _SHORT_MONTHS:
|
|
2080
|
+
max_day = 30
|
|
2081
|
+
for d in days:
|
|
2082
|
+
if d > max_day:
|
|
2083
|
+
continue
|
|
2084
|
+
for h in hours:
|
|
2085
|
+
for min_val in minutes:
|
|
2086
|
+
for s in seconds:
|
|
2087
|
+
try:
|
|
2088
|
+
dt = datetime.datetime(y, m, d, h, min_val, s, tzinfo=self.time_zone)
|
|
2089
|
+
if dt >= point:
|
|
2090
|
+
return dt
|
|
2091
|
+
except ValueError:
|
|
2092
|
+
continue
|
|
2093
|
+
return None
|
|
2094
|
+
|
|
2095
|
+
def get_left_nearest_datetime(self, point: datetime.datetime) -> datetime.datetime | None:
|
|
2096
|
+
""" search and return datetime in left from point """
|
|
2097
|
+
l_point: datetime.datetime = self.get_left_nearest_date(point)
|
|
2098
|
+
""" time in left from point """
|
|
2099
|
+
if l_point is None:
|
|
2100
|
+
return None
|
|
2101
|
+
is_this_day: bool = l_point.date() == point.date()
|
|
2102
|
+
""" flag of points equaling """
|
|
2103
|
+
for hour in range(point.hour if is_this_day else 23, -1, -1) if self.hour == 0xff else (self.hour,):
|
|
2104
|
+
l_point = l_point.replace(hour=hour)
|
|
2105
|
+
for minute in range(point.minute if is_this_day and l_point.hour == point.hour else 59, -1, -1) if self.minute == 0xff else (self.minute,):
|
|
2106
|
+
l_point = l_point.replace(minute=minute)
|
|
2107
|
+
for second in range(point.second if is_this_day and l_point.hour == point.hour and
|
|
2108
|
+
l_point.minute == point.minute else 59, -1, -1) if self.second == 0xff else (self.second,):
|
|
2109
|
+
l_point = l_point.replace(second=second)
|
|
2110
|
+
for microsecond in range(point.microsecond if is_this_day and l_point.hour == point.hour and
|
|
2111
|
+
l_point.minute == point.minute and
|
|
2112
|
+
l_point.second == point.second else 990000, -1, -10000) if self.hundredths == 0xff else (self.hundredths * 10000,):
|
|
2113
|
+
l_point = l_point.replace(microsecond=microsecond)
|
|
2114
|
+
if l_point > point:
|
|
2115
|
+
continue
|
|
2116
|
+
else:
|
|
2117
|
+
return l_point
|
|
2118
|
+
return None
|
|
2119
|
+
|
|
2120
|
+
|
|
2121
|
+
class Date(__DateTime, __Date, SimpleDataType):
|
|
2122
|
+
"""date"""
|
|
2123
|
+
TAG = TAG(b'\x1a')
|
|
2124
|
+
_separators = ('.', '.', '-')
|
|
2125
|
+
|
|
2126
|
+
def __init__(self, value: datetime.datetime | datetime.date | bytearray | bytes | str | int = None):
|
|
2127
|
+
super(Date, self).__init__(value)
|
|
2128
|
+
self.check_date(self.contents)
|
|
2129
|
+
|
|
2130
|
+
@property
|
|
2131
|
+
def DEFAULT(self): return b'\x07\xe4\x01\x01\x03'
|
|
2132
|
+
|
|
2133
|
+
def __len__(self) -> int: return 5
|
|
2134
|
+
|
|
2135
|
+
@deprecated("use parse")
|
|
2136
|
+
def from_str(self, value: str) -> bytes:
|
|
2137
|
+
return self.strpdate(value)
|
|
2138
|
+
|
|
2139
|
+
@classmethod
|
|
2140
|
+
def parse(cls, value: str) -> Self:
|
|
2141
|
+
return cls(bytearray(cls.strpdate(value)))
|
|
2142
|
+
|
|
2143
|
+
def from_datetime(self, value: datetime.datetime) -> bytes:
|
|
2144
|
+
return bytes(((value.year >> 8) & 0xFF, value.year & 0xFF, value.month, value.day, value.weekday() + 1))
|
|
2145
|
+
|
|
2146
|
+
def from_date(self, value: datetime.date) -> bytes:
|
|
2147
|
+
return bytes(((value.year >> 8) & 0xFF, value.year & 0xFF, value.month, value.day, value.weekday() + 1))
|
|
2148
|
+
|
|
2149
|
+
def to_datetime(self) -> datetime.date:
|
|
2150
|
+
year_highbyte, year_lowbyte, month, day_of_month, _ = self.contents
|
|
2151
|
+
year = year_highbyte*256+year_lowbyte
|
|
2152
|
+
return datetime.date(year=year if year != 0xffff else datetime.MINYEAR,
|
|
2153
|
+
month=month if month not in {0xff, 0xfe, 0xfd} else 1,
|
|
2154
|
+
day=day_of_month if day_of_month not in {0xff, 0xfe, 0xfd} else 1)
|
|
2155
|
+
|
|
2156
|
+
def __str__(self):
|
|
2157
|
+
return self.strfdate
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
class Time(__DateTime, __Time, SimpleDataType):
|
|
2161
|
+
"""time"""
|
|
2162
|
+
TAG = TAG(b'\x1b')
|
|
2163
|
+
_separators = (':', ':', '.')
|
|
2164
|
+
|
|
2165
|
+
def __init__(self, value: datetime.datetime | datetime.time | bytearray | bytes | str = None):
|
|
2166
|
+
super(Time, self).__init__(value)
|
|
2167
|
+
self.check_time()
|
|
2168
|
+
|
|
2169
|
+
def __len__(self) -> int: return 4
|
|
2170
|
+
|
|
2171
|
+
@property
|
|
2172
|
+
def DEFAULT(self): return b'\x00\x00\x00\x00'
|
|
2173
|
+
|
|
2174
|
+
def from_str(self, value: str) -> bytes:
|
|
2175
|
+
return self.strptime(value)
|
|
2176
|
+
|
|
2177
|
+
@classmethod
|
|
2178
|
+
def parse(cls, value: str) -> Self:
|
|
2179
|
+
return cls(bytearray(cls.strptime(value)))
|
|
2180
|
+
|
|
2181
|
+
def from_datetime(self, value: datetime.datetime) -> bytes:
|
|
2182
|
+
return bytes((value.hour, value.minute, value.second, value.microsecond // 10_000))
|
|
2183
|
+
|
|
2184
|
+
def from_time(self, value: datetime.time) -> bytes:
|
|
2185
|
+
return bytes((value.hour, value.minute, value.second, value.microsecond // 10_000))
|
|
2186
|
+
|
|
2187
|
+
def __str__(self):
|
|
2188
|
+
return self.strftime
|
|
2189
|
+
|
|
2190
|
+
def to_time(self) -> datetime.time:
|
|
2191
|
+
""" return python time. Used 00 instead 'NOT SPECIFIED' """
|
|
2192
|
+
hour, minute, second, hundredths = self.contents
|
|
2193
|
+
return datetime.time(hour=hour if hour != 0xff else 0,
|
|
2194
|
+
minute=minute if minute != 0xff else 0,
|
|
2195
|
+
second=second if second != 0xff else 0,
|
|
2196
|
+
microsecond=hundredths*10000 if hundredths != 0xff else 0)
|
|
2197
|
+
|
|
2198
|
+
def get_left_nearest_time(self, point: datetime.time) -> datetime.time | None:
|
|
2199
|
+
""" search and return time in left from point """
|
|
2200
|
+
l_point: datetime.time = self.to_time()
|
|
2201
|
+
""" time in left from point """
|
|
2202
|
+
for hour in range(point.hour, -1, -1) if self.hour == 0xff else (self.hour,):
|
|
2203
|
+
l_point = l_point.replace(hour=hour)
|
|
2204
|
+
for minute in range(point.minute if l_point.hour == point.hour else 59, -1, -1) if self.minute == 0xff else (self.minute,):
|
|
2205
|
+
l_point = l_point.replace(minute=minute)
|
|
2206
|
+
for second in range(point.second if l_point.hour == point.hour and
|
|
2207
|
+
l_point.minute == point.minute else 59, -1, -1) if self.second == 0xff else (self.second,):
|
|
2208
|
+
l_point = l_point.replace(second=second)
|
|
2209
|
+
for microsecond in range(point.microsecond if l_point.hour == point.hour and
|
|
2210
|
+
l_point.minute == point.minute and
|
|
2211
|
+
l_point.second == point.second else 990000, -1, -10000) if self.hundredths == 0xff else (self.hundredths * 10000,):
|
|
2212
|
+
l_point = l_point.replace(microsecond=microsecond)
|
|
2213
|
+
if l_point > point:
|
|
2214
|
+
continue
|
|
2215
|
+
else:
|
|
2216
|
+
return l_point
|
|
2217
|
+
return None
|
|
2218
|
+
|
|
2219
|
+
@classmethod
|
|
2220
|
+
def from_float(cls, value: float, second: bool = False, hundredths: bool = False) -> Self:
|
|
2221
|
+
"""new instance from part of day"""
|
|
2222
|
+
if 0 <= value < 1:
|
|
2223
|
+
res = bytearray(4)
|
|
2224
|
+
div, res[3] = divmod(int(value * 8640000), 100)
|
|
2225
|
+
div, res[2] = divmod(div, 60)
|
|
2226
|
+
div, res[1] = divmod(div, 60)
|
|
2227
|
+
div, res[0] = divmod(div, 24)
|
|
2228
|
+
if not second:
|
|
2229
|
+
res[2] = res[3] = 0xff
|
|
2230
|
+
elif not hundredths:
|
|
2231
|
+
res[3] = 0xff
|
|
2232
|
+
return cls(res)
|
|
2233
|
+
else:
|
|
2234
|
+
raise ValueError(F"for Time float: got {value=}, expected 0..0.999999")
|
|
2235
|
+
|
|
2236
|
+
|
|
2237
|
+
__types: dict[bytes, type[CommonDataType]] = {
|
|
2238
|
+
b'\x00': NullData,
|
|
2239
|
+
b'\x01': Array,
|
|
2240
|
+
b'\x02': Structure,
|
|
2241
|
+
b'\x03': Boolean,
|
|
2242
|
+
b'\x04': BitString,
|
|
2243
|
+
b'\x05': DoubleLong,
|
|
2244
|
+
b'\x06': DoubleLongUnsigned,
|
|
2245
|
+
b'\x09': OctetString,
|
|
2246
|
+
b'\x0C': Utf8String,
|
|
2247
|
+
b'\x0D': Bcd,
|
|
2248
|
+
b'\x0F': Integer,
|
|
2249
|
+
b'\x10': Long,
|
|
2250
|
+
b'\x11': Unsigned,
|
|
2251
|
+
b'\x12': LongUnsigned,
|
|
2252
|
+
b'\x13': Long64,
|
|
2253
|
+
b'\x14': Long64Unsigned,
|
|
2254
|
+
b'\x16': Enum,
|
|
2255
|
+
b'\x17': Float32,
|
|
2256
|
+
b'\x18': Float64,
|
|
2257
|
+
b'\x19': DateTime,
|
|
2258
|
+
b'\x20': Date,
|
|
2259
|
+
b'\x21': Time
|
|
2260
|
+
}
|
|
2261
|
+
""" Common data type dictionary """
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
CommonDataTypes: TypeAlias = NullData | Array | Structure | Boolean | BitString | DoubleLong | DoubleLongUnsigned | OctetString | VisibleString | Utf8String | Bcd | Integer | \
|
|
2265
|
+
Long | Unsigned | LongUnsigned | CompactArray | Long64 | Long64Unsigned | Enum | Float32 | Float64 | DateTime | Date | Time
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
_SCALERS: dict[bytes, int] = {it.to_bytes(1, "big"): 0 for it in range(1, 256)}
|
|
2269
|
+
"""custom scaler depend from unit. initiate by 0 all"""
|
|
2270
|
+
if unit_table := config_parser.get_values("DLMS", "Unit"):
|
|
2271
|
+
for par in unit_table:
|
|
2272
|
+
_SCALERS[par["e"].to_bytes()] = par.get("scaler", 0)
|
|
2273
|
+
|
|
2274
|
+
|
|
2275
|
+
class Unit(Enum, elements=tuple(range(1, 256))):
|
|
2276
|
+
""""""
|
|
2277
|
+
|
|
2278
|
+
|
|
2279
|
+
def get_unit_scaler(unit_contents: bytes) -> int:
|
|
2280
|
+
return _SCALERS[unit_contents]
|
|
2281
|
+
|
|
2282
|
+
|
|
2283
|
+
class ScalUnitType(Structure, ReportMixin):
|
|
2284
|
+
""" DLMS UA 1000-1 Ed. 14 4.3.2 Register scaler_unit"""
|
|
2285
|
+
scaler: Integer
|
|
2286
|
+
unit: Unit
|
|
2287
|
+
|
|
2288
|
+
def get_report(self) -> Report:
|
|
2289
|
+
if (unit_rep := self.unit.get_report()).log.lev != logging.INFO:
|
|
2290
|
+
return Report(
|
|
2291
|
+
msg=str(self),
|
|
2292
|
+
log=unit_rep.log
|
|
2293
|
+
)
|
|
2294
|
+
else:
|
|
2295
|
+
msg = ""
|
|
2296
|
+
if (scaler := int(self.scaler)) == 0:
|
|
2297
|
+
...
|
|
2298
|
+
else:
|
|
2299
|
+
msg = "*10"
|
|
2300
|
+
if scaler == 1:
|
|
2301
|
+
...
|
|
2302
|
+
else:
|
|
2303
|
+
for char in str(scaler):
|
|
2304
|
+
match char:
|
|
2305
|
+
case '-': res = "\u207b"
|
|
2306
|
+
case '0': res = "\u2070"
|
|
2307
|
+
case '1': res = "\u00b9"
|
|
2308
|
+
case '2': res = "\u00b2"
|
|
2309
|
+
case '3': res = "\u00b3"
|
|
2310
|
+
case '4': res = "\u2074"
|
|
2311
|
+
case '5': res = "\u2075"
|
|
2312
|
+
case '6': res = "\u2076"
|
|
2313
|
+
case '7': res = "\u2077"
|
|
2314
|
+
case '8': res = "\u2078"
|
|
2315
|
+
case '9': res = "\u2079"
|
|
2316
|
+
case _: raise RuntimeError
|
|
2317
|
+
msg += res
|
|
2318
|
+
return Report(F"{msg} {self.unit.get_name()}", log=INFO_LOG)
|
|
2319
|
+
|
|
2320
|
+
|
|
2321
|
+
def check[T: CommonDataType](data: Optional[CommonDataType], expected_type: type[T]) -> T:
|
|
2322
|
+
"""validate data with DLMS type"""
|
|
2323
|
+
if isinstance(data, expected_type):
|
|
2324
|
+
return data
|
|
2325
|
+
if data is None:
|
|
2326
|
+
raise TypeError("data is missing")
|
|
2327
|
+
raise TypeError(F"got {type(data)}, expected {d_t}")
|
|
2328
|
+
|
|
2329
|
+
|
|
2330
|
+
def optional_check[T: CommonDataType](data: Optional[CommonDataType], expected_type: type[T]) -> Optional[T]:
|
|
2331
|
+
"""validate data with DLMS type, skip None"""
|
|
2332
|
+
if (
|
|
2333
|
+
isinstance(data, expected_type)
|
|
2334
|
+
or data is None
|
|
2335
|
+
):
|
|
2336
|
+
return data
|
|
2337
|
+
raise TypeError(F"got {type(data)}, expected {d_t}")
|
|
2338
|
+
|
|
2339
|
+
|
|
2340
|
+
def encoding2semver(value: bytes) -> SemVer:
|
|
2341
|
+
"""convert any CDT encoding to SemVer2.0.
|
|
2342
|
+
:param value: CDT encoding
|
|
2343
|
+
:return: a new class semver.Version
|
|
2344
|
+
:raises ValueError, TypeError: for SemVer"""
|
|
2345
|
+
data = get_common_data_type_from(value[:1])(value)
|
|
2346
|
+
return SemVer.parse(
|
|
2347
|
+
version=data.contents,
|
|
2348
|
+
optional_minor_and_patch=True)
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
SimpleDataTypes: tuple[CommonDataType, ...] = (
|
|
2352
|
+
NullData,
|
|
2353
|
+
Boolean,
|
|
2354
|
+
BitString,
|
|
2355
|
+
DoubleLong,
|
|
2356
|
+
DoubleLongUnsigned,
|
|
2357
|
+
OctetString,
|
|
2358
|
+
VisibleString,
|
|
2359
|
+
Utf8String,
|
|
2360
|
+
Bcd,
|
|
2361
|
+
Integer,
|
|
2362
|
+
Long,
|
|
2363
|
+
Unsigned,
|
|
2364
|
+
LongUnsigned,
|
|
2365
|
+
Long64,
|
|
2366
|
+
Long64Unsigned,
|
|
2367
|
+
Enum,
|
|
2368
|
+
Float32,
|
|
2369
|
+
Float64,
|
|
2370
|
+
DateTime,
|
|
2371
|
+
Date,
|
|
2372
|
+
Time,
|
|
2373
|
+
# more
|
|
2374
|
+
)
|
|
2375
|
+
|
|
2376
|
+
ComplexDataTypes: tuple[CommonDataType, ...] = (
|
|
2377
|
+
Array,
|
|
2378
|
+
Structure,
|
|
2379
|
+
CompactArray
|
|
2402
2380
|
)
|