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.
- dlmsadapter-0.1.0/PKG-INFO +17 -0
- dlmsadapter-0.1.0/README.md +2 -0
- dlmsadapter-0.1.0/pyproject.toml +32 -0
- dlmsadapter-0.1.0/setup.cfg +4 -0
- dlmsadapter-0.1.0/setup.py +12 -0
- dlmsadapter-0.1.0/src/DLMSadapter.egg-info/PKG-INFO +17 -0
- dlmsadapter-0.1.0/src/DLMSadapter.egg-info/SOURCES.txt +13 -0
- dlmsadapter-0.1.0/src/DLMSadapter.egg-info/dependency_links.txt +1 -0
- dlmsadapter-0.1.0/src/DLMSadapter.egg-info/entry_points.txt +2 -0
- dlmsadapter-0.1.0/src/DLMSadapter.egg-info/requires.txt +1 -0
- dlmsadapter-0.1.0/src/DLMSadapter.egg-info/top_level.txt +3 -0
- dlmsadapter-0.1.0/src/__init__.py +0 -0
- dlmsadapter-0.1.0/src/main.py +46 -0
- dlmsadapter-0.1.0/src/xml.py +621 -0
- dlmsadapter-0.1.0/test/test_xml.py +90 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DLMS-SPODES>=0.76.6
|
|
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)
|