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

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