DLMS-SPODES 0.87.13__py3-none-any.whl → 0.87.16__py3-none-any.whl

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