DLMSAdapter 0.1.0__tar.gz

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.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.1
2
+ Name: DLMSadapter
3
+ Version: 0.1.0
4
+ Summary: dlms-spodes
5
+ Home-page: https://github.com/youserj/DlmsSPODES
6
+ Author-email: Serj Kotilevski <youserj@outlook.com>
7
+ Project-URL: Source, https://github.com/youserj/SPODESclient_prj
8
+ Keywords: dlms,spodes,adapter
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: DLMS-SPODES>=0.76.6
15
+
16
+ # DLMSadapter
17
+ keep data from/to DLMS client
@@ -0,0 +1,2 @@
1
+ # DLMSadapter
2
+ keep data from/to DLMS client
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools",
4
+ "setuptools-scm"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [tool.setuptools]
8
+ package-dir = {"" = "src"}
9
+
10
+ [project]
11
+ name = "DLMSadapter"
12
+ version = "0.1.0"
13
+ authors = [
14
+ {name="Serj Kotilevski", email="youserj@outlook.com"}
15
+ ]
16
+ dependencies = [
17
+ "DLMS-SPODES >= 0.76.6",
18
+ ]
19
+ description="dlms-spodes"
20
+ readme = "README.md"
21
+ requires-python = ">=3.12"
22
+ keywords=['dlms', 'spodes', 'adapter']
23
+ classifiers = [
24
+ "Programming Language :: Python :: 3",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ ]
28
+ [project.urls]
29
+ Source = "https://github.com/youserj/SPODESclient_prj"
30
+
31
+ [project.scripts]
32
+ DLMS_SPODES_client = "DLMS_SPODES_client:call_script"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ import setuptools
2
+
3
+
4
+ setuptools.setup(
5
+ long_description_content_type="text/markdown",
6
+ url="https://github.com/youserj/DlmsSPODES",
7
+ classifiers=[
8
+ "Development Status :: 3 - Alpha",
9
+ "Programming Language :: Python :: 3.11",
10
+ "License :: OSI Approved :: MIT License"
11
+ ],
12
+ )
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.1
2
+ Name: DLMSadapter
3
+ Version: 0.1.0
4
+ Summary: dlms-spodes
5
+ Home-page: https://github.com/youserj/DlmsSPODES
6
+ Author-email: Serj Kotilevski <youserj@outlook.com>
7
+ Project-URL: Source, https://github.com/youserj/SPODESclient_prj
8
+ Keywords: dlms,spodes,adapter
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: DLMS-SPODES>=0.76.6
15
+
16
+ # DLMSadapter
17
+ keep data from/to DLMS client
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ src/__init__.py
5
+ src/main.py
6
+ src/xml.py
7
+ src/DLMSadapter.egg-info/PKG-INFO
8
+ src/DLMSadapter.egg-info/SOURCES.txt
9
+ src/DLMSadapter.egg-info/dependency_links.txt
10
+ src/DLMSadapter.egg-info/entry_points.txt
11
+ src/DLMSadapter.egg-info/requires.txt
12
+ src/DLMSadapter.egg-info/top_level.txt
13
+ test/test_xml.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ DLMS_SPODES_client = DLMS_SPODES_client:call_script
@@ -0,0 +1 @@
1
+ DLMS-SPODES>=0.76.6
@@ -0,0 +1,3 @@
1
+ __init__
2
+ main
3
+ xml
File without changes
@@ -0,0 +1,46 @@
1
+ from abc import ABC, abstractmethod
2
+ from DLMS_SPODES.cosem_interface_classes.collection import Collection, ServerId, ServerVersion, Template
3
+ from DLMS_SPODES.version import AppVersion as SemVer
4
+
5
+
6
+ type_title: str = "DLMSServerType"
7
+ data_title: str = "DLMSServerData"
8
+ template_title: str = "DLMSServerTemplate"
9
+
10
+
11
+ class Adapter(ABC):
12
+ """universal adapter for keep/recovery DLMS data"""
13
+
14
+ @classmethod
15
+ @abstractmethod
16
+ def get_version(cls) -> SemVer:
17
+ """:return current adapter version"""
18
+
19
+ @abstractmethod
20
+ def create_type(self, col: Collection):
21
+ """keep type from collection(source) to destination(file(xml, json,...), sql, etc...). Save all attributes. For types only STATIC save """
22
+
23
+ @abstractmethod
24
+ def keep_data(self, col: Collection, ass_id: int = 3) -> bool:
25
+ """Save attributes WRITABLE and STATIC if possible. Use LDN as ID"""
26
+
27
+ @abstractmethod
28
+ def get_data(self, col: Collection):
29
+ """ set attribute values from file by. validation ID's """
30
+
31
+ @abstractmethod
32
+ def get_collection(self,
33
+ m: bytes,
34
+ sid: ServerId,
35
+ ver: ServerVersion) -> Collection:
36
+ """get Collection by m: manufacturer, t: type, ver: version"""
37
+
38
+ @abstractmethod
39
+ def create_template(self,
40
+ name: str,
41
+ template: Template):
42
+ """keep used values to template by collections with <name>"""
43
+
44
+ @abstractmethod
45
+ def get_template(self, name: str) -> Template:
46
+ """load template by <name>"""
@@ -0,0 +1,621 @@
1
+ from dataclasses import dataclass, field
2
+ from itertools import count
3
+ import copy
4
+ import xml.etree.ElementTree as ET
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+ import logging
8
+ from DLMS_SPODES.cosem_interface_classes.collection import Collection, ServerId, ServerVersion, cst, ClassID, ic, ut, cdt, AssociationLN, Template
9
+ from DLMS_SPODES.cosem_interface_classes.association_ln.ver0 import ObjectListElement, AttributeAccessItem, AccessMode, is_attr_writable
10
+ from DLMS_SPODES.cosem_interface_classes import implementations as impl, collection
11
+ from DLMS_SPODES.version import AppVersion as SemVer
12
+ from DLMS_SPODES import exceptions as exc
13
+ from .main import Adapter
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class AdapterException(Exception):
20
+ """"""
21
+
22
+
23
+ root: Path = Path(".")
24
+ """root for file as example"""
25
+ template_path = root / "Template"
26
+ types_path = root / "Types"
27
+ if not types_path.exists():
28
+ types_path.mkdir()
29
+
30
+
31
+ @dataclass
32
+ class Xml40(Adapter):
33
+ _root_tag: str = field(init=False, default="Objects")
34
+ _template_root_tag: str = field(init=False, default="template.objects")
35
+ keep_path: Path = field(init=False, default=root / "XML_devices")
36
+ template_path: Path = field(init=False, default=root / "Templates")
37
+
38
+ def __post_init__(self):
39
+ if not self.keep_path.exists():
40
+ self.keep_path.mkdir()
41
+ if not self.template_path.exists():
42
+ self.template_path.mkdir()
43
+ @classmethod
44
+ def get_version(cls) -> SemVer:
45
+ return SemVer(4, 1)
46
+
47
+ def __get_root_node(self, col: Collection) -> ET.Element:
48
+ root_node = ET.Element(self._root_tag, attrib={"version": str(self.get_version())})
49
+ ET.SubElement(root_node, 'dlms_ver').text = str(col.dlms_ver)
50
+ ET.SubElement(root_node, 'country').text = str(col.country.value)
51
+ if col.country_ver:
52
+ ET.SubElement(root_node, 'country_ver').text = str(col.country_ver)
53
+ if col.manufacturer is not None:
54
+ ET.SubElement(root_node, 'manufacturer').text = col.manufacturer.decode("utf-8")
55
+ if col.server_id is not None:
56
+ ET.SubElement(root_node, 'server_type').text = col.server_id.value.encoding.hex()
57
+ if col.server_ver is not None:
58
+ ET.SubElement(root_node, 'server_ver', attrib={"instance": "1"}).text = str(col.server_ver.get_semver())
59
+ return root_node
60
+
61
+ def create_type(self, col: Collection):
62
+ if not isinstance(col.manufacturer, bytes):
63
+ raise AdapterException(F"{col} hasn't manufacturer parameter")
64
+ if not isinstance(col.server_id, ServerId):
65
+ raise AdapterException(F"{col} hasn't {ServerId.__class__.__name__} parameter")
66
+ if not isinstance(col.server_ver, ServerVersion):
67
+ raise AdapterException(F"{col} hasn't {ServerVersion.__class__.__name__} parameter")
68
+ root_node = self.__get_root_node(col)
69
+ objs: dict[cst.LogicalName, set[int]] = dict()
70
+ """key: LN, value: not writable and readable container"""
71
+ for ass in filter(lambda it: it.logical_name.e != 0, col.get_objects_by_class_id(ClassID.ASSOCIATION_LN)):
72
+ if ass.object_list is None:
73
+ logger.warning(F"for {ass} got empty <object_list>. skip it")
74
+ continue
75
+ for obj_el in ass.object_list:
76
+ if str(obj_el.logical_name) in ("0.0.40.0.0.255", "0.0.42.0.0.255"):
77
+ """skip LDN and current_association"""
78
+ continue
79
+ elif obj_el.logical_name in objs:
80
+ """"""
81
+ else:
82
+ objs[obj_el.logical_name] = set()
83
+ for access in obj_el.access_rights.attribute_access[1:]: # without ln
84
+ if not access.access_mode.is_writable() and access.access_mode.is_readable():
85
+ objs[obj_el.logical_name].add(int(access.attribute_id))
86
+ o2 = list()
87
+ """container sort by AssociationLN first"""
88
+ for ln in objs.keys():
89
+ obj = col.get_object(ln)
90
+ if obj.CLASS_ID == ClassID.ASSOCIATION_LN:
91
+ o2.insert(0, obj)
92
+ else:
93
+ o2.append(obj)
94
+ for obj in o2:
95
+ object_node = ET.SubElement(root_node, "obj", attrib={'ln': str(obj.logical_name.get_report().msg)})
96
+ if obj.CLASS_ID == ClassID.ASSOCIATION_LN:
97
+ ET.SubElement(object_node, "ver").text = str(obj.VERSION)
98
+ v = objs[obj.logical_name]
99
+ for i, attr in filter(lambda it: it[0] != 1, obj.get_index_with_attributes()):
100
+ el: ic.ICAElement = obj.get_attr_element(i)
101
+ if el.classifier == ic.Classifier.STATIC and ((i in v) or el.DATA_TYPE == impl.profile_generic.CaptureObjectsDisplayReadout):
102
+ if attr is None:
103
+ logger.error(F"for {obj} attr: {i} not set, value is absense")
104
+ else:
105
+ ET.SubElement(object_node, "attr", attrib={"i": str(i)}).text = attr.encoding.hex()
106
+ elif isinstance(el.DATA_TYPE, ut.CHOICE): # need keep all CHOICES types if possible
107
+ if attr is None:
108
+ logger.error(F"for {obj} attr: {i} type not set, value is absense")
109
+ else:
110
+ ET.SubElement(object_node, "attr", attrib={"i": str(i)}).text = str(attr.TAG[0])
111
+ else:
112
+ logger.info(F"for {obj} attr: {i} value not need. skipped")
113
+ if len(object_node) == 0:
114
+ root_node.remove(object_node)
115
+ # TODO: '<!DOCTYPE ITE_util_tree SYSTEM "setting.dtd"> or xsd
116
+ xml_string = ET.tostring(root_node, encoding='cp1251', method='xml')
117
+ if not (man_path := types_path / col.manufacturer.decode("ascii")).exists():
118
+ man_path.mkdir()
119
+ if not (type_path := man_path / col.server_id.value.encoding.hex()).exists():
120
+ type_path.mkdir()
121
+ ver_path = type_path / F"{col.server_ver.get_semver()}.typ"
122
+ with open(ver_path, "wb") as f:
123
+ f.write(xml_string)
124
+
125
+ def __get_keep_path(self, col: Collection) -> Path:
126
+ if (ldn := col.LDN.value) is None:
127
+ raise exc.EmptyObj(F"No LDN value in collection")
128
+ return (self.keep_path / ldn.contents.hex()).with_suffix(".xml")
129
+
130
+ def keep_data(self, col: Collection, ass_id: int = 3) -> bool:
131
+ path = self.__get_keep_path(col)
132
+ root_node = self.__get_root_node(col)
133
+ is_empty: bool = True
134
+ parent_col = self.get_collection(
135
+ m=col.manufacturer,
136
+ sid=col.server_id,
137
+ ver=col.server_ver)
138
+ obj_list_el: ObjectListElement
139
+ a_a: AttributeAccessItem
140
+ for obj_list_el in col.getASSOCIATION(ass_id).object_list:
141
+ obj = col.get_object(obj_list_el.logical_name)
142
+ parent_obj = parent_col.get_object(obj_list_el.logical_name)
143
+ object_node = None
144
+ for a_a in obj_list_el.access_rights.attribute_access:
145
+ if (i := int(a_a.attribute_id)) == 1:
146
+ """skip ln"""
147
+ elif obj.get_attr_element(i).classifier == ic.Classifier.DYNAMIC:
148
+ """skip DYNAMIC attributes"""
149
+ elif (attr := obj.get_attr(i)) is None:
150
+ """skip empty attributes"""
151
+ elif parent_obj.get_attr(i) == attr:
152
+ """skip not changed attr value"""
153
+ else:
154
+ is_empty = False
155
+ if object_node is None:
156
+ object_node = ET.SubElement(root_node, "object", attrib={'ln': str(obj.logical_name)})
157
+ ET.SubElement(object_node, "attr", attrib={'index': str(i)}).text = attr.encoding.hex()
158
+ if not is_empty:
159
+ # TODO: '<!DOCTYPE ITE_util_tree SYSTEM "setting.dtd"> or xsd
160
+ xml_string = ET.tostring(root_node, encoding='cp1251', method='xml')
161
+ with open(path, "wb") as f:
162
+ f.write(xml_string)
163
+ else:
164
+ logger.warning("nothing save. all attributes according with origin collection")
165
+ return not is_empty
166
+
167
+ def get_data(self, col: Collection):
168
+ path = self.__get_keep_path(col)
169
+ tree = ET.parse(path)
170
+ root_node = tree.getroot()
171
+ if root_node.tag != self._root_tag:
172
+ raise ValueError(F"ERROR: Root tag got {root_node.tag}, expected {self._root_tag}")
173
+ root_version = SemVer.from_str(root_node.attrib.get('version', '1.0.0'))
174
+ if (dlms_ver := root_node.findtext("dlms_ver")) is not None:
175
+ col.set_dlms_ver(int(dlms_ver))
176
+ if (country := root_node.findtext("country")) is not None:
177
+ col.set_country(collection.CountrySpecificIdentifiers(int(country)))
178
+ if (country_ver := root_node.findtext("country_ver")) is not None:
179
+ col.set_country_ver(ServerVersion(
180
+ par=b'\x00\x00\x60\x01\x06\xff\x02', # 0.0.96.1.6.255:2
181
+ value=cdt.OctetString(bytearray(country_ver.encode(encoding="ascii")))
182
+ ))
183
+ if (manufacturer := root_node.findtext("manufacturer")) is not None:
184
+ col.set_manufacturer(manufacturer.encode("utf-8"))
185
+ if (server_id := root_node.findtext("server_type")) is not None:
186
+ col.set_server_id(ServerId(
187
+ par=b'\x00\x00\x60\x01\x01\xff\x02', # 0.0.96.1.1.255:2
188
+ value=cdt.get_instance_and_pdu_from_value(bytes.fromhex(server_id))[0]
189
+ ))
190
+ if (server_ver := root_node.findtext("server_ver")) is not None:
191
+ col.set_server_ver(ServerVersion(
192
+ par=b'\x00\x00\x00\x02\x00\xff\x02',
193
+ value=cdt.OctetString(bytearray(server_ver.encode(encoding="ascii")))
194
+ ))
195
+ logger.info(F"Версия: {root_version}, {path=}")
196
+ match root_version:
197
+ case SemVer(3, 1 | 2):
198
+ for obj in root_node.findall("object"):
199
+ ln: str = obj.attrib.get('ln', 'is absence')
200
+ logical_name: cst.LogicalName = cst.LogicalName.from_obis(ln)
201
+ if not col.is_in_collection(logical_name):
202
+ logger.error(F"got object with {ln=} not find in collection. Skip it attribute values")
203
+ continue
204
+ else:
205
+ new_object = col.get_object(logical_name)
206
+ indexes: list[int] = list()
207
+ """ got attributes indexes for current object """
208
+ for attr in obj.findall('attribute'):
209
+ index: str = attr.attrib.get('index')
210
+ if index.isdigit():
211
+ indexes.append(int(index))
212
+ else:
213
+ raise ValueError(F'ERROR: for obj with {ln=} got index {index} and it is not digital')
214
+ try:
215
+ new_object.set_attr(indexes[-1], bytes.fromhex(attr.text))
216
+ except exc.NoObject as e:
217
+ logger.error(F"Can't fill {new_object} attr: {indexes[-1]}. Skip. {e}.")
218
+ break
219
+ except exc.ITEApplication as e:
220
+ logger.error(F"Can't fill {new_object} attr: {indexes[-1]}. {e}")
221
+ except IndexError:
222
+ logger.error(F'Object "{new_object}" not has attr: {index}')
223
+ except TypeError as e:
224
+ logger.error(F'Object {new_object} attr:{index} do not write, encoding wrong : {e}')
225
+ except ValueError as e:
226
+ logger.error(F'Object {new_object} attr:{index} do not fill: {e}')
227
+ except AttributeError as e:
228
+ logger.error(F'Object {new_object} attr:{index} do not fill: {e}')
229
+ case SemVer(4, 0 | 1):
230
+ for obj in root_node.findall("object"):
231
+ ln: str = obj.attrib.get("ln", 'is absence')
232
+ logical_name: cst.LogicalName = cst.LogicalName(ln)
233
+ if not col.is_in_collection(logical_name):
234
+ raise ValueError(F"got object with {ln=} not find in collection. Abort attribute setting")
235
+ else:
236
+ new_object = col.get_object(logical_name)
237
+ for attr in obj.findall("attr"):
238
+ index: int = int(attr.attrib.get("index"))
239
+ try:
240
+ new_object.set_attr(index, bytes.fromhex(attr.text))
241
+ except exc.NoObject as e:
242
+ logger.error(F"Can't fill {new_object} attr: {index}. Skip. {e}.")
243
+ break
244
+ except exc.ITEApplication as e:
245
+ logger.error(F"Can't fill {new_object} attr: {index}. {e}")
246
+ except IndexError:
247
+ logger.error(F'Object "{new_object}" not has attr: {index}')
248
+ except TypeError as e:
249
+ logger.error(F'Object {new_object} attr:{index} do not write, encoding wrong : {e}')
250
+ except ValueError as e:
251
+ logger.error(F'Object {new_object} attr:{index} do not fill: {e}')
252
+ except AttributeError as e:
253
+ logger.error(F'Object {new_object} attr:{index} do not fill: {e}')
254
+ case _ as error:
255
+ raise exc.VersionError(error, additional='Xml')
256
+
257
+ def get_collection(self,
258
+ m: bytes,
259
+ sid: ServerId,
260
+ ver: ServerVersion) -> Collection:
261
+ path = get_col_path(m, sid, ver)
262
+ tree = ET.parse(path)
263
+ r_n = tree.getroot()
264
+ new = Collection(
265
+ dlms_ver=int(r_n.findtext("dlms_ver", "6")),
266
+ country=collection.CountrySpecificIdentifiers(int(r_n.findtext("country", "7"))),
267
+ man=r_n.findtext("manufacturer").encode("utf-8"),
268
+ s_id=ServerId(
269
+ par=b'\x00\x00\x60\x01\x01\xff\x02',
270
+ value=cdt.get_instance_and_pdu_from_value(bytes.fromhex(r_n.findtext("server_type")))[0]),
271
+ s_ver=ver,
272
+ c_ver=ServerVersion(
273
+ par=b'\x00\x00\x00\x02\x00\xff\x02',
274
+ value=cdt.OctetString(bytearray(r_n.findtext("server_ver").encode(encoding="ascii"))))
275
+ )
276
+ if r_n.tag != self._root_tag:
277
+ raise ValueError(F"ERROR: Root tag got {r_n.tag}, expected {self._root_tag}")
278
+ root_version: SemVer = SemVer.from_str(r_n.attrib.get('version', '1.0.0'))
279
+ logger.info(F'Версия: {root_version}, {path=}')
280
+ match root_version:
281
+ case SemVer(3, 0 | 1 | 2):
282
+ attempts: iter = count(3, -1)
283
+ """ attempts counter """
284
+ while len(r_n) != 0 and next(attempts):
285
+ logger.info(F'{attempts=}')
286
+ for obj in r_n.findall('object'):
287
+ ln: str = obj.attrib.get('ln', 'is absence')
288
+ class_id: str = obj.findtext('class_id')
289
+ if not class_id:
290
+ logger.warning(F"skip create DLMS {ln} from Xml. Class ID is absence")
291
+ continue
292
+ version: str | None = obj.findtext('version')
293
+ try:
294
+ logical_name: cst.LogicalName = cst.LogicalName.from_obis(ln)
295
+ if not new.is_in_collection(logical_name):
296
+ new_object = new.add(class_id=ut.CosemClassId(class_id),
297
+ version=None if version is None else cdt.Unsigned(version),
298
+ logical_name=logical_name)
299
+ else:
300
+ new_object = new.get_object(logical_name.contents)
301
+ except TypeError as e:
302
+ logger.error(F'Object {obj.attrib["name"]} not created : {e}')
303
+ continue
304
+ except ValueError as e:
305
+ logger.error(F'Object {obj.attrib["name"]} not created. {class_id=} {version=} {ln=}: {e}')
306
+ continue
307
+ indexes: list[int] = list()
308
+ """ got attributes indexes for current object """
309
+ for attr in obj.findall('attribute'):
310
+ index: str = attr.attrib.get('index')
311
+ if index.isdigit():
312
+ indexes.append(int(index))
313
+ else:
314
+ raise ValueError(F'ERROR: for {new_object.logical_name if new_object is not None else ""} got index {index} and it is not digital')
315
+ try:
316
+ match len(attr.text), new_object.get_attr_element(indexes[-1]).DATA_TYPE:
317
+ case 1 | 2, ut.CHOICE():
318
+ if new_object.get_attr(indexes[-1]) is None:
319
+ new_object.set_attr(indexes[-1], int(attr.text))
320
+ else:
321
+ """not need set"""
322
+ case 1 | 2, data_type if data_type.TAG[0] == int(attr.text): """ ordering by old"""
323
+ case 1 | 2, data_type: raise ValueError(F'Got {attr.text} attribute Tag, expected {data_type}')
324
+ case _:
325
+ record_time: str = attr.attrib.get('record_time')
326
+ if record_time is not None:
327
+ new_object.set_record_time(indexes[-1], bytes.fromhex(record_time))
328
+ new_object.set_attr(indexes[-1], bytes.fromhex(attr.text))
329
+ obj.remove(attr)
330
+ except ut.UserfulTypesException as e:
331
+ if attr.attrib.get("forced", None):
332
+ new_object.set_attr_force(indexes[-1], cdt.get_common_data_type_from(int(attr.text).to_bytes(1, "big"))())
333
+ logger.warning(F"set to {new_object} attr: {indexes[-1]} forced value after. {e}.")
334
+ except exc.NoObject as e:
335
+ logger.error(F"Can't fill {new_object} attr: {indexes[-1]}. Skip. {e}.")
336
+ break
337
+ except exc.ITEApplication as e:
338
+ logger.error(F"Can't fill {new_object} attr: {indexes[-1]}. {e}")
339
+ except IndexError:
340
+ logger.error(F'Object "{new_object}" not has attr: {index}')
341
+ except TypeError as e:
342
+ logger.error(F'Object {new_object} attr:{index} do not write, encoding wrong : {e}')
343
+ except ValueError as e:
344
+ logger.error(F'Object {new_object} attr:{index} do not fill: {e}')
345
+ except AttributeError as e:
346
+ logger.error(F'Object {new_object} attr:{index} do not fill: {e}')
347
+ if len(obj.findall('attribute')) == 0:
348
+ r_n.remove(obj)
349
+ logger.info(F'Not parsed DLMS objects: {len(r_n)}')
350
+ case SemVer(4, 0 | 1):
351
+ attempts: iter = count(3, -1)
352
+ """ attempts counter """
353
+ while len(r_n) != 0 and next(attempts):
354
+ logger.info(F'{attempts=}')
355
+ for obj in r_n.findall("obj"):
356
+ ln: str = obj.attrib.get('ln', 'is absence')
357
+ version: str | None = obj.findtext("ver")
358
+ try:
359
+ logical_name: cst.LogicalName = cst.LogicalName.from_obis(ln)
360
+ if version: # only for AssociationLN
361
+ new_object: AssociationLN = new.add_if_missing(
362
+ class_id=ClassID.ASSOCIATION_LN,
363
+ version=cdt.Unsigned(version),
364
+ logical_name=logical_name)
365
+ new.add_if_missing( # current association with know version
366
+ class_id=ClassID.ASSOCIATION_LN,
367
+ version=cdt.Unsigned(version),
368
+ logical_name=cst.LogicalName.from_obis("0.0.40.0.0.255"))
369
+ else:
370
+ new_object = new.get_object(logical_name.contents)
371
+ except TypeError as e:
372
+ logger.error(F'Object {obj.attrib["ln"]} not created : {e}')
373
+ continue
374
+ except ValueError as e:
375
+ logger.error(F'Object {obj.attrib["ln"]} not created. {version=} {ln=}: {e}')
376
+ continue
377
+ for attr in obj.findall("attr"):
378
+ i: int = int(attr.attrib.get("i"))
379
+ try:
380
+ if len(attr.text) <= 2: # set only type with default value
381
+ data_type = new_object.get_attr_element(i).DATA_TYPE
382
+ if isinstance(data_type, ut.CHOICE):
383
+ new_object.set_attr(i, int(attr.text))
384
+ elif data_type.TAG[0] == int(attr.text):
385
+ """ ordering by old"""
386
+ else:
387
+ raise ValueError(F'Got {attr.text} attribute Tag, expected {data_type}')
388
+ else: # set common value
389
+ new_object.set_attr(i, bytes.fromhex(attr.text))
390
+ if new_object.CLASS_ID == ClassID.ASSOCIATION_LN and i == 2: # setup new root_node from AssociationLN.object_list
391
+ for obj_el in new_object.object_list:
392
+ # obj_el: ObjectListElement
393
+ new.add_if_missing(
394
+ class_id=obj_el.class_id,
395
+ version=obj_el.version,
396
+ logical_name=obj_el.logical_name)
397
+ obj.remove(attr)
398
+ except ut.UserfulTypesException as e:
399
+ if attr.attrib.get("forced", None):
400
+ new_object.set_attr_force(i, cdt.get_common_data_type_from(int(attr.text).to_bytes(1, "big"))())
401
+ logger.warning(F"set to {new_object} attr: {i} forced value after. {e}.")
402
+ except exc.NoObject as e:
403
+ logger.error(F"Can't fill {new_object} attr: {i}. Skip. {e}.")
404
+ break
405
+ except exc.ITEApplication as e:
406
+ logger.error(F"Can't fill {new_object} attr: {i}. {e}")
407
+ except IndexError:
408
+ logger.error(F'Object "{new_object}" not has attr: {i}')
409
+ except TypeError as e:
410
+ logger.error(F'Object {new_object} attr:{i} do not write, encoding wrong : {e}')
411
+ except ValueError as e:
412
+ logger.error(F'Object {new_object} attr:{i} do not fill: {e}')
413
+ except AttributeError as e:
414
+ logger.error(F'Object {new_object} attr:{i} do not fill: {e}')
415
+ if len(obj.findall("attr")) == 0:
416
+ r_n.remove(obj)
417
+ logger.info(F'Not parsed DLMS root_node: {len(r_n)}')
418
+ return new
419
+
420
+ def __get_template_root_node(self,
421
+ collections: list[Collection]) -> ET.Element:
422
+ r_n = ET.Element(self._template_root_tag, attrib={"version": str(self.get_version())})
423
+ """root node"""
424
+ ET.SubElement(r_n, 'dlms_ver').text = str(collections[0].dlms_ver)
425
+ ET.SubElement(r_n, 'country').text = str(collections[0].country.value)
426
+ ET.SubElement(r_n, 'country_ver').text = str(collections[0].country_ver)
427
+ for col in collections:
428
+ manufacture_node = ET.SubElement(r_n, 'manufacturer')
429
+ manufacture_node.text = col.manufacturer.decode("utf-8")
430
+ server_type_node = ET.SubElement(manufacture_node, 'server_type')
431
+ server_type_node.text = col.server_id.value.encoding.hex()
432
+ server_ver_node = ET.SubElement(server_type_node, 'server_ver', attrib={"instance": "1"})
433
+ server_ver_node.text = str(col.server_ver.get_semver())
434
+ return r_n
435
+
436
+ def create_template(self,
437
+ name: str,
438
+ template: Template):
439
+ used_copy = copy.deepcopy(template.used)
440
+ r_n = self.__get_template_root_node(collections=template.collections)
441
+ r_n.attrib["decode"] = "1"
442
+ if template.verified:
443
+ r_n.attrib["verified"] = "1"
444
+ for col in template.collections:
445
+ for ln, indexes in copy.copy(used_copy).items():
446
+ try:
447
+ obj = col.get_object(ln)
448
+ object_node = ET.SubElement(
449
+ r_n,
450
+ "object",
451
+ attrib={"ln": str(obj.logical_name)})
452
+ for i in tuple(indexes):
453
+ attr = obj.get_attr(i)
454
+ if isinstance(attr, cdt.CommonDataType):
455
+ attr_el = ET.SubElement(
456
+ object_node,
457
+ "attr",
458
+ {"name": obj.get_attr_element(i).NAME,
459
+ "index": str(i)})
460
+ if isinstance(attr, cdt.SimpleDataType):
461
+ attr_el.text = str(attr)
462
+ elif isinstance(attr, cdt.ComplexDataType):
463
+ attr_el.attrib["type"] = "array" if attr.TAG == b'\x01' else "struct" # todo: make better
464
+ stack: list = [(attr_el, "attr_el_name", iter(attr))]
465
+ while stack:
466
+ node, a_name, value_it = stack[-1]
467
+ value = next(value_it, None)
468
+ if value:
469
+ if not isinstance(a_name, str):
470
+ a_name = next(a_name).NAME
471
+ if isinstance(value, cdt.Array):
472
+ stack.append((ET.SubElement(node,
473
+ "array",
474
+ attrib={"name": a_name}), "ar_name", iter(value)))
475
+ elif isinstance(value, cdt.Structure):
476
+ stack.append((ET.SubElement(node, "struct"), iter(value.ELEMENTS), iter(value)))
477
+ else:
478
+ ET.SubElement(node,
479
+ "simple",
480
+ attrib={"name": a_name}).text = str(value)
481
+ else:
482
+ stack.pop()
483
+ indexes.remove(i)
484
+ else:
485
+ logger.error(F"skip record {obj}:attr={i} with value={attr}")
486
+ if len(indexes) == 0:
487
+ used_copy.pop(ln)
488
+ except exc.NoObject as e:
489
+ logger.warning(F"skip obj with {ln=} in {template.collections.index(col)} collection: {e}")
490
+ continue
491
+ if len(used_copy) == 0:
492
+ logger.info(F"success decoding: used {template.collections.index(col) + 1} from {len(template.collections)} collections")
493
+ break
494
+ if len(used_copy) != 0:
495
+ raise ValueError(F"failed decoding: {used_copy}")
496
+ with open(
497
+ (self.template_path / name).with_suffix(".xml"),
498
+ mode="wb") as f:
499
+ f.write(ET.tostring(
500
+ element=r_n,
501
+ encoding="utf-8",
502
+ method="xml",
503
+ xml_declaration=True))
504
+
505
+ def get_template(self, name: str) -> Template:
506
+ path = (self.template_path / name).with_suffix(".xml")
507
+ used: collection.UsedAttributes = dict()
508
+ cols = list()
509
+ tree = ET.parse(path)
510
+ r_n = tree.getroot()
511
+ if r_n.tag != self._template_root_tag:
512
+ raise ValueError(F"ERROR: Root tag got {r_n.tag}, expected {self._template_root_tag}")
513
+ root_version: SemVer = SemVer.from_str(r_n.attrib.get('version', '1.0.0'))
514
+ logger.info(F'Версия: {root_version}, {path=}')
515
+ for manufacturer_node in r_n.findall("manufacturer"):
516
+ for server_type_node in manufacturer_node.findall("server_type"):
517
+ for server_ver_node in server_type_node.findall("server_ver"):
518
+ cols.append(self.get_collection(
519
+ m=manufacturer_node.text.encode("utf-8"),
520
+ sid=ServerId(
521
+ par=b'\x00\x00\x60\x01\x01\xff\x02',
522
+ value=cdt.get_instance_and_pdu_from_value(bytes.fromhex(server_type_node.text))[0]),
523
+ ver=ServerVersion(
524
+ par=b'\x00\x00\x00\x02\x00\xff\x02',
525
+ value=cdt.OctetString(bytearray(server_ver_node.text.encode(encoding="ascii")))
526
+ )
527
+ ))
528
+ match root_version:
529
+ case SemVer(4, 0 | 1):
530
+ for obj in r_n.findall('object'):
531
+ ln: str = obj.attrib.get("ln", 'is absence')
532
+ obis = collection.OBIS.fromhex(ln)
533
+ objs: list[ic.COSEMInterfaceClasses] = list()
534
+ for col in cols:
535
+ if not col.is_in_collection(obis):
536
+ logger.warning(F"got object with {ln=} not find in collection: {col}")
537
+ else:
538
+ objs.append(col.get_object(obis))
539
+ used[obis] = set()
540
+ for attr in obj.findall("attr"):
541
+ index: int = int(attr.attrib.get("index"))
542
+ used[obis].add(index)
543
+ try:
544
+ match attr.attrib.get("type", "simple"):
545
+ case "simple":
546
+ for new_object in objs:
547
+ new_object.set_attr(index, attr.text)
548
+ case "array" | "struct":
549
+ stack = [(list(), iter(attr))]
550
+ while stack:
551
+ v1, v2 = stack[-1]
552
+ v = next(v2, None)
553
+ if v is None:
554
+ stack.pop()
555
+ elif v.tag == "simple":
556
+ v1.append(v.text)
557
+ else:
558
+ v1.append(list())
559
+ stack.append((v1[-1], iter(v)))
560
+ for new_object in objs:
561
+ new_object.set_attr(index, v1)
562
+ except exc.ITEApplication as e:
563
+ logger.error(F"Can't fill {new_object} attr: {index}. {e}")
564
+ except IndexError:
565
+ logger.error(F'Object "{new_object}" not has attr: {index}')
566
+ except TypeError as e:
567
+ logger.error(F'Object {new_object} attr:{index} do not write, encoding wrong : {e}')
568
+ except ValueError as e:
569
+ logger.error(F'Object {new_object} attr:{index} do not fill: {e}')
570
+ except AttributeError as e:
571
+ logger.error(F'Object {new_object} attr:{index} do not fill: {e}')
572
+ case _ as error:
573
+ raise exc.VersionError(error, additional='Xml')
574
+ return Template(
575
+ collections=cols,
576
+ used=used,
577
+ verified=bool(int(r_n.findtext("verified", default="0"))))
578
+
579
+
580
+ def get_manufactures_container() -> dict[bytes, dict[bytes, dict[SemVer | cdt.CommonDataType, Path]]]:
581
+ logger.info(F"create manufacturer configuration container")
582
+ ret: dict[bytes, dict[bytes, dict[SemVer, Path]]] = dict()
583
+ for m_path in types_path.iterdir():
584
+ if m_path.is_dir():
585
+ if len(m_path.name) == 3:
586
+ man = m_path.name.encode("ascii")
587
+ # elif len(m.name) == 6:
588
+ # man = bytes.fromhex(m.name)
589
+ else:
590
+ logger.warning(F"skip <{m_path}>: not recognized like manufacturer")
591
+ continue
592
+ ret[man] = dict()
593
+ for sid_path in m_path.iterdir():
594
+ if sid_path.is_dir():
595
+ ret[man][server_id := bytes.fromhex(sid_path.name)] = dict()
596
+ for ver_path in sid_path.iterdir():
597
+ if ver_path.is_file() and ver_path.suffix == ".typ":
598
+ if (v := SemVer.from_str(ver_path.stem)) == SemVer(0, 0, 0): # todo: make Appversion other result if None
599
+ try:
600
+ v = bytes.fromhex(ver_path.stem)
601
+ except ValueError as e:
602
+ logger.error(F"skip type, wrong file name {ver_path}")
603
+ continue
604
+ ret[man][server_id][v] = ver_path
605
+ return ret
606
+
607
+
608
+ @lru_cache(maxsize=100)
609
+ def get_col_path(m: bytes, sid: ServerId, ver: ServerVersion) -> Path:
610
+ """one recursion collection get way. ret: file, is_searched"""
611
+ if (man := get_manufactures_container().get(m)) is None:
612
+ raise AdapterException(F"no support manufacturer: {m}")
613
+ elif (sid := man.get(sid.value.encoding)) is None:
614
+ raise AdapterException(F"no support type {sid}, with manufacturer: {m}")
615
+ elif path := sid.get(ver.get_semver()):
616
+ logger.info(F"got collection from library by path: {path}")
617
+ return path
618
+ elif isinstance(semver := ver.get_semver(), SemVer) and (searched_version := semver.select_nearest(filter(lambda v: isinstance(v, SemVer), sid.keys()))):
619
+ return sid.get(searched_version)
620
+ else:
621
+ raise AdapterException(F"no support version {ver} with manufacturer: {m}, identifier: {sid}")
@@ -0,0 +1,90 @@
1
+ import shutil
2
+ import time
3
+ import unittest
4
+ from pathlib import Path
5
+ from DLMS_SPODES.cosem_interface_classes import collection
6
+ from DLMS_SPODES.types import cdt
7
+ from src.xml import Xml40, get_manufactures_container
8
+ import logging
9
+
10
+
11
+ adapter = Xml40()
12
+ logger = logging.getLogger(__name__)
13
+ logger.level = logging.INFO
14
+
15
+
16
+ class TestType(unittest.TestCase):
17
+ def test_create_adapter(self):
18
+ adapter_ = Xml40()
19
+
20
+ def test_create_type(self):
21
+ col = collection.Collection(
22
+ man=b'XXX',
23
+ s_id=collection.ServerId(b'1234567', cdt.OctetString(bytearray(b'M2M-1'))),
24
+ s_ver=collection.ServerVersion(b'1234560', cdt.OctetString(bytearray(b'1.4.2'))))
25
+ print(col)
26
+ col.LDN.set_attr(2, bytearray(b'XXX0000000001234'))
27
+ adapter.create_type(col)
28
+
29
+ def test_get_man(self):
30
+ c = get_manufactures_container()
31
+ print(c)
32
+
33
+ def test_get_collection(self):
34
+ col = adapter.get_collection(
35
+ m=b"KPZ",
36
+ sid=collection.ServerId(
37
+ par=bytes.fromhex("0000600102ff02"),
38
+ value=cdt.OctetString(bytearray(b'M2M_1'))),
39
+ ver=collection.ServerVersion(
40
+ par=bytes.fromhex("0000000201ff02"),
41
+ value=cdt.OctetString(bytearray(b"1.7.3"))))
42
+ print(col)
43
+ col.LDN.set_attr(2, bytearray(b"KPZ00001234567890")) # need for test
44
+ col2 = col.copy()
45
+ # keep path
46
+ clock_obj = col.get_object("0.0.1.0.0.255")
47
+ clock_obj.set_attr(3, 100) # change any value for test
48
+ iccid_obj = col.get_object("0.128.25.6.0.255")
49
+ iccid_obj.set_attr(2, "01 02 03 04 05")
50
+ adapter.keep_data(col)
51
+ # get data
52
+ adapter.get_data(col2)
53
+ print(col2)
54
+
55
+ def test_template(self):
56
+ col = adapter.get_collection(
57
+ m=b"KPZ",
58
+ sid=collection.ServerId(
59
+ par=bytes.fromhex("0000600102ff02"),
60
+ value=cdt.OctetString(bytearray(b'M2M_3'))),
61
+ ver=collection.ServerVersion(
62
+ par=bytes.fromhex("0000000201ff02"),
63
+ value=cdt.OctetString(bytearray(b"1.4.15"))))
64
+ col2 = adapter.get_collection(
65
+ m=b"102",
66
+ sid=collection.ServerId(
67
+ par=bytes.fromhex("0000600102ff02"),
68
+ value=cdt.OctetString(bytearray(b'M2M_3'))),
69
+ ver=collection.ServerVersion(
70
+ par=bytes.fromhex("0000000201ff02"),
71
+ value=cdt.OctetString(bytearray(b"1.3.30"))))
72
+ clock_obj = col.get_object("0.0.1.0.0.255")
73
+ clock_obj.set_attr(3, 120)
74
+ act_cal = col.get_object("0.0.13.0.0.255")
75
+ act_cal.day_profile_table_passive.append((1, [("11:00", "01 01 01 01 01 01", 1)]))
76
+ used = {
77
+ clock_obj.logical_name: {3},
78
+ act_cal.logical_name: {9}
79
+ }
80
+ adapter.create_template(
81
+ name="template_test1",
82
+ template=collection.Template(
83
+ collections=[col, col2],
84
+ used={
85
+ collection.OBIS(clock_obj.logical_name.contents): {3},
86
+ collection.OBIS(act_cal.logical_name.contents): {9}
87
+ }
88
+ ))
89
+ template = adapter.get_template("template_test1")
90
+ print(template)